From 9f204817b04a188365d2abbaf0aa3c31ff3ceb8c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 26 Nov 2024 20:01:02 +0530 Subject: [PATCH] feat: convert MikroORM entities to DML entities (#10043) * feat: convert MikroORM entities to DML entities * feat: wip on repository changes * continue repositories and types rework * fix order repository usage * continue to update product category repository * Add foreign key as part of the inferred DML type * ../../core/types/src/dml/index.ts * ../../core/types/src/dml/index.ts * fix: relationships mapping * handle nullable foreign keys types * handle nullable foreign keys types * handle nullable foreign keys types * continue to update product category repository * fix all product category repositories issues * fix product category service types * fix product module service types * fix product module service types * fix repository template type * refactor: use a singleton DMLToMikroORM factory instance Since the MikroORM MetadataStorage is global, we will also have to turn DML to MikroORM entities conversion use a global bucket as well * refactor: update product module to use DML in tests * wip: tests * WIP product linkable fixes * continue type fixing and start test fixing * test: fix more tests * fix repository * fix pivot table computaion + fix mikro orm repository * fix many to many management and configuration * fix many to many management and configuration * fix many to many management and configuration * update product tag relation configuration * Introduce experimental dml hooks to fix some issues with categories * more fixes * fix product tests * add missing id prefixes * fix product category handle management * test: fix more failing tests * test: make it all green * test: fix breaking tests * fix: build issues * fix: build issues * fix: more breaking tests * refactor: fix issues after merge * refactor: fix issues after merge * refactor: surpress types error * test: fix DML failing tests * improve many to many inference + tests * Wip fix columns from product entity * remove product model before create hook and manage handle validation and transformation at the service level * test: fix breaking unit tests * fix: product module service to not update handle on product update * fix define link and joiner config * test: fix joiner config test * test: fix joiner config test * fix joiner config primary keys * Fix joiner config builder * Fix joiner config builder * test: remove only modifier from test * refactor: remove hooks usage from product collection * refactor: remove hooks usage from product-option * refactor: remove hooks usage for computing category handle * refactor: remove hooks usage from productCategory model * refactor: remove hooks from DML * refactor: remove cruft * cleanup * re add foerign key indexes * chore: remove unused types * refactor: cleanup * migration and models configuration adjustments * cleanup * fix random ordering * fix * test: fix product-category tests * test: update breaking DML tests * test: array assertion to not care about ordering * fix: temporarily apply id ordering for products * fix ordering * fix ordering remove logs --------- Co-authored-by: adrien2p Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --- .../__tests__/product/admin/product.spec.ts | 9 +- .../__tests__/product/store/product.spec.ts | 7 +- packages/core/types/src/dal/index.ts | 10 +- .../core/types/src/dal/repository-service.ts | 67 +- packages/core/types/src/dal/utils.ts | 40 +- packages/core/types/src/dml/index.ts | 49 +- packages/core/utils/package.json | 2 +- .../__tests__/mikro-orm-repository.spec.ts | 6 +- .../src/dal/mikro-orm/mikro-orm-repository.ts | 218 +++--- .../src/dml/__tests__/entity-builder.spec.ts | 667 ++++++++++++------ packages/core/utils/src/dml/entity-builder.ts | 8 + .../utils/src/dml/helpers/create-graphql.ts | 2 +- .../dml/helpers/create-mikro-orm-entity.ts | 53 +- .../helpers/entity-builder/define-property.ts | 39 +- .../entity-builder/define-relationship.ts | 225 ++++-- .../dml/helpers/mikro-orm/apply-indexes.ts | 30 +- .../integration-tests/__tests__/enum.spec.ts | 6 +- .../__tests__/has-one-belongs-to.spec.ts | 6 +- .../__tests__/many-to-many.spec.ts | 97 ++- .../__tests__/many-to-one.spec.ts | 6 +- .../__tests__/migrations-generate.spec.ts | 7 +- .../__tests__/joiner-config-builder.spec.ts | 2 +- .../core/utils/src/modules-sdk/build-query.ts | 18 +- .../core/utils/src/modules-sdk/define-link.ts | 1 + .../src/modules-sdk/joiner-config-builder.ts | 102 ++- .../modules-sdk/medusa-internal-service.ts | 4 +- packages/core/utils/src/modules-sdk/module.ts | 11 +- .../modules/order/src/repositories/claim.ts | 2 +- .../order/src/repositories/exchange.ts | 2 +- .../modules/order/src/repositories/order.ts | 2 +- .../modules/order/src/repositories/return.ts | 2 +- .../order/src/utils/base-repository-find.ts | 2 +- .../product-category/data/index.ts | 23 +- .../__fixtures__/product/data/categories.ts | 3 + .../product/data/create-product.ts | 16 +- .../__fixtures__/product/data/products.ts | 3 + .../__fixtures__/product/index.ts | 28 +- .../__tests__/product-category.ts | 37 +- .../product-categories.spec.ts | 7 +- .../product-collections.spec.ts | 9 +- .../product-options.spec.ts | 18 +- .../product-tags.spec.ts | 11 +- .../product-types.spec.ts | 6 +- .../product-module-service/products.spec.ts | 19 +- .../integration-tests/__tests__/product.ts | 108 +-- packages/modules/product/src/joiner-config.ts | 2 - .../migrations/.snapshot-medusa-product.json | 432 ++++++++---- .../src/migrations/Migration20241125090957.ts | 173 +++++ .../product/src/models/product-category.ts | 173 +---- .../product/src/models/product-collection.ts | 94 +-- .../product/src/models/product-image.ts | 106 +-- .../src/models/product-option-value.ts | 104 +-- .../product/src/models/product-option.ts | 108 +-- .../modules/product/src/models/product-tag.ts | 90 +-- .../product/src/models/product-type.ts | 81 +-- .../product/src/models/product-variant.ts | 246 ++----- .../modules/product/src/models/product.ts | 290 ++------ .../src/repositories/product-category.ts | 201 ++++-- .../product/src/repositories/product.ts | 10 +- .../product/src/services/product-category.ts | 41 +- .../src/services/product-module-service.ts | 113 +-- 61 files changed, 2315 insertions(+), 1939 deletions(-) create mode 100644 packages/modules/product/src/migrations/Migration20241125090957.ts diff --git a/integration-tests/http/__tests__/product/admin/product.spec.ts b/integration-tests/http/__tests__/product/admin/product.spec.ts index e7e90014a4..4a09504938 100644 --- a/integration-tests/http/__tests__/product/admin/product.spec.ts +++ b/integration-tests/http/__tests__/product/admin/product.spec.ts @@ -74,7 +74,8 @@ medusaIntegrationTestRunner({ // BREAKING: Type input changed from {type: {value: string}} to {type_id: string} type_id: baseType.id, tags: [{ id: baseTag1.id }, { id: baseTag2.id }], - images: [{ + images: [ + { url: "image-one", }, { @@ -139,7 +140,6 @@ medusaIntegrationTestRunner({ ]) ) }) - it("returns a list of products with all statuses when no status or invalid status is provided", async () => { const res = await api @@ -991,7 +991,10 @@ medusaIntegrationTestRunner({ }) it("should get a product with images ordered by rank", async () => { - const res = await api.get(`/admin/products/${baseProduct.id}`, adminHeaders) + const res = await api.get( + `/admin/products/${baseProduct.id}`, + adminHeaders + ) expect(res.data.product.images).toEqual( expect.arrayContaining([ diff --git a/integration-tests/http/__tests__/product/store/product.spec.ts b/integration-tests/http/__tests__/product/store/product.spec.ts index 4cbd997319..f4f449b53c 100644 --- a/integration-tests/http/__tests__/product/store/product.spec.ts +++ b/integration-tests/http/__tests__/product/store/product.spec.ts @@ -1436,7 +1436,7 @@ medusaIntegrationTestRunner({ }, { url: "image-two", - } + }, ], }) @@ -1487,7 +1487,10 @@ medusaIntegrationTestRunner({ }) it("should retrieve product with images ordered by rank", async () => { - const response = await api.get(`/store/products/${product.id}`, storeHeaders) + const response = await api.get( + `/store/products/${product.id}`, + storeHeaders + ) expect(response.data.product.images).toEqual( expect.arrayContaining([ diff --git a/packages/core/types/src/dal/index.ts b/packages/core/types/src/dal/index.ts index aafe8fab74..ab99c2b130 100644 --- a/packages/core/types/src/dal/index.ts +++ b/packages/core/types/src/dal/index.ts @@ -1,3 +1,4 @@ +import { InferEntityType } from "../dml" import { Dictionary, FilterQuery, Order } from "./utils" export { FilterQuery, OperatorMap } from "./utils" @@ -22,7 +23,7 @@ export interface BaseFilterable { /** * The options to apply when retrieving an item. */ -export interface OptionsQuery { +export interface OptionsQuery { /** * Relations to populate in the retrieved items. */ @@ -54,7 +55,7 @@ export interface OptionsQuery { /** * Load strategy (e.g for mikro orm it accept select-in or joined) */ - strategy?: 'select-in' | 'joined' | string & {} + strategy?: "select-in" | "joined" | (string & {}) } /** @@ -66,12 +67,13 @@ export type FindOptions = { /** * The filters to apply on the items. */ - where: FilterQuery & BaseFilterable> + where: FilterQuery> & + BaseFilterable>> /** * The options to apply when retrieving the items. */ - options?: OptionsQuery + options?: OptionsQuery> } /** diff --git a/packages/core/types/src/dal/repository-service.ts b/packages/core/types/src/dal/repository-service.ts index 598c919d4f..6aae45c990 100644 --- a/packages/core/types/src/dal/repository-service.ts +++ b/packages/core/types/src/dal/repository-service.ts @@ -1,16 +1,23 @@ import { RepositoryTransformOptions } from "../common" import { Context } from "../shared-context" import { - BaseFilterable, - FilterQuery, FilterQuery as InternalFilterQuery, FindOptions, UpsertWithReplaceConfig, } from "./index" +import { EntityClass } from "@mikro-orm/core" +import { IDmlEntity, InferTypeOf } from "../dml" type EntityClassName = string type EntityValues = { id: string }[] +/** + * Either infer the properties from a DML object or from a Mikro orm class prototype. + */ +export type InferRepositoryReturnType = T extends IDmlEntity + ? InferTypeOf + : EntityClass["prototype"] + export type PerformedActions = { created: Record updated: Record @@ -22,7 +29,7 @@ export type PerformedActions = { * This layer helps to separate the business logic (service layer) from accessing the * ORM directly and allows to switch to another ORM without changing the business logic. */ -interface BaseRepositoryService { +interface BaseRepositoryService { transaction( task: (transactionManager: TManager) => Promise, context?: { @@ -42,22 +49,28 @@ interface BaseRepositoryService { ): Promise } -export interface RepositoryService extends BaseRepositoryService { - find(options?: FindOptions, context?: Context): Promise +export interface RepositoryService extends BaseRepositoryService { + find( + options?: FindOptions, + context?: Context + ): Promise[]> findAndCount( options?: FindOptions, context?: Context - ): Promise<[T[], number]> + ): Promise<[InferRepositoryReturnType[], number]> - create(data: any[], context?: Context): Promise - - update(data: { entity; update }[], context?: Context): Promise - - delete( - idsOrPKs: FilterQuery & BaseFilterable>, + create( + data: any[], context?: Context - ): Promise + ): Promise[]> + + update( + data: { entity; update }[], + context?: Context + ): Promise[]> + + delete(idsOrPKs: FindOptions["where"], context?: Context): Promise /** * Soft delete entities and cascade to related entities if configured. @@ -74,37 +87,45 @@ export interface RepositoryService extends BaseRepositoryService { | InternalFilterQuery | InternalFilterQuery[], context?: Context - ): Promise<[T[], Record]> + ): Promise<[InferRepositoryReturnType[], Record]> restore( idsOrFilter: string[] | InternalFilterQuery, context?: Context - ): Promise<[T[], Record]> + ): Promise<[InferRepositoryReturnType[], Record]> - upsert(data: any[], context?: Context): Promise + upsert( + data: any[], + context?: Context + ): Promise[]> upsertWithReplace( data: any[], - config?: UpsertWithReplaceConfig, + config?: UpsertWithReplaceConfig>, context?: Context - ): Promise<{ entities: T[]; performedActions: PerformedActions }> + ): Promise<{ + entities: InferRepositoryReturnType[] + performedActions: PerformedActions + }> } -export interface TreeRepositoryService - extends BaseRepositoryService { +export interface TreeRepositoryService extends BaseRepositoryService { find( options?: FindOptions, transformOptions?: RepositoryTransformOptions, context?: Context - ): Promise + ): Promise[]> findAndCount( options?: FindOptions, transformOptions?: RepositoryTransformOptions, context?: Context - ): Promise<[T[], number]> + ): Promise<[InferRepositoryReturnType[], number]> - create(data: unknown[], context?: Context): Promise + create( + data: unknown[], + context?: Context + ): Promise[]> delete(ids: string[], context?: Context): Promise } diff --git a/packages/core/types/src/dal/utils.ts b/packages/core/types/src/dal/utils.ts index f99a7e49f4..90151ec62b 100644 --- a/packages/core/types/src/dal/utils.ts +++ b/packages/core/types/src/dal/utils.ts @@ -1,3 +1,5 @@ +import { Constructor } from "../modules-sdk" + type ExpandProperty = T extends (infer U)[] ? NonNullable : NonNullable export type Dictionary = { @@ -84,25 +86,29 @@ type FilterValue = type PrevLimit = [never, 0, 1, 2] +export type FilterQueryProperties = { + [Key in keyof T]?: T[Key] extends + | boolean + | number + | string + | bigint + | symbol + | Date + ? T[Key] | OperatorMap + : T[Key] extends infer U + ? U extends { [x: number]: infer V } + ? V extends object + ? FilterQuery, PrevLimit[Prev]> + : never + : never + : never +} + export type FilterQuery = Prev extends never ? never - : { - [Key in keyof T]?: T[Key] extends - | boolean - | number - | string - | bigint - | symbol - | Date - ? T[Key] | OperatorMap - : T[Key] extends infer U - ? U extends { [x: number]: infer V } - ? V extends object - ? FilterQuery, PrevLimit[Prev]> - : never - : never - : never - } + : T extends Constructor + ? FilterQueryProperties + : FilterQueryProperties declare type QueryOrder = "ASC" | "DESC" | "asc" | "desc" diff --git a/packages/core/types/src/dml/index.ts b/packages/core/types/src/dml/index.ts index bb6436de5d..7e2cff4f50 100644 --- a/packages/core/types/src/dml/index.ts +++ b/packages/core/types/src/dml/index.ts @@ -55,6 +55,13 @@ export type RelationshipTypes = | "belongsTo" | "manyToMany" +/** + * Return true if the relationship is nullable + */ +export type IsNullableRelation = T extends () => IDmlEntity | null + ? true + : false + /** * The meta-data returned by the property parse method */ @@ -135,14 +142,12 @@ export interface EntityConstructor extends Function { * "belongsTo" relation meaning "hasOne" and "ManyToOne" */ export type InferForeignKeys = { - [K in keyof Schema as Schema[K] extends { type: infer Type } - ? Type extends RelationshipTypes - ? `${K & string}_id` - : never - : never]: Schema[K] extends { type: infer Type } - ? Type extends RelationshipTypes - ? string - : never + [K in keyof Schema as Schema[K] extends { type: "belongsTo" } + ? `${K & string}_id` + : never]: Schema[K] extends { type: "belongsTo" } + ? null extends Schema[K]["$dataType"] + ? string | null + : string : never } @@ -182,19 +187,21 @@ export type InferManyToManyFields = InferHasManyFields * Inferring the types of the schema fields from the DML * entity */ -export type InferSchemaFields = Prettify<{ - [K in keyof Schema]: Schema[K] extends RelationshipType - ? Schema[K]["type"] extends "belongsTo" - ? InferBelongsToFields - : Schema[K]["type"] extends "hasOne" - ? InferHasOneFields - : Schema[K]["type"] extends "hasMany" - ? InferHasManyFields - : Schema[K]["type"] extends "manyToMany" - ? InferManyToManyFields - : never - : Schema[K]["$dataType"] -}> +export type InferSchemaFields = Prettify< + { + [K in keyof Schema]: Schema[K] extends RelationshipType + ? Schema[K]["type"] extends "belongsTo" + ? InferBelongsToFields + : Schema[K]["type"] extends "hasOne" + ? InferHasOneFields + : Schema[K]["type"] extends "hasMany" + ? InferHasManyFields + : Schema[K]["type"] extends "manyToMany" + ? InferManyToManyFields + : never + : Schema[K]["$dataType"] + } & InferForeignKeys +> /** * Helper to infer the schema type of a DmlEntity diff --git a/packages/core/utils/package.json b/packages/core/utils/package.json index f583a0e2c8..fc313c10b9 100644 --- a/packages/core/utils/package.json +++ b/packages/core/utils/package.json @@ -69,7 +69,7 @@ "scripts": { "build": "rimraf dist && tsc --build", "watch": "tsc --build --watch", - "test": "jest --silent=false --bail --maxWorkers=50% --forceExit --testPathIgnorePatterns='/integration-tests/' -- src/**/__tests__/**/*.ts", + "test": "jest --silent --bail --maxWorkers=50% --forceExit --testPathIgnorePatterns='/integration-tests/' -- src/**/__tests__/**/*.ts", "test:integration": "jest --silent --bail --runInBand --forceExit -- src/**/integration-tests/__tests__/**/*.ts" } } diff --git a/packages/core/utils/src/dal/mikro-orm/integration-tests/__tests__/mikro-orm-repository.spec.ts b/packages/core/utils/src/dal/mikro-orm/integration-tests/__tests__/mikro-orm-repository.spec.ts index 8ffef62387..2c5f51541f 100644 --- a/packages/core/utils/src/dal/mikro-orm/integration-tests/__tests__/mikro-orm-repository.spec.ts +++ b/packages/core/utils/src/dal/mikro-orm/integration-tests/__tests__/mikro-orm-repository.spec.ts @@ -120,9 +120,9 @@ class Entity3 { } } -const Entity1Repository = mikroOrmBaseRepositoryFactory(Entity1) -const Entity2Repository = mikroOrmBaseRepositoryFactory(Entity2) -const Entity3Repository = mikroOrmBaseRepositoryFactory(Entity3) +const Entity1Repository = mikroOrmBaseRepositoryFactory(Entity1) +const Entity2Repository = mikroOrmBaseRepositoryFactory(Entity2) +const Entity3Repository = mikroOrmBaseRepositoryFactory(Entity3) describe("mikroOrmRepository", () => { let orm!: MikroORM diff --git a/packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts b/packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts index 16939238fc..c4281adfe9 100644 --- a/packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts +++ b/packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts @@ -1,8 +1,10 @@ import { - BaseFilterable, Context, DAL, FilterQuery, + FindOptions, + InferEntityType, + InferRepositoryReturnType, FilterQuery as InternalFilterQuery, PerformedActions, RepositoryService, @@ -15,11 +17,10 @@ import { EntityName, EntityProperty, EntitySchema, + LoadStrategy, FilterQuery as MikroFilterQuery, FindOptions as MikroOptions, - LoadStrategy, ReferenceType, - RequiredEntityData, } from "@mikro-orm/core" import { SqlEntityManager } from "@mikro-orm/postgresql" import { @@ -28,6 +29,7 @@ import { MedusaError, promiseAll, } from "../../common" +import { toMikroORMEntity } from "../../dml" import { buildQuery } from "../../modules-sdk/build-query" import { getSoftDeletedCascadedEntitiesIdsMappedBy, @@ -37,7 +39,7 @@ import { dbErrorMapper } from "./db-error-mapper" import { mikroOrmSerializer } from "./mikro-orm-serializer" import { mikroOrmUpdateDeletedAtRecursively } from "./utils" -export class MikroOrmBase { +export class MikroOrmBase { readonly manager_: any protected constructor({ manager }) { @@ -90,10 +92,12 @@ export class MikroOrmBase { * related ones. */ -export class MikroOrmBaseRepository - extends MikroOrmBase +export class MikroOrmBaseRepository + extends MikroOrmBase implements RepositoryService { + entity: EntityClass> + constructor(...args: any[]) { // @ts-ignore super(...arguments) @@ -144,43 +148,55 @@ export class MikroOrmBaseRepository }) } - create(data: unknown[], context?: Context): Promise { - throw new Error("Method not implemented.") - } - - update(data: { entity; update }[], context?: Context): Promise { - throw new Error("Method not implemented.") - } - - delete( - idsOrPKs: FilterQuery & BaseFilterable>, + create( + data: unknown[], context?: Context - ): Promise { + ): Promise[]> { throw new Error("Method not implemented.") } - find(options?: DAL.FindOptions, context?: Context): Promise { + update( + data: { entity; update }[], + context?: Context + ): Promise[]> { + throw new Error("Method not implemented.") + } + + delete(idsOrPKs: FindOptions["where"], context?: Context): Promise { + throw new Error("Method not implemented.") + } + + find( + options?: DAL.FindOptions, + context?: Context + ): Promise[]> { throw new Error("Method not implemented.") } findAndCount( options?: DAL.FindOptions, context?: Context - ): Promise<[T[], number]> { + ): Promise<[InferRepositoryReturnType[], number]> { throw new Error("Method not implemented.") } - upsert(data: unknown[], context: Context = {}): Promise { + upsert( + data: unknown[], + context: Context = {} + ): Promise[]> { throw new Error("Method not implemented.") } upsertWithReplace( data: unknown[], - config: UpsertWithReplaceConfig = { + config: UpsertWithReplaceConfig> = { relations: [], }, context: Context = {} - ): Promise<{ entities: T[]; performedActions: PerformedActions }> { + ): Promise<{ + entities: InferRepositoryReturnType[] + performedActions: PerformedActions + }> { throw new Error("Method not implemented.") } @@ -188,10 +204,10 @@ export class MikroOrmBaseRepository filters: | string | string[] - | (FilterQuery & BaseFilterable>) - | (FilterQuery & BaseFilterable>)[], + | DAL.FindOptions["where"] + | DAL.FindOptions["where"][], sharedContext: Context = {} - ): Promise<[T[], Record]> { + ): Promise<[InferRepositoryReturnType[], Record]> { const entities = await this.find({ where: filters as any }, sharedContext) const date = new Date() @@ -212,8 +228,8 @@ export class MikroOrmBaseRepository async restore( idsOrFilter: string[] | InternalFilterQuery, sharedContext: Context = {} - ): Promise<[T[], Record]> { - const query = buildQuery(idsOrFilter, { + ): Promise<[InferRepositoryReturnType[], Record]> { + const query = buildQuery(idsOrFilter, { withDeleted: true, }) @@ -245,13 +261,13 @@ export class MikroOrmBaseRepository findOptions.where = { $and: [findOptions.where, { $or: retrieveConstraintsToApply(q) }], - } as unknown as DAL.FilterQuery + } as unknown as DAL.FindOptions["where"] } } export class MikroOrmBaseTreeRepository< - T extends object = object -> extends MikroOrmBase { + const T extends object = object +> extends MikroOrmBase { constructor() { // @ts-ignore super(...arguments) @@ -261,7 +277,7 @@ export class MikroOrmBaseTreeRepository< options?: DAL.FindOptions, transformOptions?: RepositoryTransformOptions, context?: Context - ): Promise { + ): Promise[]> { throw new Error("Method not implemented.") } @@ -269,15 +285,21 @@ export class MikroOrmBaseTreeRepository< options?: DAL.FindOptions, transformOptions?: RepositoryTransformOptions, context?: Context - ): Promise<[T[], number]> { + ): Promise<[InferRepositoryReturnType[], number]> { throw new Error("Method not implemented.") } - create(data: unknown[], context?: Context): Promise { + create( + data: unknown[], + context?: Context + ): Promise[]> { throw new Error("Method not implemented.") } - update(data: unknown[], context?: Context): Promise { + update( + data: unknown[], + context?: Context + ): Promise[]> { throw new Error("Method not implemented.") } @@ -286,12 +308,18 @@ export class MikroOrmBaseTreeRepository< } } -export function mikroOrmBaseRepositoryFactory( - entity: any +export function mikroOrmBaseRepositoryFactory( + entity: T ): { new ({ manager }: { manager: any }): MikroOrmBaseRepository } { + const mikroOrmEntity = toMikroORMEntity(entity) as EntityClass< + InferEntityType + > + class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository { + entity = mikroOrmEntity + // @ts-ignore constructor(...args: any[]) { // @ts-ignore @@ -315,19 +343,19 @@ export function mikroOrmBaseRepositoryFactory( }) } - async create(data: any[], context?: Context): Promise { + async create( + data: any[], + context?: Context + ): Promise[]> { const manager = this.getActiveManager(context) const entities = data.map((data_) => { - return manager.create( - entity as EntityName, - data_ as RequiredEntityData - ) + return manager.create(this.entity, data_) }) manager.persist(entities) - return entities + return entities as InferRepositoryReturnType[] } /** @@ -350,7 +378,7 @@ export function mikroOrmBaseRepositoryFactory( const relations = manager .getDriver() .getMetadata() - .get(entity.name).relations + .get(this.entity.name).relations // In case an empty array is provided for a collection relation of type m:n, this relation needs to be init in order to be // able to perform an application cascade action. @@ -397,7 +425,10 @@ export function mikroOrmBaseRepositoryFactory( } } - async update(data: { entity; update }[], context?: Context): Promise { + async update( + data: { entity; update }[], + context?: Context + ): Promise[]> { const manager = this.getActiveManager(context) await this.initManyToManyToDetachAllItemsIfNeeded(data, context) @@ -411,17 +442,17 @@ export function mikroOrmBaseRepositoryFactory( } async delete( - filters: FilterQuery & BaseFilterable>, + filters: FindOptions["where"], context?: Context ): Promise { const manager = this.getActiveManager(context) - await manager.nativeDelete(entity as EntityName, filters as any) + await manager.nativeDelete(this.entity, filters) } async find( - options: DAL.FindOptions = { where: {} }, + options: DAL.FindOptions = { where: {} } as DAL.FindOptions, context?: Context - ): Promise { + ): Promise[]> { const manager = this.getActiveManager(context) const findOptions_ = { ...options } @@ -443,17 +474,17 @@ export function mikroOrmBaseRepositoryFactory( findOptions: findOptions_, }) - return await manager.find( - entity as EntityName, + return (await manager.find( + this.entity as EntityName, findOptions_.where as MikroFilterQuery, findOptions_.options as MikroOptions - ) + )) as InferRepositoryReturnType[] } async findAndCount( - findOptions: DAL.FindOptions = { where: {} }, + findOptions: DAL.FindOptions = { where: {} } as DAL.FindOptions, context: Context = {} - ): Promise<[T[], number]> { + ): Promise<[InferRepositoryReturnType[], number]> { const manager = this.getActiveManager(context) const findOptions_ = { ...findOptions } @@ -467,18 +498,22 @@ export function mikroOrmBaseRepositoryFactory( findOptions: findOptions_, }) - return await manager.findAndCount( - entity as EntityName, - findOptions_.where as MikroFilterQuery, - findOptions_.options as MikroOptions - ) + return (await manager.findAndCount( + this.entity, + findOptions_.where, + findOptions_.options as any // MikroOptions + )) as [InferRepositoryReturnType[], number] } - async upsert(data: any[], context: Context = {}): Promise { + async upsert( + data: any[], + context: Context = {} + ): Promise[]> { const manager = this.getActiveManager(context) - const primaryKeys = - MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity) + const primaryKeys = MikroOrmAbstractBaseRepository_.retrievePrimaryKeys( + this.entity + ) let primaryKeysCriteria: { [key: string]: any }[] = [] if (primaryKeys.length === 1) { @@ -497,7 +532,7 @@ export function mikroOrmBaseRepositoryFactory( })) } - let allEntities: T[][] = [] + let allEntities: InferRepositoryReturnType[][] = [] if (primaryKeysCriteria.length) { allEntities = await Promise.all( @@ -513,7 +548,10 @@ export function mikroOrmBaseRepositoryFactory( const existingEntities = allEntities.flat() - const existingEntitiesMap = new Map() + const existingEntitiesMap = new Map< + string, + InferRepositoryReturnType + >() existingEntities.forEach((entity) => { if (entity) { const key = @@ -525,9 +563,9 @@ export function mikroOrmBaseRepositoryFactory( } }) - const upsertedEntities: T[] = [] - const createdEntities: T[] = [] - const updatedEntities: T[] = [] + const upsertedEntities: InferRepositoryReturnType[] = [] + const createdEntities: InferRepositoryReturnType[] = [] + const updatedEntities: InferRepositoryReturnType[] = [] data.forEach((data_) => { // In case the data provided are just strings, then we build an object with the primary key as the key and the data as the valuecd - @@ -542,8 +580,8 @@ export function mikroOrmBaseRepositoryFactory( const updatedType = manager.assign(existingEntity, data_) updatedEntities.push(updatedType) } else { - const newEntity = manager.create(entity, data_) - createdEntities.push(newEntity) + const newEntity = manager.create(this.entity, data_) + createdEntities.push(newEntity as InferRepositoryReturnType) } }) @@ -558,7 +596,7 @@ export function mikroOrmBaseRepositoryFactory( } // TODO return the all, created, updated entities - return upsertedEntities + return upsertedEntities as InferRepositoryReturnType[] } // UpsertWithReplace does several things to simplify module implementation. @@ -570,11 +608,14 @@ export function mikroOrmBaseRepositoryFactory( // We only support 1-level depth of upserts. We don't support custom fields on the many-to-many pivot tables for now async upsertWithReplace( data: any[], - config: UpsertWithReplaceConfig = { + config: UpsertWithReplaceConfig> = { relations: [], }, context: Context = {} - ): Promise<{ entities: T[]; performedActions: PerformedActions }> { + ): Promise<{ + entities: InferRepositoryReturnType[] + performedActions: PerformedActions + }> { const performedActions: PerformedActions = { created: {}, updated: {}, @@ -593,7 +634,7 @@ export function mikroOrmBaseRepositoryFactory( const allRelations = manager .getDriver() .getMetadata() - .get(entity.name).relations + .get(this.entity.name).relations const nonexistentRelations = arrayDifference( (config.relations as any) ?? [], @@ -624,7 +665,11 @@ export function mikroOrmBaseRepositoryFactory( ) }) - const mainEntity = this.getEntityWithId(manager, entity.name, entryCopy) + const mainEntity = this.getEntityWithId( + manager, + this.entity.name, + entryCopy + ) reconstructedResponse.push({ ...mainEntity, ...reconstructedEntry }) originalDataMap.set(mainEntity.id, entry) @@ -634,7 +679,7 @@ export function mikroOrmBaseRepositoryFactory( let { orderedEntities: upsertedTopLevelEntities, performedActions: performedActions_, - } = await this.upsertMany_(manager, entity.name, toUpsert) + } = await this.upsertMany_(manager, this.entity.name, toUpsert) this.mergePerformedActions(performedActions, performedActions_) @@ -954,10 +999,10 @@ export function mikroOrmBaseRepositoryFactory( filters: | string | string[] - | (FilterQuery & BaseFilterable>) - | (FilterQuery & BaseFilterable>)[], + | DAL.FindOptions["where"] + | DAL.FindOptions["where"][], sharedContext: Context = {} - ): Promise<[T[], Record]> { + ): Promise<[InferRepositoryReturnType[], Record]> { if (Array.isArray(filters) && !filters.filter(Boolean).length) { return [[], {}] } @@ -975,10 +1020,10 @@ export function mikroOrmBaseRepositoryFactory( filters: | string | string[] - | (FilterQuery & BaseFilterable>) - | (FilterQuery & BaseFilterable>)[], + | DAL.FindOptions["where"] + | DAL.FindOptions["where"][], sharedContext: Context = {} - ): Promise<[T[], Record]> { + ): Promise<[InferRepositoryReturnType[], Record]> { if (Array.isArray(filters) && !filters.filter(Boolean).length) { return [[], {}] } @@ -996,11 +1041,12 @@ export function mikroOrmBaseRepositoryFactory( filters: | string | string[] - | (FilterQuery & BaseFilterable>) - | (FilterQuery & BaseFilterable>)[] - ) { - const primaryKeys = - MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity) + | DAL.FindOptions["where"] + | DAL.FindOptions["where"][] + ): DAL.FindOptions["where"] { + const primaryKeys = MikroOrmAbstractBaseRepository_.retrievePrimaryKeys( + this.entity + ) const filterArray = Array.isArray(filters) ? filters : [filters] const normalizedFilters: FilterQuery = { @@ -1014,7 +1060,7 @@ export function mikroOrmBaseRepositoryFactory( }), } - return normalizedFilters + return normalizedFilters as DAL.FindOptions["where"] } } diff --git a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts index d15497428a..144d559007 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -4,14 +4,15 @@ import { DmlEntity } from "../entity" import { model } from "../entity-builder" import { DuplicateIdPropertyError } from "../errors" import { - createMikrORMEntity, - toMikroOrmEntities, + mikroORMEntityBuilder, toMikroORMEntity, + toMikroOrmEntities, } from "../helpers/create-mikro-orm-entity" describe("Entity builder", () => { beforeEach(() => { MetadataStorage.clear() + mikroORMEntityBuilder.clear() }) const defaultColumnMetadata = { @@ -992,12 +993,14 @@ describe("Entity builder", () => { }) const User = toMikroORMEntity(user) - expectTypeOf(new User()).toMatchTypeOf<{ + expectTypeOf(new User()).toEqualTypeOf<{ id: number username: string email: string role: "moderator" | "admin" | "guest" deleted_at: Date | null + created_at: Date + updated_at: Date }>() const metaData = MetadataStorage.getMetadataFromDecorator(User) @@ -1939,6 +1942,11 @@ describe("Entity builder", () => { expression: 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_unique" ON "user" (email) WHERE deleted_at IS NULL', }, + { + expression: + 'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL', + name: "IDX_user_deleted_at", + }, ]) expect(metaData.filters).toEqual({ @@ -2050,6 +2058,11 @@ describe("Entity builder", () => { expression: 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_unique" ON "platform"."user" (email) WHERE deleted_at IS NULL', }, + { + expression: + 'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "platform"."user" (deleted_at) WHERE deleted_at IS NULL', + name: "IDX_user_deleted_at", + }, ]) expect(metaData.filters).toEqual({ @@ -2160,6 +2173,11 @@ describe("Entity builder", () => { expression: 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_myEmail_unique" ON "user" (myEmail) WHERE deleted_at IS NULL', }, + { + expression: + 'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL', + name: "IDX_user_deleted_at", + }, ]) expect(metaData.filters).toEqual({ @@ -2819,7 +2837,6 @@ describe("Entity builder", () => { reference: "scalar", setter: false, type: "string", - isForeignKey: true, persist: false, }, created_at: { @@ -2943,12 +2960,21 @@ describe("Entity builder", () => { nullable: false, onDelete: undefined, reference: "m:1", - isForeignKey: true, }, ...defaultColumnMetadata, }) expect(metaData.indexes).toEqual([ + { + expression: + 'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL', + name: "IDX_user_group_id", + }, + { + expression: + 'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL', + name: "IDX_user_deleted_at", + }, { expression: 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_account_unique" ON "user" (email, account) WHERE deleted_at IS NULL', @@ -2969,11 +2995,6 @@ describe("Entity builder", () => { 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_unique-name" ON "user" (organization, account, group_id) WHERE deleted_at IS NULL', name: "IDX_unique-name", }, - { - expression: - 'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL', - name: "IDX_user_group_id", - }, ]) }) @@ -3025,6 +3046,16 @@ describe("Entity builder", () => { ) expect(metaData.indexes).toEqual([ + { + expression: + 'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL', + name: "IDX_user_group_id", + }, + { + expression: + 'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL', + name: "IDX_user_deleted_at", + }, { expression: 'CREATE INDEX IF NOT EXISTS "IDX_user_organization_account" ON "user" (organization, account) WHERE email IS NOT NULL AND deleted_at IS NULL', @@ -3050,11 +3081,6 @@ describe("Entity builder", () => { 'CREATE INDEX IF NOT EXISTS "IDX_user_account_group_id" ON "user" (account, group_id) WHERE is_owner IS TRUE AND deleted_at IS NULL', name: "IDX_user_account_group_id", }, - { - expression: - 'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL', - name: "IDX_user_group_id", - }, ]) }) @@ -3121,6 +3147,11 @@ describe("Entity builder", () => { 'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL', name: "IDX_user_group_id", }, + { + expression: + 'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL', + name: "IDX_user_deleted_at", + }, ]) const Setting = toMikroORMEntity(setting) @@ -3132,6 +3163,11 @@ describe("Entity builder", () => { 'CREATE INDEX IF NOT EXISTS "IDX_setting_user_id" ON "setting" (user_id) WHERE deleted_at IS NULL', name: "IDX_setting_user_id", }, + { + expression: + 'CREATE INDEX IF NOT EXISTS "IDX_setting_deleted_at" ON "setting" (deleted_at) WHERE deleted_at IS NULL', + name: "IDX_setting_deleted_at", + }, ]) }) }) @@ -3543,7 +3579,6 @@ describe("Entity builder", () => { nullable: false, onDelete: "cascade", reference: "m:1", - isForeignKey: true, }, created_at: { reference: "scalar", @@ -3616,6 +3651,11 @@ describe("Entity builder", () => { } }>() + const userInstance = new User() + expectTypeOf< + (typeof userInstance)["email"]["user_id"] + >().toEqualTypeOf() + expectTypeOf(new Email()).toMatchTypeOf<{ email: string isVerified: boolean @@ -3631,6 +3671,7 @@ describe("Entity builder", () => { } } }>() + expectTypeOf(new Email().user_id).toEqualTypeOf() const metaData = MetadataStorage.getMetadataFromDecorator(User) expect(metaData.className).toEqual("User") @@ -3740,7 +3781,6 @@ describe("Entity builder", () => { name: "user_id", getter: false, setter: false, - isForeignKey: true, persist: false, }, created_at: { @@ -3810,6 +3850,11 @@ describe("Entity builder", () => { } }>() + const userInstance = new User() + expectTypeOf<(typeof userInstance)["email"]["user_id"]>().toEqualTypeOf< + string | null + >() + expectTypeOf(new Email()).toMatchTypeOf<{ email: string isVerified: boolean @@ -3822,6 +3867,7 @@ describe("Entity builder", () => { } } | null }>() + expectTypeOf(new Email().user_id).toEqualTypeOf() const metaData = MetadataStorage.getMetadataFromDecorator(User) expect(metaData.className).toEqual("User") @@ -3931,7 +3977,6 @@ describe("Entity builder", () => { name: "user_id", getter: false, setter: false, - isForeignKey: true, persist: false, }, created_at: { @@ -4121,7 +4166,6 @@ describe("Entity builder", () => { mapToPk: true, fieldName: "user_id", nullable: false, - isForeignKey: true, }, created_at: { reference: "scalar", @@ -4310,7 +4354,6 @@ describe("Entity builder", () => { mapToPk: true, fieldName: "user_id", nullable: true, - isForeignKey: true, }, created_at: { reference: "scalar", @@ -4566,7 +4609,6 @@ describe("Entity builder", () => { name: "user_id", getter: false, setter: false, - isForeignKey: true, persist: false, }, created_at: { @@ -4765,7 +4807,6 @@ describe("Entity builder", () => { name: "user_id", getter: false, setter: false, - isForeignKey: true, persist: false, }, created_at: { @@ -4872,7 +4913,6 @@ describe("Entity builder", () => { mapToPk: true, nullable: false, onDelete: undefined, - isForeignKey: true, }, children: { cascade: undefined, @@ -4983,7 +5023,6 @@ describe("Entity builder", () => { name: "parent_id", type: "string", columnType: "text", - isForeignKey: true, persist: false, reference: "scalar", getter: false, @@ -5110,8 +5149,9 @@ describe("Entity builder", () => { reference: "m:n", name: "teams", entity: "Team", + owner: true, pivotTable: "team_users", - mappedBy: "users", + inversedBy: "users", }, created_at: { reference: "scalar", @@ -5177,7 +5217,9 @@ describe("Entity builder", () => { users: { reference: "m:n", name: "users", + mappedBy: "teams", entity: "User", + owner: false, pivotTable: "team_users", }, created_at: { @@ -5288,8 +5330,9 @@ describe("Entity builder", () => { reference: "m:n", name: "teams", entity: "Team", + owner: true, pivotTable: "team_users", - mappedBy: "users", + inversedBy: "users", }, created_at: { reference: "scalar", @@ -5356,6 +5399,8 @@ describe("Entity builder", () => { reference: "m:n", name: "users", entity: "User", + owner: false, + mappedBy: "teams", pivotTable: "team_users", }, created_at: { @@ -5500,6 +5545,188 @@ describe("Entity builder", () => { reference: "m:n", name: "teams", entity: "Team", + owner: true, + pivotTable: "team_users", + inversedBy: "users", + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "created_at", + fieldName: "created_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + updated_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "updated_at", + fieldName: "updated_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + onUpdate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + deleted_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "deleted_at", + fieldName: "deleted_at", + nullable: true, + getter: false, + setter: false, + }, + }) + + const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team) + expect(teamMetaData.className).toEqual("Team") + expect(teamMetaData.path).toEqual("Team") + expect(teamMetaData.properties).toEqual({ + id: { + reference: "scalar", + type: "number", + columnType: "integer", + name: "id", + fieldName: "id", + nullable: false, + getter: false, + setter: false, + }, + name: { + reference: "scalar", + type: "string", + columnType: "text", + name: "name", + fieldName: "name", + nullable: false, + getter: false, + setter: false, + }, + users: { + reference: "m:n", + name: "users", + entity: "User", + owner: false, + pivotTable: "team_users", + mappedBy: "teams", + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "created_at", + fieldName: "created_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + updated_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "updated_at", + fieldName: "updated_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + onUpdate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + deleted_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "deleted_at", + fieldName: "deleted_at", + nullable: true, + getter: false, + setter: false, + }, + }) + }) + + test("define mappedBy on both sides and reverse order of registering entities", () => { + const team = model.define("team", { + id: model.number(), + name: model.text(), + users: model.manyToMany(() => user, { mappedBy: "teams" }), + }) + + const user = model.define("user", { + id: model.number(), + username: model.text(), + teams: model.manyToMany(() => team, { mappedBy: "users" }), + }) + + const Team = toMikroORMEntity(team) + const User = toMikroORMEntity(user) + + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + teams: { + id: number + name: string + users: { + id: number + username: string + }[] + }[] + }>() + + expectTypeOf(new Team()).toMatchTypeOf<{ + id: number + name: string + users: { + id: number + username: string + teams: { + id: number + name: string + }[] + }[] + }>() + + const metaData = MetadataStorage.getMetadataFromDecorator(User) + expect(metaData.className).toEqual("User") + expect(metaData.path).toEqual("User") + expect(metaData.properties).toEqual({ + id: { + reference: "scalar", + type: "number", + columnType: "integer", + name: "id", + fieldName: "id", + nullable: false, + getter: false, + setter: false, + }, + username: { + reference: "scalar", + type: "string", + columnType: "text", + name: "username", + fieldName: "username", + nullable: false, + getter: false, + setter: false, + }, + teams: { + reference: "m:n", + name: "teams", + entity: "Team", + owner: false, pivotTable: "team_users", mappedBy: "users", }, @@ -5568,11 +5795,8 @@ describe("Entity builder", () => { reference: "m:n", name: "users", entity: "User", + owner: true, pivotTable: "team_users", - /** - * The other side should be inversed in order for Mikro ORM - * to work. Both sides cannot have mappedBy. - */ inversedBy: "teams", }, created_at: { @@ -5613,190 +5837,6 @@ describe("Entity builder", () => { }) }) - test("define mappedBy on both sides and reverse order of registering entities", () => { - const team = model.define("team", { - id: model.number(), - name: model.text(), - users: model.manyToMany(() => user, { mappedBy: "teams" }), - }) - - const user = model.define("user", { - id: model.number(), - username: model.text(), - teams: model.manyToMany(() => team, { mappedBy: "users" }), - }) - - const entityBuilder = createMikrORMEntity() - const Team = entityBuilder(team) - const User = entityBuilder(user) - - expectTypeOf(new User()).toMatchTypeOf<{ - id: number - username: string - teams: { - id: number - name: string - users: { - id: number - username: string - }[] - }[] - }>() - - expectTypeOf(new Team()).toMatchTypeOf<{ - id: number - name: string - users: { - id: number - username: string - teams: { - id: number - name: string - }[] - }[] - }>() - - const metaData = MetadataStorage.getMetadataFromDecorator(User) - expect(metaData.className).toEqual("User") - expect(metaData.path).toEqual("User") - expect(metaData.properties).toEqual({ - id: { - reference: "scalar", - type: "number", - columnType: "integer", - name: "id", - fieldName: "id", - nullable: false, - getter: false, - setter: false, - }, - username: { - reference: "scalar", - type: "string", - columnType: "text", - name: "username", - fieldName: "username", - nullable: false, - getter: false, - setter: false, - }, - teams: { - reference: "m:n", - name: "teams", - entity: "Team", - pivotTable: "team_users", - /** - * The other side should be inversed in order for Mikro ORM - * to work. Both sides cannot have mappedBy. - */ - inversedBy: "users", - }, - created_at: { - reference: "scalar", - type: "date", - columnType: "timestamptz", - name: "created_at", - fieldName: "created_at", - defaultRaw: "now()", - onCreate: expect.any(Function), - nullable: false, - getter: false, - setter: false, - }, - updated_at: { - reference: "scalar", - type: "date", - columnType: "timestamptz", - name: "updated_at", - fieldName: "updated_at", - defaultRaw: "now()", - onCreate: expect.any(Function), - onUpdate: expect.any(Function), - nullable: false, - getter: false, - setter: false, - }, - deleted_at: { - reference: "scalar", - type: "date", - columnType: "timestamptz", - name: "deleted_at", - fieldName: "deleted_at", - nullable: true, - getter: false, - setter: false, - }, - }) - - const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team) - expect(teamMetaData.className).toEqual("Team") - expect(teamMetaData.path).toEqual("Team") - expect(teamMetaData.properties).toEqual({ - id: { - reference: "scalar", - type: "number", - columnType: "integer", - name: "id", - fieldName: "id", - nullable: false, - getter: false, - setter: false, - }, - name: { - reference: "scalar", - type: "string", - columnType: "text", - name: "name", - fieldName: "name", - nullable: false, - getter: false, - setter: false, - }, - users: { - reference: "m:n", - name: "users", - entity: "User", - pivotTable: "team_users", - mappedBy: "teams", - }, - created_at: { - reference: "scalar", - type: "date", - columnType: "timestamptz", - name: "created_at", - fieldName: "created_at", - defaultRaw: "now()", - onCreate: expect.any(Function), - nullable: false, - getter: false, - setter: false, - }, - updated_at: { - reference: "scalar", - type: "date", - columnType: "timestamptz", - name: "updated_at", - fieldName: "updated_at", - defaultRaw: "now()", - onCreate: expect.any(Function), - onUpdate: expect.any(Function), - nullable: false, - getter: false, - setter: false, - }, - deleted_at: { - reference: "scalar", - type: "date", - columnType: "timestamptz", - name: "deleted_at", - fieldName: "deleted_at", - nullable: true, - getter: false, - setter: false, - }, - }) - }) - test("define multiple many to many relationships to the same entity", () => { const team = model.define("team", { id: model.number(), @@ -5816,9 +5856,8 @@ describe("Entity builder", () => { teams: model.manyToMany(() => team, { mappedBy: "users" }), }) - const entityBuilder = createMikrORMEntity() - const Team = entityBuilder(team) - const User = entityBuilder(user) + const Team = toMikroORMEntity(team) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -5886,19 +5925,17 @@ describe("Entity builder", () => { reference: "m:n", name: "teams", entity: "Team", + owner: false, pivotTable: "team_users", - /** - * The other side should be inversed in order for Mikro ORM - * to work. Both sides cannot have mappedBy. - */ - inversedBy: "users", + mappedBy: "users", }, activeTeams: { reference: "m:n", name: "activeTeams", entity: "Team", + owner: false, pivotTable: "team_users", - inversedBy: "activeTeamsUsers", + mappedBy: "activeTeamsUsers", }, created_at: { reference: "scalar", @@ -5965,15 +6002,17 @@ describe("Entity builder", () => { reference: "m:n", name: "users", entity: "User", + owner: true, pivotTable: "team_users", - mappedBy: "teams", + inversedBy: "teams", }, activeTeamsUsers: { reference: "m:n", name: "activeTeamsUsers", entity: "User", + owner: true, pivotTable: "team_users", - mappedBy: "activeTeams", + inversedBy: "activeTeams", }, created_at: { reference: "scalar", @@ -6086,8 +6125,9 @@ describe("Entity builder", () => { reference: "m:n", name: "teams", entity: "Team", + owner: true, pivotTable: "platform.team_users", - mappedBy: "users", + inversedBy: "users", }, created_at: { reference: "scalar", @@ -6155,6 +6195,8 @@ describe("Entity builder", () => { reference: "m:n", name: "users", entity: "User", + owner: false, + mappedBy: "teams", pivotTable: "platform.team_users", }, created_at: { @@ -6195,13 +6237,168 @@ describe("Entity builder", () => { }) }) + test("should compute the pivot table name correctly", () => { + const team = model.define("teamSquad", { + id: model.number(), + name: model.text(), + users: model.manyToMany(() => user), + }) + + const user = model.define("RandomUser", { + id: model.number(), + username: model.text(), + teams: model.manyToMany(() => team, { + mappedBy: "users", + }), + }) + + const User = toMikroORMEntity(user) + const Team = toMikroORMEntity(team) + + const metaData = MetadataStorage.getMetadataFromDecorator(User) + expect(metaData.className).toEqual("RandomUser") + expect(metaData.path).toEqual("RandomUser") + expect(metaData.properties).toEqual({ + id: { + reference: "scalar", + type: "number", + columnType: "integer", + name: "id", + fieldName: "id", + nullable: false, + getter: false, + setter: false, + }, + username: { + reference: "scalar", + type: "string", + columnType: "text", + name: "username", + fieldName: "username", + nullable: false, + getter: false, + setter: false, + }, + teams: { + reference: "m:n", + name: "teams", + entity: "TeamSquad", + owner: true, + pivotTable: "random_user_team_squads", + inversedBy: "users", + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "created_at", + fieldName: "created_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + updated_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "updated_at", + fieldName: "updated_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + onUpdate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + deleted_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "deleted_at", + fieldName: "deleted_at", + nullable: true, + getter: false, + setter: false, + }, + }) + + const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team) + expect(teamMetaData.className).toEqual("TeamSquad") + expect(teamMetaData.path).toEqual("TeamSquad") + expect(teamMetaData.properties).toEqual({ + id: { + reference: "scalar", + type: "number", + columnType: "integer", + name: "id", + fieldName: "id", + nullable: false, + getter: false, + setter: false, + }, + name: { + reference: "scalar", + type: "string", + columnType: "text", + name: "name", + fieldName: "name", + nullable: false, + getter: false, + setter: false, + }, + users: { + reference: "m:n", + name: "users", + entity: "RandomUser", + owner: false, + mappedBy: "teams", + pivotTable: "random_user_team_squads", + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "created_at", + fieldName: "created_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + updated_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "updated_at", + fieldName: "updated_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + onUpdate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + deleted_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "deleted_at", + fieldName: "deleted_at", + nullable: true, + getter: false, + setter: false, + }, + }) + }) + test("define custom pivot table name", () => { const team = model.define("team", { id: model.number(), name: model.text(), - users: model.manyToMany(() => user, { - pivotTable: "users_teams", - }), + users: model.manyToMany(() => user), }) const user = model.define("user", { @@ -6270,8 +6467,9 @@ describe("Entity builder", () => { reference: "m:n", name: "teams", entity: "Team", + owner: true, pivotTable: "users_teams", - mappedBy: "users", + inversedBy: "users", }, created_at: { reference: "scalar", @@ -6337,7 +6535,9 @@ describe("Entity builder", () => { users: { reference: "m:n", name: "users", + owner: false, entity: "User", + mappedBy: "teams", pivotTable: "users_teams", }, created_at: { @@ -6454,7 +6654,6 @@ describe("Entity builder", () => { mapToPk: true, fieldName: "user_id", nullable: false, - isForeignKey: true, }, user: { reference: "scalar", @@ -6473,7 +6672,6 @@ describe("Entity builder", () => { mapToPk: true, fieldName: "team_id", nullable: false, - isForeignKey: true, }, team: { reference: "scalar", @@ -6549,8 +6747,9 @@ describe("Entity builder", () => { reference: "m:n", name: "teams", entity: "Team", + owner: true, pivotEntity: "TeamUsers", - mappedBy: "users", + inversedBy: "users", }, created_at: { reference: "scalar", @@ -6617,6 +6816,8 @@ describe("Entity builder", () => { reference: "m:n", name: "users", entity: "User", + owner: false, + mappedBy: "teams", pivotEntity: "TeamUsers", }, created_at: { diff --git a/packages/core/utils/src/dml/entity-builder.ts b/packages/core/utils/src/dml/entity-builder.ts index c6aa9ce3fc..ac4396d7eb 100644 --- a/packages/core/utils/src/dml/entity-builder.ts +++ b/packages/core/utils/src/dml/entity-builder.ts @@ -56,6 +56,14 @@ export type ManyToManyOptions = RelationshipOptions & * @ignore */ pivotEntity?: never + /** + * The column name in the pivot table that for the current entity + */ + joinColumn?: string + /** + * The column name in the pivot table for the opposite entity + */ + inverseJoinColumn?: string } | { /** diff --git a/packages/core/utils/src/dml/helpers/create-graphql.ts b/packages/core/utils/src/dml/helpers/create-graphql.ts index b89b9a5dc3..915a53babd 100644 --- a/packages/core/utils/src/dml/helpers/create-graphql.ts +++ b/packages/core/utils/src/dml/helpers/create-graphql.ts @@ -1,8 +1,8 @@ import type { PropertyType } from "@medusajs/types" import { DmlEntity } from "../entity" import { parseEntityName } from "./entity-builder/parse-entity-name" -import { getGraphQLAttributeFromDMLPropety } from "./graphql-builder/get-attribute" import { setGraphQLRelationship } from "./graphql-builder/set-relationship" +import { getGraphQLAttributeFromDMLPropety } from "./graphql-builder/get-attribute" export function generateGraphQLFromEntity>( entity: T diff --git a/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts b/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts index de7e5120da..a599da2546 100644 --- a/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts +++ b/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts @@ -1,4 +1,5 @@ import type { + Constructor, DMLSchema, EntityConstructor, IDmlEntity, @@ -6,10 +7,11 @@ import type { PropertyType, } from "@medusajs/types" import { Entity, Filter } from "@mikro-orm/core" -import { mikroOrmSoftDeletableFilterOptions } from "../../dal" + import { DmlEntity } from "../entity" -import { DuplicateIdPropertyError } from "../errors" import { IdProperty } from "../properties/id" +import { DuplicateIdPropertyError } from "../errors" +import { mikroOrmSoftDeletableFilterOptions } from "../../dal" import { applySearchable } from "./entity-builder/apply-searchable" import { defineProperty } from "./entity-builder/define-property" import { defineRelationship } from "./entity-builder/define-relationship" @@ -21,7 +23,7 @@ import { applyEntityIndexes, applyIndexes } from "./mikro-orm/apply-indexes" * value is a function that can be used to convert DML entities * to Mikro ORM entities. */ -export function createMikrORMEntity() { +function createMikrORMEntity() { /** * The following property is used to track many to many relationship * between two entities. It is needed because we have to mark one @@ -35,20 +37,21 @@ export function createMikrORMEntity() { * - [user.teams]: true // the teams relationship on user is an owner * - [team.users] // cannot be an owner */ - // TODO: if we use the util toMikroOrmEntities then a new builder will be used each time, lets think about this. Currently if means that with many to many we need to use the same builder - const MANY_TO_MANY_TRACKED_RELATIONS: Record = {} + let MANY_TO_MANY_TRACKED_RELATIONS: Record = {} + let ENTITIES: Record> = {} /** * A helper function to define a Mikro ORM entity from a * DML entity. */ - return function createEntity>( - entity: T - ): Infer { + function createEntity>(entity: T): Infer { class MikroORMEntity {} const { schema, cascades, indexes: entityIndexes = [] } = entity.parse() const { modelName, tableName } = parseEntityName(entity) + if (ENTITIES[modelName]) { + return ENTITIES[modelName] as Infer + } /** * Assigning name to the class constructor, so that it matches @@ -80,11 +83,14 @@ export function createMikrORMEntity() { hasIdAlreadyDefined = true } - defineProperty(MikroORMEntity, name, property as PropertyType) + defineProperty(MikroORMEntity, property as PropertyType, { + propertyName: name, + tableName, + }) applyIndexes(MikroORMEntity, tableName, field) applySearchable(MikroORMEntity, field) } else { - defineRelationship(MikroORMEntity, field, cascades, context) + defineRelationship(MikroORMEntity, entity, field, cascades, context) applySearchable(MikroORMEntity, field) } }) @@ -94,12 +100,31 @@ export function createMikrORMEntity() { /** * Converting class to a MikroORM entity */ - return Entity({ tableName })( + const RegisteredEntity = Entity({ tableName })( Filter(mikroOrmSoftDeletableFilterOptions)(MikroORMEntity) ) as Infer + + ENTITIES[modelName] = RegisteredEntity + return RegisteredEntity } + + /** + * Clear the internally tracked entities and relationships + */ + createEntity.clear = function () { + MANY_TO_MANY_TRACKED_RELATIONS = {} + ENTITIES = {} + } + return createEntity } +/** + * Helper function to convert DML entities to MikroORM entity. Use + * "toMikroORMEntity" if you are ensure the input is a DML entity + * or not. + */ +export const mikroORMEntityBuilder = createMikrORMEntity() + /** * Takes a DML entity and returns a Mikro ORM entity otherwise * return the input idempotently @@ -111,7 +136,7 @@ export const toMikroORMEntity = ( let mikroOrmEntity: T | EntityConstructor = entity if (DmlEntity.isDmlEntity(entity)) { - mikroOrmEntity = createMikrORMEntity()(entity) + mikroOrmEntity = mikroORMEntityBuilder(entity) } return mikroOrmEntity as T extends IDmlEntity ? Infer : T @@ -123,11 +148,9 @@ export const toMikroORMEntity = ( * @param entities */ export const toMikroOrmEntities = function (entities: T) { - const entityBuilder = createMikrORMEntity() - return entities.map((entity) => { if (DmlEntity.isDmlEntity(entity)) { - return entityBuilder(entity) + return mikroORMEntityBuilder(entity) } return entity diff --git a/packages/core/utils/src/dml/helpers/entity-builder/define-property.ts b/packages/core/utils/src/dml/helpers/entity-builder/define-property.ts index 13fd90d7c0..15a5962732 100644 --- a/packages/core/utils/src/dml/helpers/entity-builder/define-property.ts +++ b/packages/core/utils/src/dml/helpers/entity-builder/define-property.ts @@ -16,6 +16,7 @@ import { Utils, } from "@mikro-orm/core" import { PrimaryKeyModifier } from "../../properties/primary-key" +import { applyEntityIndexes } from "../mikro-orm/apply-indexes" /** * DML entity data types to PostgreSQL data types via @@ -64,7 +65,8 @@ const PROPERTY_TYPES: { const SPECIAL_PROPERTIES: { [propertyName: string]: ( MikroORMEntity: EntityConstructor, - field: PropertyMetadata + field: PropertyMetadata, + tableName: string ) => void } = { created_at: (MikroORMEntity, field) => { @@ -88,6 +90,21 @@ const SPECIAL_PROPERTIES: { onUpdate: () => new Date(), })(MikroORMEntity.prototype, field.fieldName) }, + deleted_at: (MikroORMEntity, field, tableName) => { + Property({ + columnType: "timestamptz", + type: "date", + nullable: true, + fieldName: field.fieldName, + })(MikroORMEntity.prototype, field.fieldName) + + applyEntityIndexes(MikroORMEntity, tableName, [ + { + on: ["deleted_at"], + where: "deleted_at IS NULL", + }, + ]) + }, } /** @@ -95,8 +112,8 @@ const SPECIAL_PROPERTIES: { */ export function defineProperty( MikroORMEntity: EntityConstructor, - propertyName: string, - property: PropertyType + property: PropertyType, + { tableName, propertyName }: { tableName: string; propertyName: string } ) { const field = property.parse(propertyName) /** @@ -112,18 +129,18 @@ export function defineProperty( } if (SPECIAL_PROPERTIES[field.fieldName]) { - SPECIAL_PROPERTIES[field.fieldName](MikroORMEntity, field) + SPECIAL_PROPERTIES[field.fieldName](MikroORMEntity, field, tableName) return } - /** - * Defining an big number property - * A big number property always comes with a raw_{{ fieldName }} column - * where the config of the bigNumber is set. - * The `raw_` field is generated during DML schema generation as a json - * dataType. - */ if (field.dataType.name === "bigNumber") { + /** + * Defining an big number property + * A big number property always comes with a raw_{{ fieldName }} column + * where the config of the bigNumber is set. + * The `raw_` field is generated during DML schema generation as a json + * dataType. + */ MikroOrmBigNumberProperty({ nullable: field.nullable, fieldName: field.fieldName, diff --git a/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts b/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts index fdf8c392c3..fd86676d78 100644 --- a/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts +++ b/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts @@ -16,17 +16,77 @@ import { rel, } from "@mikro-orm/core" import { camelToSnakeCase, pluralize } from "../../../common" -import { ForeignKey } from "../../../dal/mikro-orm/decorators/foreign-key" import { DmlEntity } from "../../entity" import { HasMany } from "../../relations/has-many" import { HasOne } from "../../relations/has-one" import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many" +import { applyEntityIndexes } from "../mikro-orm/apply-indexes" import { parseEntityName } from "./parse-entity-name" type Context = { MANY_TO_MANY_TRACKED_RELATIONS: Record } +function retrieveOtherSideRelationshipManyToMany({ + relationship, + relatedEntity, + relatedModelName, + entity, +}: { + relationship: RelationshipMetadata + relatedEntity: DmlEntity< + Record | RelationshipType>, + any + > + relatedModelName: string + entity: DmlEntity +}): [string, RelationshipType] { + if (relationship.mappedBy) { + return [ + relationship.mappedBy, + relatedEntity.parse().schema[relationship.mappedBy], + ] as [string, RelationshipType] + } + + /** + * Since we don't have the information about the other side of the + * relationship, we will try to find all the other side many to many that refers to the current entity. + * If there is any, we will try to find if at least one of them has a mappedBy. + */ + const potentialOtherSide = Object.entries(relatedEntity.schema) + .filter(([, propConfig]) => DmlManyToMany.isManyToMany(propConfig)) + .filter(([prop, propConfig]) => { + const parsedProp = propConfig.parse(prop) as RelationshipMetadata + + const relatedEntity = + typeof parsedProp.entity === "function" + ? parsedProp.entity() + : undefined + + if (!relatedEntity) { + throw new Error( + `Invalid relationship reference for "${relatedModelName}.${prop}". Make sure to define the relationship using a factory function` + ) + } + + return ( + (parsedProp.mappedBy === relationship.name && + parseEntityName(relatedEntity).modelName === + parseEntityName(entity).modelName) || + parseEntityName(relatedEntity).modelName === + parseEntityName(entity).modelName + ) + }) as unknown as [string, RelationshipType][] + + if (potentialOtherSide.length > 1) { + throw new Error( + `Invalid relationship reference for "${entity.name}.${relationship.name}". Make sure to set the mappedBy property on one side or the other or both.` + ) + } + + return potentialOtherSide[0] ?? [] +} + /** * Validates a many to many relationship without mappedBy and checks if the other side of the relationship is defined and possesses mappedBy. * @param MikroORMEntity @@ -39,6 +99,7 @@ function validateManyToManyRelationshipWithoutMappedBy({ relationship, relatedEntity, relatedModelName, + entity, }: { MikroORMEntity: EntityConstructor relationship: RelationshipMetadata @@ -47,42 +108,23 @@ function validateManyToManyRelationshipWithoutMappedBy({ any > relatedModelName: string + entity: DmlEntity }) { /** * Since we don't have the information about the other side of the * relationship, we will try to find all the other side many to many that refers to the current entity. * If there is any, we will try to find if at least one of them has a mappedBy. */ - const potentialOtherSides = Object.entries(relatedEntity.schema) - .filter(([, propConfig]) => DmlManyToMany.isManyToMany(propConfig)) - .filter(([prop, propConfig]) => { - const parsedProp = propConfig.parse(prop) as RelationshipMetadata - const relatedEntity = - typeof parsedProp.entity === "function" - ? parsedProp.entity() - : undefined + const [, potentialOtherSide] = retrieveOtherSideRelationshipManyToMany({ + relationship, + relatedEntity, + relatedModelName, + entity, + }) - if (!relatedEntity) { - throw new Error( - `Invalid relationship reference for "${relatedModelName}.${prop}". Make sure to define the relationship using a factory function` - ) - } - - return parseEntityName(relatedEntity).modelName === MikroORMEntity.name - }) as unknown as [string, RelationshipType][] - - if (potentialOtherSides.length) { - const hasMappedBy = potentialOtherSides.some( - ([, propConfig]) => !!propConfig.parse("").mappedBy - ) - if (!hasMappedBy) { - throw new Error( - `Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". "mappedBy" should be defined on one side or the other.` - ) - } - } else { + if (!potentialOtherSide) { throw new Error( - `Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". The other side of the relationship is missing.` + `Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". "mappedBy" should be defined on one side or the other.` ) } } @@ -140,6 +182,7 @@ export function defineHasManyRelationship( */ export function defineBelongsToRelationship( MikroORMEntity: EntityConstructor, + entity: DmlEntity, relationship: RelationshipMetadata, relatedEntity: DmlEntity< Record | RelationshipType>, @@ -194,8 +237,7 @@ export function defineBelongsToRelationship( return } - this[relationship.name] ??= this[foreignKeyName] - this[foreignKeyName] ??= this[relationship.name]?.id + this[foreignKeyName] ??= this[relationship.name]?.id ?? null } /** @@ -222,7 +264,6 @@ export function defineBelongsToRelationship( nullable: relationship.nullable, onDelete: shouldCascade ? "cascade" : undefined, })(MikroORMEntity.prototype, foreignKeyName) - ForeignKey()(MikroORMEntity.prototype, foreignKeyName) if (DmlManyToMany.isManyToMany(otherSideRelation)) { Property({ @@ -239,6 +280,13 @@ export function defineBelongsToRelationship( })(MikroORMEntity.prototype, relationship.name) } + const { tableName } = parseEntityName(entity) + applyEntityIndexes(MikroORMEntity, tableName, [ + { + on: [foreignKeyName], + where: "deleted_at IS NULL", + }, + ]) applyForeignKeyAssignationHooks(foreignKeyName) return } @@ -270,7 +318,14 @@ export function defineBelongsToRelationship( nullable: relationship.nullable, persist: false, })(MikroORMEntity.prototype, foreignKeyName) - ForeignKey()(MikroORMEntity.prototype, foreignKeyName) + + const { tableName } = parseEntityName(entity) + applyEntityIndexes(MikroORMEntity, tableName, [ + { + on: [foreignKeyName], + where: "deleted_at IS NULL", + }, + ]) applyForeignKeyAssignationHooks(foreignKeyName) return @@ -289,6 +344,7 @@ export function defineBelongsToRelationship( */ export function defineManyToManyRelationship( MikroORMEntity: EntityConstructor, + entity: DmlEntity, relationship: RelationshipMetadata, relatedEntity: DmlEntity< Record | RelationshipType>, @@ -296,59 +352,60 @@ export function defineManyToManyRelationship( >, { relatedModelName, + relatedTableName, pgSchema, - }: { relatedModelName: string; pgSchema: string | undefined }, + }: { + relatedModelName: string + pgSchema: string | undefined + relatedTableName: string + }, { MANY_TO_MANY_TRACKED_RELATIONS }: Context ) { let mappedBy = relationship.mappedBy let inversedBy: undefined | string let pivotEntityName: undefined | string let pivotTableName: undefined | string + let joinColumn: undefined | string = relationship.options.joinColumn + let inverseJoinColumn: undefined | string = + relationship.options.inverseJoinColumn + + const [otherSideRelationshipProperty, otherSideRelationship] = + retrieveOtherSideRelationshipManyToMany({ + relationship, + relatedEntity, + relatedModelName, + entity, + }) /** * Validating other side of relationship when mapped by is defined */ if (mappedBy) { - const otherSideRelation = relatedEntity.parse().schema[mappedBy] - if (!otherSideRelation) { + if (!otherSideRelationship) { throw new Error( `Missing property "${mappedBy}" on "${relatedModelName}" entity. Make sure to define it as a relationship` ) } - if (!DmlManyToMany.isManyToMany(otherSideRelation)) { + if (!DmlManyToMany.isManyToMany(otherSideRelationship)) { throw new Error( `Invalid relationship reference for "${mappedBy}" on "${relatedModelName}" entity. Make sure to define a manyToMany relationship` ) } - - /** - * Check if the other side has defined a mapped by and if that - * mapping is already tracked as the owner. - * - * - If yes, we will inverse our mapped by - * - Otherwise, we will track ourselves as the owner. - */ - if ( - otherSideRelation.parse(mappedBy).mappedBy && - MANY_TO_MANY_TRACKED_RELATIONS[`${relatedModelName}.${mappedBy}`] - ) { - inversedBy = mappedBy - mappedBy = undefined - } else { - MANY_TO_MANY_TRACKED_RELATIONS[ - `${MikroORMEntity.name}.${relationship.name}` - ] = true - } } else { validateManyToManyRelationshipWithoutMappedBy({ MikroORMEntity, relationship, relatedEntity, relatedModelName, + entity, }) } + MANY_TO_MANY_TRACKED_RELATIONS[ + `${MikroORMEntity.name}.${relationship.name}` + ] = true + /** * Validating pivot entity when it is defined and computing * its name @@ -371,6 +428,10 @@ export function defineManyToManyRelationship( } if (!pivotEntityName) { + const { tableName } = parseEntityName(entity) + let tableNameWithoutSchema: string + let relatedTableNameWithoutSchema: string + /** * Pivot table name is created as follows (when not explicitly provided) * @@ -379,20 +440,58 @@ export function defineManyToManyRelationship( * - Converting them from camelCase to snake_case. * - And finally pluralizing the second entity name. */ + + let [schema, ...tableTokens] = tableName.split(".") + if (!tableTokens.length) { + tableNameWithoutSchema = schema + } else { + tableNameWithoutSchema = tableTokens.join(".") + } + + const [relatedSchema, ...relatedTableTokens] = relatedTableName.split(".") + if (!relatedTableTokens.length) { + relatedTableNameWithoutSchema = relatedSchema + } else { + relatedTableNameWithoutSchema = relatedTableTokens.join(".") + } + pivotTableName = relationship.options.pivotTable ?? - [MikroORMEntity.name.toLowerCase(), relatedModelName.toLowerCase()] + otherSideRelationship.parse("").options.pivotTable ?? + [tableNameWithoutSchema, relatedTableNameWithoutSchema] .sort() .map((token, index) => { if (index === 1) { - return pluralize(camelToSnakeCase(token)) + return pluralize(token) } - return camelToSnakeCase(token) + return token }) .join("_") } + const otherSideRelationOptions = otherSideRelationship.parse("").options + + const isOwner = + !!joinColumn || + !!inverseJoinColumn || + !!relationship.options.pivotTable || + /** + * We can't infer it from the current entity so lets + * look at the otherside configuration as well to make a choice + */ + (!otherSideRelationOptions.pivotTable && + !otherSideRelationOptions.joinColumn && + !otherSideRelationOptions.inverseJoinColumn && + !MANY_TO_MANY_TRACKED_RELATIONS[ + `${relatedModelName}.${otherSideRelationshipProperty}` + ]) + + const mappedByProp = isOwner ? "inversedBy" : "mappedBy" + const mappedByPropValue = + mappedBy ?? inversedBy ?? otherSideRelationshipProperty + ManyToMany({ + owner: isOwner, entity: relatedModelName, ...(pivotTableName ? { @@ -402,8 +501,9 @@ export function defineManyToManyRelationship( } : {}), ...(pivotEntityName ? { pivotEntity: pivotEntityName } : {}), - ...(mappedBy ? { mappedBy: mappedBy as any } : {}), - ...(inversedBy ? { inversedBy: inversedBy as any } : {}), + ...({ [mappedByProp]: mappedByPropValue } as any), + ...(joinColumn ? { joinColumn } : {}), + ...(inverseJoinColumn ? { inverseJoinColumn } : {}), })(MikroORMEntity.prototype, relationship.name) } @@ -412,6 +512,7 @@ export function defineManyToManyRelationship( */ export function defineRelationship( MikroORMEntity: EntityConstructor, + entity: DmlEntity, relationship: RelationshipMetadata, cascades: EntityCascades, context: Context @@ -474,6 +575,7 @@ export function defineRelationship( case "belongsTo": defineBelongsToRelationship( MikroORMEntity, + entity, relationship, relatedEntity, relatedEntityInfo @@ -482,6 +584,7 @@ export function defineRelationship( case "manyToMany": defineManyToManyRelationship( MikroORMEntity, + entity, relationship, relatedEntity, relatedEntityInfo, diff --git a/packages/core/utils/src/dml/helpers/mikro-orm/apply-indexes.ts b/packages/core/utils/src/dml/helpers/mikro-orm/apply-indexes.ts index 4acd61c719..f6b43f7da4 100644 --- a/packages/core/utils/src/dml/helpers/mikro-orm/apply-indexes.ts +++ b/packages/core/utils/src/dml/helpers/mikro-orm/apply-indexes.ts @@ -3,7 +3,6 @@ import { EntityIndex, PropertyMetadata, } from "@medusajs/types" -import { MetadataStorage } from "@mikro-orm/core" import { createPsqlIndexStatementHelper } from "../../../common" import { validateIndexFields } from "../mikro-orm/build-indexes" @@ -38,8 +37,7 @@ export function applyEntityIndexes( tableName: string, entityIndexes: EntityIndex[] = [] ) { - const foreignKeyIndexes = applyForeignKeyIndexes(MikroORMEntity) - const indexes = [...entityIndexes, ...foreignKeyIndexes] + const indexes = [...entityIndexes] indexes.forEach((index) => { validateIndexFields(MikroORMEntity, index) @@ -55,29 +53,3 @@ export function applyEntityIndexes( entityIndexStatement.MikroORMIndex()(MikroORMEntity) }) } - -/* - When a "oneToMany" relationship is found on the MikroORM entity, we create an index by default - on the foreign key property. -*/ -function applyForeignKeyIndexes(MikroORMEntity: EntityConstructor) { - const foreignKeyIndexes: EntityIndex[] = [] - - for (const foreignKey of getEntityForeignKeys(MikroORMEntity)) { - foreignKeyIndexes.push({ - on: [foreignKey], - where: "deleted_at IS NULL", - }) - } - - return foreignKeyIndexes -} - -function getEntityForeignKeys(MikroORMEntity: EntityConstructor) { - const properties = - MetadataStorage.getMetadataFromDecorator(MikroORMEntity).properties - - return Object.keys(properties).filter( - (propertyName) => properties[propertyName].isForeignKey - ) -} diff --git a/packages/core/utils/src/dml/integration-tests/__tests__/enum.spec.ts b/packages/core/utils/src/dml/integration-tests/__tests__/enum.spec.ts index dd36df9def..8cbddb5b33 100644 --- a/packages/core/utils/src/dml/integration-tests/__tests__/enum.spec.ts +++ b/packages/core/utils/src/dml/integration-tests/__tests__/enum.spec.ts @@ -4,7 +4,10 @@ import { MikroORM, } from "@mikro-orm/core" import { model } from "../../entity-builder" -import { toMikroOrmEntities } from "../../helpers/create-mikro-orm-entity" +import { + mikroORMEntityBuilder, + toMikroOrmEntities, +} from "../../helpers/create-mikro-orm-entity" import { createDatabase, dropDatabase } from "pg-god" import { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal" import { EntityConstructor } from "@medusajs/types" @@ -28,6 +31,7 @@ describe("EntityBuilder | enum", () => { beforeEach(async () => { MetadataStorage.clear() + mikroORMEntityBuilder.clear() const user = model.define("user", { id: model.id().primaryKey(), diff --git a/packages/core/utils/src/dml/integration-tests/__tests__/has-one-belongs-to.spec.ts b/packages/core/utils/src/dml/integration-tests/__tests__/has-one-belongs-to.spec.ts index 17a8ea6b2f..eab1935f02 100644 --- a/packages/core/utils/src/dml/integration-tests/__tests__/has-one-belongs-to.spec.ts +++ b/packages/core/utils/src/dml/integration-tests/__tests__/has-one-belongs-to.spec.ts @@ -1,6 +1,9 @@ import { MetadataStorage, MikroORM } from "@mikro-orm/core" import { model } from "../../entity-builder" -import { toMikroOrmEntities } from "../../helpers/create-mikro-orm-entity" +import { + mikroORMEntityBuilder, + toMikroOrmEntities, +} from "../../helpers/create-mikro-orm-entity" import { createDatabase, dropDatabase } from "pg-god" import { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal" import { EntityConstructor } from "@medusajs/types" @@ -24,6 +27,7 @@ describe("hasOne - belongTo", () => { beforeEach(async () => { MetadataStorage.clear() + mikroORMEntityBuilder.clear() const team = model.define("team", { id: model.id().primaryKey(), diff --git a/packages/core/utils/src/dml/integration-tests/__tests__/many-to-many.spec.ts b/packages/core/utils/src/dml/integration-tests/__tests__/many-to-many.spec.ts index 36e47099c5..cfece63b5c 100644 --- a/packages/core/utils/src/dml/integration-tests/__tests__/many-to-many.spec.ts +++ b/packages/core/utils/src/dml/integration-tests/__tests__/many-to-many.spec.ts @@ -1,12 +1,17 @@ +import { join } from "path" import { MetadataStorage, MikroORM } from "@mikro-orm/core" import { model } from "../../entity-builder" -import { toMikroOrmEntities } from "../../helpers/create-mikro-orm-entity" +import { + mikroORMEntityBuilder, + toMikroOrmEntities, +} from "../../helpers/create-mikro-orm-entity" import { createDatabase, dropDatabase } from "pg-god" import { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal" import { EntityConstructor } from "@medusajs/types" import { pgGodCredentials } from "../utils" import { FileSystem } from "../../../common" -import { join } from "path" + +jest.setTimeout(30000) export const fileSystem = new FileSystem( join(__dirname, "../../integration-tests-migrations-many-to-many") @@ -27,6 +32,7 @@ describe("manyToMany - manyToMany", () => { beforeEach(async () => { MetadataStorage.clear() + mikroORMEntityBuilder.clear() const team = model.define("team", { id: model.id().primaryKey(), @@ -191,43 +197,108 @@ describe("manyToMany - manyToMany", () => { }) }) - it(`should fail to load the dml's if both side of the relation are missing the mappedBy options`, () => { + it(`should not fail to load the dml's if both side of the relation are missing the mappedBy options`, () => { + mikroORMEntityBuilder.clear() + const team = model.define("team", { id: model.id().primaryKey(), name: model.text(), users: model.manyToMany(() => user, { - pivotEntity: () => squad, + pivot_table: "team_users", }), }) - const squad = model.define("teamUsers", { + const user = model.define("user", { id: model.id().primaryKey(), - user: model.belongsTo(() => user, { mappedBy: "squads" }), - squad: model.belongsTo(() => team, { mappedBy: "users" }), + username: model.text(), + squads: model.manyToMany(() => team), + }) + + ;[User, Team] = toMikroOrmEntities([user, team]) + + const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team) + expect((teamMetaData.properties as any).users.mappedBy).toBe("squads") + expect((teamMetaData.properties as any).users.owner).toBe(false) + + const userMetaData = MetadataStorage.getMetadataFromDecorator(User) + expect((userMetaData.properties as any).squads.mappedBy).not.toBeDefined() + expect((userMetaData.properties as any).squads.inversedBy).toBe("users") + expect((userMetaData.properties as any).squads.owner).toBe(true) + }) + + it(`should load the dml's correclty when both side of the relation are specifying the mappedBy options without pivot table`, () => { + mikroORMEntityBuilder.clear() + + const team = model.define("team", { + id: model.id().primaryKey(), + name: model.text(), + users: model.manyToMany(() => user, { + mappedBy: "squads", + }), }) const user = model.define("user", { id: model.id().primaryKey(), username: model.text(), squads: model.manyToMany(() => team, { - pivotEntity: () => squad, + mappedBy: "users", }), }) + let [User, Team] = toMikroOrmEntities([user, team]) + + const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team) + expect((teamMetaData.properties as any).users.mappedBy).toBe("squads") + expect((teamMetaData.properties as any).users.owner).toBe(false) + expect((teamMetaData.properties as any).users.pivotTable).toBe("team_users") + + const userMetaData = MetadataStorage.getMetadataFromDecorator(User) + expect((userMetaData.properties as any).squads.mappedBy).not.toBeDefined() + expect((userMetaData.properties as any).squads.inversedBy).toBe("users") + expect((userMetaData.properties as any).squads.owner).toBe(true) + expect((userMetaData.properties as any).squads.pivotTable).toBe( + "team_users" + ) + }) + + it(`should fail to load the dml's if both side of the relation are missing the mappedBy options and multiple relations points to the same entity`, () => { + mikroORMEntityBuilder.clear() + + const team = model.define("team", { + id: model.id().primaryKey(), + name: model.text(), + users: model.manyToMany(() => user, { + pivot_table: "team_users", + }), + users2: model.manyToMany(() => user, { + pivot_table: "team_users2", + }), + }) + + const user = model.define("user", { + id: model.id().primaryKey(), + username: model.text(), + squads: model.manyToMany(() => team), + squads2: model.manyToMany(() => team), + }) + let error!: Error + try { - ;[User, Squad, Team] = toMikroOrmEntities([user, squad, team]) + ;[User, Team] = toMikroOrmEntities([user, team]) } catch (e) { error = e } - expect(error).toBeTruthy() - expect(error.message).toEqual( - 'Invalid relationship reference for "User.squads". "mappedBy" should be defined on one side or the other.' + expect(error).toBeDefined() + expect(error?.message).toEqual( + 'Invalid relationship reference for "user.squads". Make sure to set the mappedBy property on one side or the other or both.' ) }) it(`should fail to load the dml's if the relation is defined only on one side`, () => { + mikroORMEntityBuilder.clear() + const team = model.define("team", { id: model.id().primaryKey(), name: model.text(), @@ -248,7 +319,7 @@ describe("manyToMany - manyToMany", () => { expect(error).toBeTruthy() expect(error.message).toEqual( - 'Invalid relationship reference for "Team.users". The other side of the relationship is missing.' + 'Invalid relationship reference for "Team.users". "mappedBy" should be defined on one side or the other.' ) }) }) diff --git a/packages/core/utils/src/dml/integration-tests/__tests__/many-to-one.spec.ts b/packages/core/utils/src/dml/integration-tests/__tests__/many-to-one.spec.ts index 07a4c7cf1e..a44ce4826d 100644 --- a/packages/core/utils/src/dml/integration-tests/__tests__/many-to-one.spec.ts +++ b/packages/core/utils/src/dml/integration-tests/__tests__/many-to-one.spec.ts @@ -1,6 +1,9 @@ import { MetadataStorage, MikroORM } from "@mikro-orm/core" import { model } from "../../entity-builder" -import { toMikroOrmEntities } from "../../helpers/create-mikro-orm-entity" +import { + mikroORMEntityBuilder, + toMikroOrmEntities, +} from "../../helpers/create-mikro-orm-entity" import { createDatabase, dropDatabase } from "pg-god" import { CustomTsMigrationGenerator, @@ -30,6 +33,7 @@ describe("manyToOne - belongTo", () => { beforeEach(async () => { MetadataStorage.clear() + mikroORMEntityBuilder.clear() const team = model.define("team", { id: model.id().primaryKey(), diff --git a/packages/core/utils/src/migrations/integration-tests/__tests__/migrations-generate.spec.ts b/packages/core/utils/src/migrations/integration-tests/__tests__/migrations-generate.spec.ts index b6fb09f245..1005cc6e83 100644 --- a/packages/core/utils/src/migrations/integration-tests/__tests__/migrations-generate.spec.ts +++ b/packages/core/utils/src/migrations/integration-tests/__tests__/migrations-generate.spec.ts @@ -4,9 +4,11 @@ import { MetadataStorage } from "@mikro-orm/core" import { Migrations } from "../../index" import { FileSystem } from "../../../common" -import { DmlEntity, model } from "../../../dml" +import { DmlEntity, mikroORMEntityBuilder, model } from "../../../dml" import { defineMikroOrmCliConfig } from "../../../modules-sdk" +jest.setTimeout(30000) + const DB_HOST = process.env.DB_HOST ?? "localhost" const DB_USERNAME = process.env.DB_USERNAME ?? "" const DB_PASSWORD = process.env.DB_PASSWORD ?? " " @@ -29,7 +31,8 @@ describe("Generate migrations", () => { afterEach(async () => { await fs.cleanup() MetadataStorage.clear() - }, 300 * 1000) + mikroORMEntityBuilder.clear() + }) test("generate migrations for a single entity", async () => { const User = model.define("User", { diff --git a/packages/core/utils/src/modules-sdk/__tests__/joiner-config-builder.spec.ts b/packages/core/utils/src/modules-sdk/__tests__/joiner-config-builder.spec.ts index a0cfdd5450..e7e93dfc5d 100644 --- a/packages/core/utils/src/modules-sdk/__tests__/joiner-config-builder.spec.ts +++ b/packages/core/utils/src/modules-sdk/__tests__/joiner-config-builder.spec.ts @@ -282,7 +282,7 @@ describe("joiner-config-builder", () => { }) }) - it.only("should return a full joiner configuration with custom aliases overriding defaults", () => { + it("should return a full joiner configuration with custom aliases overriding defaults", () => { const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, { models: [FulfillmentSet], alias: [ diff --git a/packages/core/utils/src/modules-sdk/build-query.ts b/packages/core/utils/src/modules-sdk/build-query.ts index 63da37fa6e..3dbbd22924 100644 --- a/packages/core/utils/src/modules-sdk/build-query.ts +++ b/packages/core/utils/src/modules-sdk/build-query.ts @@ -1,4 +1,4 @@ -import { DAL, FindConfig } from "@medusajs/types" +import { DAL, FindConfig, InferRepositoryReturnType } from "@medusajs/types" import { deduplicate, isObject } from "../common" import { SoftDeletableFilterKey } from "../dal/mikro-orm/mikro-orm-soft-deletable-filter" @@ -10,17 +10,19 @@ type FilterFlags = { withDeleted?: boolean } -export function buildQuery( +export function buildQuery( filters: Record = {}, - config: FindConfig & { primaryKeyFields?: string | string[] } = {} + config: FindConfig> & { + primaryKeyFields?: string | string[] + } = {} ): Required> { - const where: DAL.FilterQuery = {} + const where = {} as DAL.FilterQuery const filterFlags: FilterFlags = {} buildWhere(filters, where, filterFlags) delete config.primaryKeyFields - const findOptions: DAL.OptionsQuery = { + const findOptions: DAL.FindOptions["options"] = { populate: deduplicate(config.relations ?? []), fields: config.select as string[], limit: (Number.isSafeInteger(config.take) && config.take) || undefined, @@ -28,7 +30,9 @@ export function buildQuery( } if (config.order) { - findOptions.orderBy = config.order as DAL.OptionsQuery["orderBy"] + findOptions.orderBy = config.order as Required< + DAL.FindOptions + >["options"]["orderBy"] } if (config.withDeleted || filterFlags.withDeleted) { @@ -50,7 +54,7 @@ export function buildQuery( Object.assign(findOptions, config.options) } - return { where, options: findOptions } + return { where, options: findOptions } as Required> } function buildWhere( diff --git a/packages/core/utils/src/modules-sdk/define-link.ts b/packages/core/utils/src/modules-sdk/define-link.ts index 7d215b3234..6ecc1c432d 100644 --- a/packages/core/utils/src/modules-sdk/define-link.ts +++ b/packages/core/utils/src/modules-sdk/define-link.ts @@ -314,6 +314,7 @@ ${serviceBObj.module}: { const isModuleAPrimaryKeyValid = moduleAPrimaryKeys.includes(serviceAPrimaryKey) + if (!isModuleAPrimaryKeyValid) { throw new Error( `Primary key ${serviceAPrimaryKey} is not defined on service ${serviceAObj.module}` diff --git a/packages/core/utils/src/modules-sdk/joiner-config-builder.ts b/packages/core/utils/src/modules-sdk/joiner-config-builder.ts index ba80eb9033..8c5809b6fb 100644 --- a/packages/core/utils/src/modules-sdk/joiner-config-builder.ts +++ b/packages/core/utils/src/modules-sdk/joiner-config-builder.ts @@ -9,7 +9,6 @@ import * as path from "path" import { dirname, join, normalize } from "path" import { camelToSnakeCase, - deduplicate, getCallerFilePath, isObject, lowerCaseFirst, @@ -165,29 +164,40 @@ export function defineJoinerConfig( } linkableKeys = mergedLinkableKeys - if (!primaryKeys && modelDefinitions.size) { + /** + * Merge custom primary keys from the joiner config with the infered primary keys + * from the models. + * + * TODO: Maybe worth looking into the real needs for primary keys. + * It can happen that we could just remove that but we need to investigate (looking at the + * lookups from the remote joiner to identify which entity a property refers to) + */ + primaryKeys ??= [] + const finalPrimaryKeys = new Set(primaryKeys) + if (modelDefinitions.size) { const linkConfig = buildLinkConfigFromModelObjects( serviceName, Object.fromEntries(modelDefinitions) ) - primaryKeys = deduplicate( - Object.values(linkConfig).flatMap((entityLinkConfig) => { - return (Object.values(entityLinkConfig as any) as any[]) - .filter((linkableConfig) => isObject(linkableConfig)) - .map((linkableConfig) => { - // @ts-ignore - return linkableConfig.primaryKey - }) - }) - ) + Object.values(linkConfig).flatMap((entityLinkConfig) => { + return Object.values( + entityLinkConfig as Record + ) + .filter((linkableConfig) => isObject(linkableConfig)) + .forEach((linkableConfig) => { + finalPrimaryKeys.add(linkableConfig.primaryKey) + }) + }) } + primaryKeys = Array.from(finalPrimaryKeys.add("id")) + // TODO: In the context of DML add a validation on primary keys and linkable keys if the consumer provide them manually. follow up pr return { serviceName, - primaryKeys: primaryKeys ?? ["id"], + primaryKeys, schema, linkableKeys: linkableKeys, alias: [ @@ -342,26 +352,44 @@ export function buildLinkableKeysFromMikroOrmObjects( export function buildLinkConfigFromModelObjects< const ServiceName extends string, const T extends Record> ->(serviceName: ServiceName, models: T): InfersLinksConfig { +>( + serviceName: ServiceName, + models: T, + linkableKeys: Record = {} +): InfersLinksConfig { + // In case some models have been provided to a custom joiner config, the linkable will be limited + // to that set of models. We dont want to expose models that should not be linkable. + const linkableModels = Object.values(linkableKeys) const linkConfig = {} as InfersLinksConfig for (const model of Object.values(models) ?? []) { - if (!DmlEntity.isDmlEntity(model)) { + const classLikeModelName = upperCaseFirst(model.name) + + if ( + !DmlEntity.isDmlEntity(model) || + (linkableModels.length && !linkableModels.includes(classLikeModelName)) + ) { continue } const schema = model.schema - // @ts-ignore + + /** + * When using a linkable, if a specific linkable property is not specified, the toJSON + * function will be called and return the first linkable available for this model. + */ const modelLinkConfig = (linkConfig[lowerCaseFirst(model.name)] ??= { toJSON: function () { const linkables = Object.entries(this) .filter(([name]) => name !== "toJSON") .map(([, object]) => object) - const lastIndex = linkables.length - 1 - return linkables[lastIndex] + return linkables[0] }, }) + /** + * Build all linkable properties for the model + */ for (const [property, value] of Object.entries(schema)) { if (BaseRelationship.isRelationship(value)) { continue @@ -378,10 +406,46 @@ export function buildLinkConfigFromModelObjects< primaryKey: property, serviceName, field: lowerCaseFirst(model.name), - entity: upperCaseFirst(model.name), + entity: classLikeModelName, } } } + + /** + * If the joiner config specify some custom linkable keys, we merge them with the + * existing linkable keys infered from the model above. + */ + const linkableKeysPerModel = Object.entries(linkableKeys).reduce( + (acc, [key, entityName]) => { + acc[entityName] ??= [] + acc[entityName].push(key) + return acc + }, + {} + ) + + for (const linkableKey of linkableKeysPerModel[classLikeModelName] ?? []) { + const snakeCasedModelName = camelToSnakeCase(toCamelCase(model.name)) + + // Linkable keys by default are prepared with snake cased model name _id + // So to be able to compare only the property we have to remove the first part + const inferredReferenceProperty = linkableKey.replace( + `${snakeCasedModelName}_`, + "" + ) + + if (modelLinkConfig[inferredReferenceProperty]) { + continue + } + + modelLinkConfig[linkableKey] = { + linkable: linkableKey, + primaryKey: linkableKey, + serviceName, + field: lowerCaseFirst(model.name), + entity: upperCaseFirst(model.name), + } + } } return linkConfig as InfersLinksConfig diff --git a/packages/core/utils/src/modules-sdk/medusa-internal-service.ts b/packages/core/utils/src/modules-sdk/medusa-internal-service.ts index cdd358c38e..3689869adb 100644 --- a/packages/core/utils/src/modules-sdk/medusa-internal-service.ts +++ b/packages/core/utils/src/modules-sdk/medusa-internal-service.ts @@ -2,9 +2,9 @@ import { BaseFilterable, Context, FilterQuery, + FilterQuery as InternalFilterQuery, FindConfig, InferEntityType, - FilterQuery as InternalFilterQuery, ModulesSdkTypes, PerformedActions, UpsertWithReplaceConfig, @@ -62,7 +62,7 @@ export function MedusaInternalService< } static applyFreeTextSearchFilter( - filters: FilterQuery, + filters: FilterQuery & { q?: string }, config: FindConfig ): void { if (isDefined(filters?.q)) { diff --git a/packages/core/utils/src/modules-sdk/module.ts b/packages/core/utils/src/modules-sdk/module.ts index 7e995957da..3f96a94315 100644 --- a/packages/core/utils/src/modules-sdk/module.ts +++ b/packages/core/utils/src/modules-sdk/module.ts @@ -49,15 +49,22 @@ export function Module< DmlEntity.isDmlEntity(model) ) + // TODO: Custom joiner config should take precedence over the DML auto generated linkable + // Thats in the case of manually providing models in custom joiner config. + // TODO: Add support for non linkable modifier DML object to be skipped from the linkable generation + + const linkableKeys = service.prototype.__joinerConfig().linkableKeys + if (dmlObjects.length) { linkable = buildLinkConfigFromModelObjects( serviceName, - modelObjects + modelObjects, + linkableKeys ) as Linkable } else { linkable = buildLinkConfigFromLinkableKeys( serviceName, - service.prototype.__joinerConfig().linkableKeys + linkableKeys ) as Linkable } } diff --git a/packages/modules/order/src/repositories/claim.ts b/packages/modules/order/src/repositories/claim.ts index b24c674ec1..142528f407 100644 --- a/packages/modules/order/src/repositories/claim.ts +++ b/packages/modules/order/src/repositories/claim.ts @@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils" import { OrderClaim } from "@models" import { setFindMethods } from "../utils/base-repository-find" -export class OrderClaimRepository extends DALUtils.mikroOrmBaseRepositoryFactory( +export class OrderClaimRepository extends DALUtils.mikroOrmBaseRepositoryFactory( OrderClaim ) {} diff --git a/packages/modules/order/src/repositories/exchange.ts b/packages/modules/order/src/repositories/exchange.ts index f2d7d82281..915552bdf9 100644 --- a/packages/modules/order/src/repositories/exchange.ts +++ b/packages/modules/order/src/repositories/exchange.ts @@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils" import { OrderExchange } from "@models" import { setFindMethods } from "../utils/base-repository-find" -export class OrderExchangeRepository extends DALUtils.mikroOrmBaseRepositoryFactory( +export class OrderExchangeRepository extends DALUtils.mikroOrmBaseRepositoryFactory( OrderExchange ) {} diff --git a/packages/modules/order/src/repositories/order.ts b/packages/modules/order/src/repositories/order.ts index 63e98c58e2..84fd45ebc7 100644 --- a/packages/modules/order/src/repositories/order.ts +++ b/packages/modules/order/src/repositories/order.ts @@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils" import { Order } from "@models" import { setFindMethods } from "../utils/base-repository-find" -export class OrderRepository extends DALUtils.mikroOrmBaseRepositoryFactory( +export class OrderRepository extends DALUtils.mikroOrmBaseRepositoryFactory( Order ) {} diff --git a/packages/modules/order/src/repositories/return.ts b/packages/modules/order/src/repositories/return.ts index 9f5c760d8b..00f407a819 100644 --- a/packages/modules/order/src/repositories/return.ts +++ b/packages/modules/order/src/repositories/return.ts @@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils" import { Return } from "@models" import { setFindMethods } from "../utils/base-repository-find" -export class ReturnRepository extends DALUtils.mikroOrmBaseRepositoryFactory( +export class ReturnRepository extends DALUtils.mikroOrmBaseRepositoryFactory( Return ) {} diff --git a/packages/modules/order/src/utils/base-repository-find.ts b/packages/modules/order/src/utils/base-repository-find.ts index 7bb9a5cbf0..7055f57659 100644 --- a/packages/modules/order/src/utils/base-repository-find.ts +++ b/packages/modules/order/src/utils/base-repository-find.ts @@ -118,7 +118,7 @@ export function setFindMethods(klass: Constructor, entity: any) { klass.prototype.findAndCount = async function findAndCount( this: any, - findOptions: DAL.FindOptions = { where: {} }, + findOptions: DAL.FindOptions = { where: {} } as DAL.FindOptions, context: Context = {} ): Promise<[T[], number]> { const manager = this.getActiveManager(context) diff --git a/packages/modules/product/integration-tests/__fixtures__/product-category/data/index.ts b/packages/modules/product/integration-tests/__fixtures__/product-category/data/index.ts index 43ef9965e7..ee27eed260 100644 --- a/packages/modules/product/integration-tests/__fixtures__/product-category/data/index.ts +++ b/packages/modules/product/integration-tests/__fixtures__/product-category/data/index.ts @@ -1,3 +1,5 @@ +import { kebabCase } from "@medusajs/framework/utils" + export const productCategoriesData = [ { id: "category-0", @@ -25,7 +27,12 @@ export const productCategoriesData = [ name: "category 1 b 1", parent_category_id: "category-1-b", }, -] +].map((entry) => { + return { + handle: kebabCase(entry.name), + ...entry, + } +}) export const productCategoriesRankData = [ { @@ -64,7 +71,12 @@ export const productCategoriesRankData = [ parent_category_id: "category-0-0", rank: 2, }, -] +].map((entry) => { + return { + handle: kebabCase(entry.name), + ...entry, + } +}) export const eletronicsCategoriesData = [ { @@ -188,4 +200,9 @@ export const eletronicsCategoriesData = [ name: "Mini Models", parent_category_id: "iphones", }, -] +].map((entry) => { + return { + handle: kebabCase(entry.name), + ...entry, + } +}) diff --git a/packages/modules/product/integration-tests/__fixtures__/product/data/categories.ts b/packages/modules/product/integration-tests/__fixtures__/product/data/categories.ts index 65bb62c5ad..fd51ee3cd8 100644 --- a/packages/modules/product/integration-tests/__fixtures__/product/data/categories.ts +++ b/packages/modules/product/integration-tests/__fixtures__/product/data/categories.ts @@ -2,16 +2,19 @@ export const categoriesData = [ { id: "category-0", name: "category 0", + handle: "category-0", parent_category_id: null, }, { id: "category-1", name: "category 1", + handle: "category-1", parent_category_id: "category-0", }, { id: "category-1-a", name: "category 1 a", + handle: "category-1-a", parent_category_id: "category-1", }, ] diff --git a/packages/modules/product/integration-tests/__fixtures__/product/data/create-product.ts b/packages/modules/product/integration-tests/__fixtures__/product/data/create-product.ts index e62291063f..2a6da5a2ca 100644 --- a/packages/modules/product/integration-tests/__fixtures__/product/data/create-product.ts +++ b/packages/modules/product/integration-tests/__fixtures__/product/data/create-product.ts @@ -1,10 +1,11 @@ import { ProductTypes } from "@medusajs/framework/types" -import { ProductStatus } from "@medusajs/framework/utils" -import { Image } from "@models" +import { ProductStatus, toHandle } from "@medusajs/framework/utils" +import { ProductImage } from "@models" import faker from "faker" export const buildProductOnlyData = ({ title, + handle, description, subtitle, is_giftcard, @@ -14,6 +15,7 @@ export const buildProductOnlyData = ({ status, }: { title?: string + handle?: string description?: string subtitle?: string is_giftcard?: boolean @@ -22,15 +24,17 @@ export const buildProductOnlyData = ({ images?: { id?: string; url: string }[] status?: ProductStatus } = {}) => { + title ??= faker.commerce.productName() return { - title: title ?? faker.commerce.productName(), + title: title as string, + handle: handle ?? toHandle(title!), description: description ?? faker.commerce.productName(), subtitle: subtitle ?? faker.commerce.productName(), is_giftcard: is_giftcard ?? false, discountable: discountable ?? true, thumbnail: thumbnail as string, status: status ?? ProductStatus.PUBLISHED, - images: (images ?? []) as Image[], + images: (images ?? []) as ProductImage[], } } @@ -48,7 +52,7 @@ export const buildProductAndRelationsData = ({ options, variants, collection_id, -}: Partial) => { +}: Partial & { tags: { value: string }[] }) => { const defaultOptionTitle = "test-option" const defaultOptionValue = "test-value" @@ -60,7 +64,7 @@ export const buildProductAndRelationsData = ({ discountable: discountable ?? true, thumbnail: thumbnail as string, status: status ?? ProductStatus.PUBLISHED, - images: (images ?? []) as Image[], + images: (images ?? []) as ProductImage[], type_id, tags: tags ?? [{ value: "tag-1" }], collection_id, diff --git a/packages/modules/product/integration-tests/__fixtures__/product/data/products.ts b/packages/modules/product/integration-tests/__fixtures__/product/data/products.ts index 56556967ee..a4f3485777 100644 --- a/packages/modules/product/integration-tests/__fixtures__/product/data/products.ts +++ b/packages/modules/product/integration-tests/__fixtures__/product/data/products.ts @@ -4,6 +4,7 @@ export const productsData = [ { id: "test-1", title: "product 1", + handle: "product-1", status: ProductStatus.PUBLISHED, tags: [ { @@ -15,6 +16,7 @@ export const productsData = [ { id: "test-2", title: "product", + handle: "product", status: ProductStatus.PUBLISHED, tags: [ { @@ -26,6 +28,7 @@ export const productsData = [ { id: "test-3", title: "product 3", + handle: "product-3", status: ProductStatus.PUBLISHED, tags: [ { diff --git a/packages/modules/product/integration-tests/__fixtures__/product/index.ts b/packages/modules/product/integration-tests/__fixtures__/product/index.ts index 6b965abc86..0fa2ddfedf 100644 --- a/packages/modules/product/integration-tests/__fixtures__/product/index.ts +++ b/packages/modules/product/integration-tests/__fixtures__/product/index.ts @@ -1,4 +1,8 @@ -import { ProductStatus } from "@medusajs/framework/utils" +import { + kebabCase, + ProductStatus, + toMikroORMEntity, +} from "@medusajs/framework/utils" import { SqlEntityManager } from "@mikro-orm/postgresql" import { Image, @@ -10,6 +14,7 @@ import { } from "@models" import ProductOption from "../../../src/models/product-option" +import { InferEntityType } from "@medusajs/types" export * from "./data/create-product" @@ -24,7 +29,7 @@ export async function createProductAndTags( }[] ) { const products: any[] = data.map((productData) => { - return manager.create(Product, productData) + return manager.create(toMikroORMEntity(Product), productData) }) await manager.persistAndFlush(products) @@ -42,7 +47,7 @@ export async function createProductAndTypes( }[] ) { const products: any[] = data.map((productData) => { - return manager.create(Product, productData) + return manager.create(toMikroORMEntity(Product), productData) }) await manager.persistAndFlush(products) @@ -55,7 +60,7 @@ export async function createProductVariants( data: any[] ) { const variants: any[] = data.map((variantsData) => { - return manager.create(ProductVariant, variantsData) + return manager.create(toMikroORMEntity(ProductVariant), variantsData) }) await manager.persistAndFlush(variants) @@ -72,7 +77,10 @@ export async function createCollections( }[] ) { const collections: any[] = collectionData.map((collectionData) => { - return manager.create(ProductCollection, collectionData) + if (!collectionData.handle && collectionData.title) { + collectionData.handle = kebabCase(collectionData.title) + } + return manager.create(toMikroORMEntity(ProductCollection), collectionData) }) await manager.persistAndFlush(collections) @@ -88,7 +96,7 @@ export async function createTypes( }[] ) { const types: any[] = typesData.map((typesData) => { - return manager.create(ProductType, typesData) + return manager.create(toMikroORMEntity(ProductType), typesData) }) await manager.persistAndFlush(types) @@ -112,7 +120,7 @@ export async function createOptions( }[] ) { const options: any[] = optionsData.map((option) => { - return manager.create(ProductOption, option) + return manager.create(toMikroORMEntity(ProductOption), option) }) await manager.persistAndFlush(options) @@ -125,7 +133,7 @@ export async function createImages( imagesData: string[] ) { const images: any[] = imagesData.map((img) => { - return manager.create(Image, { url: img }) + return manager.create(toMikroORMEntity(Image), { url: img }) }) await manager.persistAndFlush(images) @@ -135,8 +143,8 @@ export async function createImages( export async function assignCategoriesToProduct( manager: SqlEntityManager, - product: Product, - categories: ProductCategory[] + product: InferEntityType, + categories: InferEntityType[] ) { product.categories.add(categories) diff --git a/packages/modules/product/integration-tests/__tests__/product-category.ts b/packages/modules/product/integration-tests/__tests__/product-category.ts index 0adaabf2f7..38a0d3b07a 100644 --- a/packages/modules/product/integration-tests/__tests__/product-category.ts +++ b/packages/modules/product/integration-tests/__tests__/product-category.ts @@ -175,7 +175,7 @@ moduleIntegrationTestRunner({ include_descendants_tree: true, }, { - select: ["id", "handle"], + select: ["id", "handle", "rank"], } ) @@ -190,12 +190,14 @@ moduleIntegrationTestRunner({ mpath: "category-0.category-1.category-1-a", parent_category_id: "category-1", category_children: [], + rank: 0, }, { id: "category-1-b", handle: "category-1-b", mpath: "category-0.category-1.category-1-b", parent_category_id: "category-1", + rank: 1, category_children: [ expect.objectContaining({ id: "category-1-b-1", @@ -220,7 +222,7 @@ moduleIntegrationTestRunner({ include_ancestors_tree: true, }, { - select: ["id", "handle"], + select: ["id", "handle", "rank"], } ) @@ -235,8 +237,10 @@ moduleIntegrationTestRunner({ mpath: "electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming", parent_category_id: "high-performance", + rank: 1, parent_category: expect.objectContaining({ id: "high-performance", + rank: 1, parent_category_id: "gaming-laptops", handle: "high-performance-gaming-laptops", mpath: @@ -245,6 +249,7 @@ moduleIntegrationTestRunner({ id: "gaming-laptops", handle: "gaming-laptops", mpath: "electronics.computers.laptops.gaming-laptops", + rank: 0, parent_category_id: "laptops", parent_category: expect.objectContaining({ id: "laptops", @@ -282,7 +287,7 @@ moduleIntegrationTestRunner({ include_descendants_tree: true, }, { - select: ["id", "handle"], + select: ["id", "handle", "rank"], } ) @@ -296,6 +301,7 @@ moduleIntegrationTestRunner({ handle: "gaming-laptops", mpath: "electronics.computers.laptops.gaming-laptops", parent_category_id: "laptops", + rank: 0, category_children: [ expect.objectContaining({ id: "budget-gaming", @@ -304,6 +310,7 @@ moduleIntegrationTestRunner({ "electronics.computers.laptops.gaming-laptops.budget-gaming", parent_category_id: "gaming-laptops", category_children: [], + rank: 0, }), expect.objectContaining({ id: "high-performance", @@ -311,6 +318,7 @@ moduleIntegrationTestRunner({ mpath: "electronics.computers.laptops.gaming-laptops.high-performance", parent_category_id: "gaming-laptops", + rank: 1, category_children: expect.arrayContaining([ expect.objectContaining({ id: "vr-ready", @@ -319,6 +327,7 @@ moduleIntegrationTestRunner({ "electronics.computers.laptops.gaming-laptops.high-performance.vr-ready", parent_category_id: "high-performance", category_children: [], + rank: 0, }), expect.objectContaining({ id: "4k-gaming", @@ -327,6 +336,7 @@ moduleIntegrationTestRunner({ "electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming", parent_category_id: "high-performance", category_children: [], + rank: 1, }), ]), }), @@ -407,7 +417,7 @@ moduleIntegrationTestRunner({ include_ancestors_tree: true, }, { - select: ["id", "handle"], + select: ["id", "handle", "rank"], } ) @@ -421,12 +431,15 @@ moduleIntegrationTestRunner({ handle: "category-1-a", mpath: "category-0.category-1.category-1-a", parent_category_id: "category-1", + rank: 0, parent_category: expect.objectContaining({ id: "category-1", handle: "category-1", mpath: "category-0.category-1", parent_category_id: "category-0", + rank: 0, parent_category: expect.objectContaining({ + rank: 0, id: "category-0", handle: "category-0", mpath: "category-0", @@ -440,17 +453,20 @@ moduleIntegrationTestRunner({ handle: "category-1-b", mpath: "category-0.category-1.category-1-b", parent_category_id: "category-1", + rank: 1, parent_category: expect.objectContaining({ id: "category-1", handle: "category-1", mpath: "category-0.category-1", parent_category_id: "category-0", + rank: 0, parent_category: expect.objectContaining({ id: "category-0", handle: "category-0", mpath: "category-0", parent_category_id: null, parent_category: null, + rank: 0, }), }), }, @@ -465,7 +481,7 @@ moduleIntegrationTestRunner({ include_ancestors_tree: true, }, { - select: ["id", "handle"], + select: ["id", "handle", "rank"], } ) @@ -475,17 +491,20 @@ moduleIntegrationTestRunner({ handle: "category-1-a", mpath: "category-0.category-1.category-1-a", parent_category_id: "category-1", + rank: 0, parent_category: expect.objectContaining({ id: "category-1", handle: "category-1", mpath: "category-0.category-1", parent_category_id: "category-0", + rank: 0, parent_category: expect.objectContaining({ id: "category-0", handle: "category-0", mpath: "category-0", parent_category_id: null, parent_category: null, + rank: 0, }), }), category_children: [], @@ -495,17 +514,20 @@ moduleIntegrationTestRunner({ handle: "category-1-b", mpath: "category-0.category-1.category-1-b", parent_category_id: "category-1", + rank: 1, parent_category: expect.objectContaining({ id: "category-1", handle: "category-1", mpath: "category-0.category-1", parent_category_id: "category-0", + rank: 0, parent_category: expect.objectContaining({ id: "category-0", handle: "category-0", mpath: "category-0", parent_category_id: null, parent_category: null, + rank: 0, }), }), category_children: [ @@ -514,6 +536,7 @@ moduleIntegrationTestRunner({ handle: "category-1-b-1", mpath: "category-0.category-1.category-1-b.category-1-b-1", parent_category_id: "category-1-b", + rank: 0, }), ], }, @@ -879,6 +902,7 @@ moduleIntegrationTestRunner({ await service.create([ { name: "New Category", + handle: "new-category", parent_category_id: null, }, ]) @@ -904,11 +928,13 @@ moduleIntegrationTestRunner({ await service.create([ { name: "New Category", + handle: "new-category", parent_category_id: null, rank: 0, }, { name: "New Category 2", + handle: "new-category-2", parent_category_id: null, }, ]) @@ -932,6 +958,7 @@ moduleIntegrationTestRunner({ await service.create([ { name: "New Category 2.1", + handle: "new-category-2-1", parent_category_id: productCategoryNew.id, }, ]) diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/product-categories.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/product-categories.spec.ts index 210a2c42f6..d3016d4734 100644 --- a/packages/modules/product/integration-tests/__tests__/product-module-service/product-categories.spec.ts +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/product-categories.spec.ts @@ -5,6 +5,7 @@ import { Modules, ProductEvents, ProductStatus, + toMikroORMEntity, } from "@medusajs/framework/utils" import { Product, ProductCategory } from "@models" import { @@ -31,15 +32,17 @@ moduleIntegrationTestRunner({ beforeEach(async () => { const testManager = await MikroOrmWrapper.forkManager() - productOne = testManager.create(Product, { + productOne = testManager.create(toMikroORMEntity(Product), { id: "product-1", title: "product 1", + handle: "product-1", status: ProductStatus.PUBLISHED, }) - productTwo = testManager.create(Product, { + productTwo = testManager.create(toMikroORMEntity(Product), { id: "product-2", title: "product 2", + handle: "product-2", status: ProductStatus.PUBLISHED, }) diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/product-collections.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/product-collections.spec.ts index 2e4fa2bd62..73d761b715 100644 --- a/packages/modules/product/integration-tests/__tests__/product-module-service/product-collections.spec.ts +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/product-collections.spec.ts @@ -5,6 +5,7 @@ import { Modules, ProductEvents, ProductStatus, + toMikroORMEntity, } from "@medusajs/framework/utils" import { Product, ProductCollection } from "@models" import { @@ -31,15 +32,17 @@ moduleIntegrationTestRunner({ beforeEach(async () => { const testManager = await MikroOrmWrapper.forkManager() - productOne = testManager.create(Product, { + productOne = testManager.create(toMikroORMEntity(Product), { id: "product-1", title: "product 1", + handle: "product-1", status: ProductStatus.PUBLISHED, }) - productTwo = testManager.create(Product, { + productTwo = testManager.create(toMikroORMEntity(Product), { id: "product-2", title: "product 2", + handle: "product-2", status: ProductStatus.PUBLISHED, }) @@ -47,11 +50,13 @@ moduleIntegrationTestRunner({ { id: "test-1", title: "collection 1", + handle: "collection-1", products: [productOne], }, { id: "test-2", title: "collection", + handle: "collection", products: [productTwo], }, ] diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/product-options.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/product-options.spec.ts index cc3fe9baac..67c934342b 100644 --- a/packages/modules/product/integration-tests/__tests__/product-module-service/product-options.spec.ts +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/product-options.spec.ts @@ -1,5 +1,9 @@ import { IProductModuleService } from "@medusajs/framework/types" -import { Modules, ProductStatus } from "@medusajs/framework/utils" +import { + Modules, + ProductStatus, + toMikroORMEntity, +} from "@medusajs/framework/utils" import { Product, ProductOption } from "@models" import { moduleIntegrationTestRunner } from "@medusajs/test-utils" @@ -16,25 +20,27 @@ moduleIntegrationTestRunner({ beforeEach(async () => { const testManager = await MikroOrmWrapper.forkManager() - productOne = testManager.create(Product, { + productOne = testManager.create(toMikroORMEntity(Product), { id: "product-1", title: "product 1", + handle: "product-1", status: ProductStatus.PUBLISHED, }) - productTwo = testManager.create(Product, { + productTwo = testManager.create(toMikroORMEntity(Product), { id: "product-2", title: "product 2", + handle: "product-2", status: ProductStatus.PUBLISHED, }) - optionOne = testManager.create(ProductOption, { + optionOne = testManager.create(toMikroORMEntity(ProductOption), { id: "option-1", title: "option 1", product: productOne, }) - optionTwo = testManager.create(ProductOption, { + optionTwo = testManager.create(toMikroORMEntity(ProductOption), { id: "option-2", title: "option 1", product: productTwo, @@ -198,7 +204,7 @@ moduleIntegrationTestRunner({ it("should return requested attributes when requested through config", async () => { const option = await service.retrieveProductOption(optionOne.id, { - select: ["id", "product.title"], + select: ["id", "product.handle", "product.title"], relations: ["product"], }) diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/product-tags.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/product-tags.spec.ts index d197df4baa..b8a91be243 100644 --- a/packages/modules/product/integration-tests/__tests__/product-module-service/product-tags.spec.ts +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/product-tags.spec.ts @@ -5,6 +5,7 @@ import { Modules, ProductEvents, ProductStatus, + toMikroORMEntity, } from "@medusajs/framework/utils" import { Product, ProductTag } from "@models" import { @@ -35,25 +36,27 @@ moduleIntegrationTestRunner({ beforeEach(async () => { const testManager = await MikroOrmWrapper.forkManager() - productOne = testManager.create(Product, { + productOne = testManager.create(toMikroORMEntity(Product), { id: "product-1", title: "product 1", + handle: "product-1", status: ProductStatus.PUBLISHED, }) - productTwo = testManager.create(Product, { + productTwo = testManager.create(toMikroORMEntity(Product), { id: "product-2", title: "product 2", + handle: "product-2", status: ProductStatus.PUBLISHED, }) - tagOne = testManager.create(ProductTag, { + tagOne = testManager.create(toMikroORMEntity(ProductTag), { id: "tag-1", value: "tag 1", products: [productOne], }) - tagTwo = testManager.create(ProductTag, { + tagTwo = testManager.create(toMikroORMEntity(ProductTag), { id: "tag-2", value: "tag", products: [productTwo], diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/product-types.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/product-types.spec.ts index c33a501ea2..2851644525 100644 --- a/packages/modules/product/integration-tests/__tests__/product-module-service/product-types.spec.ts +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/product-types.spec.ts @@ -1,7 +1,7 @@ import { IProductModuleService } from "@medusajs/framework/types" import { ProductType } from "@models" import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { Modules } from "@medusajs/framework/utils" +import { Modules, toMikroORMEntity } from "@medusajs/framework/utils" jest.setTimeout(30000) @@ -15,12 +15,12 @@ moduleIntegrationTestRunner({ beforeEach(async () => { const testManager = await MikroOrmWrapper.forkManager() - typeOne = testManager.create(ProductType, { + typeOne = testManager.create(toMikroORMEntity(ProductType), { id: "type-1", value: "type 1", }) - typeTwo = testManager.create(ProductType, { + typeTwo = testManager.create(toMikroORMEntity(ProductType), { id: "type-2", value: "type", }) diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts index 3fd059e871..c3231166e1 100644 --- a/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts @@ -125,7 +125,6 @@ moduleIntegrationTestRunner({ productCategoryTwo = categories[1] productOne = service.createProducts({ - id: "product-1", title: "product 1", status: ProductStatus.PUBLISHED, options: [ @@ -136,7 +135,6 @@ moduleIntegrationTestRunner({ ], variants: [ { - id: "variant-1", title: "variant 1", options: { "opt-title": "val-1" }, }, @@ -144,12 +142,11 @@ moduleIntegrationTestRunner({ }) productTwo = service.createProducts({ - id: "product-2", title: "product 2", status: ProductStatus.PUBLISHED, - categories: [{ id: productCategoryOne.id }], collection_id: productCollectionOne.id, - tags: [{ id: tags[0].id }], + category_ids: [productCategoryOne.id], + tag_ids: [tags[0].id], options: [ { title: "size", @@ -162,7 +159,6 @@ moduleIntegrationTestRunner({ ], variants: [ { - id: "variant-2", title: "variant 2", options: { size: "large", @@ -170,7 +166,6 @@ moduleIntegrationTestRunner({ }, }, { - id: "variant-3", title: "variant 3", options: { size: "small", @@ -389,11 +384,11 @@ moduleIntegrationTestRunner({ const existingVariant1 = product.variants.find( (v) => v.title === "new variant 1" - ) + )! const existingVariant2 = product.variants.find( (v) => v.title === "new variant 2" - ) + )! await service.upsertProductVariants([ { @@ -793,10 +788,14 @@ moduleIntegrationTestRunner({ }) it("should do a partial update on the options of a variant successfully", async () => { + const variantToUpdate = productTwo.variants.find( + (variant) => variant.title === "variant 3" + )! + await service.updateProducts(productTwo.id, { variants: [ { - id: "variant-3", + id: variantToUpdate.id, options: { size: "small", color: "blue" }, }, ], diff --git a/packages/modules/product/integration-tests/__tests__/product.ts b/packages/modules/product/integration-tests/__tests__/product.ts index 84e0da968a..3b310d995f 100644 --- a/packages/modules/product/integration-tests/__tests__/product.ts +++ b/packages/modules/product/integration-tests/__tests__/product.ts @@ -7,12 +7,17 @@ import { createProductVariants, } from "../__fixtures__/product" -import { IProductModuleService, ProductDTO } from "@medusajs/framework/types" +import { + InferEntityType, + IProductModuleService, + ProductDTO, +} from "@medusajs/framework/types" import { Module, Modules, ProductStatus, kebabCase, + toMikroORMEntity, } from "@medusajs/framework/utils" import { moduleIntegrationTestRunner } from "@medusajs/test-utils" import { SqlEntityManager } from "@mikro-orm/postgresql" @@ -27,7 +32,7 @@ import { variantsData, } from "../__fixtures__/product/data" -jest.setTimeout(30000) +jest.setTimeout(300000) type Service = IProductModuleService & { productService_: ProductService @@ -50,17 +55,19 @@ moduleIntegrationTestRunner({ service: ProductModuleService, }).linkable - expect(Object.keys(linkable)).toEqual([ - "product", - "productVariant", - "productOption", - "productOptionValue", - "productType", - "productImage", - "productTag", - "productCollection", - "productCategory", - ]) + expect(Object.keys(linkable)).toHaveLength(8) + expect(Object.keys(linkable)).toEqual( + expect.arrayContaining([ + "product", + "productVariant", + "productOption", + "productOptionValue", + "productType", + "productTag", + "productCollection", + "productCategory", + ]) + ) Object.keys(linkable).forEach((key) => { delete linkable[key].toJSON @@ -119,15 +126,6 @@ moduleIntegrationTestRunner({ field: "productType", }, }, - productImage: { - id: { - linkable: "product_image_id", - entity: "ProductImage", - primaryKey: "id", - serviceName: "product", - field: "productImage", - }, - }, productTag: { id: { linkable: "product_tag_id", @@ -160,16 +158,17 @@ moduleIntegrationTestRunner({ describe("Product Service", () => { let testManager: SqlEntityManager - let products!: Product[] - let productOne: Product - let categories!: ProductCategory[] + let products!: InferEntityType[] + let productOne: InferEntityType + let categories!: InferEntityType[] describe("retrieve", () => { beforeEach(async () => { testManager = await MikroOrmWrapper.forkManager() - productOne = testManager.create(Product, { + productOne = testManager.create(toMikroORMEntity(Product), { id: "product-1", title: "product 1", + handle: "product-1", status: ProductStatus.PUBLISHED, }) @@ -243,9 +242,10 @@ moduleIntegrationTestRunner({ beforeEach(async () => { testManager = await MikroOrmWrapper.forkManager() - productOne = testManager.create(Product, { + productOne = testManager.create(toMikroORMEntity(Product), { id: "product-1", title: "product 1", + handle: "product-1", status: ProductStatus.PUBLISHED, }) @@ -435,23 +435,23 @@ moduleIntegrationTestRunner({ }) describe("relation: categories", () => { - let workingProduct: Product - let workingCategory: ProductCategory + let workingProduct: InferEntityType + let workingCategory: InferEntityType beforeEach(async () => { testManager = await MikroOrmWrapper.forkManager() products = await createProductAndTags(testManager, productsData) - workingProduct = products.find((p) => p.id === "test-1") as Product + workingProduct = products.find((p) => p.id === "test-1")! categories = [] for (const entry of categoriesData) { categories.push((await categoryService.create([entry]))[0]) } workingCategory = (await testManager.findOne( - ProductCategory, + toMikroORMEntity(ProductCategory), "category-1" - )) as ProductCategory + ))! workingProduct = await assignCategoriesToProduct( testManager, @@ -477,9 +477,7 @@ moduleIntegrationTestRunner({ } ) - const product = products.find( - (p) => p.id === workingProduct.id - ) as unknown as Product + const product = products.find((p) => p.id === workingProduct.id)! expect(product).toEqual( expect.objectContaining({ @@ -534,32 +532,34 @@ moduleIntegrationTestRunner({ }) describe("relation: collections", () => { - let workingProduct: Product - let workingProductTwo: Product - let workingCollection: ProductCollection - let workingCollectionTwo: ProductCollection + let workingProduct: InferEntityType + let workingProductTwo: InferEntityType + let workingCollection: InferEntityType + let workingCollectionTwo: InferEntityType const collectionData = [ { id: "test-1", title: "col 1", + handle: "col-1", }, { id: "test-2", title: "col 2", + handle: "col-2", }, ] beforeEach(async () => { testManager = await MikroOrmWrapper.forkManager() await createCollections(testManager, collectionData) - workingCollection = (await testManager.findOne( - ProductCollection, + workingCollection = await testManager.findOne( + toMikroORMEntity(ProductCollection), "test-1" - )) as ProductCollection + ) workingCollectionTwo = (await testManager.findOne( - ProductCollection, + toMikroORMEntity(ProductCollection), "test-2" - )) as ProductCollection + ))! products = await createProductAndTags(testManager, [ { @@ -575,10 +575,8 @@ moduleIntegrationTestRunner({ }, ]) - workingProduct = products.find((p) => p.id === "test-1") as Product - workingProductTwo = products.find( - (p) => p.id === "test-2" - ) as Product + workingProduct = products.find((p) => p.id === "test-1")! + workingProductTwo = products.find((p) => p.id === "test-2")! }) it("should filter by collection relation and scope fields", async () => { @@ -588,7 +586,12 @@ moduleIntegrationTestRunner({ collection_id: workingCollection.id, }, { - select: ["title", "collection.title"], + select: [ + "title", + "handle", + "collection.title", + "collection.handle", + ], relations: ["collection"], } ) @@ -618,7 +621,12 @@ moduleIntegrationTestRunner({ collection_id: [workingCollection.id, workingCollectionTwo.id], }, { - select: ["title", "collection.title"], + select: [ + "title", + "handle", + "collection.title", + "collection.handle", + ], relations: ["collection"], } ) diff --git a/packages/modules/product/src/joiner-config.ts b/packages/modules/product/src/joiner-config.ts index c59334ee56..92758d133d 100644 --- a/packages/modules/product/src/joiner-config.ts +++ b/packages/modules/product/src/joiner-config.ts @@ -9,7 +9,6 @@ import { ProductType, ProductVariant, } from "@models" -import ProductImage from "./models/product-image" import { default as schema } from "./schema" export const joinerConfig = defineJoinerConfig(Modules.PRODUCT, { @@ -20,7 +19,6 @@ export const joinerConfig = defineJoinerConfig(Modules.PRODUCT, { ProductOption, ProductOptionValue, ProductType, - ProductImage, ProductTag, ProductCollection, ProductCategory, diff --git a/packages/modules/product/src/migrations/.snapshot-medusa-product.json b/packages/modules/product/src/migrations/.snapshot-medusa-product.json index ce2f18059e..15cd6347d0 100644 --- a/packages/modules/product/src/migrations/.snapshot-medusa-product.json +++ b/packages/modules/product/src/migrations/.snapshot-medusa-product.json @@ -82,6 +82,15 @@ "default": "0", "mappedType": "integer" }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, "parent_category_id": { "name": "parent_category_id", "type": "text", @@ -122,28 +131,42 @@ "nullable": true, "length": 6, "mappedType": "datetime" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "json" } }, "name": "product_category", "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], + "keyName": "IDX_product_category_parent_category_id", + "columnNames": [], "composite": false, - "keyName": "IDX_product_category_deleted_at", "primary": false, - "unique": false + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_category_parent_category_id\" ON \"product_category\" (parent_category_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_category_deleted_at", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_category_deleted_at\" ON \"product_category\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_category_path", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_category_path\" ON \"product_category\" (mpath) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_category_handle_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_category_handle_unique\" ON \"product_category\" (handle) WHERE deleted_at IS NULL" }, { "keyName": "product_category_pkey", @@ -247,13 +270,20 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], - "composite": false, "keyName": "IDX_product_collection_deleted_at", + "columnNames": [], + "composite": false, "primary": false, - "unique": false + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_collection_deleted_at\" ON \"product_collection\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_collection_handle_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_collection_handle_unique\" ON \"product_collection\" (handle) WHERE deleted_at IS NULL" }, { "keyName": "product_collection_pkey", @@ -334,13 +364,20 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], - "composite": false, "keyName": "IDX_product_tag_deleted_at", + "columnNames": [], + "composite": false, "primary": false, - "unique": false + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_tag_deleted_at\" ON \"product_tag\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_tag_value_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_tag_value_unique\" ON \"product_tag\" (value) WHERE deleted_at IS NULL" }, { "keyName": "product_tag_pkey", @@ -377,7 +414,7 @@ }, "metadata": { "name": "metadata", - "type": "json", + "type": "jsonb", "unsigned": false, "autoincrement": false, "primary": false, @@ -421,13 +458,20 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], - "composite": false, "keyName": "IDX_product_type_deleted_at", + "columnNames": [], + "composite": false, "primary": false, - "unique": false + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_type_deleted_at\" ON \"product_type\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_type_value_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_type_value_unique\" ON \"product_type\" (value) WHERE deleted_at IS NULL" }, { "keyName": "product_type_pkey", @@ -506,6 +550,7 @@ "autoincrement": false, "primary": false, "nullable": false, + "default": "'draft'", "enumItems": [ "draft", "proposed", @@ -595,24 +640,6 @@ "nullable": true, "mappedType": "text" }, - "collection_id": { - "name": "collection_id", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "text" - }, - "type_id": { - "name": "type_id", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "text" - }, "discountable": { "name": "discountable", "type": "boolean", @@ -632,6 +659,33 @@ "nullable": true, "mappedType": "text" }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "type_id": { + "name": "type_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "collection_id": { + "name": "collection_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, "created_at": { "name": "created_at", "type": "timestamptz", @@ -663,28 +717,42 @@ "nullable": true, "length": 6, "mappedType": "datetime" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "json" } }, "name": "product", "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], + "keyName": "IDX_product_type_id", + "columnNames": [], "composite": false, - "keyName": "IDX_product_deleted_at", "primary": false, - "unique": false + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_type_id\" ON \"product\" (type_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_collection_id", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_collection_id\" ON \"product\" (collection_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_deleted_at", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_deleted_at\" ON \"product\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_handle_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_handle_unique\" ON \"product\" (handle) WHERE deleted_at IS NULL" }, { "keyName": "product_pkey", @@ -698,19 +766,6 @@ ], "checks": [], "foreignKeys": { - "product_collection_id_foreign": { - "constraintName": "product_collection_id_foreign", - "columnNames": [ - "collection_id" - ], - "localTableName": "public.product", - "referencedColumnNames": [ - "id" - ], - "referencedTableName": "public.product_collection", - "deleteRule": "set null", - "updateRule": "cascade" - }, "product_type_id_foreign": { "constraintName": "product_type_id_foreign", "columnNames": [ @@ -723,6 +778,19 @@ "referencedTableName": "public.product_type", "deleteRule": "set null", "updateRule": "cascade" + }, + "product_collection_id_foreign": { + "constraintName": "product_collection_id_foreign", + "columnNames": [ + "collection_id" + ], + "localTableName": "public.product", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.product_collection", + "deleteRule": "set null", + "updateRule": "cascade" } } }, @@ -746,15 +814,6 @@ "nullable": false, "mappedType": "text" }, - "product_id": { - "name": "product_id", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "text" - }, "metadata": { "name": "metadata", "type": "jsonb", @@ -764,6 +823,15 @@ "nullable": true, "mappedType": "json" }, + "product_id": { + "name": "product_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, "created_at": { "name": "created_at", "type": "timestamptz", @@ -801,13 +869,28 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], + "keyName": "IDX_product_option_product_id", + "columnNames": [], "composite": false, - "keyName": "IDX_product_option_deleted_at", "primary": false, - "unique": false + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_option_product_id\" ON \"product_option\" (product_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_option_deleted_at", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_option_deleted_at\" ON \"product_option\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_option_product_id_title_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_option_product_id_title_unique\" ON \"product_option\" (product_id, title) WHERE deleted_at IS NULL" }, { "keyName": "product_option_pkey", @@ -856,15 +939,6 @@ "nullable": false, "mappedType": "text" }, - "option_id": { - "name": "option_id", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "text" - }, "metadata": { "name": "metadata", "type": "jsonb", @@ -874,6 +948,15 @@ "nullable": true, "mappedType": "json" }, + "option_id": { + "name": "option_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, "created_at": { "name": "created_at", "type": "timestamptz", @@ -911,13 +994,28 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], + "keyName": "IDX_product_option_value_option_id", + "columnNames": [], "composite": false, - "keyName": "IDX_product_option_value_deleted_at", "primary": false, - "unique": false + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_option_value_option_id\" ON \"product_option_value\" (option_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_option_value_deleted_at", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_option_value_deleted_at\" ON \"product_option_value\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_option_value_option_id_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_option_value_option_id_unique\" ON \"product_option_value\" (option_id, value) WHERE deleted_at IS NULL" }, { "keyName": "product_option_value_pkey", @@ -975,6 +1073,25 @@ "nullable": true, "mappedType": "json" }, + "rank": { + "name": "rank", + "type": "integer", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "integer" + }, + "product_id": { + "name": "product_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, "created_at": { "name": "created_at", "type": "timestamptz", @@ -1006,38 +1123,34 @@ "nullable": true, "length": 6, "mappedType": "datetime" - }, - "rank": { - "name": "rank", - "type": "integer", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "default": "0", - "mappedType": "integer" - }, - "product_id": { - "name": "product_id", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "text" } }, "name": "image", "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], + "keyName": "IDX_image_product_id", + "columnNames": [], "composite": false, - "keyName": "IDX_product_image_deleted_at", "primary": false, - "unique": false + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_image_product_id\" ON \"image\" (product_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_image_deleted_at", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_image_deleted_at\" ON \"image\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_image_url", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_image_url\" ON \"image\" (url) WHERE deleted_at IS NULL" }, { "keyName": "image_pkey", @@ -1310,39 +1423,39 @@ }, "weight": { "name": "weight", - "type": "numeric", + "type": "integer", "unsigned": false, "autoincrement": false, "primary": false, "nullable": true, - "mappedType": "decimal" + "mappedType": "integer" }, "length": { "name": "length", - "type": "numeric", + "type": "integer", "unsigned": false, "autoincrement": false, "primary": false, "nullable": true, - "mappedType": "decimal" + "mappedType": "integer" }, "height": { "name": "height", - "type": "numeric", + "type": "integer", "unsigned": false, "autoincrement": false, "primary": false, "nullable": true, - "mappedType": "decimal" + "mappedType": "integer" }, "width": { "name": "width", - "type": "numeric", + "type": "integer", "unsigned": false, "autoincrement": false, "primary": false, "nullable": true, - "mappedType": "decimal" + "mappedType": "integer" }, "metadata": { "name": "metadata", @@ -1409,13 +1522,52 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], + "keyName": "IDX_product_variant_product_id", + "columnNames": [], "composite": false, - "keyName": "IDX_product_variant_deleted_at", "primary": false, - "unique": false + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_variant_product_id\" ON \"product_variant\" (product_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_variant_deleted_at", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_variant_deleted_at\" ON \"product_variant\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_variant_sku_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_variant_sku_unique\" ON \"product_variant\" (sku) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_variant_barcode_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_variant_barcode_unique\" ON \"product_variant\" (barcode) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_variant_ean_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_variant_ean_unique\" ON \"product_variant\" (ean) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_product_variant_upc_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_variant_upc_unique\" ON \"product_variant\" (upc) WHERE deleted_at IS NULL" }, { "keyName": "product_variant_pkey", diff --git a/packages/modules/product/src/migrations/Migration20241125090957.ts b/packages/modules/product/src/migrations/Migration20241125090957.ts new file mode 100644 index 0000000000..f203486e9b --- /dev/null +++ b/packages/modules/product/src/migrations/Migration20241125090957.ts @@ -0,0 +1,173 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20241125090957 extends Migration { + async up(): Promise { + this.addSql( + 'alter table if exists "product" drop constraint if exists "product_status_check";' + ) + + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_product_category_parent_category_id" ON "product_category" (parent_category_id) 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 UNIQUE INDEX IF NOT EXISTS "IDX_category_handle_unique" ON "product_category" (handle) WHERE deleted_at IS NULL;' + ) + + this.addSql( + 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_collection_handle_unique" ON "product_collection" (handle) WHERE deleted_at IS NULL;' + ) + + this.addSql('drop index if exists "IDX_product_image_deleted_at";') + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_image_deleted_at" ON "image" (deleted_at) WHERE deleted_at IS NULL;' + ) + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_product_image_url" ON "image" (url) WHERE deleted_at IS NULL;' + ) + + this.addSql( + 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_tag_value_unique" ON "product_tag" (value) WHERE deleted_at IS NULL;' + ) + + this.addSql( + 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_type_value_unique" ON "product_type" (value) WHERE deleted_at IS NULL;' + ) + + this.addSql( + 'alter table if exists "product" alter column "status" type text using ("status"::text);' + ) + this.addSql( + "alter table if exists \"product\" add constraint \"product_status_check\" check (\"status\" in ('draft', 'proposed', 'published', 'rejected'));" + ) + this.addSql( + 'alter table if exists "product" alter column "status" set default \'draft\';' + ) + 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 UNIQUE INDEX IF NOT EXISTS "IDX_product_handle_unique" ON "product" (handle) WHERE deleted_at IS NULL;' + ) + + this.addSql( + 'alter table if exists "product_option" alter column "product_id" type text using ("product_id"::text);' + ) + this.addSql( + 'alter table if exists "product_option" alter column "product_id" set not null;' + ) + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_product_option_product_id" ON "product_option" (product_id) WHERE deleted_at IS NULL;' + ) + 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_value_option_id" ON "product_option_value" (option_id) WHERE deleted_at IS NULL;' + ) + 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( + 'alter table if exists "product_variant" alter column "weight" type integer using ("weight"::integer);' + ) + this.addSql( + 'alter table if exists "product_variant" alter column "length" type integer using ("length"::integer);' + ) + this.addSql( + 'alter table if exists "product_variant" alter column "height" type integer using ("height"::integer);' + ) + this.addSql( + 'alter table if exists "product_variant" alter column "width" type integer using ("width"::integer);' + ) + 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 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 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;' + ) + } + + async down(): Promise { + this.addSql( + 'alter table if exists "product" drop constraint if exists "product_status_check";' + ) + + this.addSql( + 'drop index if exists "IDX_product_category_parent_category_id";' + ) + this.addSql('drop index if exists "IDX_product_category_path";') + this.addSql('drop index if exists "IDX_category_handle_unique";') + + this.addSql('drop index if exists "IDX_collection_handle_unique";') + + this.addSql('drop index if exists "IDX_image_deleted_at";') + this.addSql('drop index if exists "IDX_product_image_url";') + this.addSql( + 'create index if not exists "IDX_product_image_deleted_at" on "image" ("deleted_at");' + ) + + this.addSql('drop index if exists "IDX_tag_value_unique";') + + this.addSql('drop index if exists "IDX_type_value_unique";') + + this.addSql( + 'alter table if exists "product" alter column "status" drop default;' + ) + this.addSql( + 'alter table if exists "product" alter column "status" type text using ("status"::text);' + ) + this.addSql( + "alter table if exists \"product\" add constraint \"product_status_check\" check (\"status\" in ('draft', 'proposed', 'published', 'rejected'));" + ) + this.addSql('drop index if exists "IDX_product_type_id";') + this.addSql('drop index if exists "IDX_product_collection_id";') + this.addSql('drop index if exists "IDX_product_handle_unique";') + + this.addSql( + 'alter table if exists "product_option" alter column "product_id" type text using ("product_id"::text);' + ) + this.addSql( + 'alter table if exists "product_option" alter column "product_id" drop not null;' + ) + this.addSql('drop index if exists "IDX_product_option_product_id";') + this.addSql('drop index if exists "IDX_option_product_id_title_unique";') + + this.addSql('drop index if exists "IDX_product_option_value_option_id";') + this.addSql('drop index if exists "IDX_option_value_option_id_unique";') + + this.addSql( + 'alter table if exists "product_variant" alter column "weight" type numeric using ("weight"::numeric);' + ) + this.addSql( + 'alter table if exists "product_variant" alter column "length" type numeric using ("length"::numeric);' + ) + this.addSql( + 'alter table if exists "product_variant" alter column "height" type numeric using ("height"::numeric);' + ) + this.addSql( + 'alter table if exists "product_variant" alter column "width" type numeric using ("width"::numeric);' + ) + this.addSql('drop index if exists "IDX_product_variant_product_id";') + this.addSql('drop index if exists "IDX_product_variant_sku_unique";') + this.addSql('drop index if exists "IDX_product_variant_barcode_unique";') + this.addSql('drop index if exists "IDX_product_variant_ean_unique";') + this.addSql('drop index if exists "IDX_product_variant_upc_unique";') + } +} diff --git a/packages/modules/product/src/models/product-category.ts b/packages/modules/product/src/models/product-category.ts index 8f86be88cb..a80badde66 100644 --- a/packages/modules/product/src/models/product-category.ts +++ b/packages/modules/product/src/models/product-category.ts @@ -1,140 +1,45 @@ -import { - DALUtils, - Searchable, - createPsqlIndexStatementHelper, - generateEntityId, - kebabCase, -} from "@medusajs/framework/utils" -import { - BeforeCreate, - Collection, - Entity, - EventArgs, - Filter, - Index, - ManyToMany, - ManyToOne, - OnInit, - OneToMany, - PrimaryKey, - Property, -} from "@mikro-orm/core" +import { model } from "@medusajs/framework/utils" 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: "integer", - nullable: false, - default: 0, +const ProductCategory = model + .define("ProductCategory", { + id: model.id({ prefix: "pcat" }).primaryKey(), + name: model.text().searchable(), + description: model.text().searchable().default(""), + handle: model.text().searchable(), + mpath: model.text(), + is_active: model.boolean().default(false), + is_internal: model.boolean().default(false), + rank: model.number().default(0), + metadata: model.json().nullable(), + parent_category: model + .belongsTo(() => ProductCategory, { + mappedBy: "category_children", + }) + .nullable(), + category_children: model.hasMany(() => ProductCategory, { + mappedBy: "parent_category", + }), + products: model.manyToMany(() => Product, { + mappedBy: "categories", + }), }) - rank: number - - @ManyToOne(() => ProductCategory, { - columnType: "text", - fieldName: "parent_category_id", - nullable: true, - mapToPk: true, - onDelete: "cascade", + .cascades({ + delete: ["category_children"], }) - 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(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 - - @Property({ columnType: "jsonb", nullable: true }) - metadata: Record | null = null - - @ManyToMany(() => Product, (product) => product.categories) - products = new Collection(this) - - @OnInit() - async onInit() { - this.id = generateEntityId(this.id, "pcat") - this.parent_category_id ??= this.parent_category?.id ?? null - } - - @BeforeCreate() - async onCreate(args: EventArgs) { - 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) - } - - this.mpath = `${this.mpath ? this.mpath + "." : ""}${this.id}` - } -} + .indexes([ + { + name: "IDX_product_category_path", + on: ["mpath"], + unique: false, + where: "deleted_at IS NULL", + }, + { + name: "IDX_category_handle_unique", + on: ["handle"], + unique: true, + where: "deleted_at IS NULL", + }, + ]) export default ProductCategory diff --git a/packages/modules/product/src/models/product-collection.ts b/packages/modules/product/src/models/product-collection.ts index 87f44b353b..36905e64b7 100644 --- a/packages/modules/product/src/models/product-collection.ts +++ b/packages/modules/product/src/models/product-collection.ts @@ -1,81 +1,23 @@ -import { - BeforeCreate, - Collection, - Entity, - Filter, - Index, - OneToMany, - OnInit, - PrimaryKey, - Property, -} from "@mikro-orm/core" - -import { - createPsqlIndexStatementHelper, - DALUtils, - generateEntityId, - kebabCase, - Searchable, -} from "@medusajs/framework/utils" +import { model } from "@medusajs/framework/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(this) - - @Property({ columnType: "jsonb", nullable: true }) - metadata?: Record | null - - @Property({ - onCreate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", +const ProductCollection = model + .define("ProductCollection", { + id: model.id({ prefix: "pcol" }).primaryKey(), + title: model.text().searchable(), + handle: model.text(), + metadata: model.json().nullable(), + products: model.hasMany(() => Product, { + mappedBy: "collection", + }), }) - 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) - } - } -} + .indexes([ + { + name: "IDX_collection_handle_unique", + on: ["handle"], + unique: true, + where: "deleted_at IS NULL", + }, + ]) export default ProductCollection diff --git a/packages/modules/product/src/models/product-image.ts b/packages/modules/product/src/models/product-image.ts index c1bd26768a..0b605ec051 100644 --- a/packages/modules/product/src/models/product-image.ts +++ b/packages/modules/product/src/models/product-image.ts @@ -1,88 +1,26 @@ -import { - BeforeCreate, - Entity, - Filter, - Index, - ManyToOne, - OnInit, - PrimaryKey, - Property, - Rel, -} from "@mikro-orm/core" - -import { - DALUtils, - createPsqlIndexStatementHelper, - generateEntityId, -} from "@medusajs/framework/utils" +import { model } from "@medusajs/framework/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 | 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 - - @Property({ columnType: "integer", default: 0 }) - rank: number - - @ManyToOne(() => Product, { - columnType: "text", - onDelete: "cascade", - fieldName: "product_id", - mapToPk: true, - }) - product_id: string - - @ManyToOne(() => Product, { - persist: false, - }) - product: Rel - - @OnInit() - onInit() { - this.id = generateEntityId(this.id, "img") - } - - @BeforeCreate() - onCreate() { - this.id = generateEntityId(this.id, "img") - } -} +const ProductImage = model + .define( + { tableName: "image", name: "ProductImage" }, + { + id: model.id({ prefix: "img" }).primaryKey(), + url: model.text(), + metadata: model.json().nullable(), + rank: model.number().default(0), + product: model.belongsTo(() => Product, { + mappedBy: "images", + }), + } + ) + .indexes([ + { + name: "IDX_product_image_url", + on: ["url"], + unique: false, + where: "deleted_at IS NULL", + }, + ]) export default ProductImage diff --git a/packages/modules/product/src/models/product-option-value.ts b/packages/modules/product/src/models/product-option-value.ts index a3f0486f5f..21e0aa7461 100644 --- a/packages/modules/product/src/models/product-option-value.ts +++ b/packages/modules/product/src/models/product-option-value.ts @@ -1,87 +1,27 @@ -import { - DALUtils, - createPsqlIndexStatementHelper, - generateEntityId, -} from "@medusajs/framework/utils" -import { - BeforeCreate, - Collection, - Entity, - Filter, - Index, - ManyToMany, - ManyToOne, - OnInit, - PrimaryKey, - Property, -} from "@mikro-orm/core" +import { model } from "@medusajs/framework/utils" 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", +const ProductOptionValue = model + .define("ProductOptionValue", { + id: model.id({ prefix: "optval" }).primaryKey(), + value: model.text(), + metadata: model.json().nullable(), + option: model + .belongsTo(() => ProductOption, { + mappedBy: "values", + }) + .nullable(), + variants: model.manyToMany(() => ProductVariant, { + mappedBy: "options", + }), }) - option_id: string | null - - @ManyToOne(() => ProductOption, { - nullable: true, - persist: false, - }) - option: ProductOption | null - - @ManyToMany(() => ProductVariant, (variant) => variant.options) - variants = new Collection(this) - - @Property({ columnType: "jsonb", nullable: true }) - metadata?: Record | 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 - } -} + .indexes([ + { + name: "IDX_option_value_option_id_unique", + on: ["option_id", "value"], + unique: true, + where: "deleted_at IS NULL", + }, + ]) export default ProductOptionValue diff --git a/packages/modules/product/src/models/product-option.ts b/packages/modules/product/src/models/product-option.ts index d75b3bd73f..553209193a 100644 --- a/packages/modules/product/src/models/product-option.ts +++ b/packages/modules/product/src/models/product-option.ts @@ -1,93 +1,29 @@ -import { - DALUtils, - Searchable, - createPsqlIndexStatementHelper, - generateEntityId, -} from "@medusajs/framework/utils" -import { - BeforeCreate, - Cascade, - Collection, - Entity, - Filter, - Index, - ManyToOne, - OnInit, - OneToMany, - PrimaryKey, - Property, -} from "@mikro-orm/core" +import { model } from "@medusajs/framework/utils" 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", +const ProductOption = model + .define("ProductOption", { + id: model.id({ prefix: "opt" }).primaryKey(), + title: model.text().searchable(), + metadata: model.json().nullable(), + product: model.belongsTo(() => Product, { + mappedBy: "options", + }), + values: model.hasMany(() => ProductOptionValue, { + mappedBy: "option", + }), }) - product_id: string | null - - @ManyToOne(() => Product, { - persist: false, - nullable: true, + .cascades({ + delete: ["values"], }) - product: Product | null - - @OneToMany(() => ProductOptionValue, (value) => value.option, { - cascade: [Cascade.PERSIST, "soft-remove" as any], - }) - values = new Collection(this) - - @Property({ columnType: "jsonb", nullable: true }) - metadata?: Record | 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 - } -} + .indexes([ + { + name: "IDX_option_product_id_title_unique", + on: ["product_id", "title"], + unique: true, + where: "deleted_at IS NULL", + }, + ]) export default ProductOption diff --git a/packages/modules/product/src/models/product-tag.ts b/packages/modules/product/src/models/product-tag.ts index 7f1313d3d1..e3a7389a9a 100644 --- a/packages/modules/product/src/models/product-tag.ts +++ b/packages/modules/product/src/models/product-tag.ts @@ -1,73 +1,25 @@ -import { - BeforeCreate, - Collection, - Entity, - Filter, - Index, - ManyToMany, - OnInit, - PrimaryKey, - Property, -} from "@mikro-orm/core" - -import { - DALUtils, - Searchable, - createPsqlIndexStatementHelper, - generateEntityId, -} from "@medusajs/framework/utils" +import { model } from "@medusajs/framework/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 | 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(this) - - @OnInit() - @BeforeCreate() - onInit() { - this.id = generateEntityId(this.id, "ptag") - } -} +const ProductTag = model + .define( + { tableName: "product_tag", name: "ProductTag" }, + { + id: model.id({ prefix: "ptag" }).primaryKey(), + value: model.text().searchable(), + metadata: model.json().nullable(), + products: model.manyToMany(() => Product, { + mappedBy: "tags", + }), + } + ) + .indexes([ + { + name: "IDX_tag_value_unique", + on: ["value"], + unique: true, + where: "deleted_at IS NULL", + }, + ]) export default ProductTag diff --git a/packages/modules/product/src/models/product-type.ts b/packages/modules/product/src/models/product-type.ts index 2fd981c2e8..904c9ee6c0 100644 --- a/packages/modules/product/src/models/product-type.ts +++ b/packages/modules/product/src/models/product-type.ts @@ -1,67 +1,22 @@ -import { - BeforeCreate, - Entity, - Filter, - Index, - OnInit, - PrimaryKey, - Property, -} from "@mikro-orm/core" +import { model } from "@medusajs/framework/utils" +import { Product } from "@models" -import { - DALUtils, - Searchable, - createPsqlIndexStatementHelper, - generateEntityId, -} from "@medusajs/framework/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 | null - - @Property({ - onCreate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", +const ProductType = model + .define("ProductType", { + id: model.id({ prefix: "ptyp" }).primaryKey(), + value: model.text().searchable(), + metadata: model.json().nullable(), + product: model.hasMany(() => Product, { + mappedBy: "type", + }), }) - 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") - } -} + .indexes([ + { + name: "IDX_type_value_unique", + on: ["value"], + unique: true, + where: "deleted_at IS NULL", + }, + ]) export default ProductType diff --git a/packages/modules/product/src/models/product-variant.ts b/packages/modules/product/src/models/product-variant.ts index c52640afc2..b5e203f98a 100644 --- a/packages/modules/product/src/models/product-variant.ts +++ b/packages/modules/product/src/models/product-variant.ts @@ -1,187 +1,69 @@ -import { - createPsqlIndexStatementHelper, - DALUtils, - generateEntityId, - Searchable, -} from "@medusajs/framework/utils" -import { - BeforeCreate, - Collection, - Entity, - Filter, - Index, - ManyToMany, - ManyToOne, - OnInit, - PrimaryKey, - Property, -} from "@mikro-orm/core" +import { model } from "@medusajs/framework/utils" 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 - - @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 | null - - @Property({ - columnType: "integer", - nullable: true, - default: 0, +const ProductVariant = model + .define("ProductVariant", { + id: model.id({ prefix: "variant" }).primaryKey(), + title: model.text().searchable(), + sku: model.text().searchable().nullable(), + barcode: model.text().searchable().nullable(), + ean: model.text().searchable().nullable(), + upc: model.text().searchable().nullable(), + allow_backorder: model.boolean().default(false), + manage_inventory: model.boolean().default(true), + hs_code: model.text().nullable(), + origin_country: model.text().nullable(), + mid_code: model.text().nullable(), + material: model.text().nullable(), + weight: model.number().nullable(), + length: model.number().nullable(), + height: model.number().nullable(), + width: model.number().nullable(), + metadata: model.json().nullable(), + variant_rank: model.number().default(0).nullable(), + product: model + .belongsTo(() => Product, { + mappedBy: "variants", + }) + .nullable(), + options: model.manyToMany(() => ProductOptionValue, { + pivotTable: "product_variant_option", + mappedBy: "variants", + joinColumn: "variant_id", + inverseJoinColumn: "option_value_id", + }), }) - 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(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 - } -} + .indexes([ + { + name: "IDX_product_variant_product_id", + on: ["product_id"], + unique: false, + where: "deleted_at IS NULL", + }, + { + name: "IDX_product_variant_sku_unique", + on: ["sku"], + unique: true, + where: "deleted_at IS NULL", + }, + { + name: "IDX_product_variant_barcode_unique", + on: ["barcode"], + unique: true, + where: "deleted_at IS NULL", + }, + { + name: "IDX_product_variant_ean_unique", + on: ["ean"], + unique: true, + where: "deleted_at IS NULL", + }, + { + name: "IDX_product_variant_upc_unique", + on: ["upc"], + unique: true, + where: "deleted_at IS NULL", + }, + ]) export default ProductVariant diff --git a/packages/modules/product/src/models/product.ts b/packages/modules/product/src/models/product.ts index e2caae6bc0..f56c5d27c6 100644 --- a/packages/modules/product/src/models/product.ts +++ b/packages/modules/product/src/models/product.ts @@ -1,222 +1,86 @@ -import { - BeforeCreate, - Cascade, - Collection, - Entity, - Enum, - Filter, - Index, - ManyToMany, - ManyToOne, - OneToMany, - OnInit, - PrimaryKey, - Property, -} from "@mikro-orm/core" +import { model, ProductUtils } from "@medusajs/framework/utils" -import { - createPsqlIndexStatementHelper, - DALUtils, - generateEntityId, - ProductUtils, - Searchable, - toHandle, -} from "@medusajs/framework/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 ProductImage from "./product-image" +import ProductOption from "./product-option" import ProductVariant from "./product-variant" +import ProductCategory from "./product-category" +import ProductCollection from "./product-collection" -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, +const Product = model + .define("Product", { + id: model.id({ prefix: "prod" }).primaryKey(), + title: model.text().searchable(), + handle: model.text(), + subtitle: model.text().searchable().nullable(), + description: model.text().searchable().nullable(), + is_giftcard: model.boolean().default(false), + status: model + .enum(ProductUtils.ProductStatus) + .default(ProductUtils.ProductStatus.DRAFT), + thumbnail: model.text().nullable(), + weight: model.text().nullable(), + length: model.text().nullable(), + height: model.text().nullable(), + width: model.text().nullable(), + origin_country: model.text().nullable(), + hs_code: model.text().nullable(), + mid_code: model.text().nullable(), + material: model.text().nullable(), + discountable: model.boolean().default(true), + external_id: model.text().nullable(), + metadata: model.json().nullable(), + variants: model.hasMany(() => ProductVariant, { + mappedBy: "product", + }), + type: model + .belongsTo(() => ProductType, { + mappedBy: "product", + }) + .nullable(), + tags: model.manyToMany(() => ProductTag, { + mappedBy: "products", + pivotTable: "product_tags", + }), + options: model.hasMany(() => ProductOption, { + mappedBy: "product", + }), + images: model.hasMany(() => ProductImage, { + mappedBy: "product", + }), + collection: model + .belongsTo(() => ProductCollection, { + mappedBy: "products", + }) + .nullable(), + categories: model.manyToMany(() => ProductCategory, { + pivotTable: "product_category_product", + mappedBy: "products", + }), }) - 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, + .cascades({ + delete: ["variants", "options", "images"], }) - options = new Collection(this) - - @Searchable() - @OneToMany(() => ProductVariant, (variant) => variant.product, { - cascade: ["soft-remove"] as any, - }) - variants = new Collection(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(this) - - @OneToMany(() => ProductImage, (image) => image.product_id, { - cascade: [Cascade.PERSIST, Cascade.REMOVE], - }) - images = new Collection(this) - - @ManyToMany(() => ProductCategory, "products", { - owner: true, - pivotTable: "product_category_product", - }) - categories = new Collection(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 | 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 = toHandle(this.title) - } - } -} + .indexes([ + { + name: "IDX_product_handle_unique", + on: ["handle"], + unique: true, + where: "deleted_at IS NULL", + }, + { + name: "IDX_product_type_id", + on: ["type_id"], + unique: false, + where: "deleted_at IS NULL", + }, + { + name: "IDX_product_collection_id", + on: ["collection_id"], + unique: false, + where: "deleted_at IS NULL", + }, + ]) export default Product diff --git a/packages/modules/product/src/repositories/product-category.ts b/packages/modules/product/src/repositories/product-category.ts index 98e67bc2ca..e6f1429ff7 100644 --- a/packages/modules/product/src/repositories/product-category.ts +++ b/packages/modules/product/src/repositories/product-category.ts @@ -1,30 +1,30 @@ import { Context, DAL, + InferEntityType, ProductCategoryTransformOptions, ProductTypes, } from "@medusajs/framework/types" import { DALUtils, isDefined, MedusaError } from "@medusajs/framework/utils" -import { - EntityDTO, - LoadStrategy, - FilterQuery as MikroFilterQuery, - FindOptions as MikroOptions, - RequiredEntityData, -} from "@mikro-orm/core" +import { LoadStrategy, FindOptions as MikroOptions } from "@mikro-orm/core" import { SqlEntityManager } from "@mikro-orm/postgresql" import { ProductCategory } from "@models" import { UpdateCategoryInput } from "@types" // eslint-disable-next-line max-len -export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeRepository { +export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeRepository< + typeof ProductCategory +> { buildFindOptions( - findOptions: DAL.FindOptions = { where: {} }, + findOptions: DAL.FindOptions = { where: {} }, familyOptions: ProductCategoryTransformOptions = {} ) { const findOptions_ = { ...findOptions } - findOptions_.options ??= { - orderBy: { rank: "ASC" }, + findOptions_.options ??= {} + findOptions_.options.orderBy = { + id: "ASC", + rank: "ASC", + ...findOptions_.options.orderBy, } const fields = (findOptions_.options.fields ??= []) @@ -69,17 +69,19 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito } async find( - findOptions: DAL.FindOptions = { where: {} }, + findOptions: DAL.FindOptions = { where: {} }, transformOptions: ProductCategoryTransformOptions = {}, context: Context = {} - ): Promise { + ): Promise[]> { const manager = super.getActiveManager(context) const findOptions_ = this.buildFindOptions(findOptions, transformOptions) - const productCategories = await manager.find( - ProductCategory, - findOptions_.where as MikroFilterQuery, - { ...findOptions_.options } as MikroOptions + const productCategories = await manager.find< + InferEntityType + >( + ProductCategory.name, + findOptions_.where, + { ...findOptions_.options } as any // TODO ) if ( @@ -102,7 +104,9 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito return this.sortCategoriesByRank(categoriesTree) } - sortCategoriesByRank(categories: ProductCategory[]): ProductCategory[] { + sortCategoriesByRank( + categories: InferEntityType[] + ): InferEntityType[] { const sortedCategories = categories.sort((a, b) => a.rank - b.rank) for (const category of sortedCategories) { @@ -122,12 +126,12 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito descendants?: boolean ancestors?: boolean }, - productCategories: ProductCategory[], - findOptions: DAL.FindOptions & { + productCategories: InferEntityType[], + findOptions: DAL.FindOptions & { serialize?: boolean } = { where: {} }, context: Context = {} - ): Promise { + ): Promise[]> { const { serialize = true } = findOptions delete findOptions.serialize @@ -173,17 +177,17 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito ...findOptions.options, limit: undefined, offset: 0, - } as MikroOptions + } as MikroOptions delete where.id delete where.mpath delete where.parent_category_id const categoriesInTree = serialize - ? await this.serialize( - await manager.find(ProductCategory, where, options) + ? await this.serialize[]>( + await manager.find(ProductCategory.name, where, options) ) - : await manager.find(ProductCategory, where, options) + : await manager.find(ProductCategory.name, where, options) const categoriesById = new Map(categoriesInTree.map((cat) => [cat.id, cat])) @@ -235,18 +239,18 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito } async findAndCount( - findOptions: DAL.FindOptions = { where: {} }, + findOptions: DAL.FindOptions = { where: {} }, transformOptions: ProductCategoryTransformOptions = {}, context: Context = {} - ): Promise<[ProductCategory[], number]> { + ): Promise<[InferEntityType[], number]> { const manager = super.getActiveManager(context) const findOptions_ = this.buildFindOptions(findOptions, transformOptions) - const [productCategories, count] = await manager.findAndCount( - ProductCategory, - findOptions_.where as MikroFilterQuery, - findOptions_.options as MikroOptions - ) + const [productCategories, count] = (await manager.findAndCount( + ProductCategory.name, + findOptions_.where, + findOptions_.options as any + )) as unknown as [InferEntityType[], number] if ( !transformOptions.includeDescendantsTree && @@ -271,19 +275,23 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito async delete(ids: string[], context: Context = {}): Promise { const manager = super.getActiveManager(context) await this.baseDelete(ids, context) - await manager.nativeDelete(ProductCategory, { id: ids }, {}) + await manager.nativeDelete(ProductCategory.name, { id: ids }, {}) } async softDelete( ids: string[], context: Context = {} - ): Promise<[ProductCategory[], Record]> { + ): Promise< + [InferEntityType[], Record] + > { const manager = super.getActiveManager(context) await this.baseDelete(ids, context) const categories = await Promise.all( ids.map(async (id) => { - const productCategory = await manager.findOne(ProductCategory, { + const productCategory = await manager.findOne< + InferEntityType + >(ProductCategory.name, { id, }) @@ -306,11 +314,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito async restore( ids: string[], context: Context = {} - ): Promise<[ProductCategory[], Record]> { + ): Promise< + [InferEntityType[], Record] + > { const manager = super.getActiveManager(context) const categories = await Promise.all( ids.map(async (id) => { - const productCategory = await manager.findOneOrFail(ProductCategory, { + const productCategory = await manager.findOneOrFail< + InferEntityType + >(ProductCategory.name, { id, }) manager.assign(productCategory, { deleted_at: null }) @@ -327,12 +339,14 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito await Promise.all( ids.map(async (id) => { - const productCategory = await manager.findOne( - ProductCategory, + const productCategory = await manager.findOne< + InferEntityType + >( + ProductCategory.name, { id }, { populate: ["category_children"], - } + } as any // TODO ) if (!productCategory) { @@ -357,13 +371,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito async create( data: ProductTypes.CreateProductCategoryDTO[], context: Context = {} - ): Promise { + ): Promise[]> { const manager = super.getActiveManager(context) const categories = await Promise.all( data.map(async (entry, i) => { - const categoryData: Partial> = { ...entry } - const siblingsCount = await manager.count(ProductCategory, { + const categoryData: Partial> = { + ...entry, + } + const siblingsCount = await manager.count(ProductCategory.name, { parent_category_id: categoryData?.parent_category_id || null, }) @@ -378,14 +394,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito } // Set the base mpath if the category has a parent. The model `create` hook will append the own id to the base mpath. + let parentCategory: InferEntityType | null = + null const parentCategoryId = categoryData.parent_category_id ?? categoryData.parent_category?.id if (parentCategoryId) { - const parentCategory = await manager.findOne( - ProductCategory, - parentCategoryId - ) + parentCategory = await manager.findOne< + InferEntityType + >(ProductCategory.name, parentCategoryId) if (!parentCategory) { throw new MedusaError( @@ -393,14 +410,28 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito `Parent category with id: '${parentCategoryId}' does not exist` ) } - - categoryData.mpath = parentCategory.mpath } - return manager.create( - ProductCategory, - categoryData as RequiredEntityData + const result = await manager.create< + InferEntityType + >( + ProductCategory.name, + categoryData as unknown as InferEntityType ) + + /** + * Since "mpath" calculation relies on the id of the created + * category, we have to compute it after calling manager.create. So + * that we can access the "category.id" which is under the hood + * defined by DML. + */ + manager.assign(result, { + mpath: parentCategory + ? `${parentCategory.mpath}.${result.id}` + : result.id, + }) + + return result }) ) @@ -411,14 +442,18 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito async update( data: UpdateCategoryInput[], context: Context = {} - ): Promise { + ): Promise[]> { const manager = super.getActiveManager(context) const categories = await Promise.all( data.map(async (entry, i) => { - const categoryData: Partial> = { ...entry } - let productCategory = (await manager.findOne(ProductCategory, { + const categoryData: Partial> = { + ...entry, + } + let productCategory = await manager.findOne< + InferEntityType + >(ProductCategory.name, { id: categoryData.id, - })) as ProductCategory + }) if (!productCategory) { throw new MedusaError( @@ -465,10 +500,9 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito ) )[0] - const newParentCategory = await manager.findOne( - ProductCategory, - categoryData.parent_category_id - ) + const newParentCategory = await manager.findOne< + InferEntityType + >(ProductCategory.name, categoryData.parent_category_id) if (!newParentCategory) { throw new MedusaError( @@ -487,13 +521,13 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito ) function updateMpathRecursively( - category: ProductCategory, + category: InferEntityType, newBaseMpath: string ) { const newMpath = `${newBaseMpath}.${category.id}` category.mpath = newMpath for (let child of category.category_children) { - child = manager.getReference(ProductCategory, child.id) + child = manager.getReference(ProductCategory.name, child.id) manager.assign( child, categoryDataChildrenMap.get(child.id) ?? {} @@ -502,12 +536,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito } } - updateMpathRecursively(productCategory!, newParentCategory.mpath!) + updateMpathRecursively( + productCategory!, + (newParentCategory as any).mpath! + ) // categoryData.mpath = `${newParentCategory.mpath}.${productCategory.id}` } // Rerank the siblings in the new parent - const siblingsCount = await manager.count(ProductCategory, { + const siblingsCount = await manager.count(ProductCategory.name, { parent_category_id: categoryData.parent_category_id, }) @@ -534,7 +571,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito return productCategory // If only the rank changed, we need to rerank all siblings. } else if (isDefined(categoryData.rank)) { - const siblingsCount = await manager.count(ProductCategory, { + const siblingsCount = await manager.count(ProductCategory.name, { parent_category_id: productCategory.parent_category_id, }) @@ -546,7 +583,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito await this.rerankAllSiblings( manager, productCategory, - categoryData as Partial> & { + categoryData as Partial> & { rank: number } ) @@ -569,9 +606,11 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito protected async rerankSiblingsAfterDeletion( manager: SqlEntityManager, - removedSibling: Partial + removedSibling: Partial> ) { - const affectedSiblings = await manager.find(ProductCategory, { + const affectedSiblings = await manager.find< + InferEntityType + >(ProductCategory.name, { parent_category_id: removedSibling.parent_category_id, rank: { $gt: removedSibling.rank }, }) @@ -586,9 +625,11 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito protected async rerankSiblingsAfterCreation( manager: SqlEntityManager, - addedSibling: Partial> + addedSibling: Partial> ) { - const affectedSiblings = await manager.find(ProductCategory, { + const affectedSiblings = await manager.find< + InferEntityType + >(ProductCategory.name, { parent_category_id: addedSibling.parent_category_id, rank: { $gte: addedSibling.rank }, }) @@ -603,16 +644,22 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito protected async rerankAllSiblings( manager: SqlEntityManager, - originalSibling: Partial & { rank: number }, - updatedSibling: Partial> & { rank: number } + originalSibling: Partial> & { + rank: number + }, + updatedSibling: Partial> & { + rank: number + } ) { if (originalSibling.rank === updatedSibling.rank) { return } if (originalSibling.rank < updatedSibling.rank) { - const siblings = await manager.find( - ProductCategory, + const siblings = await manager.find< + InferEntityType + >( + ProductCategory.name, { parent_category_id: originalSibling.parent_category_id, rank: { $gt: originalSibling.rank, $lte: updatedSibling.rank }, @@ -627,8 +674,10 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito manager.persist(updatedSiblings) } else { - const siblings = await manager.find( - ProductCategory, + const siblings = await manager.find< + InferEntityType + >( + ProductCategory.name, { parent_category_id: originalSibling.parent_category_id, rank: { $gte: updatedSibling.rank, $lt: originalSibling.rank }, diff --git a/packages/modules/product/src/repositories/product.ts b/packages/modules/product/src/repositories/product.ts index c53db0e0d2..3b44d82a74 100644 --- a/packages/modules/product/src/repositories/product.ts +++ b/packages/modules/product/src/repositories/product.ts @@ -1,11 +1,11 @@ import { Product } from "@models" import { Context, DAL } from "@medusajs/framework/types" -import { SqlEntityManager } from "@mikro-orm/postgresql" import { DALUtils } from "@medusajs/framework/utils" +import { SqlEntityManager } from "@mikro-orm/postgresql" // eslint-disable-next-line max-len -export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( +export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( Product ) { constructor(...args: any[]) { @@ -19,7 +19,9 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory = { where: {} }, + findOptions: DAL.FindOptions = { + where: {}, + }, context: Context = {} ): Promise { const manager = this.getActiveManager(context) @@ -29,7 +31,7 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory = {}, @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise> { if (!isDefined(productCategoryId)) { throw new MedusaError( MedusaError.Types.NOT_FOUND, @@ -41,7 +42,7 @@ export default class ProductCategoryService { ) } - const queryOptions = ModulesSdkUtils.buildQuery( + const queryOptions = ModulesSdkUtils.buildQuery( { id: productCategoryId, }, @@ -67,7 +68,7 @@ export default class ProductCategoryService { ) } - return productCategories[0] as ProductCategory + return productCategories[0] } @InjectManager("productCategoryRepository_") @@ -75,7 +76,7 @@ export default class ProductCategoryService { filters: ProductTypes.FilterableProductCategoryProps = {}, config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { const transformOptions = { includeDescendantsTree: filters?.include_descendants_tree || false, includeAncestorsTree: filters?.include_ancestors_tree || false, @@ -94,17 +95,14 @@ export default class ProductCategoryService { delete filters.q } - const queryOptions = ModulesSdkUtils.buildQuery( - filters, - config - ) + const queryOptions = ModulesSdkUtils.buildQuery(filters, config) queryOptions.where ??= {} - return (await this.productCategoryRepository_.find( + return await this.productCategoryRepository_.find( queryOptions, transformOptions, sharedContext - )) as ProductCategory[] + ) } @InjectManager("productCategoryRepository_") @@ -112,7 +110,7 @@ export default class ProductCategoryService { filters: ProductTypes.FilterableProductCategoryProps = {}, config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductCategory[], number]> { + ): Promise<[InferEntityType[], number]> { const transformOptions = { includeDescendantsTree: filters?.include_descendants_tree || false, includeAncestorsTree: filters?.include_ancestors_tree || false, @@ -131,37 +129,34 @@ export default class ProductCategoryService { delete filters.q } - const queryOptions = ModulesSdkUtils.buildQuery( - filters, - config - ) + const queryOptions = ModulesSdkUtils.buildQuery(filters, config) queryOptions.where ??= {} - return (await this.productCategoryRepository_.findAndCount( + return await this.productCategoryRepository_.findAndCount( queryOptions, transformOptions, sharedContext - )) as [ProductCategory[], number] + ) } @InjectTransactionManager("productCategoryRepository_") async create( data: ProductTypes.CreateProductCategoryDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { - return (await ( + ): Promise[]> { + return await ( this.productCategoryRepository_ as unknown as ProductCategoryRepository - ).create(data, sharedContext)) as ProductCategory[] + ).create(data, sharedContext) } @InjectTransactionManager("productCategoryRepository_") async update( data: UpdateCategoryInput[], @MedusaContext() sharedContext: Context = {} - ): Promise { - return (await ( + ): Promise[]> { + return await ( this.productCategoryRepository_ as unknown as ProductCategoryRepository - ).update(data, sharedContext)) as ProductCategory[] + ).update(data, sharedContext) } @InjectTransactionManager("productCategoryRepository_") diff --git a/packages/modules/product/src/services/product-module-service.ts b/packages/modules/product/src/services/product-module-service.ts index 623eb8b870..bfa33fe718 100644 --- a/packages/modules/product/src/services/product-module-service.ts +++ b/packages/modules/product/src/services/product-module-service.ts @@ -3,6 +3,7 @@ import { DAL, FindConfig, IEventBusModuleService, + InferEntityType, InternalModuleDeclaration, ModuleJoinerConfig, ModulesSdkTypes, @@ -105,15 +106,31 @@ export default class ProductModuleService implements ProductTypes.IProductModuleService { protected baseRepository_: DAL.RepositoryService - protected readonly productService_: ModulesSdkTypes.IMedusaInternalService - protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService + protected readonly productService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > + protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > protected readonly productCategoryService_: ProductCategoryService - protected readonly productTagService_: ModulesSdkTypes.IMedusaInternalService - protected readonly productCollectionService_: ModulesSdkTypes.IMedusaInternalService - protected readonly productImageService_: ModulesSdkTypes.IMedusaInternalService - protected readonly productTypeService_: ModulesSdkTypes.IMedusaInternalService - protected readonly productOptionService_: ModulesSdkTypes.IMedusaInternalService - protected readonly productOptionValueService_: ModulesSdkTypes.IMedusaInternalService + protected readonly productTagService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > + protected readonly productCollectionService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > + protected readonly productImageService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > + protected readonly productTypeService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > + protected readonly productOptionService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > + protected readonly productOptionValueService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > protected readonly eventBusModuleService_?: IEventBusModuleService constructor( @@ -211,12 +228,16 @@ export default class ProductModuleService return { ...config, order: { + id: "ASC", ...config?.order, - ...(hasImagesRelation ? { - images: { - rank: "ASC", - }, - } : {}), + ...(hasImagesRelation + ? { + images: { + rank: "ASC", + ...((config?.order?.images as object) ?? {}), + }, + } + : {}), }, } } @@ -256,7 +277,7 @@ export default class ProductModuleService protected async createVariants_( data: ProductTypes.CreateProductVariantDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { if (data.some((v) => !v.product_id)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -332,8 +353,8 @@ export default class ProductModuleService (variant): variant is ProductTypes.CreateProductVariantDTO => !variant.id ) - let created: ProductVariant[] = [] - let updated: ProductVariant[] = [] + let created: InferEntityType[] = [] + let updated: InferEntityType[] = [] if (forCreate.length) { created = await this.createVariants_(forCreate, sharedContext) @@ -400,7 +421,7 @@ export default class ProductModuleService protected async updateVariants_( data: UpdateProductVariantInput[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { // Validation step const variantIdsToUpdate = data.map(({ id }) => id) const variants = await this.productVariantService_.list( @@ -535,8 +556,8 @@ export default class ProductModuleService (tag): tag is ProductTypes.CreateProductTagDTO => !tag.id ) - let created: ProductTag[] = [] - let updated: ProductTag[] = [] + let created: InferEntityType[] = [] + let updated: InferEntityType[] = [] if (forCreate.length) { created = await this.productTagService_.create(forCreate, sharedContext) @@ -665,8 +686,8 @@ export default class ProductModuleService (type): type is ProductTypes.CreateProductTypeDTO => !type.id ) - let created: ProductType[] = [] - let updated: ProductType[] = [] + let created: InferEntityType[] = [] + let updated: InferEntityType[] = [] if (forCreate.length) { created = await this.productTypeService_.create(forCreate, sharedContext) @@ -763,7 +784,7 @@ export default class ProductModuleService protected async createOptions_( data: ProductTypes.CreateProductOptionDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { if (data.some((v) => !v.product_id)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -810,8 +831,8 @@ export default class ProductModuleService (option): option is ProductTypes.CreateProductOptionDTO => !option.id ) - let created: ProductOption[] = [] - let updated: ProductOption[] = [] + let created: InferEntityType[] = [] + let updated: InferEntityType[] = [] if (forCreate.length) { created = await this.createOptions_(forCreate, sharedContext) @@ -876,7 +897,7 @@ export default class ProductModuleService protected async updateOptions_( data: UpdateProductOptionInput[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { // Validation step if (data.some((option) => !option.id)) { throw new MedusaError( @@ -985,7 +1006,7 @@ export default class ProductModuleService async createCollections_( data: ProductTypes.CreateProductCollectionDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { const normalizedInput = data.map( ProductModuleService.normalizeCreateProductCollectionInput ) @@ -1030,8 +1051,8 @@ export default class ProductModuleService !collection.id ) - let created: ProductCollection[] = [] - let updated: ProductCollection[] = [] + let created: InferEntityType[] = [] + let updated: InferEntityType[] = [] if (forCreate.length) { created = await this.createCollections_(forCreate, sharedContext) @@ -1125,7 +1146,7 @@ export default class ProductModuleService protected async updateCollections_( data: UpdateCollectionInput[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { const normalizedInput = data.map( ProductModuleService.normalizeUpdateProductCollectionInput ) as UpdateCollectionInput[] @@ -1139,7 +1160,7 @@ export default class ProductModuleService sharedContext ) - const collections: ProductCollection[] = [] + const collections: InferEntityType[] = [] const updateSelectorAndData = updatedCollections.flatMap( (collectionData) => { @@ -1178,7 +1199,7 @@ export default class ProductModuleService collections.push({ ...collectionData, products: productsToUpdate ?? [], - } as ProductCollection) + } as InferEntityType) return result } @@ -1208,7 +1229,12 @@ export default class ProductModuleService ): Promise< ProductTypes.ProductCategoryDTO[] | ProductTypes.ProductCategoryDTO > { - const input = Array.isArray(data) ? data : [data] + const input = (Array.isArray(data) ? data : [data]).map( + (productCategory) => { + productCategory.handle ??= kebabCase(productCategory.name) + return productCategory + } + ) const categories = await this.productCategoryService_.create( input, @@ -1255,8 +1281,8 @@ export default class ProductModuleService !category.id ) - let created: ProductCategory[] = [] - let updated: ProductCategory[] = [] + let created: InferEntityType[] = [] + let updated: InferEntityType[] = [] if (forCreate.length) { created = await this.productCategoryService_.create( @@ -1406,8 +1432,8 @@ export default class ProductModuleService (product): product is ProductTypes.CreateProductDTO => !product.id ) - let created: Product[] = [] - let updated: Product[] = [] + let created: InferEntityType[] = [] + let updated: InferEntityType[] = [] if (forCreate.length) { created = await this.createProducts_(forCreate, sharedContext) @@ -1494,7 +1520,7 @@ export default class ProductModuleService protected async createProducts_( data: ProductTypes.CreateProductDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { const normalizedInput = await promiseAll( data.map(async (d) => { const normalized = await this.normalizeCreateProductInput( @@ -1581,7 +1607,7 @@ export default class ProductModuleService protected async updateProducts_( data: UpdateProductInput[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { const normalizedInput = await promiseAll( data.map(async (d) => { const normalized = await this.normalizeUpdateProductInput( @@ -1733,6 +1759,13 @@ export default class ProductModuleService ) { this.validateProductPayload(productData) + if (!productData.title) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Product title is required` + ) + } + const options = productData.options const missingOptionsVariants: string[] = [] @@ -1872,7 +1905,7 @@ export default class ProductModuleService variants: | ProductTypes.CreateProductVariantDTO[] | ProductTypes.UpdateProductVariantDTO[], - options: ProductOption[] + options: InferEntityType[] ): | ProductTypes.CreateProductVariantDTO[] | ProductTypes.UpdateProductVariantDTO[] { @@ -1945,7 +1978,7 @@ export default class ProductModuleService | ProductTypes.CreateProductVariantDTO | UpdateProductVariantInput ) & { options: { id: string }[]; product_id: string })[], - variants: ProductVariant[] + variants: InferEntityType[] ) { for (const variantData of data) { const existingVariant = variants.find((v) => {