chore(): Reorganize modules (#7210)
**What** Move all modules to the modules directory
This commit is contained in:
committed by
GitHub
parent
7a351eef09
commit
4eae25e1ef
14
packages/modules/product/src/index.ts
Normal file
14
packages/modules/product/src/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
moduleDefinition,
|
||||
revertMigration,
|
||||
runMigrations,
|
||||
} from "./module-definition"
|
||||
|
||||
export default moduleDefinition
|
||||
export { revertMigration, runMigrations }
|
||||
|
||||
export * from "./initialize"
|
||||
// TODO: remove export from models and services
|
||||
export * from "./models"
|
||||
export * from "./services"
|
||||
export * from "./types"
|
||||
34
packages/modules/product/src/initialize/index.ts
Normal file
34
packages/modules/product/src/initialize/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
InternalModuleDeclaration,
|
||||
MedusaModule,
|
||||
MODULE_PACKAGE_NAMES,
|
||||
Modules,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService, ModulesSdkTypes } from "@medusajs/types"
|
||||
|
||||
import { InitializeModuleInjectableDependencies } from "@types"
|
||||
import { moduleDefinition } from "../module-definition"
|
||||
|
||||
export const initialize = async (
|
||||
options?:
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
| ExternalModuleDeclaration
|
||||
| InternalModuleDeclaration,
|
||||
injectedDependencies?: InitializeModuleInjectableDependencies
|
||||
): Promise<IProductModuleService> => {
|
||||
const serviceKey = Modules.PRODUCT
|
||||
|
||||
const loaded = await MedusaModule.bootstrap<IProductModuleService>({
|
||||
moduleKey: serviceKey,
|
||||
defaultPath: MODULE_PACKAGE_NAMES[Modules.PRODUCT],
|
||||
declaration: options as
|
||||
| InternalModuleDeclaration
|
||||
| ExternalModuleDeclaration,
|
||||
injectedDependencies,
|
||||
moduleExports: moduleDefinition,
|
||||
})
|
||||
|
||||
return loaded[serviceKey]
|
||||
}
|
||||
99
packages/modules/product/src/joiner-config.ts
Normal file
99
packages/modules/product/src/joiner-config.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { MapToConfig } from "@medusajs/utils"
|
||||
import {
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductCollection,
|
||||
ProductOption,
|
||||
ProductTag,
|
||||
ProductType,
|
||||
ProductVariant,
|
||||
} from "@models"
|
||||
import ProductImage from "./models/product-image"
|
||||
|
||||
export const LinkableKeys = {
|
||||
product_id: Product.name,
|
||||
product_handle: Product.name,
|
||||
variant_id: ProductVariant.name,
|
||||
variant_sku: ProductVariant.name,
|
||||
product_option_id: ProductOption.name,
|
||||
product_type_id: ProductType.name,
|
||||
product_category_id: ProductCategory.name,
|
||||
product_collection_id: ProductCollection.name,
|
||||
product_tag_id: ProductTag.name,
|
||||
product_image_id: ProductImage.name,
|
||||
}
|
||||
|
||||
const entityLinkableKeysMap: MapToConfig = {}
|
||||
Object.entries(LinkableKeys).forEach(([key, value]) => {
|
||||
entityLinkableKeysMap[value] ??= []
|
||||
entityLinkableKeysMap[value].push({
|
||||
mapTo: key,
|
||||
valueFrom: key.split("_").pop()!,
|
||||
})
|
||||
})
|
||||
export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap
|
||||
|
||||
export const joinerConfig: ModuleJoinerConfig = {
|
||||
serviceName: Modules.PRODUCT,
|
||||
primaryKeys: ["id", "handle"],
|
||||
linkableKeys: LinkableKeys,
|
||||
alias: [
|
||||
{
|
||||
name: ["product", "products"],
|
||||
args: {
|
||||
entity: "Product",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_variant", "product_variants", "variant", "variants"],
|
||||
args: {
|
||||
entity: "ProductVariant",
|
||||
methodSuffix: "Variants",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_option", "product_options"],
|
||||
args: {
|
||||
entity: "ProductOption",
|
||||
methodSuffix: "Options",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_type", "product_types"],
|
||||
args: {
|
||||
entity: "ProductType",
|
||||
methodSuffix: "Types",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_image", "product_images"],
|
||||
args: {
|
||||
entity: "ProductImage",
|
||||
methodSuffix: "Images",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_tag", "product_tags"],
|
||||
args: {
|
||||
entity: "ProductTag",
|
||||
methodSuffix: "Tags",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_collection", "product_collections"],
|
||||
args: {
|
||||
entity: "ProductCollection",
|
||||
methodSuffix: "Collections",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["product_category", "product_categories"],
|
||||
args: {
|
||||
entity: "ProductCategory",
|
||||
methodSuffix: "Categories",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
30
packages/modules/product/src/loaders/connection.ts
Normal file
30
packages/modules/product/src/loaders/connection.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { InternalModuleDeclaration, LoaderOptions } from "@medusajs/modules-sdk"
|
||||
import { ModulesSdkTypes } from "@medusajs/types"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
import * as ProductModels from "../models"
|
||||
|
||||
export default async (
|
||||
{
|
||||
options,
|
||||
container,
|
||||
logger,
|
||||
}: LoaderOptions<
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>,
|
||||
moduleDeclaration?: InternalModuleDeclaration
|
||||
): Promise<void> => {
|
||||
const entities = Object.values(ProductModels) as unknown as EntitySchema[]
|
||||
const pathToMigrations = __dirname + "/../migrations"
|
||||
|
||||
await ModulesSdkUtils.mikroOrmConnectionLoader({
|
||||
moduleName: "product",
|
||||
entities,
|
||||
container,
|
||||
options,
|
||||
moduleDeclaration,
|
||||
logger,
|
||||
pathToMigrations,
|
||||
})
|
||||
}
|
||||
10
packages/modules/product/src/loaders/container.ts
Normal file
10
packages/modules/product/src/loaders/container.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import * as ModuleModels from "@models"
|
||||
import * as ModuleRepositories from "@repositories"
|
||||
import * as ModuleServices from "@services"
|
||||
|
||||
export default ModulesSdkUtils.moduleContainerLoaderFactory({
|
||||
moduleModels: ModuleModels,
|
||||
moduleRepositories: ModuleRepositories,
|
||||
moduleServices: ModuleServices,
|
||||
})
|
||||
2
packages/modules/product/src/loaders/index.ts
Normal file
2
packages/modules/product/src/loaders/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./connection"
|
||||
export * from "./container"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class InitialSetup20240315083440 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
const productTables = await this.execute(
|
||||
"select * from information_schema.tables where table_name = 'product' and table_schema = 'public'"
|
||||
)
|
||||
|
||||
if (productTables.length > 0) {
|
||||
// This is so we can still run the api tests, remove completely once that is not needed
|
||||
this.addSql(
|
||||
`alter table "product_option_value" alter column "variant_id" drop not null;`
|
||||
)
|
||||
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;`
|
||||
)
|
||||
}
|
||||
|
||||
/* --- 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 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('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 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 "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"));');
|
||||
|
||||
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");');
|
||||
// TODO: Batch updating composite unique index in MikroORM is faulty. Should be added back when issue has been resolved.
|
||||
this.addSql(`drop index if exists "UniqProductCategoryParentIdRank";`)
|
||||
|
||||
/* --- 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"));');
|
||||
this.addSql('create table if not exists "product_variant_option" ("variant_id" text not null, "option_value_id" text not null, constraint "product_variant_option_pkey" primary key ("variant_id", "option_value_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_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_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_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_category" add constraint "product_category_parent_category_id_foreign" foreign key ("parent_category_id") references "product_category" ("id") on update cascade on delete cascade;');
|
||||
}
|
||||
}
|
||||
9
packages/modules/product/src/models/index.ts
Normal file
9
packages/modules/product/src/models/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
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"
|
||||
export { default as ProductOption } from "./product-option"
|
||||
export { default as ProductOptionValue } from "./product-option-value"
|
||||
export { default as Image } from "./product-image"
|
||||
148
packages/modules/product/src/models/product-category.ts
Normal file
148
packages/modules/product/src/models/product-category.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import {
|
||||
DALUtils,
|
||||
Searchable,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
kebabCase,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
EventArgs,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
OneToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import Product from "./product"
|
||||
|
||||
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 {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: false })
|
||||
name?: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", default: "", nullable: false })
|
||||
description?: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: false })
|
||||
handle?: string
|
||||
|
||||
@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
|
||||
|
||||
@ManyToOne(() => ProductCategory, {
|
||||
columnType: "text",
|
||||
fieldName: "parent_category_id",
|
||||
nullable: true,
|
||||
mapToPk: true,
|
||||
onDelete: "cascade",
|
||||
})
|
||||
parent_category_id?: string | null
|
||||
|
||||
@ManyToOne(() => ProductCategory, { nullable: true, persist: false })
|
||||
parent_category?: ProductCategory
|
||||
|
||||
@OneToMany({
|
||||
entity: () => ProductCategory,
|
||||
mappedBy: (productCategory) => productCategory.parent_category,
|
||||
})
|
||||
category_children = new Collection<ProductCategory>(this)
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at?: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
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<Product>(this)
|
||||
|
||||
@OnInit()
|
||||
async onInit() {
|
||||
this.id = generateEntityId(this.id, "pcat")
|
||||
this.parent_category_id ??= this.parent_category?.id ?? null
|
||||
}
|
||||
|
||||
@BeforeCreate()
|
||||
async onCreate(args: EventArgs<ProductCategory>) {
|
||||
this.id = generateEntityId(this.id, "pcat")
|
||||
this.parent_category_id ??= this.parent_category?.id ?? null
|
||||
|
||||
if (!this.handle && this.name) {
|
||||
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}.`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductCategory
|
||||
81
packages/modules/product/src/models/product-collection.ts
Normal file
81
packages/modules/product/src/models/product-collection.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
OneToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
createPsqlIndexStatementHelper,
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
kebabCase,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
|
||||
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 {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
handle?: string
|
||||
|
||||
@OneToMany(() => Product, (product) => product.collection)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_collection_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "pcol")
|
||||
|
||||
if (!this.handle && this.title) {
|
||||
this.handle = kebabCase(this.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductCollection
|
||||
75
packages/modules/product/src/models/product-image.ts
Normal file
75
packages/modules/product/src/models/product-image.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
DALUtils,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
|
||||
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 {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
url: string
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_image_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.images)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@OnInit()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "img")
|
||||
}
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "img")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductImage
|
||||
87
packages/modules/product/src/models/product-option-value.ts
Normal file
87
packages/modules/product/src/models/product-option-value.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
DALUtils,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { ProductOption, ProductVariant } from "./index"
|
||||
|
||||
const optionValueOptionIdIndexName = "IDX_option_value_option_id_unique"
|
||||
const optionValueOptionIdIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: optionValueOptionIdIndexName,
|
||||
tableName: "product_option_value",
|
||||
columns: ["option_id", "value"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
optionValueOptionIdIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: "product_option_value" })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
class ProductOptionValue {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
value: string
|
||||
|
||||
@ManyToOne(() => ProductOption, {
|
||||
columnType: "text",
|
||||
fieldName: "option_id",
|
||||
mapToPk: true,
|
||||
nullable: true,
|
||||
onDelete: "cascade",
|
||||
})
|
||||
option_id: string | null
|
||||
|
||||
@ManyToOne(() => ProductOption, {
|
||||
nullable: true,
|
||||
persist: false,
|
||||
})
|
||||
option: ProductOption | null
|
||||
|
||||
@ManyToMany(() => ProductVariant, (variant) => variant.options)
|
||||
variants = new Collection<ProductVariant>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_option_value_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "optval")
|
||||
this.option_id ??= this.option?.id ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductOptionValue
|
||||
93
packages/modules/product/src/models/product-option.ts
Normal file
93
packages/modules/product/src/models/product-option.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
DALUtils,
|
||||
Searchable,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
OneToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { Product } from "./index"
|
||||
import ProductOptionValue from "./product-option-value"
|
||||
|
||||
const optionProductIdTitleIndexName = "IDX_option_product_id_title_unique"
|
||||
const optionProductIdTitleIndexStatement = createPsqlIndexStatementHelper({
|
||||
name: optionProductIdTitleIndexName,
|
||||
tableName: "product_option",
|
||||
columns: ["product_id", "title"],
|
||||
unique: true,
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
|
||||
optionProductIdTitleIndexStatement.MikroORMIndex()
|
||||
@Entity({ tableName: "product_option" })
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
class ProductOption {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
columnType: "text",
|
||||
fieldName: "product_id",
|
||||
mapToPk: true,
|
||||
nullable: true,
|
||||
onDelete: "cascade",
|
||||
})
|
||||
product_id: string | null
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
persist: false,
|
||||
nullable: true,
|
||||
})
|
||||
product: Product | null
|
||||
|
||||
@OneToMany(() => ProductOptionValue, (value) => value.option, {
|
||||
cascade: [Cascade.PERSIST, "soft-remove" as any],
|
||||
})
|
||||
values = new Collection<ProductOptionValue>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_option_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "opt")
|
||||
this.product_id ??= this.product?.id ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductOption
|
||||
73
packages/modules/product/src/models/product-tag.ts
Normal file
73
packages/modules/product/src/models/product-tag.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
DALUtils,
|
||||
Searchable,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
|
||||
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 {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
value: string
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_tag_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.tags)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "ptag")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductTag
|
||||
67
packages/modules/product/src/models/product-type.ts
Normal file
67
packages/modules/product/src/models/product-type.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
DALUtils,
|
||||
Searchable,
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
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 {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
value: string
|
||||
|
||||
@Property({ columnType: "json", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_type_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "ptyp")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductType
|
||||
203
packages/modules/product/src/models/product-variant.ts
Normal file
203
packages/modules/product/src/models/product-variant.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import {
|
||||
createPsqlIndexStatementHelper,
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
optionalNumericSerializer,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { Product, ProductOptionValue } from "@models"
|
||||
|
||||
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 {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
sku?: string | null
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
barcode?: string | null
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
ean?: string | null
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
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
|
||||
@Property({
|
||||
columnType: "numeric",
|
||||
default: 100,
|
||||
serializer: optionalNumericSerializer,
|
||||
})
|
||||
inventory_quantity?: number = 100
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
allow_backorder?: boolean = false
|
||||
|
||||
@Property({ columnType: "boolean", default: true })
|
||||
manage_inventory?: boolean = true
|
||||
|
||||
@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
|
||||
|
||||
// TODO: replace with BigNumber, or in this case a normal int should work
|
||||
@Property({
|
||||
columnType: "numeric",
|
||||
nullable: true,
|
||||
default: 0,
|
||||
serializer: optionalNumericSerializer,
|
||||
})
|
||||
variant_rank?: number | null
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
columnType: "text",
|
||||
nullable: true,
|
||||
onDelete: "cascade",
|
||||
fieldName: "product_id",
|
||||
mapToPk: true,
|
||||
})
|
||||
product_id: string | null
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
persist: false,
|
||||
nullable: true,
|
||||
})
|
||||
product: Product | null
|
||||
|
||||
@ManyToMany(() => ProductOptionValue, "variants", {
|
||||
owner: true,
|
||||
pivotTable: "product_variant_option",
|
||||
joinColumn: "variant_id",
|
||||
inverseJoinColumn: "option_value_id",
|
||||
})
|
||||
options = new Collection<ProductOptionValue>(this)
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_variant_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "variant")
|
||||
this.product_id ??= this.product?.id ?? null
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductVariant
|
||||
224
packages/modules/product/src/models/product.ts
Normal file
224
packages/modules/product/src/models/product.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Enum,
|
||||
Filter,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
createPsqlIndexStatementHelper,
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
kebabCase,
|
||||
ProductUtils,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
import ProductCategory from "./product-category"
|
||||
import ProductCollection from "./product-collection"
|
||||
import ProductImage from "./product-image"
|
||||
import ProductOption from "./product-option"
|
||||
import ProductTag from "./product-tag"
|
||||
import ProductType from "./product-type"
|
||||
import ProductVariant from "./product-variant"
|
||||
|
||||
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 {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
handle?: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
subtitle?: string | null
|
||||
|
||||
@Searchable()
|
||||
@Property({
|
||||
columnType: "text",
|
||||
nullable: true,
|
||||
})
|
||||
description?: string | null
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
is_giftcard!: boolean
|
||||
|
||||
@Enum(() => ProductUtils.ProductStatus)
|
||||
@Property({ default: ProductUtils.ProductStatus.DRAFT })
|
||||
status!: ProductUtils.ProductStatus
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
thumbnail?: string | null
|
||||
|
||||
@OneToMany(() => ProductOption, (o) => o.product, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
options = new Collection<ProductOption>(this)
|
||||
|
||||
@Searchable()
|
||||
@OneToMany(() => ProductVariant, (variant) => variant.product, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
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
|
||||
|
||||
@Searchable()
|
||||
@ManyToOne(() => ProductCollection, {
|
||||
columnType: "text",
|
||||
nullable: true,
|
||||
fieldName: "collection_id",
|
||||
mapToPk: true,
|
||||
onDelete: "set null",
|
||||
})
|
||||
collection_id: string | null
|
||||
|
||||
@ManyToOne(() => ProductCollection, {
|
||||
nullable: true,
|
||||
persist: false,
|
||||
})
|
||||
collection: ProductCollection | null
|
||||
|
||||
@ManyToOne(() => ProductType, {
|
||||
columnType: "text",
|
||||
nullable: true,
|
||||
fieldName: "type_id",
|
||||
mapToPk: true,
|
||||
onDelete: "set null",
|
||||
})
|
||||
type_id: string | null
|
||||
|
||||
@ManyToOne(() => ProductType, {
|
||||
nullable: true,
|
||||
persist: false,
|
||||
})
|
||||
type: ProductType | null
|
||||
|
||||
@ManyToMany(() => ProductTag, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_tags",
|
||||
index: "IDX_product_tag_id",
|
||||
})
|
||||
tags = new Collection<ProductTag>(this)
|
||||
|
||||
@ManyToMany(() => ProductImage, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_images",
|
||||
joinColumn: "product_id",
|
||||
inverseJoinColumn: "image_id",
|
||||
})
|
||||
images = new Collection<ProductImage>(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",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
defaultRaw: "now()",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Product
|
||||
31
packages/modules/product/src/module-definition.ts
Normal file
31
packages/modules/product/src/module-definition.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ModuleExports } from "@medusajs/types"
|
||||
import { ProductModuleService } from "@services"
|
||||
import loadConnection from "./loaders/connection"
|
||||
import loadContainer from "./loaders/container"
|
||||
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import * as ProductModels from "@models"
|
||||
|
||||
const migrationScriptOptions = {
|
||||
moduleName: Modules.PRODUCT,
|
||||
models: ProductModels,
|
||||
pathToMigrations: __dirname + "/migrations",
|
||||
}
|
||||
|
||||
export const runMigrations = ModulesSdkUtils.buildMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
export const revertMigration = ModulesSdkUtils.buildRevertMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
|
||||
const service = ProductModuleService
|
||||
const loaders = [loadContainer, loadConnection] as any
|
||||
|
||||
export const moduleDefinition: ModuleExports = {
|
||||
service,
|
||||
loaders,
|
||||
runMigrations,
|
||||
revertMigration,
|
||||
}
|
||||
3
packages/modules/product/src/repositories/index.ts
Normal file
3
packages/modules/product/src/repositories/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
|
||||
export { ProductRepository } from "./product"
|
||||
export { ProductCategoryRepository } from "./product-category"
|
||||
471
packages/modules/product/src/repositories/product-category.ts
Normal file
471
packages/modules/product/src/repositories/product-category.ts
Normal file
@@ -0,0 +1,471 @@
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
ProductCategoryTransformOptions,
|
||||
ProductTypes,
|
||||
} from "@medusajs/types"
|
||||
import { DALUtils, MedusaError, isDefined } from "@medusajs/utils"
|
||||
import {
|
||||
LoadStrategy,
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
} 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
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeRepository<ProductCategory> {
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
||||
transformOptions: ProductCategoryTransformOptions = {},
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory[]> {
|
||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
const { includeDescendantsTree, includeAncestorsTree } = transformOptions
|
||||
findOptions_.options ??= {}
|
||||
const fields = (findOptions_.options.fields ??= [])
|
||||
|
||||
// Ref: Building descendants
|
||||
// mpath and parent_category_id needs to be added to the query for the tree building to be done accurately
|
||||
if (includeDescendantsTree || includeAncestorsTree) {
|
||||
fields.indexOf("mpath") === -1 && fields.push("mpath")
|
||||
fields.indexOf("parent_category_id") === -1 &&
|
||||
fields.push("parent_category_id")
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
const productCategories = await manager.find(
|
||||
ProductCategory,
|
||||
findOptions_.where as MikroFilterQuery<ProductCategory>,
|
||||
findOptions_.options as MikroOptions<ProductCategory>
|
||||
)
|
||||
|
||||
if (!includeDescendantsTree && !includeAncestorsTree) {
|
||||
return productCategories
|
||||
}
|
||||
|
||||
return this.buildProductCategoriesWithTree(
|
||||
{
|
||||
descendants: includeDescendantsTree,
|
||||
ancestors: includeAncestorsTree,
|
||||
},
|
||||
productCategories,
|
||||
findOptions_
|
||||
)
|
||||
}
|
||||
|
||||
async buildProductCategoriesWithTree(
|
||||
include: {
|
||||
descendants?: boolean
|
||||
ancestors?: boolean
|
||||
},
|
||||
productCategories: ProductCategory[],
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory[]> {
|
||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
const hasPopulateParentCategory = (
|
||||
findOptions.options?.populate ?? ([] as any)
|
||||
).find((pop) => pop.field === "parent_category")
|
||||
|
||||
include.ancestors = include.ancestors || hasPopulateParentCategory
|
||||
|
||||
const mpaths: any[] = []
|
||||
const parentMpaths = new Set()
|
||||
for (const cat of productCategories) {
|
||||
if (include.descendants) {
|
||||
mpaths.push({ mpath: { $like: `${cat.mpath}%` } })
|
||||
}
|
||||
|
||||
if (include.ancestors) {
|
||||
let parent = ""
|
||||
cat.mpath?.split(".").forEach((mpath) => {
|
||||
if (mpath === "") {
|
||||
return
|
||||
}
|
||||
parentMpaths.add(parent + mpath + ".")
|
||||
parent += mpath + "."
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mpaths.push({ mpath: Array.from(parentMpaths) })
|
||||
|
||||
const whereOptions = {
|
||||
...findOptions.where,
|
||||
$or: mpaths,
|
||||
}
|
||||
|
||||
if ("parent_category_id" in whereOptions) {
|
||||
delete whereOptions.parent_category_id
|
||||
}
|
||||
|
||||
if ("id" in whereOptions) {
|
||||
delete whereOptions.id
|
||||
}
|
||||
|
||||
let allCategories = await manager.find(
|
||||
ProductCategory,
|
||||
whereOptions as MikroFilterQuery<ProductCategory>,
|
||||
findOptions.options as MikroOptions<ProductCategory>
|
||||
)
|
||||
|
||||
allCategories = JSON.parse(JSON.stringify(allCategories))
|
||||
|
||||
const categoriesById = new Map(allCategories.map((cat) => [cat.id, cat]))
|
||||
|
||||
allCategories.forEach((cat: any) => {
|
||||
if (cat.parent_category_id) {
|
||||
cat.parent_category = categoriesById.get(cat.parent_category_id)
|
||||
}
|
||||
})
|
||||
|
||||
const populateChildren = (category, level = 0) => {
|
||||
const categories = allCategories.filter(
|
||||
(child) => child.parent_category_id === category.id
|
||||
)
|
||||
|
||||
if (include.descendants) {
|
||||
category.category_children = categories.map((child) => {
|
||||
return populateChildren(categoriesById.get(child.id), level + 1)
|
||||
})
|
||||
}
|
||||
|
||||
if (level === 0) {
|
||||
return category
|
||||
}
|
||||
|
||||
if (include.ancestors) {
|
||||
delete category.category_children
|
||||
}
|
||||
if (include.descendants) {
|
||||
delete category.parent_category
|
||||
}
|
||||
|
||||
return category
|
||||
}
|
||||
|
||||
const populatedProductCategories = productCategories.map((cat) => {
|
||||
const fullCategory = categoriesById.get(cat.id)
|
||||
return populateChildren(fullCategory)
|
||||
})
|
||||
|
||||
return populatedProductCategories
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
||||
transformOptions: ProductCategoryTransformOptions = {},
|
||||
context: Context = {}
|
||||
): Promise<[ProductCategory[], number]> {
|
||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
const { includeDescendantsTree, includeAncestorsTree } = transformOptions
|
||||
findOptions_.options ??= {}
|
||||
const fields = (findOptions_.options.fields ??= [])
|
||||
|
||||
// Ref: Building descendants
|
||||
// mpath and parent_category_id needs to be added to the query for the tree building to be done accurately
|
||||
if (includeDescendantsTree) {
|
||||
fields.indexOf("mpath") === -1 && fields.push("mpath")
|
||||
fields.indexOf("parent_category_id") === -1 &&
|
||||
fields.push("parent_category_id")
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
const [productCategories, count] = await manager.findAndCount(
|
||||
ProductCategory,
|
||||
findOptions_.where as MikroFilterQuery<ProductCategory>,
|
||||
findOptions_.options as MikroOptions<ProductCategory>
|
||||
)
|
||||
if (!includeDescendantsTree) {
|
||||
return [productCategories, count]
|
||||
}
|
||||
|
||||
if (!includeDescendantsTree && !includeAncestorsTree) {
|
||||
return [productCategories, count]
|
||||
}
|
||||
|
||||
return [
|
||||
await this.buildProductCategoriesWithTree(
|
||||
{
|
||||
descendants: includeDescendantsTree,
|
||||
ancestors: includeAncestorsTree,
|
||||
},
|
||||
productCategories,
|
||||
findOptions_
|
||||
),
|
||||
count,
|
||||
]
|
||||
}
|
||||
|
||||
async delete(id: string, context: Context = {}): Promise<void> {
|
||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||
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`
|
||||
)
|
||||
}
|
||||
|
||||
const conditions = this.fetchReorderConditions(
|
||||
productCategory,
|
||||
{
|
||||
parent_category_id: productCategory.parent_category_id,
|
||||
rank: productCategory.rank,
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
await this.performReordering(manager, conditions)
|
||||
await manager.nativeDelete(ProductCategory, { id: id }, {})
|
||||
}
|
||||
|
||||
async create(
|
||||
data: ProductTypes.CreateProductCategoryDTO,
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory> {
|
||||
const categoryData = { ...data }
|
||||
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 productCategory = manager.create(ProductCategory, categoryData)
|
||||
|
||||
manager.persist(productCategory)
|
||||
|
||||
return productCategory
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
data: ProductTypes.UpdateProductCategoryDTO,
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory> {
|
||||
const categoryData = { ...data }
|
||||
const manager = super.getActiveManager<SqlEntityManager>(context)
|
||||
const productCategory = await manager.findOneOrFail(ProductCategory, { id })
|
||||
|
||||
const conditions = this.fetchReorderConditions(
|
||||
productCategory,
|
||||
categoryData
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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(
|
||||
manager: SqlEntityManager,
|
||||
conditions: ReorderConditions
|
||||
): Promise<void> {
|
||||
const { shouldChangeParent, shouldChangeRank, shouldDeleteElement } =
|
||||
conditions
|
||||
|
||||
if (!(shouldChangeParent || shouldChangeRank || shouldDeleteElement)) {
|
||||
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 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,
|
||||
}))
|
||||
|
||||
// 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 }
|
||||
} else {
|
||||
shouldIncrementRank = false
|
||||
rankCondition = { $gte: originalRank, $lte: targetRank }
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
57
packages/modules/product/src/repositories/product.ts
Normal file
57
packages/modules/product/src/repositories/product.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Product } from "@models"
|
||||
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { DALUtils } from "@medusajs/utils"
|
||||
|
||||
import { UpdateProductInput } from "../types"
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Product>(
|
||||
Product
|
||||
) {
|
||||
constructor(...args: any[]) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
/**
|
||||
* In order to be able to have a strict not in categories, and prevent a product
|
||||
* to be return in the case it also belongs to other categories, we need to
|
||||
* first find all products that are in the categories, and then exclude them
|
||||
*/
|
||||
protected async mutateNotInCategoriesConstraints(
|
||||
findOptions: DAL.FindOptions<Product> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<void> {
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
if (
|
||||
"categories" in findOptions.where &&
|
||||
findOptions.where.categories?.id?.["$nin"]
|
||||
) {
|
||||
const productsInCategories = await manager.find(
|
||||
Product,
|
||||
{
|
||||
categories: {
|
||||
id: { $in: findOptions.where.categories.id["$nin"] },
|
||||
},
|
||||
},
|
||||
{
|
||||
fields: ["id"],
|
||||
}
|
||||
)
|
||||
|
||||
const productIds = productsInCategories.map((product) => product.id)
|
||||
|
||||
if (productIds.length) {
|
||||
findOptions.where.id = { $nin: productIds }
|
||||
delete findOptions.where.categories?.id
|
||||
|
||||
if (Object.keys(findOptions.where.categories).length === 0) {
|
||||
delete findOptions.where.categories
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
packages/modules/product/src/scripts/bin/run-seed.ts
Normal file
37
packages/modules/product/src/scripts/bin/run-seed.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import * as ProductModels from "@models"
|
||||
import {
|
||||
createProductCategories,
|
||||
createProducts,
|
||||
createProductVariants,
|
||||
} from "../seed-utils"
|
||||
import { EOL } from "os"
|
||||
|
||||
const args = process.argv
|
||||
const path = args.pop() as string
|
||||
|
||||
export default (async () => {
|
||||
const { config } = await import("dotenv")
|
||||
config()
|
||||
if (!path) {
|
||||
throw new Error(
|
||||
`filePath is required.${EOL}Example: medusa-product-seed <filePath>`
|
||||
)
|
||||
}
|
||||
|
||||
const run = ModulesSdkUtils.buildSeedScript({
|
||||
moduleName: Modules.PRODUCT,
|
||||
models: ProductModels,
|
||||
pathToMigrations: __dirname + "/../../migrations",
|
||||
seedHandler: async ({ manager, data }) => {
|
||||
const { productCategoriesData, productsData, variantsData } = data
|
||||
await createProductCategories(manager, productCategoriesData)
|
||||
await createProducts(manager, productsData)
|
||||
await createProductVariants(manager, variantsData)
|
||||
},
|
||||
})
|
||||
await run({ path })
|
||||
})()
|
||||
54
packages/modules/product/src/scripts/seed-utils.ts
Normal file
54
packages/modules/product/src/scripts/seed-utils.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { Product, ProductCategory, ProductVariant } 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
|
||||
}
|
||||
|
||||
export async function createProducts(manager: SqlEntityManager, data: any[]) {
|
||||
const products: any[] = data.map((productData) => {
|
||||
return manager.create(Product, productData)
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(products)
|
||||
|
||||
return products
|
||||
}
|
||||
|
||||
export async function createProductVariants(
|
||||
manager: SqlEntityManager,
|
||||
data: any[]
|
||||
) {
|
||||
const variants: any[] = data.map((variantsData) => {
|
||||
return manager.create(ProductVariant, variantsData)
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(variants)
|
||||
|
||||
return variants
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { asValue } from "awilix"
|
||||
|
||||
export const nonExistingProductId = "non-existing-id"
|
||||
|
||||
export const productRepositoryMock = {
|
||||
productRepository: asValue({
|
||||
find: jest.fn().mockImplementation(async ({ where: { id } }) => {
|
||||
if (id === nonExistingProductId) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{}]
|
||||
}),
|
||||
findAndCount: jest.fn().mockResolvedValue([[], 0]),
|
||||
getFreshManager: jest.fn().mockResolvedValue({}),
|
||||
}),
|
||||
}
|
||||
227
packages/modules/product/src/services/__tests__/product.spec.ts
Normal file
227
packages/modules/product/src/services/__tests__/product.spec.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import {
|
||||
nonExistingProductId,
|
||||
productRepositoryMock,
|
||||
} from "../__fixtures__/product"
|
||||
import { createMedusaContainer } from "@medusajs/utils"
|
||||
import { asValue } from "awilix"
|
||||
import ContainerLoader from "../../loaders/container"
|
||||
|
||||
describe("Product service", function () {
|
||||
let container
|
||||
|
||||
beforeEach(async function () {
|
||||
jest.clearAllMocks()
|
||||
|
||||
container = createMedusaContainer()
|
||||
container.register("manager", asValue({}))
|
||||
|
||||
await ContainerLoader({ container })
|
||||
|
||||
container.register(productRepositoryMock)
|
||||
})
|
||||
|
||||
it("should retrieve a product", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
const productId = "existing-product"
|
||||
|
||||
await productService.retrieve(productId)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
id: productId,
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: undefined,
|
||||
offset: 0,
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to retrieve a product", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
|
||||
const err = await productService
|
||||
.retrieve(nonExistingProductId)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
id: nonExistingProductId,
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: undefined,
|
||||
offset: 0,
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
expect(err.message).toBe(
|
||||
`Product with id: ${nonExistingProductId} was not found`
|
||||
)
|
||||
})
|
||||
|
||||
it("should list products", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
|
||||
const filters = {}
|
||||
const config = {
|
||||
relations: [],
|
||||
}
|
||||
|
||||
await productService.list(filters, config)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: 0,
|
||||
orderBy: {
|
||||
id: "ASC",
|
||||
},
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it("should list products with filters", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
|
||||
const filters = {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
}
|
||||
const config = {
|
||||
relations: [],
|
||||
}
|
||||
|
||||
await productService.list(filters, config)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: 0,
|
||||
orderBy: {
|
||||
id: "ASC",
|
||||
},
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it("should list products with filters and relations", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
|
||||
const filters = {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
}
|
||||
const config = {
|
||||
relations: ["tags"],
|
||||
}
|
||||
|
||||
await productService.list(filters, config)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: 0,
|
||||
orderBy: {
|
||||
id: "ASC",
|
||||
},
|
||||
withDeleted: undefined,
|
||||
populate: ["tags"],
|
||||
},
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it("should list and count the products with filters and relations", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
|
||||
const filters = {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
}
|
||||
const config = {
|
||||
relations: ["tags"],
|
||||
}
|
||||
|
||||
await productService.listAndCount(filters, config)
|
||||
|
||||
expect(productRepository.findAndCount).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: 0,
|
||||
orderBy: {
|
||||
id: "ASC",
|
||||
},
|
||||
withDeleted: undefined,
|
||||
populate: ["tags"],
|
||||
},
|
||||
},
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
})
|
||||
3
packages/modules/product/src/services/index.ts
Normal file
3
packages/modules/product/src/services/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as ProductService } from "./product"
|
||||
export { default as ProductCategoryService } from "./product-category"
|
||||
export { default as ProductModuleService } from "./product-module-service"
|
||||
169
packages/modules/product/src/services/product-category.ts
Normal file
169
packages/modules/product/src/services/product-category.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||
import {
|
||||
FreeTextSearchFilterKey,
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
isDefined,
|
||||
} from "@medusajs/utils"
|
||||
import { ProductCategory } from "@models"
|
||||
import { ProductCategoryRepository } from "@repositories"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productCategoryRepository: DAL.TreeRepositoryService
|
||||
}
|
||||
|
||||
export default class ProductCategoryService<
|
||||
TEntity extends ProductCategory = ProductCategory
|
||||
> {
|
||||
protected readonly productCategoryRepository_: DAL.TreeRepositoryService
|
||||
|
||||
constructor({ productCategoryRepository }: InjectedDependencies) {
|
||||
this.productCategoryRepository_ = productCategoryRepository
|
||||
}
|
||||
|
||||
@InjectManager("productCategoryRepository_")
|
||||
async retrieve(
|
||||
productCategoryId: string,
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity> {
|
||||
if (!isDefined(productCategoryId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`"productCategoryId" must be defined`
|
||||
)
|
||||
}
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||
{
|
||||
id: productCategoryId,
|
||||
},
|
||||
config
|
||||
)
|
||||
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: true,
|
||||
}
|
||||
|
||||
const productCategories = await this.productCategoryRepository_.find(
|
||||
queryOptions,
|
||||
transformOptions,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (!productCategories?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`ProductCategory with id: ${productCategoryId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return productCategories[0] as TEntity
|
||||
}
|
||||
|
||||
@InjectManager("productCategoryRepository_")
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductCategoryProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: filters?.include_descendants_tree || false,
|
||||
includeAncestorsTree: filters?.include_ancestors_tree || false,
|
||||
}
|
||||
delete filters.include_descendants_tree
|
||||
delete filters.include_ancestors_tree
|
||||
|
||||
// Apply free text search filter
|
||||
if (filters?.q) {
|
||||
config.filters ??= {}
|
||||
config.filters[FreeTextSearchFilterKey] = {
|
||||
value: filters.q,
|
||||
fromEntity: ProductCategory.name,
|
||||
}
|
||||
|
||||
delete filters.q
|
||||
}
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
queryOptions.where ??= {}
|
||||
|
||||
return (await this.productCategoryRepository_.find(
|
||||
queryOptions,
|
||||
transformOptions,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectManager("productCategoryRepository_")
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductCategoryProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], number]> {
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: filters?.include_descendants_tree || false,
|
||||
includeAncestorsTree: filters?.include_ancestors_tree || false,
|
||||
}
|
||||
delete filters.include_descendants_tree
|
||||
delete filters.include_ancestors_tree
|
||||
|
||||
// Apply free text search filter
|
||||
if (filters?.q) {
|
||||
config.filters ??= {}
|
||||
config.filters[FreeTextSearchFilterKey] = {
|
||||
value: filters.q,
|
||||
fromEntity: ProductCategory.name,
|
||||
}
|
||||
|
||||
delete filters.q
|
||||
}
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
queryOptions.where ??= {}
|
||||
|
||||
return (await this.productCategoryRepository_.findAndCount(
|
||||
queryOptions,
|
||||
transformOptions,
|
||||
sharedContext
|
||||
)) as [TEntity[], number]
|
||||
}
|
||||
|
||||
@InjectTransactionManager("productCategoryRepository_")
|
||||
async create(
|
||||
data: ProductTypes.CreateProductCategoryDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity> {
|
||||
return (await (
|
||||
this.productCategoryRepository_ as unknown as ProductCategoryRepository
|
||||
).create(data, sharedContext)) as TEntity
|
||||
}
|
||||
|
||||
@InjectTransactionManager("productCategoryRepository_")
|
||||
async update(
|
||||
id: string,
|
||||
data: ProductTypes.UpdateProductCategoryDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity> {
|
||||
return (await (
|
||||
this.productCategoryRepository_ as unknown as ProductCategoryRepository
|
||||
).update(id, data, sharedContext)) as TEntity
|
||||
}
|
||||
|
||||
@InjectTransactionManager("productCategoryRepository_")
|
||||
async delete(
|
||||
id: string,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
await this.productCategoryRepository_.delete(id, sharedContext)
|
||||
}
|
||||
}
|
||||
1463
packages/modules/product/src/services/product-module-service.ts
Normal file
1463
packages/modules/product/src/services/product-module-service.ts
Normal file
File diff suppressed because it is too large
Load Diff
82
packages/modules/product/src/services/product.ts
Normal file
82
packages/modules/product/src/services/product.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
FindConfig,
|
||||
ProductTypes,
|
||||
BaseFilterable,
|
||||
FilterableProductProps,
|
||||
} from "@medusajs/types"
|
||||
import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { Product } from "@models"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
type NormalizedFilterableProductProps = ProductTypes.FilterableProductProps & {
|
||||
categories?: {
|
||||
id: string | { $in: string[] }
|
||||
}
|
||||
}
|
||||
|
||||
export default class ProductService<
|
||||
TEntity extends Product = Product
|
||||
> extends ModulesSdkUtils.internalModuleServiceFactory<InjectedDependencies>(
|
||||
Product
|
||||
)<TEntity> {
|
||||
protected readonly productRepository_: DAL.RepositoryService<TEntity>
|
||||
|
||||
constructor({ productRepository }: InjectedDependencies) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(...arguments)
|
||||
|
||||
this.productRepository_ = productRepository
|
||||
}
|
||||
|
||||
@InjectManager("productRepository_")
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductProps = {},
|
||||
config: FindConfig<TEntity> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return await super.list(
|
||||
ProductService.normalizeFilters(filters),
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
@InjectManager("productRepository_")
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductProps = {},
|
||||
config: FindConfig<any> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], number]> {
|
||||
return await super.listAndCount(
|
||||
ProductService.normalizeFilters(filters),
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
protected static normalizeFilters(
|
||||
filters: FilterableProductProps = {}
|
||||
): NormalizedFilterableProductProps {
|
||||
const normalized = filters as NormalizedFilterableProductProps
|
||||
if (normalized.category_id) {
|
||||
if (Array.isArray(normalized.category_id)) {
|
||||
normalized.categories = {
|
||||
id: { $in: normalized.category_id },
|
||||
}
|
||||
} else {
|
||||
normalized.categories = {
|
||||
id: normalized.category_id as string,
|
||||
}
|
||||
}
|
||||
delete normalized.category_id
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
67
packages/modules/product/src/types/index.ts
Normal file
67
packages/modules/product/src/types/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { IEventBusModuleService, Logger, ProductTypes } from "@medusajs/types"
|
||||
|
||||
export type InitializeModuleInjectableDependencies = {
|
||||
logger?: Logger
|
||||
eventBusModuleService?: IEventBusModuleService
|
||||
}
|
||||
|
||||
export type ProductCategoryEventData = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export enum ProductCategoryEvents {
|
||||
CATEGORY_UPDATED = "product-category.updated",
|
||||
CATEGORY_CREATED = "product-category.created",
|
||||
CATEGORY_DELETED = "product-category.deleted",
|
||||
}
|
||||
|
||||
export type ProductEventData = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export enum ProductEvents {
|
||||
PRODUCT_UPDATED = "product.updated",
|
||||
PRODUCT_CREATED = "product.created",
|
||||
PRODUCT_DELETED = "product.deleted",
|
||||
}
|
||||
|
||||
export type ProductCollectionEventData = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export enum ProductCollectionEvents {
|
||||
COLLECTION_UPDATED = "product-collection.updated",
|
||||
COLLECTION_CREATED = "product-collection.created",
|
||||
COLLECTION_DELETED = "product-collection.deleted",
|
||||
}
|
||||
|
||||
export type UpdateProductInput = ProductTypes.UpdateProductDTO & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type UpdateProductCollection =
|
||||
ProductTypes.UpdateProductCollectionDTO & {
|
||||
products?: string[]
|
||||
}
|
||||
|
||||
export type CreateProductCollection =
|
||||
ProductTypes.CreateProductCollectionDTO & {
|
||||
products?: string[]
|
||||
}
|
||||
|
||||
export type UpdateCollectionInput = ProductTypes.UpdateProductCollectionDTO & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type UpdateTypeInput = ProductTypes.UpdateProductTypeDTO & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type UpdateProductVariantInput = ProductTypes.UpdateProductVariantDTO & {
|
||||
id: string
|
||||
product_id?: string | null
|
||||
}
|
||||
|
||||
export type UpdateProductOptionInput = ProductTypes.UpdateProductOptionDTO & {
|
||||
id: string
|
||||
}
|
||||
7
packages/modules/product/src/utils/index.ts
Normal file
7
packages/modules/product/src/utils/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function shouldForceTransaction(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
export function doNotForceTransaction(): boolean {
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user