diff --git a/packages/core/types/src/dal/index.ts b/packages/core/types/src/dal/index.ts index 7e3a858553359..ab99c2b13034b 100644 --- a/packages/core/types/src/dal/index.ts +++ b/packages/core/types/src/dal/index.ts @@ -23,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. */ @@ -73,7 +73,7 @@ export type FindOptions = { /** * The options to apply when retrieving the items. */ - options?: OptionsQuery, any> + options?: OptionsQuery> } /** diff --git a/packages/core/types/src/dal/repository-service.ts b/packages/core/types/src/dal/repository-service.ts index 598c919d4f694..6aae45c990a4e 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]> - - create(data: any[], context?: Context): Promise + ): Promise<[InferRepositoryReturnType[], number]> - update(data: { entity; update }[], context?: Context): Promise + create( + data: any[], + context?: Context + ): Promise[]> - delete( - idsOrPKs: FilterQuery & BaseFilterable>, + update( + data: { entity; update }[], context?: Context - ): Promise + ): 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 f99a7e49f4750..90151ec62b99d 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/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 8ffef62387006..2c5f51541f739 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 902f16fa37ad5..822c181f9cf9d 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 @@ -3,8 +3,9 @@ import { Context, DAL, FilterQuery, - InferEntityType, FilterQuery as InternalFilterQuery, + InferEntityType, + InferRepositoryReturnType, PerformedActions, RepositoryService, RepositoryTransformOptions, @@ -92,7 +93,7 @@ export class MikroOrmBase { * related ones. */ -export class MikroOrmBaseRepository +export class MikroOrmBaseRepository extends MikroOrmBase implements RepositoryService { @@ -148,11 +149,17 @@ export class MikroOrmBaseRepository }) } - create(data: unknown[], context?: Context): Promise { + create( + data: unknown[], + context?: Context + ): Promise[]> { throw new Error("Method not implemented.") } - update(data: { entity; update }[], context?: Context): Promise { + update( + data: { entity; update }[], + context?: Context + ): Promise[]> { throw new Error("Method not implemented.") } @@ -163,28 +170,37 @@ export class MikroOrmBaseRepository throw new Error("Method not implemented.") } - find(options?: DAL.FindOptions, context?: Context): Promise { + 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.") } @@ -192,10 +208,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() @@ -216,8 +232,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, }) @@ -290,7 +306,7 @@ export class MikroOrmBaseTreeRepository< } } -export function mikroOrmBaseRepositoryFactory( +export function mikroOrmBaseRepositoryFactory( entity: T ): { new ({ manager }: { manager: any }): MikroOrmBaseRepository @@ -328,7 +344,7 @@ export function mikroOrmBaseRepositoryFactory( async create( data: any[], context?: Context - ): Promise[]> { + ): Promise[]> { const manager = this.getActiveManager(context) const entities = data.map((data_) => { @@ -340,7 +356,7 @@ export function mikroOrmBaseRepositoryFactory( manager.persist(entities) - return entities as InferEntityType[] + return entities as InferRepositoryReturnType[] } /** @@ -410,7 +426,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) @@ -432,9 +451,9 @@ export function mikroOrmBaseRepositoryFactory( } 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 } @@ -456,17 +475,17 @@ export function mikroOrmBaseRepositoryFactory( findOptions: findOptions_, }) - return await manager.find( + return (await manager.find( 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 } @@ -480,14 +499,17 @@ export function mikroOrmBaseRepositoryFactory( findOptions: findOptions_, }) - return await manager.findAndCount( + return (await manager.findAndCount( entity as EntityName, findOptions_.where as MikroFilterQuery, findOptions_.options as 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( @@ -511,7 +533,7 @@ export function mikroOrmBaseRepositoryFactory( })) } - let allEntities: T[][] = [] + let allEntities: InferRepositoryReturnType[][] = [] if (primaryKeysCriteria.length) { allEntities = await Promise.all( @@ -527,7 +549,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 = @@ -539,9 +564,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 - @@ -556,8 +581,8 @@ export function mikroOrmBaseRepositoryFactory( const updatedType = manager.assign(existingEntity, data_) updatedEntities.push(updatedType) } else { - const newEntity = manager.create(this.entity, data_) - createdEntities.push(newEntity) + const newEntity = manager.create(this.entity, data_) + createdEntities.push(newEntity as InferRepositoryReturnType) } }) @@ -572,7 +597,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. @@ -584,11 +609,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: {}, @@ -972,10 +1000,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 [[], {}] } @@ -993,10 +1021,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 [[], {}] } @@ -1014,9 +1042,9 @@ export function mikroOrmBaseRepositoryFactory( filters: | string | string[] - | (FilterQuery & BaseFilterable>) - | (FilterQuery & BaseFilterable>)[] - ) { + | DAL.FindOptions["where"] + | DAL.FindOptions["where"][] + ): DAL.FindOptions["where"] { const primaryKeys = MikroOrmAbstractBaseRepository_.retrievePrimaryKeys( this.entity ) @@ -1033,7 +1061,7 @@ export function mikroOrmBaseRepositoryFactory( }), } - return normalizedFilters + return normalizedFilters as DAL.FindOptions["where"] } } diff --git a/packages/core/utils/src/modules-sdk/build-query.ts b/packages/core/utils/src/modules-sdk/build-query.ts index 63da37fa6ebac..3dbbd22924b6e 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/medusa-internal-service.ts b/packages/core/utils/src/modules-sdk/medusa-internal-service.ts index cdd358c38e28e..3689869adbe48 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)) {