From 3dee91426e1c98f43caff4b4515b2f1b7359d753 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Tue, 2 Apr 2024 14:27:12 +0200 Subject: [PATCH] feat: Correctly define all product module models and SQL migration script (#6906) The only thing remaining is modifying `upsertWithReplace` to handle many-to-many constraints correctly and not cascade updates to them. This is something that we should do separately though. --- .changeset/brave-beds-join.md | 5 + .../migrations/.snapshot-medusa-products.json | 69 +----------- ...00756.ts => InitialSetup20240401153642.ts} | 99 +++++++++-------- .../product/src/models/product-category.ts | 47 +++++--- .../product/src/models/product-collection.ts | 37 +++--- packages/product/src/models/product-image.ts | 22 ++-- .../src/models/product-option-value.ts | 21 +--- packages/product/src/models/product-option.ts | 19 +--- packages/product/src/models/product-tag.ts | 27 +++-- packages/product/src/models/product-type.ts | 26 +++-- .../src/models/product-variant-option.ts | 10 +- .../product/src/models/product-variant.ts | 105 +++++++++++------- packages/product/src/models/product.ts | 64 ++++++----- 13 files changed, 257 insertions(+), 294 deletions(-) create mode 100644 .changeset/brave-beds-join.md rename packages/product/src/migrations/{InitialSetup20240325200756.ts => InitialSetup20240401153642.ts} (78%) diff --git a/.changeset/brave-beds-join.md b/.changeset/brave-beds-join.md new file mode 100644 index 0000000000..58679f41fa --- /dev/null +++ b/.changeset/brave-beds-join.md @@ -0,0 +1,5 @@ +--- +"@medusajs/product": patch +--- + +Correctly define all product module models and SQL migration script diff --git a/packages/product/src/migrations/.snapshot-medusa-products.json b/packages/product/src/migrations/.snapshot-medusa-products.json index e8ea283c36..a345393b0e 100644 --- a/packages/product/src/migrations/.snapshot-medusa-products.json +++ b/packages/product/src/migrations/.snapshot-medusa-products.json @@ -126,15 +126,6 @@ "primary": false, "unique": false }, - { - "keyName": "IDX_product_category_handle", - "columnNames": [ - "handle" - ], - "composite": false, - "primary": false, - "unique": true - }, { "keyName": "product_category_pkey", "columnNames": [ @@ -157,7 +148,7 @@ "id" ], "referencedTableName": "public.product_category", - "deleteRule": "set null", + "deleteRule": "cascade", "updateRule": "cascade" } } @@ -245,15 +236,6 @@ "primary": false, "unique": false }, - { - "keyName": "IDX_product_collection_handle_unique", - "columnNames": [ - "handle" - ], - "composite": false, - "primary": false, - "unique": true - }, { "keyName": "product_collection_pkey", "columnNames": [ @@ -790,15 +772,6 @@ "primary": false, "unique": false }, - { - "keyName": "IDX_product_handle_unique", - "columnNames": [ - "handle" - ], - "composite": false, - "primary": false, - "unique": true - }, { "keyName": "product_pkey", "columnNames": [ @@ -1503,42 +1476,6 @@ "primary": false, "unique": false }, - { - "keyName": "IDX_product_variant_sku_unique", - "columnNames": [ - "sku" - ], - "composite": false, - "primary": false, - "unique": true - }, - { - "keyName": "IDX_product_variant_barcode_unique", - "columnNames": [ - "barcode" - ], - "composite": false, - "primary": false, - "unique": true - }, - { - "keyName": "IDX_product_variant_ean_unique", - "columnNames": [ - "ean" - ], - "composite": false, - "primary": false, - "unique": true - }, - { - "keyName": "IDX_product_variant_upc_unique", - "columnNames": [ - "upc" - ], - "composite": false, - "primary": false, - "unique": true - }, { "keyName": "product_variant_pkey", "columnNames": [ @@ -1662,7 +1599,7 @@ "id" ], "referencedTableName": "public.product_option_value", - "deleteRule": "set null", + "deleteRule": "cascade", "updateRule": "cascade" }, "product_variant_option_variant_id_foreign": { @@ -1675,7 +1612,7 @@ "id" ], "referencedTableName": "public.product_variant", - "deleteRule": "set null", + "deleteRule": "cascade", "updateRule": "cascade" } } diff --git a/packages/product/src/migrations/InitialSetup20240325200756.ts b/packages/product/src/migrations/InitialSetup20240401153642.ts similarity index 78% rename from packages/product/src/migrations/InitialSetup20240325200756.ts rename to packages/product/src/migrations/InitialSetup20240401153642.ts index 1a732aee02..3991f2c9d2 100644 --- a/packages/product/src/migrations/InitialSetup20240325200756.ts +++ b/packages/product/src/migrations/InitialSetup20240401153642.ts @@ -2,7 +2,6 @@ import { Migration } from '@mikro-orm/migrations'; export class InitialSetup20240315083440 extends Migration { async up(): Promise { - // TODO: These migrations that get generated don't even reflect the models, write by hand. const productTables = await this.execute( "select * from information_schema.tables where table_name = 'product' and table_schema = 'public'" ) @@ -15,78 +14,88 @@ export class InitialSetup20240315083440 extends Migration { this.addSql( `alter table "product_variant" alter column "inventory_quantity" drop not null;` ) + this.addSql( + `alter table "product_category" add column "deleted_at" timestamptz null;` + ) } - this.addSql('create table if not exists "product_category" ("id" text not null, "name" text not null, "description" text not null default \'\', "handle" text not null, "mpath" text not null, "is_active" boolean not null default false, "is_internal" boolean not null default false, "rank" numeric not null default 0, "parent_category_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), constraint "product_category_pkey" primary key ("id"));'); - this.addSql('create index if not exists "IDX_product_category_path" on "product_category" ("mpath");'); - - // TODO: Re-enable when we run the migration from v1 - // this.addSql('alter table if exists "product_category" add constraint "IDX_product_category_handle" unique ("handle");'); - - this.addSql('create table if not exists "product_collection" ("id" text not null, "title" text not null, "handle" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_collection_pkey" primary key ("id"));'); - this.addSql('create index if not exists "IDX_product_collection_deleted_at" on "product_collection" ("deleted_at");'); - this.addSql('alter table if exists "product_collection" add constraint "IDX_product_collection_handle_unique" unique ("handle");'); - - this.addSql('create table if not exists "image" ("id" text not null, "url" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "image_pkey" primary key ("id"));'); - this.addSql('create index if not exists "IDX_product_image_url" on "image" ("url");'); - this.addSql('create index if not exists "IDX_product_image_deleted_at" on "image" ("deleted_at");'); - - this.addSql('create table if not exists "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));'); - this.addSql('create index if not exists "IDX_product_tag_deleted_at" on "product_tag" ("deleted_at");'); - - this.addSql('create table if not exists "product_type" ("id" text not null, "value" text not null, "metadata" json null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_type_pkey" primary key ("id"));'); - this.addSql('create index if not exists "IDX_product_type_deleted_at" on "product_type" ("deleted_at");'); - + /* --- ENTITY TABLES AND INDICES --- */ this.addSql('create table if not exists "product" ("id" text not null, "title" text not null, "handle" text not null, "subtitle" text null, "description" text null, "is_giftcard" boolean not null default false, "status" text check ("status" in (\'draft\', \'proposed\', \'published\', \'rejected\')) not null, "thumbnail" text null, "weight" text null, "length" text null, "height" text null, "width" text null, "origin_country" text null, "hs_code" text null, "mid_code" text null, "material" text null, "collection_id" text null, "type_id" text null, "discountable" boolean not null default true, "external_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "metadata" jsonb null, constraint "product_pkey" primary key ("id"));'); - this.addSql('create index if not exists "IDX_product_type_id" on "product" ("type_id");'); + this.addSql('create unique index if not exists "IDX_product_handle_unique" on "product" (handle) where deleted_at is null;') + this.addSql('create index if not exists "IDX_product_type_id" on "product" ("type_id") where deleted_at is null;'); + this.addSql('create index if not exists "IDX_product_collection_id" on "product" ("collection_id") where deleted_at is null;'); this.addSql('create index if not exists "IDX_product_deleted_at" on "product" ("deleted_at");'); - this.addSql('alter table if exists "product" add constraint "IDX_product_handle_unique" unique ("handle");'); + + this.addSql('create table if not exists "product_variant" ("id" text not null, "title" text not null, "sku" text null, "barcode" text null, "ean" text null, "upc" text null, "inventory_quantity" numeric not null default 100, "allow_backorder" boolean not null default false, "manage_inventory" boolean not null default true, "hs_code" text null, "origin_country" text null, "mid_code" text null, "material" text null, "weight" numeric null, "length" numeric null, "height" numeric null, "width" numeric null, "metadata" jsonb null, "variant_rank" numeric null default 0, "product_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_variant_pkey" primary key ("id"));'); + this.addSql('create unique index if not exists "IDX_product_variant_ean_unique" on "product_variant" (ean) where deleted_at is null;') + this.addSql('create unique index if not exists "IDX_product_variant_upc_unique" on "product_variant" (upc) where deleted_at is null;') + this.addSql('create unique index if not exists "IDX_product_variant_sku_unique" on "product_variant" (sku) where deleted_at is null;') + this.addSql('create unique index if not exists "IDX_product_variant_barcode_unique" on "product_variant" (barcode) where deleted_at is null;') + this.addSql('create index if not exists "IDX_product_variant_product_id" on "product_variant" ("product_id") where deleted_at is null;'); + this.addSql('create index if not exists "IDX_product_variant_deleted_at" on "product_variant" ("deleted_at");'); this.addSql('create table if not exists "product_option" ("id" text not null, "title" text not null, "product_id" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_option_pkey" primary key ("id"));'); + this.addSql('create unique index if not exists "IDX_option_product_id_title_unique" on "product_option" (product_id, title) where deleted_at is null;') this.addSql('create index if not exists "IDX_product_option_deleted_at" on "product_option" ("deleted_at");'); this.addSql('create table if not exists "product_option_value" ("id" text not null, "value" text not null, "option_id" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_option_value_pkey" primary key ("id"));'); - this.addSql('create index if not exists "IDX_product_option_value_option_id" on "product_option_value" ("option_id");'); + this.addSql('create unique index if not exists "IDX_option_value_option_id_unique" on "product_option_value" (option_id, value) where deleted_at is null;') this.addSql('create index if not exists "IDX_product_option_value_deleted_at" on "product_option_value" ("deleted_at");'); - this.addSql('create table if not exists "product_tags" ("product_id" text not null, "product_tag_id" text not null, constraint "product_tags_pkey" primary key ("product_id", "product_tag_id"));'); - - this.addSql('create table if not exists "product_images" ("product_id" text not null, "image_id" text not null, constraint "product_images_pkey" primary key ("product_id", "image_id"));'); - - this.addSql('create table if not exists "product_category_product" ("product_id" text not null, "product_category_id" text not null, constraint "product_category_product_pkey" primary key ("product_id", "product_category_id"));'); - - this.addSql('create table if not exists "product_variant" ("id" text not null, "title" text not null, "sku" text null, "barcode" text null, "ean" text null, "upc" text null, "inventory_quantity" numeric not null default 100, "allow_backorder" boolean not null default false, "manage_inventory" boolean not null default true, "hs_code" text null, "origin_country" text null, "mid_code" text null, "material" text null, "weight" numeric null, "length" numeric null, "height" numeric null, "width" numeric null, "metadata" jsonb null, "variant_rank" numeric null default 0, "product_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_variant_pkey" primary key ("id"));'); - this.addSql('create index if not exists "IDX_product_variant_product_id" on "product_variant" ("product_id");'); - this.addSql('create index if not exists "IDX_product_variant_deleted_at" on "product_variant" ("deleted_at");'); - this.addSql('alter table if exists "product_variant" add constraint "IDX_product_variant_sku_unique" unique ("sku");'); - this.addSql('alter table if exists "product_variant" add constraint "IDX_product_variant_barcode_unique" unique ("barcode");'); - this.addSql('alter table if exists "product_variant" add constraint "IDX_product_variant_ean_unique" unique ("ean");'); - this.addSql('alter table if exists "product_variant" add constraint "IDX_product_variant_upc_unique" unique ("upc");'); - this.addSql('create table if not exists "product_variant_option" ("id" text not null, "option_value_id" text null, "variant_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_variant_option_pkey" primary key ("id"));'); + this.addSql('create unique index if not exists "IDX_variant_option_option_value_unique" on "product_variant_option" (variant_id, option_value_id) where deleted_at is null;') this.addSql('create index if not exists "IDX_product_variant_option_deleted_at" on "product_variant_option" ("deleted_at");'); - this.addSql('alter table if exists "product_category" add constraint "product_category_parent_category_id_foreign" foreign key ("parent_category_id") references "product_category" ("id") on update cascade on delete set null;'); + this.addSql('create table if not exists "image" ("id" text not null, "url" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "image_pkey" primary key ("id"));'); + this.addSql('create index if not exists "IDX_product_image_url" on "image" ("url") where deleted_at is null;'); + this.addSql('create index if not exists "IDX_product_image_deleted_at" on "image" ("deleted_at");'); + this.addSql('create table if not exists "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));'); + // TODO: We need to modify upsertWithReplace to handle unique constraints + // this.addSql('create unique index if not exists "IDX_tag_value_unique" on "product_tag" (value) where deleted_at is null;') + this.addSql('create index if not exists "IDX_product_tag_deleted_at" on "product_tag" ("deleted_at");'); + + this.addSql('create table if not exists "product_type" ("id" text not null, "value" text not null, "metadata" json null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_type_pkey" primary key ("id"));'); + this.addSql('create unique index if not exists "IDX_type_value_unique" on "product_type" (value) where deleted_at is null;') + this.addSql('create index if not exists "IDX_product_type_deleted_at" on "product_type" ("deleted_at");'); + + this.addSql('create table if not exists "product_collection" ("id" text not null, "title" text not null, "handle" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_collection_pkey" primary key ("id"));'); + this.addSql('create unique index if not exists "IDX_collection_handle_unique" on "product_collection" (handle) where deleted_at is null;') + this.addSql('create index if not exists "IDX_product_collection_deleted_at" on "product_collection" ("deleted_at");'); + + this.addSql('create table if not exists "product_category" ("id" text not null, "name" text not null, "description" text not null default \'\', "handle" text not null, "mpath" text not null, "is_active" boolean not null default false, "is_internal" boolean not null default false, "rank" numeric not null default 0, "parent_category_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_category_pkey" primary key ("id"));'); + this.addSql('create unique index if not exists "IDX_category_handle_unique" on "product_category" (handle) where deleted_at is null;') + this.addSql('create index if not exists "IDX_product_category_path" on "product_category" ("mpath") where deleted_at is null;'); + this.addSql('create index if not exists "IDX_product_category_deleted_at" on "product_collection" ("deleted_at");'); + + /* --- PIVOT TABLES --- */ + this.addSql('create table if not exists "product_tags" ("product_id" text not null, "product_tag_id" text not null, constraint "product_tags_pkey" primary key ("product_id", "product_tag_id"));'); + this.addSql('create table if not exists "product_images" ("product_id" text not null, "image_id" text not null, constraint "product_images_pkey" primary key ("product_id", "image_id"));'); + this.addSql('create table if not exists "product_category_product" ("product_id" text not null, "product_category_id" text not null, constraint "product_category_product_pkey" primary key ("product_id", "product_category_id"));'); + + + /* --- FOREIGN KEYS AND CONSTRAINTS --- */ this.addSql('alter table if exists "product" add constraint "product_collection_id_foreign" foreign key ("collection_id") references "product_collection" ("id") on update cascade on delete set null;'); this.addSql('alter table if exists "product" add constraint "product_type_id_foreign" foreign key ("type_id") references "product_type" ("id") on update cascade on delete set null;'); + this.addSql('alter table if exists "product_variant" add constraint "product_variant_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); + this.addSql('alter table if exists "product_option" add constraint "product_option_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); this.addSql('alter table if exists "product_option_value" add constraint "product_option_value_option_id_foreign" foreign key ("option_id") references "product_option" ("id") on update cascade on delete cascade;'); - this.addSql('alter table if exists "product_tags" add constraint "product_tags_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); - this.addSql('alter table if exists "product_tags" add constraint "product_tags_product_tag_id_foreign" foreign key ("product_tag_id") references "product_tag" ("id") on update cascade on delete cascade;'); + this.addSql('alter table if exists "product_variant_option" add constraint "product_variant_option_option_value_id_foreign" foreign key ("option_value_id") references "product_option_value" ("id") on update cascade on delete cascade;'); + this.addSql('alter table if exists "product_variant_option" add constraint "product_variant_option_variant_id_foreign" foreign key ("variant_id") references "product_variant" ("id") on update cascade on delete cascade;'); this.addSql('alter table if exists "product_images" add constraint "product_images_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); this.addSql('alter table if exists "product_images" add constraint "product_images_image_id_foreign" foreign key ("image_id") references "image" ("id") on update cascade on delete cascade;'); + this.addSql('alter table if exists "product_tags" add constraint "product_tags_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); + this.addSql('alter table if exists "product_tags" add constraint "product_tags_product_tag_id_foreign" foreign key ("product_tag_id") references "product_tag" ("id") on update cascade on delete cascade;'); + this.addSql('alter table if exists "product_category_product" add constraint "product_category_product_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); this.addSql('alter table if exists "product_category_product" add constraint "product_category_product_product_category_id_foreign" foreign key ("product_category_id") references "product_category" ("id") on update cascade on delete cascade;'); - this.addSql('alter table if exists "product_variant" add constraint "product_variant_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); - - this.addSql('alter table if exists "product_variant_option" add constraint "product_variant_option_option_value_id_foreign" foreign key ("option_value_id") references "product_option_value" ("id") on update cascade on delete set null;'); - this.addSql('alter table if exists "product_variant_option" add constraint "product_variant_option_variant_id_foreign" foreign key ("variant_id") references "product_variant" ("id") on update cascade on delete set null;'); + this.addSql('alter table if exists "product_category" add constraint "product_category_parent_category_id_foreign" foreign key ("parent_category_id") references "product_category" ("id") on update cascade on delete cascade;'); } } diff --git a/packages/product/src/models/product-category.ts b/packages/product/src/models/product-category.ts index 3111be1090..c1be75ac5d 100644 --- a/packages/product/src/models/product-category.ts +++ b/packages/product/src/models/product-category.ts @@ -1,29 +1,49 @@ -import { generateEntityId, kebabCase } from "@medusajs/utils" +import { + DALUtils, + createPsqlIndexStatementHelper, + generateEntityId, + kebabCase, +} from "@medusajs/utils" import { BeforeCreate, Collection, Entity, EventArgs, + Filter, Index, ManyToMany, ManyToOne, OnInit, OneToMany, - OptionalProps, PrimaryKey, Property, - Unique, } from "@mikro-orm/core" -import { DAL } from "@medusajs/types" import Product from "./product" -type OptionalFields = DAL.SoftDeletableEntityDateColumns +const categoryHandleIndexName = "IDX_category_handle_unique" +const categoryHandleIndexStatement = createPsqlIndexStatementHelper({ + name: categoryHandleIndexName, + tableName: "product_category", + columns: ["handle"], + unique: true, + where: "deleted_at IS NULL", +}) +const categoryMpathIndexName = "IDX_product_category_path" +const categoryMpathIndexStatement = createPsqlIndexStatementHelper({ + name: categoryMpathIndexName, + tableName: "product_category", + columns: ["mpath"], + unique: false, + where: "deleted_at IS NULL", +}) + +categoryMpathIndexStatement.MikroORMIndex() +categoryHandleIndexStatement.MikroORMIndex() @Entity({ tableName: "product_category" }) +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class ProductCategory { - [OptionalProps]?: OptionalFields - @PrimaryKey({ columnType: "text" }) id!: string @@ -33,17 +53,9 @@ class ProductCategory { @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 @@ -61,6 +73,7 @@ class ProductCategory { fieldName: "parent_category_id", nullable: true, mapToPk: true, + onDelete: "cascade", }) parent_category_id?: string | null @@ -88,6 +101,10 @@ class ProductCategory { }) updated_at?: Date + @Index({ name: "IDX_product_category_deleted_at" }) + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at?: Date + @ManyToMany(() => Product, (product) => product.categories) products = new Collection(this) diff --git a/packages/product/src/models/product-collection.ts b/packages/product/src/models/product-collection.ts index 5622f6dc46..41f965ad2e 100644 --- a/packages/product/src/models/product-collection.ts +++ b/packages/product/src/models/product-collection.ts @@ -6,24 +6,31 @@ import { Index, OnInit, OneToMany, - OptionalProps, PrimaryKey, Property, - Unique, } from "@mikro-orm/core" -import { DAL } from "@medusajs/types" -import { DALUtils, generateEntityId, kebabCase } from "@medusajs/utils" +import { + DALUtils, + createPsqlIndexStatementHelper, + generateEntityId, + kebabCase, +} from "@medusajs/utils" import Product from "./product" -type OptionalRelations = "products" -type OptionalFields = DAL.SoftDeletableEntityDateColumns +const collectionHandleIndexName = "IDX_collection_handle_unique" +const collectionHandleIndexStatement = createPsqlIndexStatementHelper({ + name: collectionHandleIndexName, + tableName: "product_collection", + columns: ["handle"], + unique: true, + where: "deleted_at IS NULL", +}) +collectionHandleIndexStatement.MikroORMIndex() @Entity({ tableName: "product_collection" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class ProductCollection { - [OptionalProps]?: OptionalRelations | OptionalFields - @PrimaryKey({ columnType: "text" }) id!: string @@ -31,10 +38,6 @@ class ProductCollection { title: string @Property({ columnType: "text" }) - @Unique({ - name: "IDX_product_collection_handle_unique", - properties: ["handle"], - }) handle?: string @OneToMany(() => Product, (product) => product.collection) @@ -63,16 +66,8 @@ class ProductCollection { deleted_at?: Date @OnInit() - onInit() { - this.id = generateEntityId(this.id, "pcol") - - if (!this.handle && this.title) { - this.handle = kebabCase(this.title) - } - } - @BeforeCreate() - onCreate() { + onInit() { this.id = generateEntityId(this.id, "pcol") if (!this.handle && this.title) { diff --git a/packages/product/src/models/product-image.ts b/packages/product/src/models/product-image.ts index 643fc43480..048576c420 100644 --- a/packages/product/src/models/product-image.ts +++ b/packages/product/src/models/product-image.ts @@ -6,27 +6,33 @@ import { Index, ManyToMany, OnInit, - OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" -import { DAL } from "@medusajs/types" -import { DALUtils, generateEntityId } from "@medusajs/utils" +import { + DALUtils, + createPsqlIndexStatementHelper, + generateEntityId, +} from "@medusajs/utils" import Product from "./product" -type OptionalRelations = "products" -type OptionalFields = DAL.SoftDeletableEntityDateColumns +const imageUrlIndexName = "IDX_product_image_url" +const imageUrlIndexStatement = createPsqlIndexStatementHelper({ + name: imageUrlIndexName, + tableName: "image", + columns: ["url"], + unique: false, + where: "deleted_at IS NULL", +}) +imageUrlIndexStatement.MikroORMIndex() @Entity({ tableName: "image" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class ProductImage { - [OptionalProps]?: OptionalRelations | OptionalFields - @PrimaryKey({ columnType: "text" }) id!: string - @Index({ name: "IDX_product_image_url" }) @Property({ columnType: "text" }) url: string diff --git a/packages/product/src/models/product-option-value.ts b/packages/product/src/models/product-option-value.ts index a13166bc47..07de8f0633 100644 --- a/packages/product/src/models/product-option-value.ts +++ b/packages/product/src/models/product-option-value.ts @@ -1,4 +1,3 @@ -import { DAL } from "@medusajs/types" import { DALUtils, createPsqlIndexStatementHelper, @@ -13,18 +12,10 @@ import { ManyToOne, OnInit, OneToMany, - OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" -import { ProductOption, ProductVariant, ProductVariantOption } from "./index" - -type OptionalFields = - | "allow_backorder" - | "manage_inventory" - | "option_id" - | DAL.SoftDeletableEntityDateColumns -type OptionalRelations = "product" | "option" | "variant" +import { ProductOption, ProductVariantOption } from "./index" const optionValueOptionIdIndexName = "IDX_option_value_option_id_unique" const optionValueOptionIdIndexStatement = createPsqlIndexStatementHelper({ @@ -39,8 +30,6 @@ optionValueOptionIdIndexStatement.MikroORMIndex() @Entity({ tableName: "product_option_value" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class ProductOptionValue { - [OptionalProps]?: OptionalFields | OptionalRelations - @PrimaryKey({ columnType: "text" }) id!: string @@ -52,7 +41,6 @@ class ProductOptionValue { fieldName: "option_id", mapToPk: true, nullable: true, - index: "IDX_product_option_value_option_id", onDelete: "cascade", }) option_id: string | null @@ -89,13 +77,8 @@ class ProductOptionValue { deleted_at?: Date @OnInit() - onInit() { - this.id = generateEntityId(this.id, "optval") - this.option_id ??= this.option?.id ?? null - } - @BeforeCreate() - beforeCreate() { + onInit() { this.id = generateEntityId(this.id, "optval") this.option_id ??= this.option?.id ?? null } diff --git a/packages/product/src/models/product-option.ts b/packages/product/src/models/product-option.ts index d3f027dd1e..5cf5fc4779 100644 --- a/packages/product/src/models/product-option.ts +++ b/packages/product/src/models/product-option.ts @@ -1,4 +1,3 @@ -import { DAL } from "@medusajs/types" import { DALUtils, createPsqlIndexStatementHelper, @@ -14,19 +13,12 @@ import { ManyToOne, OnInit, OneToMany, - OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" import { Product } from "./index" import ProductOptionValue from "./product-option-value" -type OptionalRelations = - | "values" - | "product" - | DAL.SoftDeletableEntityDateColumns -type OptionalFields = "product_id" - const optionProductIdTitleIndexName = "IDX_option_product_id_title_unique" const optionProductIdTitleIndexStatement = createPsqlIndexStatementHelper({ name: optionProductIdTitleIndexName, @@ -40,8 +32,6 @@ optionProductIdTitleIndexStatement.MikroORMIndex() @Entity({ tableName: "product_option" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class ProductOption { - [OptionalProps]?: OptionalRelations | OptionalFields - @PrimaryKey({ columnType: "text" }) id!: string @@ -64,7 +54,7 @@ class ProductOption { product: Product | null @OneToMany(() => ProductOptionValue, (value) => value.option, { - cascade: [Cascade.PERSIST, Cascade.REMOVE, "soft-remove" as any], + cascade: [Cascade.PERSIST, "soft-remove" as any], }) values = new Collection(this) @@ -91,13 +81,8 @@ class ProductOption { deleted_at?: Date @OnInit() - onInit() { - this.id = generateEntityId(this.id, "opt") - this.product_id ??= this.product?.id ?? null - } - @BeforeCreate() - beforeCreate() { + onInit() { this.id = generateEntityId(this.id, "opt") this.product_id ??= this.product?.id ?? null } diff --git a/packages/product/src/models/product-tag.ts b/packages/product/src/models/product-tag.ts index d58cf128e0..ccaadf3cbf 100644 --- a/packages/product/src/models/product-tag.ts +++ b/packages/product/src/models/product-tag.ts @@ -6,23 +6,30 @@ import { Index, ManyToMany, OnInit, - OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" -import { DAL } from "@medusajs/types" -import { DALUtils, generateEntityId } from "@medusajs/utils" +import { + DALUtils, + createPsqlIndexStatementHelper, + generateEntityId, +} from "@medusajs/utils" import Product from "./product" -type OptionalRelations = "products" -type OptionalFields = DAL.SoftDeletableEntityDateColumns +const tagValueIndexName = "IDX_tag_value_unique" +const tagValueIndexStatement = createPsqlIndexStatementHelper({ + name: tagValueIndexName, + tableName: "product_tag", + columns: ["value"], + unique: true, + where: "deleted_at IS NULL", +}) +tagValueIndexStatement.MikroORMIndex() @Entity({ tableName: "product_tag" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class ProductTag { - [OptionalProps]?: OptionalRelations | OptionalFields - @PrimaryKey({ columnType: "text" }) id!: string @@ -55,12 +62,8 @@ class ProductTag { products = new Collection(this) @OnInit() - onInit() { - this.id = generateEntityId(this.id, "ptag") - } - @BeforeCreate() - onCreate() { + onInit() { this.id = generateEntityId(this.id, "ptag") } } diff --git a/packages/product/src/models/product-type.ts b/packages/product/src/models/product-type.ts index 2c7cd34c18..f9f941abbc 100644 --- a/packages/product/src/models/product-type.ts +++ b/packages/product/src/models/product-type.ts @@ -4,21 +4,29 @@ import { Filter, Index, OnInit, - OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" -import { DAL } from "@medusajs/types" -import { DALUtils, generateEntityId } from "@medusajs/utils" +import { + DALUtils, + createPsqlIndexStatementHelper, + generateEntityId, +} from "@medusajs/utils" -type OptionalFields = DAL.SoftDeletableEntityDateColumns +const typeValueIndexName = "IDX_type_value_unique" +const typeValueIndexStatement = createPsqlIndexStatementHelper({ + name: typeValueIndexName, + tableName: "product_type", + columns: ["value"], + unique: true, + where: "deleted_at IS NULL", +}) +typeValueIndexStatement.MikroORMIndex() @Entity({ tableName: "product_type" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class ProductType { - [OptionalProps]?: OptionalFields - @PrimaryKey({ columnType: "text" }) id!: string @@ -48,12 +56,8 @@ class ProductType { deleted_at?: Date @OnInit() - onInit() { - this.id = generateEntityId(this.id, "ptyp") - } - @BeforeCreate() - onCreate() { + onInit() { this.id = generateEntityId(this.id, "ptyp") } } diff --git a/packages/product/src/models/product-variant-option.ts b/packages/product/src/models/product-variant-option.ts index 3b6594034e..a94d0c39ae 100644 --- a/packages/product/src/models/product-variant-option.ts +++ b/packages/product/src/models/product-variant-option.ts @@ -36,6 +36,7 @@ class ProductVariantOption { nullable: true, fieldName: "option_value_id", mapToPk: true, + onDelete: "cascade", }) option_value_id!: string @@ -50,6 +51,7 @@ class ProductVariantOption { nullable: true, fieldName: "variant_id", mapToPk: true, + onDelete: "cascade", }) variant_id: string | null @@ -79,14 +81,8 @@ class ProductVariantOption { deleted_at?: Date @OnInit() - onInit() { - this.id = generateEntityId(this.id, "varopt") - this.variant_id ??= this.variant?.id ?? null - this.option_value_id ??= this.option_value?.id ?? null - } - @BeforeCreate() - beforeCreate() { + onInit() { this.id = generateEntityId(this.id, "varopt") this.variant_id ??= this.variant?.id ?? null this.option_value_id ??= this.option_value?.id ?? null diff --git a/packages/product/src/models/product-variant.ts b/packages/product/src/models/product-variant.ts index 2fa1d042d1..a1464bafd3 100644 --- a/packages/product/src/models/product-variant.ts +++ b/packages/product/src/models/product-variant.ts @@ -1,6 +1,6 @@ -import { DAL } from "@medusajs/types" import { DALUtils, + createPsqlIndexStatementHelper, generateEntityId, optionalNumericSerializer, } from "@medusajs/utils" @@ -14,26 +14,65 @@ import { ManyToOne, OnInit, OneToMany, - OptionalProps, PrimaryKey, Property, - Unique, } from "@mikro-orm/core" import { Product } from "@models" import ProductVariantOption from "./product-variant-option" -type OptionalFields = - | "allow_backorder" - | "manage_inventory" - | "product" - | "product_id" - | DAL.SoftDeletableEntityDateColumns +const variantSkuIndexName = "IDX_product_variant_sku_unique" +const variantSkuIndexStatement = createPsqlIndexStatementHelper({ + name: variantSkuIndexName, + tableName: "product_variant", + columns: ["sku"], + unique: true, + where: "deleted_at IS NULL", +}) +const variantBarcodeIndexName = "IDX_product_variant_barcode_unique" +const variantBarcodeIndexStatement = createPsqlIndexStatementHelper({ + name: variantBarcodeIndexName, + tableName: "product_variant", + columns: ["barcode"], + unique: true, + where: "deleted_at IS NULL", +}) + +const variantEanIndexName = "IDX_product_variant_ean_unique" +const variantEanIndexStatement = createPsqlIndexStatementHelper({ + name: variantEanIndexName, + tableName: "product_variant", + columns: ["ean"], + unique: true, + where: "deleted_at IS NULL", +}) + +const variantUpcIndexName = "IDX_product_variant_upc_unique" +const variantUpcIndexStatement = createPsqlIndexStatementHelper({ + name: variantUpcIndexName, + tableName: "product_variant", + columns: ["upc"], + unique: true, + where: "deleted_at IS NULL", +}) + +const variantProductIdIndexName = "IDX_product_variant_product_id" +const variantProductIdIndexStatement = createPsqlIndexStatementHelper({ + name: variantProductIdIndexName, + tableName: "product_variant", + columns: ["product_id"], + unique: false, + where: "deleted_at IS NULL", +}) + +variantProductIdIndexStatement.MikroORMIndex() +variantSkuIndexStatement.MikroORMIndex() +variantBarcodeIndexStatement.MikroORMIndex() +variantEanIndexStatement.MikroORMIndex() +variantUpcIndexStatement.MikroORMIndex() @Entity({ tableName: "product_variant" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class ProductVariant { - [OptionalProps]?: OptionalFields - @PrimaryKey({ columnType: "text" }) id!: string @@ -41,33 +80,18 @@ class ProductVariant { 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 + // TODO: replace with BigNumber // 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 @@ -111,6 +135,7 @@ class ProductVariant { @Property({ columnType: "jsonb", nullable: true }) metadata?: Record | null + // TODO: replace with BigNumber, or in this case a normal int should work @Property({ columnType: "numeric", nullable: true, @@ -124,7 +149,6 @@ class ProductVariant { nullable: true, onDelete: "cascade", fieldName: "product_id", - index: "IDX_product_variant_product_id", mapToPk: true, }) product_id: string | null @@ -135,6 +159,15 @@ class ProductVariant { }) product: Product | null + @OneToMany( + () => ProductVariantOption, + (variantOption) => variantOption.variant, + { + cascade: [Cascade.PERSIST, "soft-remove" as any], + } + ) + options = new Collection(this) + @Property({ onCreate: () => new Date(), columnType: "timestamptz", @@ -154,23 +187,9 @@ class ProductVariant { @Property({ columnType: "timestamptz", nullable: true }) deleted_at?: Date - @OneToMany( - () => ProductVariantOption, - (variantOption) => variantOption.variant, - { - cascade: [Cascade.PERSIST, Cascade.REMOVE, "soft-remove" as any], - } - ) - options = new Collection(this) - @OnInit() - onInit() { - this.id = generateEntityId(this.id, "variant") - this.product_id ??= this.product?.id ?? null - } - @BeforeCreate() - onCreate() { + onInit() { this.id = generateEntityId(this.id, "variant") this.product_id ??= this.product?.id ?? null } diff --git a/packages/product/src/models/product.ts b/packages/product/src/models/product.ts index 5a8caebc62..6afe4a36e6 100644 --- a/packages/product/src/models/product.ts +++ b/packages/product/src/models/product.ts @@ -9,14 +9,12 @@ import { ManyToOne, OneToMany, OnInit, - OptionalProps, PrimaryKey, Property, - Unique, } from "@mikro-orm/core" -import { DAL } from "@medusajs/types" import { + createPsqlIndexStatementHelper, DALUtils, generateEntityId, kebabCase, @@ -30,19 +28,39 @@ import ProductTag from "./product-tag" import ProductType from "./product-type" import ProductVariant from "./product-variant" -type OptionalRelations = "collection" | "type" -type OptionalFields = - | "collection_id" - | "type_id" - | "is_giftcard" - | "discountable" - | DAL.SoftDeletableEntityDateColumns +const productHandleIndexName = "IDX_product_handle_unique" +const productHandleIndexStatement = createPsqlIndexStatementHelper({ + name: productHandleIndexName, + tableName: "product", + columns: ["handle"], + unique: true, + where: "deleted_at IS NULL", +}) +const productTypeIndexName = "IDX_product_type_id" +const productTypeIndexStatement = createPsqlIndexStatementHelper({ + name: productTypeIndexName, + tableName: "product", + columns: ["type_id"], + unique: false, + where: "deleted_at IS NULL", +}) + +const productCollectionIndexName = "IDX_product_collection_id" +const productCollectionIndexStatement = createPsqlIndexStatementHelper({ + name: productCollectionIndexName, + tableName: "product", + columns: ["collection_id"], + unique: false, + where: "deleted_at IS NULL", +}) + +productTypeIndexStatement.MikroORMIndex() +productCollectionIndexStatement.MikroORMIndex() +productHandleIndexStatement.MikroORMIndex() @Entity({ tableName: "product" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class Product { - [OptionalProps]?: OptionalRelations | OptionalFields - @PrimaryKey({ columnType: "text" }) id!: string @@ -50,11 +68,7 @@ class Product { title: string @Property({ columnType: "text" }) - @Unique({ - name: "IDX_product_handle_unique", - properties: ["handle"], - }) - handle?: string | null + handle?: string @Property({ columnType: "text", nullable: true }) subtitle?: string | null @@ -111,6 +125,7 @@ class Product { nullable: true, fieldName: "collection_id", mapToPk: true, + onDelete: "set null", }) collection_id: string | null @@ -124,8 +139,8 @@ class Product { columnType: "text", nullable: true, fieldName: "type_id", - index: "IDX_product_type_id", mapToPk: true, + onDelete: "set null", }) type_id: string | null @@ -145,7 +160,6 @@ class Product { @ManyToMany(() => ProductImage, "products", { owner: true, pivotTable: "product_images", - index: "IDX_product_image_id", joinColumn: "product_id", inverseJoinColumn: "image_id", }) @@ -186,18 +200,8 @@ class Product { metadata?: Record | null @OnInit() - onInit() { - this.id = generateEntityId(this.id, "prod") - this.type_id ??= this.type?.id ?? null - this.collection_id ??= this.collection?.id ?? null - - if (!this.handle && this.title) { - this.handle = kebabCase(this.title) - } - } - @BeforeCreate() - beforeCreate() { + onInit() { this.id = generateEntityId(this.id, "prod") this.type_id ??= this.type?.id ?? null this.collection_id ??= this.collection?.id ?? null