diff --git a/.changeset/sharp-rocks-retire.md b/.changeset/sharp-rocks-retire.md new file mode 100644 index 0000000000..2f985a9213 --- /dev/null +++ b/.changeset/sharp-rocks-retire.md @@ -0,0 +1,13 @@ +--- +"@medusajs/event-bus-local": patch +"@medusajs/event-bus-redis": patch +"@medusajs/link-modules": patch +"@medusajs/modules-sdk": patch +"@medusajs/inventory": patch +"@medusajs/product": patch +"@medusajs/medusa": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +Feat: Event Aggregator diff --git a/packages/auth/integration-tests/__tests__/services/module/auth-provider.spec.ts b/packages/auth/integration-tests/__tests__/services/module/auth-provider.spec.ts index 37aaa50ad2..96034b5518 100644 --- a/packages/auth/integration-tests/__tests__/services/module/auth-provider.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/module/auth-provider.spec.ts @@ -1,11 +1,11 @@ -import { IAuthModuleService } from "@medusajs/types" -import { MikroOrmWrapper } from "../../../utils" import { Modules } from "@medusajs/modules-sdk" +import { IAuthModuleService } from "@medusajs/types" import { SqlEntityManager } from "@mikro-orm/postgresql" +import { initModules } from "medusa-test-utils" import { createAuthProviders } from "../../../__fixtures__/auth-provider" import { createAuthUsers } from "../../../__fixtures__/auth-user" +import { MikroOrmWrapper } from "../../../utils" import { getInitModuleConfig } from "../../../utils/get-init-module-config" -import { initModules } from "medusa-test-utils" jest.setTimeout(30000) diff --git a/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts b/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts index 3ca27ed83e..cd812040da 100644 --- a/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts @@ -1,11 +1,11 @@ -import { IAuthModuleService } from "@medusajs/types" -import { MikroOrmWrapper } from "../../../utils" import { Modules } from "@medusajs/modules-sdk" +import { IAuthModuleService } from "@medusajs/types" import { SqlEntityManager } from "@mikro-orm/postgresql" +import { initModules } from "medusa-test-utils" import { createAuthProviders } from "../../../__fixtures__/auth-provider" import { createAuthUsers } from "../../../__fixtures__/auth-user" +import { MikroOrmWrapper } from "../../../utils" import { getInitModuleConfig } from "../../../utils/get-init-module-config" -import { initModules } from "medusa-test-utils" jest.setTimeout(30000) diff --git a/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts b/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts index b118e54ac0..a92aa56cde 100644 --- a/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts @@ -1,11 +1,10 @@ import { MedusaModule, Modules } from "@medusajs/modules-sdk" - import { IAuthModuleService } from "@medusajs/types" -import { MikroOrmWrapper } from "../../../utils" import { SqlEntityManager } from "@mikro-orm/postgresql" -import { createAuthProviders } from "../../../__fixtures__/auth-provider" -import { getInitModuleConfig } from "../../../utils/get-init-module-config" import { initModules } from "medusa-test-utils" +import { createAuthProviders } from "../../../__fixtures__/auth-provider" +import { MikroOrmWrapper } from "../../../utils" +import { getInitModuleConfig } from "../../../utils/get-init-module-config" jest.setTimeout(30000) @@ -70,7 +69,10 @@ describe("AuthModuleService - AuthProvider", () => { }, ]) - const { success, error } = await service.authenticate("notRegistered", {} as any) + const { success, error } = await service.authenticate( + "notRegistered", + {} as any + ) expect(success).toBe(false) expect(error).toEqual( @@ -79,12 +81,9 @@ describe("AuthModuleService - AuthProvider", () => { }) it("fails to authenticate using a valid provider with an invalid scope", async () => { - const { success, error } = await service.authenticate( - "emailpass", - { - authScope: "non-existing", - } as any - ) + const { success, error } = await service.authenticate("emailpass", { + authScope: "non-existing", + } as any) expect(success).toBe(false) expect(error).toEqual( diff --git a/packages/event-bus-local/src/services/event-bus-local.ts b/packages/event-bus-local/src/services/event-bus-local.ts index 418ba6cd3b..9a5e99afdb 100644 --- a/packages/event-bus-local/src/services/event-bus-local.ts +++ b/packages/event-bus-local/src/services/event-bus-local.ts @@ -1,5 +1,11 @@ import { MedusaContainer } from "@medusajs/modules-sdk" -import { EmitData, EventBusTypes, Logger, Subscriber } from "@medusajs/types" +import { + EmitData, + EventBusTypes, + Logger, + Message, + Subscriber, +} from "@medusajs/types" import { AbstractEventBusModuleService } from "@medusajs/utils" import { EventEmitter } from "events" import { ulid } from "ulid" @@ -37,14 +43,16 @@ export default class LocalEventBusService extends AbstractEventBusModuleService */ async emit(data: EmitData[]): Promise - async emit[] = string>( + async emit(data: Message[]): Promise + + async emit[] | Message[] = string>( eventOrData: TInput, data?: T, options: Record = {} ): Promise { const isBulkEmit = Array.isArray(eventOrData) - const events: EmitData[] = isBulkEmit + const events: EmitData[] | Message[] = isBulkEmit ? eventOrData : [{ eventName: eventOrData, data }] @@ -61,7 +69,8 @@ export default class LocalEventBusService extends AbstractEventBusModuleService continue } - this.eventEmitter_.emit(event.eventName, event.data) + const data = (event as EmitData).data ?? (event as Message).body + this.eventEmitter_.emit(event.eventName, data) } } diff --git a/packages/event-bus-redis/src/services/event-bus-redis.ts b/packages/event-bus-redis/src/services/event-bus-redis.ts index 5271237947..38bfa23c94 100644 --- a/packages/event-bus-redis/src/services/event-bus-redis.ts +++ b/packages/event-bus-redis/src/services/event-bus-redis.ts @@ -1,6 +1,6 @@ import { InternalModuleDeclaration } from "@medusajs/modules-sdk" -import { EmitData, Logger } from "@medusajs/types" -import { AbstractEventBusModuleService } from "@medusajs/utils" +import { EmitData, Logger, Message } from "@medusajs/types" +import { AbstractEventBusModuleService, isString } from "@medusajs/utils" import { BulkJobOptions, JobsOptions, Queue, Worker } from "bullmq" import { Redis } from "ioredis" import { BullJob, EmitOptions, EventBusRedisModuleOptions } from "../types" @@ -67,7 +67,9 @@ export default class RedisEventBusService extends AbstractEventBusModuleService */ async emit(data: EmitData[]): Promise - async emit[] = string>( + async emit(data: Message[]): Promise + + async emit[] | Message[] = string>( eventNameOrData: TInput, data?: T, options: BulkJobOptions | JobsOptions = {} @@ -84,10 +86,17 @@ export default class RedisEventBusService extends AbstractEventBusModuleService ...globalJobOptions, } as EmitOptions + const dataBody = isString(eventNameOrData) + ? data ?? (data as Message).body + : undefined + const events = isBulkEmit ? eventNameOrData.map((event) => ({ name: event.eventName, - data: { eventName: event.eventName, data: event.data }, + data: { + eventName: event.eventName, + data: (event as EmitData).data ?? (event as Message).body, + }, opts: { ...opts, // local options @@ -97,7 +106,7 @@ export default class RedisEventBusService extends AbstractEventBusModuleService : [ { name: eventNameOrData as string, - data: { eventName: eventNameOrData, data }, + data: { eventName: eventNameOrData, data: dataBody }, opts: { ...opts, // local options diff --git a/packages/inventory/src/joiner-config.ts b/packages/inventory/src/joiner-config.ts index c8d1ec0975..df2bc97484 100644 --- a/packages/inventory/src/joiner-config.ts +++ b/packages/inventory/src/joiner-config.ts @@ -1,6 +1,7 @@ import { Modules } from "@medusajs/modules-sdk" import { ModuleJoinerConfig } from "@medusajs/types" import { InventoryItem, InventoryLevel, ReservationItem } from "./models" +import moduleSchema from "./schema" export const joinerConfig: ModuleJoinerConfig = { serviceName: Modules.INVENTORY, @@ -10,46 +11,30 @@ export const joinerConfig: ModuleJoinerConfig = { inventory_level_id: InventoryLevel.name, reservation_item_id: ReservationItem.name, }, + schema: moduleSchema, alias: [ { - name: "inventory_items", - }, - { - name: "inventory", - }, - { - name: "inventory_level", + name: ["inventory_items", "inventory"], args: { + entity: "InventoryItem", + }, + }, + { + name: ["inventory_level", "inventory_levels"], + args: { + entity: "InventoryLevel", methodSuffix: "InventoryLevels", }, }, { - name: "inventory_levels", - args: { - methodSuffix: "InventoryLevels", - }, - }, - { - name: "reservation_items", - args: { - methodSuffix: "ReservationItems", - }, - }, - { - name: "reservation_item", - args: { - methodSuffix: "ReservationItems", - }, - }, - { - name: "reservation", - args: { - methodSuffix: "ReservationItems", - }, - }, - { - name: "reservations", + name: [ + "reservation", + "reservations", + "reservation_item", + "reservation_items", + ], args: { + entity: "ReservationItem", methodSuffix: "ReservationItems", }, }, diff --git a/packages/inventory/src/models/inventory-item.ts b/packages/inventory/src/models/inventory-item.ts index 61bcee9e89..418cbcbd88 100644 --- a/packages/inventory/src/models/inventory-item.ts +++ b/packages/inventory/src/models/inventory-item.ts @@ -6,9 +6,11 @@ import { DeleteDateColumn, Entity, Index, + OneToMany, PrimaryColumn, UpdateDateColumn, } from "typeorm" +import { InventoryLevel } from "./inventory-level" @Entity() export class InventoryItem { @@ -67,6 +69,12 @@ export class InventoryItem { @Column({ type: "jsonb", nullable: true }) metadata: Record | null + @OneToMany( + () => InventoryLevel, + (inventoryLevel) => inventoryLevel.inventory_item + ) + inventory_levels!: InventoryLevel[] + @BeforeInsert() private beforeInsert(): void { this.id = generateEntityId(this.id, "iitem") diff --git a/packages/inventory/src/models/inventory-level.ts b/packages/inventory/src/models/inventory-level.ts index c7814b5437..62a19e7970 100644 --- a/packages/inventory/src/models/inventory-level.ts +++ b/packages/inventory/src/models/inventory-level.ts @@ -6,9 +6,12 @@ import { DeleteDateColumn, Entity, Index, + JoinColumn, + ManyToOne, PrimaryColumn, UpdateDateColumn, } from "typeorm" +import { InventoryItem } from "./inventory-item" @Entity() @Index(["inventory_item_id", "location_id"], { unique: true }) @@ -45,6 +48,10 @@ export class InventoryLevel { @Column({ type: "jsonb", nullable: true }) metadata: Record | null + @ManyToOne(() => InventoryItem) + @JoinColumn({ name: "inventory_item_id" }) + inventory_item: InventoryItem + @BeforeInsert() private beforeInsert(): void { this.id = generateEntityId(this.id, "ilev") diff --git a/packages/inventory/src/schema/index.ts b/packages/inventory/src/schema/index.ts new file mode 100644 index 0000000000..859d6efca7 --- /dev/null +++ b/packages/inventory/src/schema/index.ts @@ -0,0 +1,55 @@ +export default ` +scalar DateTime +scalar JSON + +type InventoryItem { + id: ID! + created_at: DateTime! + updated_at: DateTime! + deleted_at: DateTime + sku: String + origin_country: String + hs_code: String + mid_code: String + material: String + weight: Int + length: Int + height: Int + width: Int + requires_shipping: Boolean! + description: String + title: String + thumbnail: String + metadata: JSON + + inventory_levels: [InventoryLevel] +} + +type InventoryLevel { + id: ID! + created_at: DateTime! + updated_at: DateTime! + deleted_at: DateTime + inventory_item_id: String! + location_id: String! + stocked_quantity: Int! + reserved_quantity: Int! + incoming_quantity: Int! + metadata: JSON +} + +type ReservationItem { + id: ID! + created_at: DateTime! + updated_at: DateTime! + deleted_at: DateTime + line_item_id: String + inventory_item_id: String! + location_id: String! + quantity: Int! + external_id: String + description: String + created_by: String + metadata: JSON +} +` diff --git a/packages/link-modules/src/definitions/product-shipping-profile.ts b/packages/link-modules/src/definitions/product-shipping-profile.ts index dee5273017..7805390d53 100644 --- a/packages/link-modules/src/definitions/product-shipping-profile.ts +++ b/packages/link-modules/src/definitions/product-shipping-profile.ts @@ -12,6 +12,9 @@ export const ProductShippingProfile: ModuleJoinerConfig = { alias: [ { name: "product_shipping_profile", + args: { + entity: "LinkProductShippingProfile", + }, }, ], primaryKeys: ["id", "product_id", "profile_id"], diff --git a/packages/link-modules/src/definitions/product-variant-inventory-item.ts b/packages/link-modules/src/definitions/product-variant-inventory-item.ts index 5901b71325..f5911a0e15 100644 --- a/packages/link-modules/src/definitions/product-variant-inventory-item.ts +++ b/packages/link-modules/src/definitions/product-variant-inventory-item.ts @@ -17,10 +17,13 @@ export const ProductVariantInventoryItem: ModuleJoinerConfig = { }, alias: [ { - name: "product_variant_inventory_item", - }, - { - name: "product_variant_inventory_items", + name: [ + "product_variant_inventory_item", + "product_variant_inventory_items", + ], + args: { + entity: "LinkProductVariantInventoryItem", + }, }, ], primaryKeys: ["id", "variant_id", "inventory_item_id"], diff --git a/packages/link-modules/src/definitions/product-variant-price-set.ts b/packages/link-modules/src/definitions/product-variant-price-set.ts index 0da36ab55a..e90b13efbf 100644 --- a/packages/link-modules/src/definitions/product-variant-price-set.ts +++ b/packages/link-modules/src/definitions/product-variant-price-set.ts @@ -1,6 +1,6 @@ -import { LINKS } from "../links" -import { ModuleJoinerConfig } from "@medusajs/types" import { Modules } from "@medusajs/modules-sdk" +import { ModuleJoinerConfig } from "@medusajs/types" +import { LINKS } from "../links" export const ProductVariantPriceSet: ModuleJoinerConfig = { serviceName: LINKS.ProductVariantPriceSet, @@ -11,10 +11,10 @@ export const ProductVariantPriceSet: ModuleJoinerConfig = { }, alias: [ { - name: "product_variant_price_set", - }, - { - name: "product_variant_price_sets", + name: ["product_variant_price_set", "product_variant_price_sets"], + args: { + entity: "LinkProductVariantPriceSet", + }, }, ], primaryKeys: ["id", "variant_id", "price_set_id"], diff --git a/packages/link-modules/src/initialize/index.ts b/packages/link-modules/src/initialize/index.ts index a020f555bc..8d6b6b56ee 100644 --- a/packages/link-modules/src/initialize/index.ts +++ b/packages/link-modules/src/initialize/index.ts @@ -1,4 +1,8 @@ -import { InternalModuleDeclaration, MedusaModule } from "@medusajs/modules-sdk" +import { + InternalModuleDeclaration, + MedusaModule, + ModuleRegistrationName, +} from "@medusajs/modules-sdk" import { ExternalModuleDeclaration, ILinkModule, @@ -15,11 +19,16 @@ import { ContainerRegistrationKeys, lowerCaseFirst, simpleHash, + toPascalCase, } from "@medusajs/utils" import * as linkDefinitions from "../definitions" import { getMigration } from "../migration" import { InitializeModuleInjectableDependencies } from "../types" -import { composeLinkName, generateGraphQLSchema } from "../utils" +import { + composeLinkName, + composeTableName, + generateGraphQLSchema, +} from "../utils" import { getLinkModuleDefinition } from "./module-definition" export const initialize = async ( @@ -98,7 +107,28 @@ export const initialize = async ( continue } - definition.schema = generateGraphQLSchema(definition, primary, foreign) + const logger = + injectedDependencies?.[ContainerRegistrationKeys.LOGGER] ?? console.log + + definition.schema = generateGraphQLSchema(definition, primary, foreign, { + logger, + }) + + definition.alias ??= [] + for (const alias of definition.alias) { + alias.args ??= {} + + alias.args.entity = toPascalCase( + "Link_" + + (definition.databaseConfig?.tableName ?? + composeTableName( + primary.serviceName, + primary.foreignKey, + foreign.serviceName, + foreign.foreignKey + )) + ) + } const moduleDefinition = getLinkModuleDefinition( definition, @@ -110,6 +140,7 @@ export const initialize = async ( key: serviceKey, registrationName: serviceKey, label: serviceKey, + dependencies: [ModuleRegistrationName.EVENT_BUS], defaultModuleDeclaration: { scope: MODULE_SCOPE.INTERNAL, resources: injectedDependencies?.[ diff --git a/packages/link-modules/src/loaders/container.ts b/packages/link-modules/src/loaders/container.ts index c563cadd82..dd3391bbe8 100644 --- a/packages/link-modules/src/loaders/container.ts +++ b/packages/link-modules/src/loaders/container.ts @@ -7,7 +7,9 @@ import { ModuleJoinerConfig, ModulesSdkTypes, } from "@medusajs/types" +import { lowerCaseFirst, simpleHash, toPascalCase } from "@medusajs/utils" import { asClass, asValue } from "awilix" +import { composeLinkName, composeTableName } from "../utils" export function containerLoader(entity, joinerConfig: ModuleJoinerConfig) { return async ( @@ -22,6 +24,29 @@ export function containerLoader(entity, joinerConfig: ModuleJoinerConfig) { ): Promise => { const [primary, foreign] = joinerConfig.relationships! + const serviceName = !joinerConfig.isReadOnlyLink + ? lowerCaseFirst( + joinerConfig.serviceName ?? + composeLinkName( + primary.serviceName, + primary.foreignKey, + foreign.serviceName, + foreign.foreignKey + ) + ) + : simpleHash(JSON.stringify(joinerConfig.extends)) + + const entityName = toPascalCase( + "Link_" + + (joinerConfig.databaseConfig?.tableName ?? + composeTableName( + primary.serviceName, + primary.foreignKey, + foreign.serviceName, + foreign.foreignKey + )) + ) + container.register({ joinerConfig: asValue(joinerConfig), primaryKey: asValue(primary.foreignKey.split(",")), @@ -35,6 +60,8 @@ export function containerLoader(entity, joinerConfig: ModuleJoinerConfig) { baseRepository: asClass(BaseRepository).singleton(), linkRepository: asClass(getLinkRepository(entity)).singleton(), + entityName: asValue(entityName), + serviceName: asValue(serviceName), }) } } diff --git a/packages/link-modules/src/services/link-module-service.ts b/packages/link-modules/src/services/link-module-service.ts index ac15ba7dea..454c3ce921 100644 --- a/packages/link-modules/src/services/link-module-service.ts +++ b/packages/link-modules/src/services/link-module-service.ts @@ -2,6 +2,7 @@ import { Context, DAL, FindConfig, + IEventBusModuleService, ILinkModule, InternalModuleDeclaration, ModuleJoinerConfig, @@ -9,6 +10,7 @@ import { SoftDeleteReturn, } from "@medusajs/types" import { + CommonEvents, InjectManager, InjectTransactionManager, MapToConfig, @@ -24,14 +26,20 @@ import { shouldForceTransaction } from "../utils" type InjectedDependencies = { baseRepository: DAL.RepositoryService linkService: LinkService + eventBusModuleService?: IEventBusModuleService primaryKey: string | string[] foreignKey: string extraFields: string[] + entityName: string + serviceName: string } export default class LinkModuleService implements ILinkModule { protected baseRepository_: DAL.RepositoryService protected readonly linkService_: LinkService + protected readonly eventBusModuleService_?: IEventBusModuleService + protected readonly entityName_: string + protected readonly serviceName_: string protected primaryKey_: string[] protected foreignKey_: string protected extraFields_: string[] @@ -40,17 +48,23 @@ export default class LinkModuleService implements ILinkModule { { baseRepository, linkService, + eventBusModuleService, primaryKey, foreignKey, extraFields, + entityName, + serviceName, }: InjectedDependencies, readonly moduleDeclaration: InternalModuleDeclaration ) { this.baseRepository_ = baseRepository this.linkService_ = linkService + this.eventBusModuleService_ = eventBusModuleService this.primaryKey_ = !Array.isArray(primaryKey) ? [primaryKey] : primaryKey this.foreignKey_ = foreignKey this.extraFields_ = extraFields + this.entityName_ = entityName + this.serviceName_ = serviceName } __joinerConfig(): ModuleJoinerConfig { @@ -188,6 +202,21 @@ export default class LinkModuleService implements ILinkModule { const links = await this.linkService_.create(data, sharedContext) + await this.eventBusModuleService_?.emit>( + (data as { id: unknown }[]).map(({ id }) => ({ + eventName: this.entityName_ + "." + CommonEvents.ATTACHED, + body: { + metadata: { + service: this.serviceName_, + action: CommonEvents.ATTACHED, + object: this.entityName_, + eventGroupId: sharedContext.eventGroupId, + }, + data: { id }, + }, + })) + ) + return await this.baseRepository_.serialize(links) } @@ -224,6 +253,22 @@ export default class LinkModuleService implements ILinkModule { this.validateFields(data) await this.linkService_.delete(data, sharedContext) + + const allData = Array.isArray(data) ? data : [data] + await this.eventBusModuleService_?.emit>( + allData.map(({ id }) => ({ + eventName: this.entityName_ + "." + CommonEvents.DETACHED, + body: { + metadata: { + service: this.serviceName_, + action: CommonEvents.DETACHED, + object: this.entityName_, + eventGroupId: sharedContext.eventGroupId, + }, + data: { id }, + }, + })) + ) } async softDelete( @@ -233,7 +278,10 @@ export default class LinkModuleService implements ILinkModule { ): Promise | void> { this.validateFields(data) - let [, cascadedEntitiesMap] = await this.softDelete_(data, sharedContext) + let [deletedEntities, cascadedEntitiesMap] = await this.softDelete_( + data, + sharedContext + ) const pk = this.primaryKey_.join(",") const entityNameToLinkableKeysMap: MapToConfig = { @@ -256,6 +304,21 @@ export default class LinkModuleService implements ILinkModule { ) } + await this.eventBusModuleService_?.emit>( + (deletedEntities as { id: string }[]).map(({ id }) => ({ + eventName: this.entityName_ + "." + CommonEvents.DETACHED, + body: { + metadata: { + service: this.serviceName_, + action: CommonEvents.DETACHED, + object: this.entityName_, + eventGroupId: sharedContext.eventGroupId, + }, + data: { id }, + }, + })) + ) + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 } @@ -263,7 +326,7 @@ export default class LinkModuleService implements ILinkModule { protected async softDelete_( data: any, @MedusaContext() sharedContext: Context = {} - ): Promise<[string[], Record]> { + ): Promise<[object[], Record]> { return await this.linkService_.softDelete(data, sharedContext) } @@ -274,7 +337,10 @@ export default class LinkModuleService implements ILinkModule { ): Promise | void> { this.validateFields(data) - let [, cascadedEntitiesMap] = await this.restore_(data, sharedContext) + let [restoredEntities, cascadedEntitiesMap] = await this.restore_( + data, + sharedContext + ) const pk = this.primaryKey_.join(",") const entityNameToLinkableKeysMap: MapToConfig = { @@ -297,6 +363,21 @@ export default class LinkModuleService implements ILinkModule { ) } + await this.eventBusModuleService_?.emit>( + (restoredEntities as { id: string }[]).map(({ id }) => ({ + eventName: this.entityName_ + "." + CommonEvents.ATTACHED, + body: { + metadata: { + service: this.serviceName_, + action: CommonEvents.ATTACHED, + object: this.entityName_, + eventGroupId: sharedContext.eventGroupId, + }, + data: { id }, + }, + })) + ) + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 } @@ -304,7 +385,7 @@ export default class LinkModuleService implements ILinkModule { async restore_( data: any, @MedusaContext() sharedContext: Context = {} - ): Promise<[string[], Record]> { + ): Promise<[object[], Record]> { return await this.linkService_.restore(data, sharedContext) } } diff --git a/packages/link-modules/src/services/link.ts b/packages/link-modules/src/services/link.ts index b93b2d2775..42a69b0840 100644 --- a/packages/link-modules/src/services/link.ts +++ b/packages/link-modules/src/services/link.ts @@ -89,7 +89,7 @@ export default class LinkService { async softDelete( data: any, @MedusaContext() sharedContext: Context = {} - ): Promise<[string[], Record]> { + ): Promise<[object[], Record]> { const filter = {} for (const key in data) { filter[key] = { $in: Array.isArray(data[key]) ? data[key] : [data[key]] } @@ -104,7 +104,7 @@ export default class LinkService { async restore( data: any, @MedusaContext() sharedContext: Context = {} - ): Promise<[string[], Record]> { + ): Promise<[object[], Record]> { const filter = {} for (const key in data) { filter[key] = { $in: Array.isArray(data[key]) ? data[key] : [data[key]] } diff --git a/packages/medusa/src/interfaces/__tests__/event-bus-service.spec.ts b/packages/medusa/src/interfaces/__tests__/event-bus-service.spec.ts index 9ef023bb88..cf4383ca12 100644 --- a/packages/medusa/src/interfaces/__tests__/event-bus-service.spec.ts +++ b/packages/medusa/src/interfaces/__tests__/event-bus-service.spec.ts @@ -16,8 +16,15 @@ class EventBus extends EventBusUtils.AbstractEventBusModuleService { options: Record ): Promise async emit(data: EventBusTypes.EmitData[]): Promise + async emit(data: EventBusTypes.Message[]): Promise - async emit[] = string>( + async emit< + T, + TInput extends + | string + | EventBusTypes.EmitData[] + | EventBusTypes.Message[] = string + >( eventOrData: TInput, data?: T, options: Record = {} diff --git a/packages/medusa/src/joiner-configs/publishable-api-key-service.ts b/packages/medusa/src/joiner-configs/publishable-api-key-service.ts index dc7bf58356..e0ada9f421 100644 --- a/packages/medusa/src/joiner-configs/publishable-api-key-service.ts +++ b/packages/medusa/src/joiner-configs/publishable-api-key-service.ts @@ -20,7 +20,9 @@ export default { alias: [ { name: ["publishable_api_key", "publishable_api_keys"], - args: { entity: "PublishableApiKey" }, + args: { + entity: "PublishableApiKey", + }, }, ], } as ModuleJoinerConfig diff --git a/packages/medusa/src/joiner-configs/shipping-profile-service.ts b/packages/medusa/src/joiner-configs/shipping-profile-service.ts index 61fd0533b3..b04b3b2263 100644 --- a/packages/medusa/src/joiner-configs/shipping-profile-service.ts +++ b/packages/medusa/src/joiner-configs/shipping-profile-service.ts @@ -5,25 +5,25 @@ export default { primaryKeys: ["id"], linkableKeys: { profile_id: "ShippingProfile" }, schema: ` - scalar Date - scalar JSON - - type ShippingProfile { - id: ID! - name: String! - type: String! - created_at: Date! - updated_at: Date! - deleted_at: Date - metadata: JSON - } - `, + scalar Date + scalar JSON + + type ShippingProfile { + id: ID! + name: String! + type: String! + created_at: Date! + updated_at: Date! + deleted_at: Date + metadata: JSON + } + `, alias: [ { - name: "shipping_profile", - }, - { - name: "shipping_profiles", + name: ["shipping_profile", "shipping_profiles"], + args: { + entity: "ShippingProfile", + }, }, ], } as ModuleJoinerConfig diff --git a/packages/medusa/src/loaders/helpers/subscribers/index.ts b/packages/medusa/src/loaders/helpers/subscribers/index.ts index 784bddd994..da5880f3cb 100644 --- a/packages/medusa/src/loaders/helpers/subscribers/index.ts +++ b/packages/medusa/src/loaders/helpers/subscribers/index.ts @@ -188,7 +188,7 @@ export class SubscriberLoader { const events = Array.isArray(event) ? event : [event] - const subscriber: Subscriber = async (data: T, eventName: string) => { + const subscriber = async (data: T, eventName: string) => { return handler({ eventName, data, @@ -200,7 +200,7 @@ export class SubscriberLoader { const subscriberId = this.inferIdentifier(fileName, config, handler) for (const e of events) { - eventBusService.subscribe(e, subscriber as Subscriber, { + eventBusService.subscribe(e, subscriber as Subscriber, { ...(config.context ?? {}), subscriberId, }) diff --git a/packages/medusa/src/loaders/medusa-app.ts b/packages/medusa/src/loaders/medusa-app.ts index 97beb9ad84..2552bb01a3 100644 --- a/packages/medusa/src/loaders/medusa-app.ts +++ b/packages/medusa/src/loaders/medusa-app.ts @@ -128,7 +128,10 @@ export const loadMedusaApp = async ( return medusaApp } - container.register("remoteLink", asValue(medusaApp.link)) + container.register( + ContainerRegistrationKeys.REMOTE_LINK, + asValue(medusaApp.link) + ) container.register( ContainerRegistrationKeys.REMOTE_QUERY, asValue(medusaApp.query) @@ -143,7 +146,7 @@ export const loadMedusaApp = async ( // Register all unresolved modules as undefined to be present in the container with undefined value by defaul // but still resolvable - for (const [, moduleDefinition] of Object.entries(ModulesDefinition)) { + for (const moduleDefinition of Object.values(ModulesDefinition)) { if (!container.hasRegistration(moduleDefinition.registrationName)) { container.register(moduleDefinition.registrationName, asValue(undefined)) } diff --git a/packages/medusa/src/services/event-bus.ts b/packages/medusa/src/services/event-bus.ts index 15d572ae24..e4f8db84c6 100644 --- a/packages/medusa/src/services/event-bus.ts +++ b/packages/medusa/src/services/event-bus.ts @@ -1,14 +1,14 @@ -import { EventBusTypes, Logger } from "@medusajs/types" +import { EmitData, EventBusTypes, Logger, Message } from "@medusajs/types" import { DatabaseErrorCode, EventBusUtils } from "@medusajs/utils" +import { EOL } from "os" import { EntityManager } from "typeorm" import { TransactionBaseService } from "../interfaces" import { StagedJob } from "../models" +import { FindConfig } from "../types/common" import { ConfigModule } from "../types/global" import { isString } from "../utils" import { sleep } from "../utils/sleep" import StagedJobService from "./staged-job" -import { FindConfig } from "../types/common" -import { EOL } from "os" type InjectedDependencies = { stagedJobService: StagedJobService @@ -118,6 +118,8 @@ export default class EventBusService */ async emit(data: EventBusTypes.EmitData[]): Promise + async emit(data: EventBusTypes.Message[]): Promise + /** * Calls all subscribers when an event occurs. * @param {string} eventName - the name of the event to be process. @@ -133,7 +135,10 @@ export default class EventBusService async emit< T, - TInput extends string | EventBusTypes.EmitData[] = string, + TInput extends + | string + | EventBusTypes.EmitData[] + | EventBusTypes.Message[] = string, TResult = TInput extends EventBusTypes.EmitData[] ? StagedJob[] : StagedJob @@ -144,16 +149,19 @@ export default class EventBusService ): Promise { const manager = this.activeManager_ const isBulkEmit = !isString(eventNameOrData) + const dataBody = isString(eventNameOrData) + ? data ?? (data as Message).body + : undefined const events: EventBusTypes.EmitData[] = isBulkEmit ? eventNameOrData.map((event) => ({ eventName: event.eventName, - data: event.data, + data: (event as EmitData).data ?? (event as Message).body.data, options: event.options, })) : [ { eventName: eventNameOrData, - data: data, + data: dataBody, options: options, }, ] diff --git a/packages/medusa/src/services/product-variant-inventory.ts b/packages/medusa/src/services/product-variant-inventory.ts index c2e1669cc4..bdae64145b 100644 --- a/packages/medusa/src/services/product-variant-inventory.ts +++ b/packages/medusa/src/services/product-variant-inventory.ts @@ -1,32 +1,40 @@ -import { EntityManager, In } from "typeorm" import { IEventBusService, IInventoryService, + IStockLocationService, InventoryItemDTO, InventoryLevelDTO, - IStockLocationService, + RemoteQueryFunction, ReservationItemDTO, ReserveQuantityContext, } from "@medusajs/types" +import { + FlagRouter, + MedusaError, + MedusaV2Flag, + isDefined, + promiseAll, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { EntityManager, In } from "typeorm" import { LineItem, Product, ProductVariant } from "../models" -import { isDefined, MedusaError, promiseAll } from "@medusajs/utils" import { PricedProduct, PricedVariant } from "../types/pricing" +import { TransactionBaseService } from "../interfaces" import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item" +import { getSetDifference } from "../utils/diff-set" import ProductVariantService from "./product-variant" import SalesChannelInventoryService from "./sales-channel-inventory" import SalesChannelLocationService from "./sales-channel-location" -import { TransactionBaseService } from "../interfaces" -import { getSetDifference } from "../utils/diff-set" type InjectedDependencies = { manager: EntityManager salesChannelLocationService: SalesChannelLocationService salesChannelInventoryService: SalesChannelInventoryService productVariantService: ProductVariantService - stockLocationService: IStockLocationService - inventoryService: IInventoryService eventBusService: IEventBusService + featureFlagRouter: FlagRouter + remoteQuery: RemoteQueryFunction } type AvailabilityContext = { @@ -42,6 +50,8 @@ class ProductVariantInventoryService extends TransactionBaseService { protected readonly salesChannelInventoryService_: SalesChannelInventoryService protected readonly productVariantService_: ProductVariantService protected readonly eventBusService_: IEventBusService + protected readonly featureFlagRouter_: FlagRouter + protected readonly remoteQuery_: RemoteQueryFunction protected get inventoryService_(): IInventoryService { return this.__container__.inventoryService @@ -56,6 +66,8 @@ class ProductVariantInventoryService extends TransactionBaseService { salesChannelInventoryService, productVariantService, eventBusService, + featureFlagRouter, + remoteQuery, }: InjectedDependencies) { // eslint-disable-next-line prefer-rest-params super(arguments[0]) @@ -64,6 +76,8 @@ class ProductVariantInventoryService extends TransactionBaseService { this.salesChannelInventoryService_ = salesChannelInventoryService this.productVariantService_ = productVariantService this.eventBusService_ = eventBusService + this.featureFlagRouter_ = featureFlagRouter + this.remoteQuery_ = remoteQuery } /** @@ -307,16 +321,29 @@ class ProductVariantInventoryService extends TransactionBaseService { } // Verify that variant exists - const variants = await this.productVariantService_ - .withTransaction(this.activeManager_) - .list( - { - id: data.map((d) => d.variantId), - }, - { - select: ["id"], - } + let variants + if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) { + variants = await this.remoteQuery_( + remoteQueryObjectFromString({ + entryPoint: "variants", + variables: { + id: data.map((d) => d.variantId), + }, + fields: ["id"], + }) ) + } else { + variants = await this.productVariantService_ + .withTransaction(this.activeManager_) + .list( + { + id: data.map((d) => d.variantId), + }, + { + select: ["id"], + } + ) + } const foundVariantIds = new Set(variants.map((v) => v.id)) const requestedVariantIds = new Set(data.map((v) => v.variantId)) @@ -404,7 +431,11 @@ class ProductVariantInventoryService extends TransactionBaseService { ): tc is ProductVariantInventoryItem => !!tc ) - return await variantInventoryRepo.save(toCreate) + const createdVariantInventoryItems = await variantInventoryRepo.save( + toCreate + ) + + return createdVariantInventoryItems } /** diff --git a/packages/modules-sdk/src/__tests__/utils/get-fields-and-relations.spec.ts b/packages/modules-sdk/src/__tests__/utils/get-fields-and-relations.spec.ts deleted file mode 100644 index 0462837b3c..0000000000 --- a/packages/modules-sdk/src/__tests__/utils/get-fields-and-relations.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { mergeTypeDefs } from "@graphql-tools/merge" -import { makeExecutableSchema } from "@graphql-tools/schema" -import { getFieldsAndRelations } from "../../utils" - -const userModule = ` -type User { - id: ID! - name: String! - blabla: WHATEVER -} - -type Post { - author: User! -} -` - -const postModule = ` -type Post { - id: ID! - title: String! - date: String -} - -type User { - posts: [Post!]! -} - -type WHATEVER { - random_field: String - post: Post -} -` - -const mergedSchema = mergeTypeDefs([userModule, postModule]) -const schema = makeExecutableSchema({ - typeDefs: mergedSchema, -}) - -const types = schema.getTypeMap() - -describe("getFieldsAndRelations", function () { - it("Should get all fields of a given entity", async function () { - const fields = getFieldsAndRelations(types, "User") - expect(fields).toEqual(expect.arrayContaining(["id", "name"])) - }) - - it("Should get all fields of a given entity and a relation", async function () { - const fields = getFieldsAndRelations(types, "User", ["posts"]) - expect(fields).toEqual( - expect.arrayContaining([ - "id", - "name", - "posts.id", - "posts.title", - "posts.date", - ]) - ) - }) - - it("Should get all fields of a given entity and many relations", async function () { - const fields = getFieldsAndRelations(types, "User", [ - "posts", - "blabla", - "blabla.post", - ]) - expect(fields).toEqual( - expect.arrayContaining([ - "id", - "name", - "posts.id", - "posts.title", - "posts.date", - "blabla.random_field", - "blabla.post.id", - "blabla.post.title", - "blabla.post.date", - ]) - ) - }) - - it("Should get all fields of a given entity and many relations limited to the relations given", async function () { - const fields = getFieldsAndRelations(types, "User", ["posts", "blabla"]) - expect(fields).toEqual( - expect.arrayContaining([ - "id", - "name", - "posts.id", - "posts.title", - "posts.date", - "blabla.random_field", - ]) - ) - }) -}) diff --git a/packages/modules-sdk/src/medusa-app.ts b/packages/modules-sdk/src/medusa-app.ts index f449911aab..d96ecf5d5d 100644 --- a/packages/modules-sdk/src/medusa-app.ts +++ b/packages/modules-sdk/src/medusa-app.ts @@ -13,6 +13,7 @@ import { ModuleJoinerConfig, ModuleServiceInitializeOptions, RemoteJoinerQuery, + RemoteQueryFunction, } from "@medusajs/types" import { ContainerRegistrationKeys, @@ -41,6 +42,7 @@ export type RunMigrationFn = ( export type MedusaModuleConfig = { [key: string | Modules]: + | string | boolean | Partial } @@ -182,10 +184,7 @@ function registerCustomJoinerConfigs(servicesConfig: ModuleJoinerConfig[]) { export type MedusaAppOutput = { modules: Record link: RemoteLink | undefined - query: ( - query: string | RemoteJoinerQuery | object, - variables?: Record - ) => Promise + query: RemoteQueryFunction entitiesMap?: Record notFound?: Record> runMigrations: RunMigrationFn @@ -201,6 +200,7 @@ export async function MedusaApp({ linkModules, remoteFetchData, injectedDependencies, + onApplicationStartCb, }: { sharedContainer?: MedusaContainer sharedResourcesConfig?: SharedResources @@ -212,6 +212,7 @@ export async function MedusaApp({ linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[] remoteFetchData?: RemoteFetchDataCallback injectedDependencies?: any + onApplicationStartCb?: () => void } = {}): Promise<{ modules: Record link: RemoteLink | undefined @@ -355,6 +356,6 @@ export async function MedusaApp({ runMigrations, } } finally { - await MedusaModule.onApplicationStart() + MedusaModule.onApplicationStart(onApplicationStartCb) } } diff --git a/packages/modules-sdk/src/medusa-module.ts b/packages/modules-sdk/src/medusa-module.ts index 21b044ba32..f5629f02df 100644 --- a/packages/modules-sdk/src/medusa-module.ts +++ b/packages/modules-sdk/src/medusa-module.ts @@ -69,7 +69,8 @@ export type LinkModuleBootstrapOptions = { } export class MedusaModule { - private static instances_: Map = new Map() + private static instances_: Map = + new Map() private static modules_: Map = new Map() private static loading_: Map> = new Map() private static joinerConfig_: Map = new Map() @@ -87,12 +88,15 @@ export class MedusaModule { }) } - public static onApplicationStart(): void { + public static onApplicationStart(onApplicationStartCb?: () => void): void { for (const instances of MedusaModule.instances_.values()) { for (const instance of Object.values(instances) as IModuleService[]) { if (instance?.__hooks) { instance.__hooks?.onApplicationStart ?.bind(instance)() + .then(() => { + onApplicationStartCb?.() + }) .catch(() => { // The module should handle this and log it return void 0 @@ -217,7 +221,9 @@ export class MedusaModule { ) if (MedusaModule.instances_.has(hashKey)) { - return MedusaModule.instances_.get(hashKey) + return MedusaModule.instances_.get(hashKey)! as { + [key: string]: T + } } if (MedusaModule.loading_.has(hashKey)) { @@ -249,7 +255,12 @@ export class MedusaModule { } } - const container = createMedusaContainer({}, sharedContainer) + // TODO: Only do that while legacy modules sharing the manager exists then remove the ternary in favor of createMedusaContainer({}, globalContainer) + const container = + modDeclaration.scope === MODULE_SCOPE.INTERNAL && + modDeclaration.resources === MODULE_RESOURCE_TYPE.SHARED + ? sharedContainer ?? createMedusaContainer() + : createMedusaContainer({}, sharedContainer) if (injectedDependencies) { for (const service in injectedDependencies) { diff --git a/packages/modules-sdk/src/utils/get-fields-and-relations.ts b/packages/modules-sdk/src/utils/get-fields-and-relations.ts deleted file mode 100644 index e5edd9e3f8..0000000000 --- a/packages/modules-sdk/src/utils/get-fields-and-relations.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { GraphQLNamedType, GraphQLObjectType, isObjectType } from "graphql" - -export function getFieldsAndRelations( - schemaTypeMap: { [key: string]: GraphQLNamedType }, - typeName: string, - relations: string[] = [] -) { - const result: string[] = [] - - function traverseFields(typeName, prefix) { - const type = schemaTypeMap[typeName] - - if (!(type instanceof GraphQLObjectType)) { - return - } - - const fields = type.getFields() - - for (const fieldName in fields) { - const field = fields[fieldName] - let fieldType = field.type as any - - while (fieldType.ofType) { - fieldType = fieldType.ofType - } - - if (!isObjectType(fieldType)) { - result.push(`${prefix}${fieldName}`) - } else if (relations.includes(prefix + fieldName)) { - traverseFields(fieldType.name, `${prefix}${fieldName}.`) - } - } - } - - traverseFields(typeName, "") - return result -} diff --git a/packages/modules-sdk/src/utils/index.ts b/packages/modules-sdk/src/utils/index.ts index ee529e7e80..d1470bdcbc 100644 --- a/packages/modules-sdk/src/utils/index.ts +++ b/packages/modules-sdk/src/utils/index.ts @@ -1,3 +1,2 @@ export * from "./clean-graphql-schema" -export * from "./get-fields-and-relations" export * from "./graphql-schema-to-fields" diff --git a/packages/pricing/src/joiner-config.ts b/packages/pricing/src/joiner-config.ts index bc3342e90e..7d94bd534c 100644 --- a/packages/pricing/src/joiner-config.ts +++ b/packages/pricing/src/joiner-config.ts @@ -8,6 +8,7 @@ import { PriceSet, PriceSetMoneyAmount, } from "@models" +import schema from "./schema" export const LinkableKeys = { money_amount_id: MoneyAmount.name, @@ -32,45 +33,30 @@ export const joinerConfig: ModuleJoinerConfig = { serviceName: Modules.PRICING, primaryKeys: ["id"], linkableKeys: LinkableKeys, + schema, alias: [ { - name: "price_set", + name: ["price_set", "price_sets"], + args: { + entity: "PriceSet", + }, }, { - name: "price_sets", - }, - { - name: "money_amount", + name: ["money_amount", "money_amounts"], args: { methodSuffix: "MoneyAmounts", + entity: "MoneyAmount", }, }, { - name: "money_amounts", - args: { - methodSuffix: "MoneyAmounts", - }, - }, - { - name: "currency", + name: ["currency", "currencies"], args: { methodSuffix: "Currencies", + entity: "Currency", }, }, { - name: "currencies", - args: { - methodSuffix: "Currencies", - }, - }, - { - name: "price_list", - args: { - methodSuffix: "PriceLists", - }, - }, - { - name: "price_lists", + name: ["price_list", "price_lists"], args: { methodSuffix: "PriceLists", }, diff --git a/packages/pricing/src/schema/index.ts b/packages/pricing/src/schema/index.ts new file mode 100644 index 0000000000..26862ca0b6 --- /dev/null +++ b/packages/pricing/src/schema/index.ts @@ -0,0 +1,17 @@ +export const schema = ` +type PriceSet { + id: String! + money_amounts: [MoneyAmount] +} + +type MoneyAmount { + id: String! + currency_code: String + currency: Currency + amount: Float + min_quantity: Float + max_quantity: Float +} +` + +export default schema diff --git a/packages/product/integration-tests/__fixtures__/event-bus/index.ts b/packages/product/integration-tests/__fixtures__/event-bus/index.ts index 3570043f08..5d0e552eef 100644 --- a/packages/product/integration-tests/__fixtures__/event-bus/index.ts +++ b/packages/product/integration-tests/__fixtures__/event-bus/index.ts @@ -1,19 +1,27 @@ import { + EmitData, EventBusTypes, IEventBusModuleService, + Message, Subscriber, } from "@medusajs/types" export class EventBusService implements IEventBusModuleService { - async emit( + emit( eventName: string, data: T, - options: Record + options?: Record ): Promise + emit(data: EmitData[]): Promise + emit(data: Message[]): Promise - async emit(data: EventBusTypes.EmitData[]): Promise - - async emit[] = string>( + async emit< + T, + TInput extends + | string + | EventBusTypes.EmitData[] + | EventBusTypes.Message[] = string + >( eventOrData: TInput, data?: T, options: Record = {} diff --git a/packages/product/integration-tests/__tests__/module.ts b/packages/product/integration-tests/__tests__/module.ts index 9e7a1b75aa..7ff9606ef6 100644 --- a/packages/product/integration-tests/__tests__/module.ts +++ b/packages/product/integration-tests/__tests__/module.ts @@ -2,6 +2,7 @@ import { MedusaModule, Modules } from "@medusajs/modules-sdk" import { IProductModuleService } from "@medusajs/types" import { kebabCase } from "@medusajs/utils" import { knex } from "knex" +import { initModules } from "medusa-test-utils" import * as CustomRepositories from "../__fixtures__/module" import { buildProductAndRelationsData, @@ -9,7 +10,6 @@ import { } from "../__fixtures__/product" import { productsData } from "../__fixtures__/product/data" import { DB_URL, TestDatabase } from "../utils" -import { initModules } from "medusa-test-utils" import { getInitModuleConfig } from "../utils/get-init-module-config" const sharedPgConnection = knex({ @@ -238,7 +238,23 @@ describe("Product module", function () { thumbnail: images[0], }) - const products = await module.create([data]) + const productsCreated = await module.create([data]) + + const products = await module.list( + { id: productsCreated[0].id }, + { + relations: [ + "images", + "categories", + "variants", + "variants.options", + "options", + "options.values", + "tags", + "type", + ], + } + ) expect(products).toHaveLength(1) diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts index 3900d1b6b3..179b53cee8 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts @@ -1,12 +1,12 @@ +import { MedusaModule, Modules } from "@medusajs/modules-sdk" import { IProductModuleService, ProductTypes } from "@medusajs/types" -import { Product, ProductCategory } from "@models" import { SqlEntityManager } from "@mikro-orm/postgresql" -import { getInitModuleConfig, TestDatabase } from "../../../utils" +import { Product, ProductCategory } from "@models" +import { initModules } from "medusa-test-utils" +import { EventBusService } from "../../../__fixtures__/event-bus" import { createProductCategories } from "../../../__fixtures__/product-category" import { productCategoriesRankData } from "../../../__fixtures__/product-category/data" -import { EventBusService } from "../../../__fixtures__/event-bus" -import { MedusaModule, Modules } from "@medusajs/modules-sdk" -import { initModules } from "medusa-test-utils/dist" +import { TestDatabase, getInitModuleConfig } from "../../../utils" describe("ProductModuleService product categories", () => { let service: IProductModuleService diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-tags.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-tags.spec.ts index 0aacbee390..4f58c7a131 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-tags.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-tags.spec.ts @@ -1,9 +1,9 @@ -import { getInitModuleConfig, TestDatabase } from "../../../utils" -import { IProductModuleService, ProductTypes } from "@medusajs/types" -import { Product, ProductTag } from "@models" -import { SqlEntityManager } from "@mikro-orm/postgresql" import { MedusaModule, Modules } from "@medusajs/modules-sdk" -import { initModules } from "medusa-test-utils/dist" +import { IProductModuleService, ProductTypes } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { Product, ProductTag } from "@models" +import { initModules } from "medusa-test-utils" +import { TestDatabase, getInitModuleConfig } from "../../../utils" describe("ProductModuleService product tags", () => { let service: IProductModuleService diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-types.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-types.spec.ts index 70776157a1..529a355b58 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-types.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-types.spec.ts @@ -1,9 +1,9 @@ -import { getInitModuleConfig, TestDatabase } from "../../../utils" -import { IProductModuleService } from "@medusajs/types" -import { ProductType } from "@models" -import { SqlEntityManager } from "@mikro-orm/postgresql" import { MedusaModule, Modules } from "@medusajs/modules-sdk" -import { initModules } from "medusa-test-utils/dist" +import { IProductModuleService } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { ProductType } from "@models" +import { initModules } from "medusa-test-utils" +import { getInitModuleConfig, TestDatabase } from "../../../utils" describe("ProductModuleService product types", () => { let service: IProductModuleService diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-variants.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-variants.spec.ts index 0b78d41a14..07d5571499 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-variants.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-variants.spec.ts @@ -1,9 +1,9 @@ -import { getInitModuleConfig, TestDatabase } from "../../../utils" -import { IProductModuleService, ProductTypes } from "@medusajs/types" -import { Product, ProductVariant } from "@models" -import { SqlEntityManager } from "@mikro-orm/postgresql" import { MedusaModule, Modules } from "@medusajs/modules-sdk" -import { initModules } from "medusa-test-utils/dist" +import { IProductModuleService, ProductTypes } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { Product, ProductVariant } from "@models" +import { initModules } from "medusa-test-utils" +import { getInitModuleConfig, TestDatabase } from "../../../utils" describe("ProductModuleService product variants", () => { let service: IProductModuleService diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts index e1d9506abd..c26f983fcd 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts @@ -13,13 +13,13 @@ import { ProductVariant, } from "@models" +import { initModules } from "medusa-test-utils" import { initialize } from "../../../../src" import { EventBusService } from "../../../__fixtures__/event-bus" import { createCollections, createTypes } from "../../../__fixtures__/product" import { createProductCategories } from "../../../__fixtures__/product-category" import { buildProductAndRelationsData } from "../../../__fixtures__/product/data/create-product" -import { DB_URL, getInitModuleConfig, TestDatabase } from "../../../utils" -import { initModules } from "medusa-test-utils" +import { DB_URL, TestDatabase, getInitModuleConfig } from "../../../utils" const beforeEach_ = async () => { await TestDatabase.setupDatabase() @@ -584,7 +584,23 @@ describe("ProductModuleService products", function () { thumbnail: images[0], }) - const products = await module.create([data]) + const productsCreated = await module.create([data]) + + const products = await module.list( + { id: productsCreated[0].id }, + { + relations: [ + "images", + "categories", + "variants", + "variants.options", + "options", + "options.values", + "tags", + "type", + ], + } + ) expect(products).toHaveLength(1) expect(products[0].images).toHaveLength(1) diff --git a/packages/product/src/joiner-config.ts b/packages/product/src/joiner-config.ts index df15dfb8d6..beffc55c0f 100644 --- a/packages/product/src/joiner-config.ts +++ b/packages/product/src/joiner-config.ts @@ -43,92 +43,57 @@ export const joinerConfig: ModuleJoinerConfig = { schema: moduleSchema, alias: [ { - name: "product", - }, - { - name: "products", - }, - { - name: "variant", + name: ["product", "products"], args: { + entity: "Product", + }, + }, + { + name: ["variant", "variants"], + args: { + entity: "ProductVariant", methodSuffix: "Variants", }, }, { - name: "variants", - args: { - methodSuffix: "Variants", - }, - }, - { - name: "product_option", + name: ["product_option", "product_options"], args: { + entity: "ProductOption", methodSuffix: "Options", }, }, { - name: "product_options", - args: { - methodSuffix: "Options", - }, - }, - { - name: "product_type", + name: ["product_type", "product_types"], args: { + entity: "ProductType", methodSuffix: "Types", }, }, { - name: "product_types", - args: { - methodSuffix: "Types", - }, - }, - { - name: "product_image", + name: ["product_image", "product_images"], args: { + entity: "ProductImage", methodSuffix: "Images", }, }, { - name: "product_images", - args: { - methodSuffix: "Images", - }, - }, - { - name: "product_tag", + name: ["product_tag", "product_tags"], args: { + entity: "ProductTag", methodSuffix: "Tags", }, }, { - name: "product_tags", - args: { - methodSuffix: "Tags", - }, - }, - { - name: "product_collection", + name: ["product_collection", "product_collections"], args: { + entity: "ProductCollection", methodSuffix: "Collections", }, }, { - name: "product_collections", - args: { - methodSuffix: "Collections", - }, - }, - { - name: "product_category", - args: { - methodSuffix: "Categories", - }, - }, - { - name: "product_categories", + name: ["product_category", "product_categories"], args: { + entity: "ProductCategory", methodSuffix: "Categories", }, }, diff --git a/packages/product/src/models/product-category.ts b/packages/product/src/models/product-category.ts index c81a0a7563..9ee9152c4f 100644 --- a/packages/product/src/models/product-category.ts +++ b/packages/product/src/models/product-category.ts @@ -7,6 +7,7 @@ import { Index, ManyToMany, ManyToOne, + OnInit, OneToMany, OptionalProps, PrimaryKey, @@ -14,8 +15,8 @@ import { Unique, } from "@mikro-orm/core" -import Product from "./product" import { DAL } from "@medusajs/types" +import Product from "./product" type OptionalFields = DAL.SoftDeletableEntityDateColumns @@ -85,6 +86,11 @@ class ProductCategory { @ManyToMany(() => Product, (product) => product.categories) products = new Collection(this) + @OnInit() + async onInit() { + this.id = generateEntityId(this.id, "pcat") + } + @BeforeCreate() async onCreate(args: EventArgs) { this.id = generateEntityId(this.id, "pcat") diff --git a/packages/product/src/models/product-collection.ts b/packages/product/src/models/product-collection.ts index 63745eda0c..21dd8bf2bc 100644 --- a/packages/product/src/models/product-collection.ts +++ b/packages/product/src/models/product-collection.ts @@ -4,6 +4,7 @@ import { Entity, Filter, Index, + OnInit, OneToMany, OptionalProps, PrimaryKey, @@ -11,9 +12,9 @@ import { Unique, } from "@mikro-orm/core" +import { DAL } from "@medusajs/types" import { DALUtils, generateEntityId, kebabCase } from "@medusajs/utils" import Product from "./product" -import { DAL } from "@medusajs/types" type OptionalRelations = "products" type OptionalFields = DAL.SoftDeletableEntityDateColumns @@ -61,6 +62,11 @@ class ProductCollection { @Property({ columnType: "timestamptz", nullable: true }) deleted_at?: Date + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "pcol") + } + @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "pcol") diff --git a/packages/product/src/models/product-image.ts b/packages/product/src/models/product-image.ts index ce25a8ea64..643fc43480 100644 --- a/packages/product/src/models/product-image.ts +++ b/packages/product/src/models/product-image.ts @@ -5,14 +5,15 @@ import { Filter, Index, ManyToMany, + OnInit, OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" +import { DAL } from "@medusajs/types" import { DALUtils, generateEntityId } from "@medusajs/utils" import Product from "./product" -import { DAL } from "@medusajs/types" type OptionalRelations = "products" type OptionalFields = DAL.SoftDeletableEntityDateColumns @@ -54,6 +55,11 @@ class ProductImage { @ManyToMany(() => Product, (product) => product.images) products = new Collection(this) + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "img") + } + @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "img") diff --git a/packages/product/src/models/product-option-value.ts b/packages/product/src/models/product-option-value.ts index a1037e969b..32dc02e864 100644 --- a/packages/product/src/models/product-option-value.ts +++ b/packages/product/src/models/product-option-value.ts @@ -1,16 +1,17 @@ +import { DAL } from "@medusajs/types" +import { DALUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, Entity, Filter, Index, ManyToOne, + OnInit, OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" import { ProductOption, ProductVariant } from "./index" -import { DALUtils, generateEntityId } from "@medusajs/utils" -import { DAL } from "@medusajs/types" type OptionalFields = | "allow_backorder" @@ -72,6 +73,11 @@ class ProductOptionValue { @Property({ columnType: "timestamptz", nullable: true }) deleted_at?: Date + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "optval") + } + @BeforeCreate() beforeCreate() { this.id = generateEntityId(this.id, "optval") diff --git a/packages/product/src/models/product-option.ts b/packages/product/src/models/product-option.ts index a3a556573d..1a5d60b0a6 100644 --- a/packages/product/src/models/product-option.ts +++ b/packages/product/src/models/product-option.ts @@ -8,6 +8,7 @@ import { Filter, Index, ManyToOne, + OnInit, OneToMany, OptionalProps, PrimaryKey, @@ -70,6 +71,11 @@ class ProductOption { @Property({ columnType: "timestamptz", nullable: true }) deleted_at?: Date + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "opt") + } + @BeforeCreate() beforeCreate() { this.id = generateEntityId(this.id, "opt") diff --git a/packages/product/src/models/product-tag.ts b/packages/product/src/models/product-tag.ts index 9f9d7ce648..d58cf128e0 100644 --- a/packages/product/src/models/product-tag.ts +++ b/packages/product/src/models/product-tag.ts @@ -5,14 +5,15 @@ import { Filter, Index, ManyToMany, + OnInit, OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" +import { DAL } from "@medusajs/types" import { DALUtils, generateEntityId } from "@medusajs/utils" import Product from "./product" -import { DAL } from "@medusajs/types" type OptionalRelations = "products" type OptionalFields = DAL.SoftDeletableEntityDateColumns @@ -53,6 +54,11 @@ class ProductTag { @ManyToMany(() => Product, (product) => product.tags) products = new Collection(this) + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "ptag") + } + @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "ptag") diff --git a/packages/product/src/models/product-type.ts b/packages/product/src/models/product-type.ts index 30a2cac909..2c7cd34c18 100644 --- a/packages/product/src/models/product-type.ts +++ b/packages/product/src/models/product-type.ts @@ -3,13 +3,14 @@ import { Entity, Filter, Index, + OnInit, OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" -import { DALUtils, generateEntityId } from "@medusajs/utils" import { DAL } from "@medusajs/types" +import { DALUtils, generateEntityId } from "@medusajs/utils" type OptionalFields = DAL.SoftDeletableEntityDateColumns @@ -46,6 +47,11 @@ class ProductType { @Property({ columnType: "timestamptz", nullable: true }) deleted_at?: Date + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "ptyp") + } + @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "ptyp") diff --git a/packages/product/src/models/product-variant.ts b/packages/product/src/models/product-variant.ts index 0c7b42b0db..622699ad5e 100644 --- a/packages/product/src/models/product-variant.ts +++ b/packages/product/src/models/product-variant.ts @@ -1,3 +1,4 @@ +import { DAL } from "@medusajs/types" import { DALUtils, generateEntityId, @@ -11,6 +12,7 @@ import { Filter, Index, ManyToOne, + OnInit, OneToMany, OptionalProps, PrimaryKey, @@ -19,7 +21,6 @@ import { } from "@mikro-orm/core" import { Product } from "@models" import ProductOptionValue from "./product-option-value" -import { DAL } from "@medusajs/types" type OptionalFields = | "allow_backorder" @@ -152,6 +153,11 @@ class ProductVariant { }) options = new Collection(this) + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "variant") + } + @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "variant") diff --git a/packages/product/src/models/product.ts b/packages/product/src/models/product.ts index 5113c51c22..1d5281a505 100644 --- a/packages/product/src/models/product.ts +++ b/packages/product/src/models/product.ts @@ -8,12 +8,14 @@ import { ManyToMany, ManyToOne, OneToMany, + OnInit, OptionalProps, PrimaryKey, Property, Unique, } from "@mikro-orm/core" +import { DAL } from "@medusajs/types" import { DALUtils, generateEntityId, @@ -22,12 +24,11 @@ import { } from "@medusajs/utils" import ProductCategory from "./product-category" import ProductCollection from "./product-collection" +import ProductImage from "./product-image" import ProductOption from "./product-option" import ProductTag from "./product-tag" import ProductType from "./product-type" import ProductVariant from "./product-variant" -import ProductImage from "./product-image" -import { DAL } from "@medusajs/types" type OptionalRelations = "collection" | "type" type OptionalFields = @@ -176,6 +177,11 @@ class Product { @Property({ columnType: "jsonb", nullable: true }) metadata?: Record | null + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "prod") + } + @BeforeCreate() beforeCreate() { this.id = generateEntityId(this.id, "prod") diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 84febcf50e..1ca7d5d3ab 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -42,12 +42,6 @@ import { ModulesSdkUtils, promiseAll, } from "@medusajs/utils" -import { entityNameToLinkableKeysMap, joinerConfig } from "./../joiner-config" -import { ProductEventData, ProductEvents } from "../types/services/product" -import { - ProductCategoryEventData, - ProductCategoryEvents, -} from "../types/services/product-category" import { ProductCategoryServiceTypes, ProductCollectionServiceTypes, @@ -55,6 +49,12 @@ import { ProductServiceTypes, ProductVariantServiceTypes, } from "@types" +import { ProductEventData, ProductEvents } from "../types/services/product" +import { + ProductCategoryEventData, + ProductCategoryEvents, +} from "../types/services/product-category" +import { entityNameToLinkableKeysMap, joinerConfig } from "./../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -371,52 +371,56 @@ export default class ProductModuleService< async createTags( data: ProductTypes.CreateProductTagDTO[], @MedusaContext() sharedContext: Context = {} - ) { + ): Promise { const productTags = await this.productTagService_.create( data, sharedContext ) - return JSON.parse(JSON.stringify(productTags)) + return await this.baseRepository_.serialize(productTags, { populate: true }) } @InjectTransactionManager("baseRepository_") async updateTags( data: ProductTypes.UpdateProductTagDTO[], @MedusaContext() sharedContext: Context = {} - ) { + ): Promise { const productTags = await this.productTagService_.update( data, sharedContext ) - return JSON.parse(JSON.stringify(productTags)) + return await this.baseRepository_.serialize(productTags, { populate: true }) } @InjectTransactionManager("baseRepository_") async createTypes( data: ProductTypes.CreateProductTypeDTO[], @MedusaContext() sharedContext: Context = {} - ) { + ): Promise { const productTypes = await this.productTypeService_.create( data, sharedContext ) - return JSON.parse(JSON.stringify(productTypes)) + return await this.baseRepository_.serialize(productTypes, { + populate: true, + }) } @InjectTransactionManager("baseRepository_") async updateTypes( data: ProductTypes.UpdateProductTypeDTO[], @MedusaContext() sharedContext: Context = {} - ) { + ): Promise { const productTypes = await this.productTypeService_.update( data, sharedContext ) - return JSON.parse(JSON.stringify(productTypes)) + return await this.baseRepository_.serialize(productTypes, { + populate: true, + }) } @InjectTransactionManager("baseRepository_") @@ -476,7 +480,7 @@ export default class ProductModuleService< async updateCollections( data: ProductTypes.UpdateProductCollectionDTO[], @MedusaContext() sharedContext: Context = {} - ) { + ): Promise { const productCollections = await this.productCollectionService_.update( data, sharedContext @@ -492,14 +496,16 @@ export default class ProductModuleService< })) ) - return JSON.parse(JSON.stringify(productCollections)) + return await this.baseRepository_.serialize(productCollections, { + populate: true, + }) } @InjectTransactionManager("baseRepository_") async createCategory( data: ProductCategoryServiceTypes.CreateProductCategoryDTO, @MedusaContext() sharedContext: Context = {} - ) { + ): Promise { const productCategory = await this.productCategoryService_.create( data, sharedContext @@ -510,7 +516,9 @@ export default class ProductModuleService< { id: productCategory.id } ) - return JSON.parse(JSON.stringify(productCategory)) + return await this.baseRepository_.serialize(productCategory, { + populate: true, + }) } @InjectTransactionManager("baseRepository_") @@ -518,7 +526,7 @@ export default class ProductModuleService< categoryId: string, data: ProductCategoryServiceTypes.UpdateProductCategoryDTO, @MedusaContext() sharedContext: Context = {} - ) { + ): Promise { const productCategory = await this.productCategoryService_.update( categoryId, data, @@ -530,7 +538,9 @@ export default class ProductModuleService< { id: productCategory.id } ) - return JSON.parse(JSON.stringify(productCategory)) + return await this.baseRepository_.serialize(productCategory, { + populate: true, + }) } @InjectManager("baseRepository_") diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/register-usage.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/register-usage.spec.ts index 3d96d4191b..b3f836bca8 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/register-usage.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/register-usage.spec.ts @@ -1,10 +1,10 @@ +import { Modules } from "@medusajs/modules-sdk" import { IPromotionModuleService } from "@medusajs/types" import { SqlEntityManager } from "@mikro-orm/postgresql" +import { initModules } from "medusa-test-utils" import { createCampaigns } from "../../../__fixtures__/campaigns" import { MikroOrmWrapper } from "../../../utils" import { getInitModuleConfig } from "../../../utils/get-init-module-config" -import { initModules } from "medusa-test-utils/dist" -import { Modules } from "@medusajs/modules-sdk" jest.setTimeout(30000) diff --git a/packages/types/src/event-bus/common.ts b/packages/types/src/event-bus/common.ts index 0032328f18..a589e83c55 100644 --- a/packages/types/src/event-bus/common.ts +++ b/packages/types/src/event-bus/common.ts @@ -1,25 +1,51 @@ export type Subscriber = ( - data: T, - eventName: string - ) => Promise - - export type SubscriberContext = { - subscriberId: string + data: T, + eventName: string +) => Promise + +export type SubscriberContext = { + subscriberId: string +} + +export type SubscriberDescriptor = { + id: string + subscriber: Subscriber +} + +export type EventHandler = ( + data: T, + eventName: string +) => Promise + +export type EmitData = { + eventName: string + data: T + options?: Record +} + +export type MessageBody = { + metadata: { + service: string + action: string + object: string + eventGroupId?: string } - - export type SubscriberDescriptor = { - id: string - subscriber: Subscriber + data: T +} + +export type Message = { + eventName: string + body: MessageBody + options?: Record +} + +export type MessageFormat = { + eventName: string + metadata: { + service: string + action: string + object: string + eventGroupId?: string } - - export type EventHandler = ( - data: T, - eventName: string - ) => Promise - - export type EmitData = { - eventName: string - data: T - options?: Record - } - \ No newline at end of file + data: T | T[] +} diff --git a/packages/types/src/event-bus/event-bus-module.ts b/packages/types/src/event-bus/event-bus-module.ts index b7e139b2cf..73b497263a 100644 --- a/packages/types/src/event-bus/event-bus-module.ts +++ b/packages/types/src/event-bus/event-bus-module.ts @@ -1,4 +1,4 @@ -import { EmitData, Subscriber, SubscriberContext } from "./common" +import { EmitData, Message, Subscriber, SubscriberContext } from "./common" export interface IEventBusModuleService { emit( @@ -7,6 +7,7 @@ export interface IEventBusModuleService { options?: Record ): Promise emit(data: EmitData[]): Promise + emit(data: Message[]): Promise subscribe( eventName: string | symbol, diff --git a/packages/types/src/event-bus/event-bus.ts b/packages/types/src/event-bus/event-bus.ts index d047e23d76..33be2c43a2 100644 --- a/packages/types/src/event-bus/event-bus.ts +++ b/packages/types/src/event-bus/event-bus.ts @@ -1,5 +1,5 @@ -import { Subscriber, SubscriberContext } from "." -import { ITransactionBaseService } from "../transaction-base/transaction-base" +import { ITransactionBaseService } from "../transaction-base" +import { EmitData, Message, Subscriber, SubscriberContext } from "./common" export interface IEventBusService extends ITransactionBaseService { subscribe( @@ -13,5 +13,8 @@ export interface IEventBusService extends ITransactionBaseService { subscriber: Subscriber, context?: SubscriberContext ): this + emit(event: string, data: T, options?: unknown): Promise + emit(data: EmitData[]): Promise + emit(data: Message[]): Promise } diff --git a/packages/types/src/inventory/service.ts b/packages/types/src/inventory/service.ts index 2d01f2864f..885693b8d4 100644 --- a/packages/types/src/inventory/service.ts +++ b/packages/types/src/inventory/service.ts @@ -14,73 +14,68 @@ import { } from "./common" import { FindConfig } from "../common" -import { ModuleJoinerConfig } from "../modules-sdk" +import { IModuleService } from "../modules-sdk" import { SharedContext } from "../shared-context" -export interface IInventoryService { - /** - * @ignore - */ - __joinerConfig(): ModuleJoinerConfig - +export interface IInventoryService extends IModuleService { /** * This method is used to retrieve a paginated list of inventory items along with the total count of available inventory items satisfying the provided filters. * @param {FilterableInventoryItemProps} selector - The filters to apply on the retrieved inventory items. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the inventory items are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a inventory item. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @return {Promise<[InventoryItemDTO[], number]>} The list of inventory items along with the total count. - * + * * @example * To retrieve a list of inventory items using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveInventoryItems (ids: string[]) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const [inventoryItems, count] = await inventoryModule.listInventoryItems({ * id: ids * }) - * + * * // do something with the inventory items or return them * } * ``` - * + * * To specify relations that should be retrieved within the inventory items: - * + * * ```ts - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveInventoryItems (ids: string[]) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const [inventoryItems, count] = await inventoryModule.listInventoryItems({ * id: ids * }, { * relations: ["inventory_level"] * }) - * + * * // do something with the inventory items or return them * } * ``` - * + * * By default, only the first `10` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveInventoryItems (ids: string[], skip: number, take: number) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const [inventoryItems, count] = await inventoryModule.listInventoryItems({ * id: ids * }, { @@ -88,7 +83,7 @@ export interface IInventoryService { * skip, * take * }) - * + * * // do something with the inventory items or return them * } * ``` @@ -102,61 +97,61 @@ export interface IInventoryService { /** * This method is used to retrieve a paginated list of reservation items along with the total count of available reservation items satisfying the provided filters. * @param {FilterableReservationItemProps} selector - The filters to apply on the retrieved reservation items. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the reservation items are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a reservation item. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @return {Promise<[ReservationItemDTO[], number]>} The list of reservation items along with the total count. - * + * * @example * To retrieve a list of reservation items using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveReservationItems (ids: string[]) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const [reservationItems, count] = await inventoryModule.listReservationItems({ * id: ids * }) - * + * * // do something with the reservation items or return them * } * ``` - * + * * To specify relations that should be retrieved within the reservation items: - * + * * ```ts - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveReservationItems (ids: string[]) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const [reservationItems, count] = await inventoryModule.listReservationItems({ * id: ids * }, { * relations: ["inventory_item"] * }) - * + * * // do something with the reservation items or return them * } * ``` - * + * * By default, only the first `10` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveReservationItems (ids: string[], skip: number, take: number) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const [reservationItems, count] = await inventoryModule.listReservationItems({ * id: ids * }, { @@ -164,7 +159,7 @@ export interface IInventoryService { * skip, * take * }) - * + * * // do something with the reservation items or return them * } * ``` @@ -178,61 +173,61 @@ export interface IInventoryService { /** * This method is used to retrieve a paginated list of inventory levels along with the total count of available inventory levels satisfying the provided filters. * @param {FilterableInventoryLevelProps} selector - The filters to apply on the retrieved inventory levels. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the inventory levels are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a inventory level. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @return {Promise<[InventoryLevelDTO[], number]>} The list of inventory levels along with the total count. - * + * * @example * To retrieve a list of inventory levels using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveInventoryLevels (inventoryItemIds: string[]) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const [inventoryLevels, count] = await inventoryModule.listInventoryLevels({ * inventory_item_id: inventoryItemIds * }) - * + * * // do something with the inventory levels or return them * } * ``` - * + * * To specify relations that should be retrieved within the inventory levels: - * + * * ```ts - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveInventoryLevels (inventoryItemIds: string[]) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const [inventoryLevels, count] = await inventoryModule.listInventoryLevels({ * inventory_item_id: inventoryItemIds * }, { * relations: ["inventory_item"] * }) - * + * * // do something with the inventory levels or return them * } * ``` - * + * * By default, only the first `10` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveInventoryLevels (inventoryItemIds: string[], skip: number, take: number) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const [inventoryLevels, count] = await inventoryModule.listInventoryLevels({ * inventory_item_id: inventoryItemIds * }, { @@ -240,7 +235,7 @@ export interface IInventoryService { * skip, * take * }) - * + * * // do something with the inventory levels or return them * } * ``` @@ -253,45 +248,45 @@ export interface IInventoryService { /** * This method is used to retrieve an inventory item by its ID - * - * @param {string} inventoryItemId - The ID of the inventory item to retrieve. - * @param {FindConfig} config - + * + * @param {string} inventoryItemId - The ID of the inventory item to retrieve. + * @param {FindConfig} config - * The configurations determining how the inventory item is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a inventory item. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved inventory item. - * + * * @example * A simple example that retrieves a inventory item by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveInventoryItem (id: string) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const inventoryItem = await inventoryModule.retrieveInventoryItem(id) - * + * * // do something with the inventory item or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveInventoryItem (id: string) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const inventoryItem = await inventoryModule.retrieveInventoryItem(id, { * relations: ["inventory_level"] * }) - * + * * // do something with the inventory item or return it * } * ``` @@ -304,28 +299,28 @@ export interface IInventoryService { /** * This method is used to retrieve an inventory level for an inventory item and a location. - * + * * @param {string} inventoryItemId - The ID of the inventory item. * @param {string} locationId - The ID of the location. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved inventory level. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveInventoryLevel ( - * inventoryItemId: string, + * inventoryItemId: string, * locationId: string * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const inventoryLevel = await inventoryModule.retrieveInventoryLevel( * inventoryItemId, * locationId * ) - * + * * // do something with the inventory level or return it * } */ @@ -337,21 +332,21 @@ export interface IInventoryService { /** * This method is used to retrieve a reservation item by its ID. - * + * * @param {string} reservationId - The ID of the reservation item. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved reservation item. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveReservationItem (id: string) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const reservationItem = await inventoryModule.retrieveReservationItem(id) - * + * * // do something with the reservation item or return it * } */ @@ -362,27 +357,27 @@ export interface IInventoryService { /** * This method is used to create a reservation item. - * + * * @param {CreateReservationItemInput} input - The details of the reservation item to create. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The created reservation item's details. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function createReservationItem (item: { * inventory_item_id: string, * location_id: string, * quantity: number * }) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const reservationItem = await inventoryModule.createReservationItems( * item * ) - * + * * // do something with the reservation item or return them * } */ @@ -393,27 +388,27 @@ export interface IInventoryService { /** * This method is used to create reservation items. - * + * * @param {CreateReservationItemInput[]} input - The details of the reservation items to create. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns { Promise} The created reservation items' details. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function createReservationItems (items: { * inventory_item_id: string, * location_id: string, * quantity: number * }[]) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const reservationItems = await inventoryModule.createReservationItems( * items * ) - * + * * // do something with the reservation items or return them * } */ @@ -424,26 +419,26 @@ export interface IInventoryService { /** * This method is used to create an inventory item. - * + * * @param {CreateInventoryItemInput} input - The details of the inventory item to create. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The created inventory item's details. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function createInventoryItem (item: { * sku: string, * requires_shipping: boolean * }) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const inventoryItem = await inventoryModule.createInventoryItem( * item * ) - * + * * // do something with the inventory item or return it * } */ @@ -454,26 +449,26 @@ export interface IInventoryService { /** * This method is used to create inventory items. - * + * * @param {CreateInventoryItemInput[]} input - The details of the inventory items to create. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The created inventory items' details. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function createInventoryItems (items: { * sku: string, * requires_shipping: boolean * }[]) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const inventoryItems = await inventoryModule.createInventoryItems( * items * ) - * + * * // do something with the inventory items or return them * } */ @@ -484,27 +479,27 @@ export interface IInventoryService { /** * This method is used to create inventory level. - * + * * @param {CreateInventoryLevelInput} data - The details of the inventory level to create. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The created inventory level's details. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function createInventoryLevel (item: { * inventory_item_id: string * location_id: string * stocked_quantity: number * }) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const inventoryLevel = await inventoryModule.createInventoryLevel( * item * ) - * + * * // do something with the inventory level or return it * } */ @@ -515,27 +510,27 @@ export interface IInventoryService { /** * This method is used to create inventory levels. - * + * * @param {CreateInventoryLevelInput[]} data - The details of the inventory levels to create. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The created inventory levels' details. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function createInventoryLevels (items: { * inventory_item_id: string * location_id: string * stocked_quantity: number * }[]) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const inventoryLevels = await inventoryModule.createInventoryLevels( * items * ) - * + * * // do something with the inventory levels or return them * } */ @@ -546,27 +541,27 @@ export interface IInventoryService { /** * This method is used to update inventory levels. Each inventory level is identified by the IDs of its associated inventory item and location. - * + * * @param {BulkUpdateInventoryLevelInput} updates - The attributes to update in each inventory level. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The updated inventory levels' details. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function updateInventoryLevels (items: { * inventory_item_id: string, * location_id: string, * stocked_quantity: number * }[]) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const inventoryLevels = await inventoryModule.updateInventoryLevels( * items * ) - * + * * // do something with the inventory levels or return them * } */ @@ -577,25 +572,25 @@ export interface IInventoryService { /** * This method is used to update an inventory level. The inventory level is identified by the IDs of its associated inventory item and location. - * + * * @param {string} inventoryItemId - The ID of the inventory item. * @param {string} locationId - The ID of the location. * @param {UpdateInventoryLevelInput} update - The attributes to update in the location level. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The updated inventory level's details. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function updateInventoryLevel ( * inventoryItemId: string, * locationId: string, * stockedQuantity: number * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const inventoryLevel = await inventoryModule.updateInventoryLevels( * inventoryItemId, * locationId, @@ -603,7 +598,7 @@ export interface IInventoryService { * stocked_quantity: stockedQuantity * } * ) - * + * * // do something with the inventory level or return it * } */ @@ -616,30 +611,30 @@ export interface IInventoryService { /** * This method is used to update an inventory item. - * + * * @param {string} inventoryItemId - The ID of the inventory item. * @param {Partial} input - The attributes to update in the inventory item. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The updated inventory item's details. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function updateInventoryItem ( * inventoryItemId: string, * sku: string * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const inventoryItem = await inventoryModule.updateInventoryItem( * inventoryItemId, * { * sku * } * ) - * + * * // do something with the inventory item or return it * } */ @@ -651,30 +646,30 @@ export interface IInventoryService { /** * This method is used to update a reservation item. - * + * * @param {string} reservationItemId - The ID of the reservation item. * @param {UpdateReservationItemInput} input - The attributes to update in the reservation item. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The updated reservation item. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function updateReservationItem ( * reservationItemId: string, * quantity: number * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const reservationItem = await inventoryModule.updateReservationItem( * reservationItemId, * { * quantity * } * ) - * + * * // do something with the reservation item or return it * } */ @@ -686,21 +681,21 @@ export interface IInventoryService { /** * This method is used to delete the reservation items associated with a line item or multiple line items. - * + * * @param {string | string[]} lineItemId - The ID(s) of the line item(s). * @param {SharedContext} context - A context used to share re9sources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the reservation items are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function deleteReservationItemsByLineItem ( * lineItemIds: string[] * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * await inventoryModule.deleteReservationItemsByLineItem( * lineItemIds * ) @@ -713,21 +708,21 @@ export interface IInventoryService { /** * This method is used to delete a reservation item or multiple reservation items by their IDs. - * + * * @param {string | string[]} reservationItemId - The ID(s) of the reservation item(s) to delete. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the reservation item(s) are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function deleteReservationItems ( * reservationItemIds: string[] * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * await inventoryModule.deleteReservationItem( * reservationItemIds * ) @@ -741,21 +736,21 @@ export interface IInventoryService { /** * This method is used to delete an inventory item or multiple inventory items. The inventory items are only soft deleted and can be restored using the * {@link restoreInventoryItem} method. - * + * * @param {string | string[]} inventoryItemId - The ID(s) of the inventory item(s) to delete. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the inventory item(s) are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function deleteInventoryItem ( * inventoryItems: string[] * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * await inventoryModule.deleteInventoryItem( * inventoryItems * ) @@ -768,21 +763,21 @@ export interface IInventoryService { /** * This method is used to restore an inventory item or multiple inventory items that were previously deleted using the {@link deleteInventoryItem} method. - * - * @param {string | string[]} inventoryItemId - The ID(s) of the inventory item(s) to restore. + * + * @param {string | string[]} inventoryItemId - The ID(s) of the inventory item(s) to restore. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the inventory item(s) are successfully restored. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function restoreInventoryItem ( * inventoryItems: string[] * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * await inventoryModule.restoreInventoryItem( * inventoryItems * ) @@ -795,21 +790,21 @@ export interface IInventoryService { /** * This method deletes the inventory item level(s) for the ID(s) of associated location(s). - * + * * @param {string | string[]} locationId - The ID(s) of the associated location(s). * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the inventory item level(s) are successfully restored. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function deleteInventoryItemLevelByLocationId ( * locationIds: string[] * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * await inventoryModule.deleteInventoryItemLevelByLocationId( * locationIds * ) @@ -822,21 +817,21 @@ export interface IInventoryService { /** * This method deletes reservation item(s) by the ID(s) of associated location(s). - * + * * @param {string | string[]} locationId - The ID(s) of the associated location(s). * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the reservation item(s) are successfully restored. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function deleteReservationItemByLocationId ( * locationIds: string[] * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * await inventoryModule.deleteReservationItemByLocationId( * locationIds * ) @@ -849,23 +844,23 @@ export interface IInventoryService { /** * This method is used to delete an inventory level. The inventory level is identified by the IDs of its associated inventory item and location. - * + * * @param {string} inventoryItemId - The ID of the associated inventory item. * @param {string} locationId - The ID of the associated location. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the inventory level(s) are successfully restored. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function deleteInventoryLevel ( * inventoryItemId: string, * locationId: string * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * await inventoryModule.deleteInventoryLevel( * inventoryItemId, * locationId @@ -880,31 +875,31 @@ export interface IInventoryService { /** * This method is used to adjust the inventory level's stocked quantity. The inventory level is identified by the IDs of its associated inventory item and location. - * + * * @param {string} inventoryItemId - The ID of the associated inventory item. * @param {string} locationId - The ID of the associated location. * @param {number} adjustment - A positive or negative number used to adjust the inventory level's stocked quantity. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The inventory level's details. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function adjustInventory ( * inventoryItemId: string, * locationId: string, * adjustment: number * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const inventoryLevel = await inventoryModule.adjustInventory( * inventoryItemId, * locationId, * adjustment * ) - * + * * // do something with the inventory level or return it. * } */ @@ -917,25 +912,25 @@ export interface IInventoryService { /** * This method is used to confirm whether the specified quantity of an inventory item is available in the specified locations. - * + * * @param {string} inventoryItemId - The ID of the inventory item to check its availability. * @param {string[]} locationIds - The IDs of the locations to check the quantity availability in. * @param {number} quantity - The quantity to check if available for the inventory item in the specified locations. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Whether the specified quantity is available for the inventory item in the specified locations. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function confirmInventory ( * inventoryItemId: string, * locationIds: string[], * quantity: number * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * return await inventoryModule.confirmInventory( * inventoryItemId, * locationIds, @@ -952,28 +947,28 @@ export interface IInventoryService { /** * This method is used to retrieve the available quantity of an inventory item within the specified locations. - * + * * @param {string} inventoryItemId - The ID of the inventory item to retrieve its quantity. * @param {string[]} locationIds - The IDs of the locations to retrieve the available quantity from. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The available quantity of the inventory item in the specified locations. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveAvailableQuantity ( * inventoryItemId: string, * locationIds: string[], * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const quantity = await inventoryModule.retrieveAvailableQuantity( * inventoryItemId, * locationIds, * ) - * + * * // do something with the quantity or return it * } */ @@ -985,28 +980,28 @@ export interface IInventoryService { /** * This method is used to retrieve the stocked quantity of an inventory item within the specified locations. - * + * * @param {string} inventoryItemId - The ID of the inventory item to retrieve its stocked quantity. * @param {string[]} locationIds - The IDs of the locations to retrieve the stocked quantity from. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The stocked quantity of the inventory item in the specified locations. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveStockedQuantity ( * inventoryItemId: string, * locationIds: string[], * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const quantity = await inventoryModule.retrieveStockedQuantity( * inventoryItemId, * locationIds, * ) - * + * * // do something with the quantity or return it * } */ @@ -1018,28 +1013,28 @@ export interface IInventoryService { /** * This method is used to retrieve the reserved quantity of an inventory item within the specified locations. - * + * * @param {string} inventoryItemId - The ID of the inventory item to retrieve its reserved quantity. * @param {string[]} locationIds - The IDs of the locations to retrieve the reserved quantity from. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The reserved quantity of the inventory item in the specified locations. - * + * * @example - * import { + * import { * initialize as initializeInventoryModule, * } from "@medusajs/inventory" - * + * * async function retrieveReservedQuantity ( * inventoryItemId: string, * locationIds: string[], * ) { * const inventoryModule = await initializeInventoryModule({}) - * + * * const quantity = await inventoryModule.retrieveReservedQuantity( * inventoryItemId, * locationIds, * ) - * + * * // do something with the quantity or return it * } */ diff --git a/packages/types/src/link-modules/index.ts b/packages/types/src/link-modules/index.ts index 60537f6740..cc97a4b1e6 100644 --- a/packages/types/src/link-modules/index.ts +++ b/packages/types/src/link-modules/index.ts @@ -1,11 +1,9 @@ import { FindConfig } from "../common" import { RestoreReturn, SoftDeleteReturn } from "../dal" -import { ModuleJoinerConfig } from "../modules-sdk" +import { IModuleService } from "../modules-sdk" import { Context } from "../shared-context" -export interface ILinkModule { - __joinerConfig(): ModuleJoinerConfig - +export interface ILinkModule extends IModuleService { list( filters?: Record, config?: FindConfig, diff --git a/packages/types/src/pricing/service.ts b/packages/types/src/pricing/service.ts index 0a88f48e95..c267516884 100644 --- a/packages/types/src/pricing/service.ts +++ b/packages/types/src/pricing/service.ts @@ -45,16 +45,11 @@ import { } from "./common" import { FindConfig } from "../common" -import { ModuleJoinerConfig } from "../modules-sdk" -import { Context } from "../shared-context" import { RestoreReturn, SoftDeleteReturn } from "../dal" +import { IModuleService } from "../modules-sdk" +import { Context } from "../shared-context" -export interface IPricingModuleService { - /** - * @ignore - */ - __joinerConfig(): ModuleJoinerConfig - +export interface IPricingModuleService extends IModuleService { /** * This method is used to calculate prices based on the provided filters and context. * diff --git a/packages/types/src/product/service.ts b/packages/types/src/product/service.ts index 9477fd31ef..2d91302757 100644 --- a/packages/types/src/product/service.ts +++ b/packages/types/src/product/service.ts @@ -1,3 +1,4 @@ +import { RestoreReturn, SoftDeleteReturn } from "../dal" import { CreateProductCategoryDTO, CreateProductCollectionDTO, @@ -28,18 +29,12 @@ import { UpdateProductTypeDTO, UpdateProductVariantDTO, } from "./common" -import { RestoreReturn, SoftDeleteReturn } from "../dal" -import { Context } from "../shared-context" import { FindConfig } from "../common" -import { ModuleJoinerConfig } from "../modules-sdk" - -export interface IProductModuleService { - /** - * @ignore - */ - __joinerConfig(): ModuleJoinerConfig +import { IModuleService } from "../modules-sdk" +import { Context } from "../shared-context" +export interface IProductModuleService extends IModuleService { /** * This method is used to retrieve a product by its ID * diff --git a/packages/types/src/shared-context.ts b/packages/types/src/shared-context.ts index 67170989a1..f110c910a4 100644 --- a/packages/types/src/shared-context.ts +++ b/packages/types/src/shared-context.ts @@ -1,6 +1,8 @@ import { EntityManager } from "typeorm" +import { Message } from "./event-bus" /** + * @deprecated use `Context` instead * @interface * * A context used to share resources, such as transaction manager, between the application and the module. @@ -16,9 +18,19 @@ export type SharedContext = { manager?: EntityManager } +export interface MessageAggregatorFormat { + groupBy?: string[] + sortBy?: { [key: string]: string[] | string | number } +} + +export interface IMessageAggregator { + save(msg: Message | Message[]): void + getMessages(format?: MessageAggregatorFormat): Record + clearMessages(): void +} + /** * @interface - * * A context used to share resources, such as transaction manager, between the application and the module. */ export type Context = { @@ -39,11 +51,20 @@ export type Context = { * A boolean value indicating whether nested transactions are enabled. */ enableNestedTransactions?: boolean + /** + * A string indicating the ID of the group to aggregate the events to be emitted at a later point. + */ + eventGroupId?: string /** * A string indicating the ID of the current transaction. */ transactionId?: string + /** + * An instance of a message aggregator, which is used to aggregate messages to be emitted at a later point. + */ + messageAggregator?: IMessageAggregator + /** * A string indicating the ID of the current request. */ diff --git a/packages/types/src/stock-location/service.ts b/packages/types/src/stock-location/service.ts index 4e52770d40..76166bcef4 100644 --- a/packages/types/src/stock-location/service.ts +++ b/packages/types/src/stock-location/service.ts @@ -6,74 +6,69 @@ import { } from "./common" import { FindConfig } from "../common/common" -import { ModuleJoinerConfig } from "../modules-sdk" +import { IModuleService } from "../modules-sdk" import { SharedContext } from "../shared-context" -export interface IStockLocationService { - /** - * @ignore - */ - __joinerConfig(): ModuleJoinerConfig - +export interface IStockLocationService extends IModuleService { /** * This method is used to retrieve a paginated list of stock locations based on optional filters and configuration. - * + * * @param {FilterableStockLocationProps} selector - The filters to apply on the retrieved stock locations. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the stock locations are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a stock location. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @return {Promise} The list of stock locations. - * + * * @example * To retrieve a list of stock locations using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeStockLocationModule, * } from "@medusajs/stock-location" - * + * * async function listStockLocations (ids: string[]) { * const stockLocationModule = await initializeStockLocationModule({}) - * + * * const stockLocations = await stockLocationModule.list({ * id: ids * }) - * + * * // do something with the stock locations or return them * } * ``` - * + * * To specify relations that should be retrieved within the stock locations: - * + * * ```ts - * import { + * import { * initialize as initializeStockLocationModule, * } from "@medusajs/stock-location" - * + * * async function listStockLocations (ids: string[]) { * const stockLocationModule = await initializeStockLocationModule({}) - * + * * const stockLocations = await stockLocationModule.list({ * id: ids * }, { * relations: ["address"] * }) - * + * * // do something with the stock locations or return them * } * ``` - * + * * By default, only the first `10` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeStockLocationModule, * } from "@medusajs/stock-location" - * + * * async function listStockLocations (ids: string[], skip: number, take: number) { * const stockLocationModule = await initializeStockLocationModule({}) - * + * * const stockLocations = await stockLocationModule.list({ * id: ids * }, { @@ -81,7 +76,7 @@ export interface IStockLocationService { * skip, * take * }) - * + * * // do something with the stock locations or return them * } * ``` @@ -94,63 +89,63 @@ export interface IStockLocationService { /** * This method is used to retrieve a paginated list of stock locations along with the total count of available stock locations satisfying the provided filters. - * + * * @param {FilterableStockLocationProps} selector - The filters to apply on the retrieved stock locations. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the stock locations are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a stock location. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @return {Promise<[StockLocationDTO[], number]>} The list of stock locations along with the total count. - * + * * @example * To retrieve a list of stock locations using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeStockLocationModule, * } from "@medusajs/stock-location" - * + * * async function listStockLocations (ids: string[]) { * const stockLocationModule = await initializeStockLocationModule({}) - * + * * const [stockLocations, count] = await stockLocationModule.listAndCount({ * id: ids * }) - * + * * // do something with the stock locations or return them * } * ``` - * + * * To specify relations that should be retrieved within the stock locations: - * + * * ```ts - * import { + * import { * initialize as initializeStockLocationModule, * } from "@medusajs/stock-location" - * + * * async function listStockLocations (ids: string[]) { * const stockLocationModule = await initializeStockLocationModule({}) - * + * * const [stockLocations, count] = await stockLocationModule.listAndCount({ * id: ids * }, { * relations: ["address"] * }) - * + * * // do something with the stock locations or return them * } * ``` - * + * * By default, only the first `10` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeStockLocationModule, * } from "@medusajs/stock-location" - * + * * async function listStockLocations (ids: string[], skip: number, take: number) { * const stockLocationModule = await initializeStockLocationModule({}) - * + * * const [stockLocations, count] = await stockLocationModule.listAndCount({ * id: ids * }, { @@ -158,7 +153,7 @@ export interface IStockLocationService { * skip, * take * }) - * + * * // do something with the stock locations or return them * } * ``` @@ -171,45 +166,45 @@ export interface IStockLocationService { /** * This method is used to retrieve a stock location by its ID - * + * * @param {string} id - The ID of the stock location - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the stock location is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a stock location. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The stock location's details. - * + * * @example * A simple example that retrieves a inventory item by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeStockLocationModule, * } from "@medusajs/stock-location" - * + * * async function retrieveStockLocation (id: string) { * const stockLocationModule = await initializeStockLocationModule({}) - * + * * const stockLocation = await stockLocationModule.retrieve(id) - * + * * // do something with the stock location or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeStockLocationModule, * } from "@medusajs/stock-location" - * + * * async function retrieveStockLocation (id: string) { * const stockLocationModule = await initializeStockLocationModule({}) - * + * * const stockLocation = await stockLocationModule.retrieve(id, { * relations: ["address"] * }) - * + * * // do something with the stock location or return it * } * ``` @@ -222,23 +217,23 @@ export interface IStockLocationService { /** * This method is used to create a stock location. - * + * * @param {CreateStockLocationInput} input - The details of the stock location to create. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The created stock location's details. - * + * * @example - * import { + * import { * initialize as initializeStockLocationModule, * } from "@medusajs/stock-location" - * + * * async function createStockLocation (name: string) { * const stockLocationModule = await initializeStockLocationModule({}) - * + * * const stockLocation = await stockLocationModule.create({ * name * }) - * + * * // do something with the stock location or return it * } */ @@ -249,24 +244,24 @@ export interface IStockLocationService { /** * This method is used to update a stock location. - * + * * @param {string} id - The ID of the stock location. * @param {UpdateStockLocationInput} input - The attributes to update in the stock location. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The stock location's details. - * + * * @example - * import { + * import { * initialize as initializeStockLocationModule, * } from "@medusajs/stock-location" - * + * * async function updateStockLocation (id:string, name: string) { * const stockLocationModule = await initializeStockLocationModule({}) - * + * * const stockLocation = await stockLocationModule.update(id, { * name * }) - * + * * // do something with the stock location or return it * } */ @@ -278,19 +273,19 @@ export interface IStockLocationService { /** * This method is used to delete a stock location. - * + * * @param {string} id - The ID of the stock location. * @param {SharedContext} context - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the stock location is successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeStockLocationModule, * } from "@medusajs/stock-location" - * + * * async function deleteStockLocation (id:string) { * const stockLocationModule = await initializeStockLocationModule({}) - * + * * await stockLocationModule.delete(id) * } */ diff --git a/packages/utils/src/common/camel-to-snake-case.ts b/packages/utils/src/common/camel-to-snake-case.ts index 83484fdde2..81355b01f1 100644 --- a/packages/utils/src/common/camel-to-snake-case.ts +++ b/packages/utils/src/common/camel-to-snake-case.ts @@ -1,2 +1,2 @@ export const camelToSnakeCase = (string) => - string.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) + string.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase() diff --git a/packages/utils/src/event-bus/__tests__/message-aggregator.spec.ts b/packages/utils/src/event-bus/__tests__/message-aggregator.spec.ts new file mode 100644 index 0000000000..7cf7c7e39b --- /dev/null +++ b/packages/utils/src/event-bus/__tests__/message-aggregator.spec.ts @@ -0,0 +1,157 @@ +import { MessageAggregator } from "../message-aggregator" + +describe("MessageAggregator", function () { + afterEach(() => { + jest.resetAllMocks + }) + + it("should group messages by any given group of keys", function () { + const aggregator = new MessageAggregator() + aggregator.save({ + eventName: "ProductVariant.created", + body: { + metadata: { + service: "ProductService", + action: "created", + object: "ProductVariant", + eventGroupId: "1", + }, + data: { id: 999 }, + }, + }) + aggregator.save({ + eventName: "Product.created", + body: { + metadata: { + service: "ProductService", + action: "created", + object: "Product", + eventGroupId: "1", + }, + data: { id: 1 }, + }, + }) + aggregator.save({ + eventName: "ProductVariant.created", + body: { + metadata: { + service: "ProductService", + action: "created", + object: "ProductVariant", + eventGroupId: "1", + }, + data: { id: 222 }, + }, + }) + aggregator.save({ + eventName: "ProductType.detached", + body: { + metadata: { + service: "ProductService", + action: "detached", + object: "ProductType", + eventGroupId: "1", + }, + data: { id: 333 }, + }, + }) + aggregator.save({ + eventName: "ProductVariant.updated", + body: { + metadata: { + service: "ProductService", + action: "updated", + object: "ProductVariant", + eventGroupId: "1", + }, + data: { id: 123 }, + }, + }) + + const format = { + groupBy: ["eventName", "body.metadata.object", "body.metadata.action"], + sortBy: { + "body.metadata.object": ["ProductType", "ProductVariant", "Product"], + "body.data.id": "asc", + }, + } + + const messages = aggregator.getMessages(format) + + expect(Object.keys(messages)).toHaveLength(4) + + const allGroups = Object.values(messages) + + expect(allGroups[0]).toEqual([ + { + eventName: "ProductType.detached", + body: { + metadata: { + service: "ProductService", + action: "detached", + object: "ProductType", + eventGroupId: "1", + }, + data: { id: 333 }, + }, + }, + ]) + + expect(allGroups[1]).toEqual([ + { + eventName: "ProductVariant.updated", + body: { + metadata: { + service: "ProductService", + action: "updated", + object: "ProductVariant", + eventGroupId: "1", + }, + data: { id: 123 }, + }, + }, + ]) + + expect(allGroups[2]).toEqual([ + { + eventName: "ProductVariant.created", + body: { + metadata: { + service: "ProductService", + action: "created", + object: "ProductVariant", + eventGroupId: "1", + }, + data: { id: 222 }, + }, + }, + { + eventName: "ProductVariant.created", + body: { + metadata: { + service: "ProductService", + action: "created", + object: "ProductVariant", + eventGroupId: "1", + }, + data: { id: 999 }, + }, + }, + ]) + + expect(allGroups[3]).toEqual([ + { + eventName: "Product.created", + body: { + metadata: { + service: "ProductService", + action: "created", + object: "Product", + eventGroupId: "1", + }, + data: { id: 1 }, + }, + }, + ]) + }) +}) diff --git a/packages/utils/src/event-bus/build-event-messages.ts b/packages/utils/src/event-bus/build-event-messages.ts new file mode 100644 index 0000000000..40eedd9184 --- /dev/null +++ b/packages/utils/src/event-bus/build-event-messages.ts @@ -0,0 +1,85 @@ +import { Context, EventBusTypes } from "@medusajs/types" +import { CommonEvents } from "./common-events" + +/** + * Build messages from message data to be consumed by the event bus and emitted to the consumer + * @param MessageFormat + * @param options + */ +export function buildEventMessages( + messageData: + | EventBusTypes.MessageFormat + | EventBusTypes.MessageFormat[], + options?: Record +): EventBusTypes.Message[] { + const messageData_ = Array.isArray(messageData) ? messageData : [messageData] + const messages: EventBusTypes.Message[] = [] + + messageData_.map((data) => { + const data_ = Array.isArray(data.data) ? data.data : [data.data] + data_.forEach((bodyData) => { + const message = { + eventName: data.eventName, + body: { + metadata: data.metadata, + data: bodyData, + }, + options, + } + messages.push(message) + }) + }) + + return messages +} + +/** + * Helper function to compose and normalize a Message to be emitted by EventBus Module + * @param eventName Name of the event to be emitted + * @param data The content of the message + * @param metadata Metadata of the message + * @param context Context from the caller service + * @param options Options to be passed to the event bus + */ +export function composeMessage( + eventName: string, + { + data, + service, + entity, + action, + context = {}, + options, + }: { + data: unknown + service: string + entity: string + action?: string + context?: Context + options?: Record + } +): EventBusTypes.Message { + const act = action || eventName.split(".").pop() + if (!action && !Object.values(CommonEvents).includes(act as CommonEvents)) { + throw new Error("Action is required if eventName is not a CommonEvent") + } + + const metadata: EventBusTypes.MessageBody["metadata"] = { + service, + object: entity, + action: act!, + } + + if (context.eventGroupId) { + metadata.eventGroupId = context.eventGroupId + } + + return { + eventName, + body: { + metadata, + data, + }, + options, + } +} diff --git a/packages/utils/src/event-bus/common-events.ts b/packages/utils/src/event-bus/common-events.ts new file mode 100644 index 0000000000..3fee0d8846 --- /dev/null +++ b/packages/utils/src/event-bus/common-events.ts @@ -0,0 +1,7 @@ +export enum CommonEvents { + CREATED = "created", + UPDATED = "updated", + DELETED = "deleted", + ATTACHED = "attached", + DETACHED = "detached", +} diff --git a/packages/utils/src/event-bus/index.ts b/packages/utils/src/event-bus/index.ts index 602d7ddfff..b62ccdaf97 100644 --- a/packages/utils/src/event-bus/index.ts +++ b/packages/utils/src/event-bus/index.ts @@ -22,6 +22,7 @@ export abstract class AbstractEventBusModuleService options: Record ): Promise abstract emit(data: EventBusTypes.EmitData[]): Promise + abstract emit(data: EventBusTypes.Message[]): Promise protected storeSubscribers({ event, @@ -101,3 +102,7 @@ export abstract class AbstractEventBusModuleService return this } } + +export * from "./build-event-messages" +export * from "./common-events" +export * from "./message-aggregator" diff --git a/packages/utils/src/event-bus/message-aggregator.ts b/packages/utils/src/event-bus/message-aggregator.ts new file mode 100644 index 0000000000..3ba3925283 --- /dev/null +++ b/packages/utils/src/event-bus/message-aggregator.ts @@ -0,0 +1,112 @@ +import { + IMessageAggregator, + Message, + MessageAggregatorFormat, +} from "@medusajs/types" + +export class MessageAggregator implements IMessageAggregator { + private messages: Message[] + + constructor() { + this.messages = [] + } + + save(msg: Message | Message[]): void { + if (!msg || (Array.isArray(msg) && msg.length === 0)) { + return + } + + if (Array.isArray(msg)) { + this.messages.push(...msg) + } else { + this.messages.push(msg) + } + } + + getMessages(format?: MessageAggregatorFormat): { + [group: string]: Message[] + } { + const { groupBy, sortBy } = format ?? {} + + if (sortBy) { + this.messages.sort((a, b) => this.compareMessages(a, b, sortBy)) + } + + let messages: { [group: string]: Message[] } = { default: this.messages } + + if (groupBy) { + const groupedMessages = this.messages.reduce<{ + [key: string]: Message[] + }>((acc, msg) => { + const key = groupBy + .map((field) => this.getValueFromPath(msg, field)) + .join("-") + if (!acc[key]) { + acc[key] = [] + } + acc[key].push(msg) + return acc + }, {}) + + messages = groupedMessages + } + + return messages + } + + clearMessages(): void { + this.messages = [] + } + + private getValueFromPath(obj: any, path: string): any { + const keys = path.split(".") + for (const key of keys) { + obj = obj[key] + if (obj === undefined) break + } + return obj + } + + private compareMessages( + a: Message, + b: Message, + sortBy: MessageAggregatorFormat["sortBy"] + ): number { + for (const key of Object.keys(sortBy!)) { + const orderCriteria = sortBy![key] + const valueA = this.getValueFromPath(a, key) + const valueB = this.getValueFromPath(b, key) + + // User defined order + if (Array.isArray(orderCriteria)) { + const indexA = orderCriteria.indexOf(valueA) + const indexB = orderCriteria.indexOf(valueB) + + if (indexA === indexB) { + continue + } else if (indexA === -1) { + return 1 + } else if (indexB === -1) { + return -1 + } else { + return indexA - indexB + } + } else { + // Ascending or descending order + let orderMultiplier = 1 + if (orderCriteria === "desc" || orderCriteria === -1) { + orderMultiplier = -1 + } + + if (valueA === valueB) { + continue + } else if (valueA < valueB) { + return -1 * orderMultiplier + } else { + return 1 * orderMultiplier + } + } + } + return 0 + } +} diff --git a/packages/utils/src/modules-sdk/decorators/inject-into-context.ts b/packages/utils/src/modules-sdk/decorators/inject-into-context.ts new file mode 100644 index 0000000000..07f8c4644d --- /dev/null +++ b/packages/utils/src/modules-sdk/decorators/inject-into-context.ts @@ -0,0 +1,30 @@ +export function InjectIntoContext( + properties: Record +): MethodDecorator { + return function ( + target: any, + propertyKey: string | symbol, + descriptor: any + ): void { + if (!target.MedusaContextIndex_) { + throw new Error( + `To apply @InjectIntoContext you have to flag a parameter using @MedusaContext` + ) + } + + const argIndex = target.MedusaContextIndex_[propertyKey] + const original = descriptor.value + descriptor.value = async function (...args: any[]) { + for (const key of Object.keys(properties)) { + args[argIndex] = args[argIndex] ?? {} + args[argIndex][key] = + args[argIndex][key] ?? + (typeof properties[key] === "function" + ? (properties[key] as Function).apply(this, args) + : properties[key]) + } + + return await original.apply(this, args) + } + } +}