feat: event aggregator (#6218)

What:
- Event Aggregator Util
- Preparation for normalizing event in a new format (backward compatible with the current format)
- GQL Schema to joiner config and some Entities configured
- Link modules emmiting events
This commit is contained in:
Carlos R. L. Rodrigues authored and GitHub committed 2024-02-05 11:59:10 +00:00
1 parent 73fd92a1af
commit 884428a1b5
66 files changed
+1123 -465

No files matched your search

+13
View File
@@ -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
@@ -1,11 +1,11 @@
import { IAuthModuleService } from "@medusajs/types"
import { MikroOrmWrapper } from "../../../utils"
import { Modules } from "@medusajs/modules-sdk" import { Modules } from "@medusajs/modules-sdk"
import { IAuthModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { initModules } from "medusa-test-utils"
import { createAuthProviders } from "../../../__fixtures__/auth-provider" import { createAuthProviders } from "../../../__fixtures__/auth-provider"
import { createAuthUsers } from "../../../__fixtures__/auth-user" import { createAuthUsers } from "../../../__fixtures__/auth-user"
import { MikroOrmWrapper } from "../../../utils"
import { getInitModuleConfig } from "../../../utils/get-init-module-config" import { getInitModuleConfig } from "../../../utils/get-init-module-config"
import { initModules } from "medusa-test-utils"
jest.setTimeout(30000) jest.setTimeout(30000)
@@ -1,11 +1,11 @@
import { IAuthModuleService } from "@medusajs/types"
import { MikroOrmWrapper } from "../../../utils"
import { Modules } from "@medusajs/modules-sdk" import { Modules } from "@medusajs/modules-sdk"
import { IAuthModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { initModules } from "medusa-test-utils"
import { createAuthProviders } from "../../../__fixtures__/auth-provider" import { createAuthProviders } from "../../../__fixtures__/auth-provider"
import { createAuthUsers } from "../../../__fixtures__/auth-user" import { createAuthUsers } from "../../../__fixtures__/auth-user"
import { MikroOrmWrapper } from "../../../utils"
import { getInitModuleConfig } from "../../../utils/get-init-module-config" import { getInitModuleConfig } from "../../../utils/get-init-module-config"
import { initModules } from "medusa-test-utils"
jest.setTimeout(30000) jest.setTimeout(30000)
@@ -1,11 +1,10 @@
import { MedusaModule, Modules } from "@medusajs/modules-sdk" import { MedusaModule, Modules } from "@medusajs/modules-sdk"
import { IAuthModuleService } from "@medusajs/types" import { IAuthModuleService } from "@medusajs/types"
import { MikroOrmWrapper } from "../../../utils"
import { SqlEntityManager } from "@mikro-orm/postgresql" 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 { 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) 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(success).toBe(false)
expect(error).toEqual( expect(error).toEqual(
@@ -79,12 +81,9 @@ describe("AuthModuleService - AuthProvider", () => {
}) })
it("fails to authenticate using a valid provider with an invalid scope", async () => { it("fails to authenticate using a valid provider with an invalid scope", async () => {
const { success, error } = await service.authenticate( const { success, error } = await service.authenticate("emailpass", {
"emailpass", authScope: "non-existing",
{ } as any)
authScope: "non-existing",
} as any
)
expect(success).toBe(false) expect(success).toBe(false)
expect(error).toEqual( expect(error).toEqual(
@@ -1,5 +1,11 @@
import { MedusaContainer } from "@medusajs/modules-sdk" 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 { AbstractEventBusModuleService } from "@medusajs/utils"
import { EventEmitter } from "events" import { EventEmitter } from "events"
import { ulid } from "ulid" import { ulid } from "ulid"
@@ -37,14 +43,16 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
*/ */
async emit<T>(data: EmitData<T>[]): Promise<void> async emit<T>(data: EmitData<T>[]): Promise<void>
async emit<T, TInput extends string | EmitData<T>[] = string>( async emit<T>(data: Message<T>[]): Promise<void>
async emit<T, TInput extends string | EmitData<T>[] | Message<T>[] = string>(
eventOrData: TInput, eventOrData: TInput,
data?: T, data?: T,
options: Record<string, unknown> = {} options: Record<string, unknown> = {}
): Promise<void> { ): Promise<void> {
const isBulkEmit = Array.isArray(eventOrData) const isBulkEmit = Array.isArray(eventOrData)
const events: EmitData[] = isBulkEmit const events: EmitData[] | Message<T>[] = isBulkEmit
? eventOrData ? eventOrData
: [{ eventName: eventOrData, data }] : [{ eventName: eventOrData, data }]
@@ -61,7 +69,8 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
continue continue
} }
this.eventEmitter_.emit(event.eventName, event.data) const data = (event as EmitData).data ?? (event as Message<T>).body
this.eventEmitter_.emit(event.eventName, data)
} }
} }
@@ -1,6 +1,6 @@
import { InternalModuleDeclaration } from "@medusajs/modules-sdk" import { InternalModuleDeclaration } from "@medusajs/modules-sdk"
import { EmitData, Logger } from "@medusajs/types" import { EmitData, Logger, Message } from "@medusajs/types"
import { AbstractEventBusModuleService } from "@medusajs/utils" import { AbstractEventBusModuleService, isString } from "@medusajs/utils"
import { BulkJobOptions, JobsOptions, Queue, Worker } from "bullmq" import { BulkJobOptions, JobsOptions, Queue, Worker } from "bullmq"
import { Redis } from "ioredis" import { Redis } from "ioredis"
import { BullJob, EmitOptions, EventBusRedisModuleOptions } from "../types" import { BullJob, EmitOptions, EventBusRedisModuleOptions } from "../types"
@@ -67,7 +67,9 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
*/ */
async emit<T>(data: EmitData<T>[]): Promise<void> async emit<T>(data: EmitData<T>[]): Promise<void>
async emit<T, TInput extends string | EmitData<T>[] = string>( async emit<T>(data: Message<T>[]): Promise<void>
async emit<T, TInput extends string | EmitData<T>[] | Message<T>[] = string>(
eventNameOrData: TInput, eventNameOrData: TInput,
data?: T, data?: T,
options: BulkJobOptions | JobsOptions = {} options: BulkJobOptions | JobsOptions = {}
@@ -84,10 +86,17 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
...globalJobOptions, ...globalJobOptions,
} as EmitOptions } as EmitOptions
const dataBody = isString(eventNameOrData)
? data ?? (data as Message<T>).body
: undefined
const events = isBulkEmit const events = isBulkEmit
? eventNameOrData.map((event) => ({ ? eventNameOrData.map((event) => ({
name: event.eventName, name: event.eventName,
data: { eventName: event.eventName, data: event.data }, data: {
eventName: event.eventName,
data: (event as EmitData).data ?? (event as Message<T>).body,
},
opts: { opts: {
...opts, ...opts,
// local options // local options
@@ -97,7 +106,7 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
: [ : [
{ {
name: eventNameOrData as string, name: eventNameOrData as string,
data: { eventName: eventNameOrData, data }, data: { eventName: eventNameOrData, data: dataBody },
opts: { opts: {
...opts, ...opts,
// local options // local options
+17 -32
View File
@@ -1,6 +1,7 @@
import { Modules } from "@medusajs/modules-sdk" import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types" import { ModuleJoinerConfig } from "@medusajs/types"
import { InventoryItem, InventoryLevel, ReservationItem } from "./models" import { InventoryItem, InventoryLevel, ReservationItem } from "./models"
import moduleSchema from "./schema"
export const joinerConfig: ModuleJoinerConfig = { export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.INVENTORY, serviceName: Modules.INVENTORY,
@@ -10,46 +11,30 @@ export const joinerConfig: ModuleJoinerConfig = {
inventory_level_id: InventoryLevel.name, inventory_level_id: InventoryLevel.name,
reservation_item_id: ReservationItem.name, reservation_item_id: ReservationItem.name,
}, },
schema: moduleSchema,
alias: [ alias: [
{ {
name: "inventory_items", name: ["inventory_items", "inventory"],
},
{
name: "inventory",
},
{
name: "inventory_level",
args: { args: {
entity: "InventoryItem",
},
},
{
name: ["inventory_level", "inventory_levels"],
args: {
entity: "InventoryLevel",
methodSuffix: "InventoryLevels", methodSuffix: "InventoryLevels",
}, },
}, },
{ {
name: "inventory_levels", name: [
args: { "reservation",
methodSuffix: "InventoryLevels", "reservations",
}, "reservation_item",
}, "reservation_items",
{ ],
name: "reservation_items",
args: {
methodSuffix: "ReservationItems",
},
},
{
name: "reservation_item",
args: {
methodSuffix: "ReservationItems",
},
},
{
name: "reservation",
args: {
methodSuffix: "ReservationItems",
},
},
{
name: "reservations",
args: { args: {
entity: "ReservationItem",
methodSuffix: "ReservationItems", methodSuffix: "ReservationItems",
}, },
}, },
@@ -6,9 +6,11 @@ import {
DeleteDateColumn, DeleteDateColumn,
Entity, Entity,
Index, Index,
OneToMany,
PrimaryColumn, PrimaryColumn,
UpdateDateColumn, UpdateDateColumn,
} from "typeorm" } from "typeorm"
import { InventoryLevel } from "./inventory-level"
@Entity() @Entity()
export class InventoryItem { export class InventoryItem {
@@ -67,6 +69,12 @@ export class InventoryItem {
@Column({ type: "jsonb", nullable: true }) @Column({ type: "jsonb", nullable: true })
metadata: Record<string, unknown> | null metadata: Record<string, unknown> | null
@OneToMany(
() => InventoryLevel,
(inventoryLevel) => inventoryLevel.inventory_item
)
inventory_levels!: InventoryLevel[]
@BeforeInsert() @BeforeInsert()
private beforeInsert(): void { private beforeInsert(): void {
this.id = generateEntityId(this.id, "iitem") this.id = generateEntityId(this.id, "iitem")
@@ -6,9 +6,12 @@ import {
DeleteDateColumn, DeleteDateColumn,
Entity, Entity,
Index, Index,
JoinColumn,
ManyToOne,
PrimaryColumn, PrimaryColumn,
UpdateDateColumn, UpdateDateColumn,
} from "typeorm" } from "typeorm"
import { InventoryItem } from "./inventory-item"
@Entity() @Entity()
@Index(["inventory_item_id", "location_id"], { unique: true }) @Index(["inventory_item_id", "location_id"], { unique: true })
@@ -45,6 +48,10 @@ export class InventoryLevel {
@Column({ type: "jsonb", nullable: true }) @Column({ type: "jsonb", nullable: true })
metadata: Record<string, unknown> | null metadata: Record<string, unknown> | null
@ManyToOne(() => InventoryItem)
@JoinColumn({ name: "inventory_item_id" })
inventory_item: InventoryItem
@BeforeInsert() @BeforeInsert()
private beforeInsert(): void { private beforeInsert(): void {
this.id = generateEntityId(this.id, "ilev") this.id = generateEntityId(this.id, "ilev")
+55
View File
@@ -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
}
`
@@ -12,6 +12,9 @@ export const ProductShippingProfile: ModuleJoinerConfig = {
alias: [ alias: [
{ {
name: "product_shipping_profile", name: "product_shipping_profile",
args: {
entity: "LinkProductShippingProfile",
},
}, },
], ],
primaryKeys: ["id", "product_id", "profile_id"], primaryKeys: ["id", "product_id", "profile_id"],
@@ -17,10 +17,13 @@ export const ProductVariantInventoryItem: ModuleJoinerConfig = {
}, },
alias: [ alias: [
{ {
name: "product_variant_inventory_item", name: [
}, "product_variant_inventory_item",
{ "product_variant_inventory_items",
name: "product_variant_inventory_items", ],
args: {
entity: "LinkProductVariantInventoryItem",
},
}, },
], ],
primaryKeys: ["id", "variant_id", "inventory_item_id"], primaryKeys: ["id", "variant_id", "inventory_item_id"],
@@ -1,6 +1,6 @@
import { LINKS } from "../links"
import { ModuleJoinerConfig } from "@medusajs/types"
import { Modules } from "@medusajs/modules-sdk" import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
export const ProductVariantPriceSet: ModuleJoinerConfig = { export const ProductVariantPriceSet: ModuleJoinerConfig = {
serviceName: LINKS.ProductVariantPriceSet, serviceName: LINKS.ProductVariantPriceSet,
@@ -11,10 +11,10 @@ export const ProductVariantPriceSet: ModuleJoinerConfig = {
}, },
alias: [ alias: [
{ {
name: "product_variant_price_set", name: ["product_variant_price_set", "product_variant_price_sets"],
}, args: {
{ entity: "LinkProductVariantPriceSet",
name: "product_variant_price_sets", },
}, },
], ],
primaryKeys: ["id", "variant_id", "price_set_id"], primaryKeys: ["id", "variant_id", "price_set_id"],
+34 -3
View File
@@ -1,4 +1,8 @@
import { InternalModuleDeclaration, MedusaModule } from "@medusajs/modules-sdk" import {
InternalModuleDeclaration,
MedusaModule,
ModuleRegistrationName,
} from "@medusajs/modules-sdk"
import { import {
ExternalModuleDeclaration, ExternalModuleDeclaration,
ILinkModule, ILinkModule,
@@ -15,11 +19,16 @@ import {
ContainerRegistrationKeys, ContainerRegistrationKeys,
lowerCaseFirst, lowerCaseFirst,
simpleHash, simpleHash,
toPascalCase,
} from "@medusajs/utils" } from "@medusajs/utils"
import * as linkDefinitions from "../definitions" import * as linkDefinitions from "../definitions"
import { getMigration } from "../migration" import { getMigration } from "../migration"
import { InitializeModuleInjectableDependencies } from "../types" import { InitializeModuleInjectableDependencies } from "../types"
import { composeLinkName, generateGraphQLSchema } from "../utils" import {
composeLinkName,
composeTableName,
generateGraphQLSchema,
} from "../utils"
import { getLinkModuleDefinition } from "./module-definition" import { getLinkModuleDefinition } from "./module-definition"
export const initialize = async ( export const initialize = async (
@@ -98,7 +107,28 @@ export const initialize = async (
continue 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( const moduleDefinition = getLinkModuleDefinition(
definition, definition,
@@ -110,6 +140,7 @@ export const initialize = async (
key: serviceKey, key: serviceKey,
registrationName: serviceKey, registrationName: serviceKey,
label: serviceKey, label: serviceKey,
dependencies: [ModuleRegistrationName.EVENT_BUS],
defaultModuleDeclaration: { defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL, scope: MODULE_SCOPE.INTERNAL,
resources: injectedDependencies?.[ resources: injectedDependencies?.[
@@ -7,7 +7,9 @@ import {
ModuleJoinerConfig, ModuleJoinerConfig,
ModulesSdkTypes, ModulesSdkTypes,
} from "@medusajs/types" } from "@medusajs/types"
import { lowerCaseFirst, simpleHash, toPascalCase } from "@medusajs/utils"
import { asClass, asValue } from "awilix" import { asClass, asValue } from "awilix"
import { composeLinkName, composeTableName } from "../utils"
export function containerLoader(entity, joinerConfig: ModuleJoinerConfig) { export function containerLoader(entity, joinerConfig: ModuleJoinerConfig) {
return async ( return async (
@@ -22,6 +24,29 @@ export function containerLoader(entity, joinerConfig: ModuleJoinerConfig) {
): Promise<void> => { ): Promise<void> => {
const [primary, foreign] = joinerConfig.relationships! 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({ container.register({
joinerConfig: asValue(joinerConfig), joinerConfig: asValue(joinerConfig),
primaryKey: asValue(primary.foreignKey.split(",")), primaryKey: asValue(primary.foreignKey.split(",")),
@@ -35,6 +60,8 @@ export function containerLoader(entity, joinerConfig: ModuleJoinerConfig) {
baseRepository: asClass(BaseRepository).singleton(), baseRepository: asClass(BaseRepository).singleton(),
linkRepository: asClass(getLinkRepository(entity)).singleton(), linkRepository: asClass(getLinkRepository(entity)).singleton(),
entityName: asValue(entityName),
serviceName: asValue(serviceName),
}) })
} }
} }
@@ -2,6 +2,7 @@ import {
Context, Context,
DAL, DAL,
FindConfig, FindConfig,
IEventBusModuleService,
ILinkModule, ILinkModule,
InternalModuleDeclaration, InternalModuleDeclaration,
ModuleJoinerConfig, ModuleJoinerConfig,
@@ -9,6 +10,7 @@ import {
SoftDeleteReturn, SoftDeleteReturn,
} from "@medusajs/types" } from "@medusajs/types"
import { import {
CommonEvents,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
MapToConfig, MapToConfig,
@@ -24,14 +26,20 @@ import { shouldForceTransaction } from "../utils"
type InjectedDependencies = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
linkService: LinkService<any> linkService: LinkService<any>
eventBusModuleService?: IEventBusModuleService
primaryKey: string | string[] primaryKey: string | string[]
foreignKey: string foreignKey: string
extraFields: string[] extraFields: string[]
entityName: string
serviceName: string
} }
export default class LinkModuleService<TLink> implements ILinkModule { export default class LinkModuleService<TLink> implements ILinkModule {
protected baseRepository_: DAL.RepositoryService protected baseRepository_: DAL.RepositoryService
protected readonly linkService_: LinkService<TLink> protected readonly linkService_: LinkService<TLink>
protected readonly eventBusModuleService_?: IEventBusModuleService
protected readonly entityName_: string
protected readonly serviceName_: string
protected primaryKey_: string[] protected primaryKey_: string[]
protected foreignKey_: string protected foreignKey_: string
protected extraFields_: string[] protected extraFields_: string[]
@@ -40,17 +48,23 @@ export default class LinkModuleService<TLink> implements ILinkModule {
{ {
baseRepository, baseRepository,
linkService, linkService,
eventBusModuleService,
primaryKey, primaryKey,
foreignKey, foreignKey,
extraFields, extraFields,
entityName,
serviceName,
}: InjectedDependencies, }: InjectedDependencies,
readonly moduleDeclaration: InternalModuleDeclaration readonly moduleDeclaration: InternalModuleDeclaration
) { ) {
this.baseRepository_ = baseRepository this.baseRepository_ = baseRepository
this.linkService_ = linkService this.linkService_ = linkService
this.eventBusModuleService_ = eventBusModuleService
this.primaryKey_ = !Array.isArray(primaryKey) ? [primaryKey] : primaryKey this.primaryKey_ = !Array.isArray(primaryKey) ? [primaryKey] : primaryKey
this.foreignKey_ = foreignKey this.foreignKey_ = foreignKey
this.extraFields_ = extraFields this.extraFields_ = extraFields
this.entityName_ = entityName
this.serviceName_ = serviceName
} }
__joinerConfig(): ModuleJoinerConfig { __joinerConfig(): ModuleJoinerConfig {
@@ -188,6 +202,21 @@ export default class LinkModuleService<TLink> implements ILinkModule {
const links = await this.linkService_.create(data, sharedContext) const links = await this.linkService_.create(data, sharedContext)
await this.eventBusModuleService_?.emit<Record<string, unknown>>(
(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<object[]>(links) return await this.baseRepository_.serialize<object[]>(links)
} }
@@ -224,6 +253,22 @@ export default class LinkModuleService<TLink> implements ILinkModule {
this.validateFields(data) this.validateFields(data)
await this.linkService_.delete(data, sharedContext) await this.linkService_.delete(data, sharedContext)
const allData = Array.isArray(data) ? data : [data]
await this.eventBusModuleService_?.emit<Record<string, unknown>>(
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( async softDelete(
@@ -233,7 +278,10 @@ export default class LinkModuleService<TLink> implements ILinkModule {
): Promise<Record<string, unknown[]> | void> { ): Promise<Record<string, unknown[]> | void> {
this.validateFields(data) this.validateFields(data)
let [, cascadedEntitiesMap] = await this.softDelete_(data, sharedContext) let [deletedEntities, cascadedEntitiesMap] = await this.softDelete_(
data,
sharedContext
)
const pk = this.primaryKey_.join(",") const pk = this.primaryKey_.join(",")
const entityNameToLinkableKeysMap: MapToConfig = { const entityNameToLinkableKeysMap: MapToConfig = {
@@ -256,6 +304,21 @@ export default class LinkModuleService<TLink> implements ILinkModule {
) )
} }
await this.eventBusModuleService_?.emit<Record<string, unknown>>(
(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 return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
} }
@@ -263,7 +326,7 @@ export default class LinkModuleService<TLink> implements ILinkModule {
protected async softDelete_( protected async softDelete_(
data: any, data: any,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<[string[], Record<string, string[]>]> { ): Promise<[object[], Record<string, string[]>]> {
return await this.linkService_.softDelete(data, sharedContext) return await this.linkService_.softDelete(data, sharedContext)
} }
@@ -274,7 +337,10 @@ export default class LinkModuleService<TLink> implements ILinkModule {
): Promise<Record<string, unknown[]> | void> { ): Promise<Record<string, unknown[]> | void> {
this.validateFields(data) this.validateFields(data)
let [, cascadedEntitiesMap] = await this.restore_(data, sharedContext) let [restoredEntities, cascadedEntitiesMap] = await this.restore_(
data,
sharedContext
)
const pk = this.primaryKey_.join(",") const pk = this.primaryKey_.join(",")
const entityNameToLinkableKeysMap: MapToConfig = { const entityNameToLinkableKeysMap: MapToConfig = {
@@ -297,6 +363,21 @@ export default class LinkModuleService<TLink> implements ILinkModule {
) )
} }
await this.eventBusModuleService_?.emit<Record<string, unknown>>(
(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 return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
} }
@@ -304,7 +385,7 @@ export default class LinkModuleService<TLink> implements ILinkModule {
async restore_( async restore_(
data: any, data: any,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<[string[], Record<string, string[]>]> { ): Promise<[object[], Record<string, string[]>]> {
return await this.linkService_.restore(data, sharedContext) return await this.linkService_.restore(data, sharedContext)
} }
} }
+2 -2
View File
@@ -89,7 +89,7 @@ export default class LinkService<TEntity> {
async softDelete( async softDelete(
data: any, data: any,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<[string[], Record<string, string[]>]> { ): Promise<[object[], Record<string, string[]>]> {
const filter = {} const filter = {}
for (const key in data) { for (const key in data) {
filter[key] = { $in: Array.isArray(data[key]) ? data[key] : [data[key]] } filter[key] = { $in: Array.isArray(data[key]) ? data[key] : [data[key]] }
@@ -104,7 +104,7 @@ export default class LinkService<TEntity> {
async restore( async restore(
data: any, data: any,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<[string[], Record<string, string[]>]> { ): Promise<[object[], Record<string, string[]>]> {
const filter = {} const filter = {}
for (const key in data) { for (const key in data) {
filter[key] = { $in: Array.isArray(data[key]) ? data[key] : [data[key]] } filter[key] = { $in: Array.isArray(data[key]) ? data[key] : [data[key]] }
@@ -16,8 +16,15 @@ class EventBus extends EventBusUtils.AbstractEventBusModuleService {
options: Record<string, unknown> options: Record<string, unknown>
): Promise<void> ): Promise<void>
async emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<void> async emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<void>
async emit<T>(data: EventBusTypes.Message<T>[]): Promise<void>
async emit<T, TInput extends string | EventBusTypes.EmitData<T>[] = string>( async emit<
T,
TInput extends
| string
| EventBusTypes.EmitData<T>[]
| EventBusTypes.Message<T>[] = string
>(
eventOrData: TInput, eventOrData: TInput,
data?: T, data?: T,
options: Record<string, unknown> = {} options: Record<string, unknown> = {}
@@ -20,7 +20,9 @@ export default {
alias: [ alias: [
{ {
name: ["publishable_api_key", "publishable_api_keys"], name: ["publishable_api_key", "publishable_api_keys"],
args: { entity: "PublishableApiKey" }, args: {
entity: "PublishableApiKey",
},
}, },
], ],
} as ModuleJoinerConfig } as ModuleJoinerConfig
@@ -5,25 +5,25 @@ export default {
primaryKeys: ["id"], primaryKeys: ["id"],
linkableKeys: { profile_id: "ShippingProfile" }, linkableKeys: { profile_id: "ShippingProfile" },
schema: ` schema: `
scalar Date scalar Date
scalar JSON scalar JSON
type ShippingProfile { type ShippingProfile {
id: ID! id: ID!
name: String! name: String!
type: String! type: String!
created_at: Date! created_at: Date!
updated_at: Date! updated_at: Date!
deleted_at: Date deleted_at: Date
metadata: JSON metadata: JSON
} }
`, `,
alias: [ alias: [
{ {
name: "shipping_profile", name: ["shipping_profile", "shipping_profiles"],
}, args: {
{ entity: "ShippingProfile",
name: "shipping_profiles", },
}, },
], ],
} as ModuleJoinerConfig } as ModuleJoinerConfig
@@ -188,7 +188,7 @@ export class SubscriberLoader {
const events = Array.isArray(event) ? event : [event] const events = Array.isArray(event) ? event : [event]
const subscriber: Subscriber<T> = async (data: T, eventName: string) => { const subscriber = async (data: T, eventName: string) => {
return handler({ return handler({
eventName, eventName,
data, data,
@@ -200,7 +200,7 @@ export class SubscriberLoader {
const subscriberId = this.inferIdentifier(fileName, config, handler) const subscriberId = this.inferIdentifier(fileName, config, handler)
for (const e of events) { for (const e of events) {
eventBusService.subscribe(e, subscriber as Subscriber<unknown>, { eventBusService.subscribe(e, subscriber as Subscriber, {
...(config.context ?? {}), ...(config.context ?? {}),
subscriberId, subscriberId,
}) })
+5 -2
View File
@@ -128,7 +128,10 @@ export const loadMedusaApp = async (
return medusaApp return medusaApp
} }
container.register("remoteLink", asValue(medusaApp.link)) container.register(
ContainerRegistrationKeys.REMOTE_LINK,
asValue(medusaApp.link)
)
container.register( container.register(
ContainerRegistrationKeys.REMOTE_QUERY, ContainerRegistrationKeys.REMOTE_QUERY,
asValue(medusaApp.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 // Register all unresolved modules as undefined to be present in the container with undefined value by defaul
// but still resolvable // but still resolvable
for (const [, moduleDefinition] of Object.entries(ModulesDefinition)) { for (const moduleDefinition of Object.values(ModulesDefinition)) {
if (!container.hasRegistration(moduleDefinition.registrationName)) { if (!container.hasRegistration(moduleDefinition.registrationName)) {
container.register(moduleDefinition.registrationName, asValue(undefined)) container.register(moduleDefinition.registrationName, asValue(undefined))
} }
+14 -6
View File
@@ -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 { DatabaseErrorCode, EventBusUtils } from "@medusajs/utils"
import { EOL } from "os"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { TransactionBaseService } from "../interfaces" import { TransactionBaseService } from "../interfaces"
import { StagedJob } from "../models" import { StagedJob } from "../models"
import { FindConfig } from "../types/common"
import { ConfigModule } from "../types/global" import { ConfigModule } from "../types/global"
import { isString } from "../utils" import { isString } from "../utils"
import { sleep } from "../utils/sleep" import { sleep } from "../utils/sleep"
import StagedJobService from "./staged-job" import StagedJobService from "./staged-job"
import { FindConfig } from "../types/common"
import { EOL } from "os"
type InjectedDependencies = { type InjectedDependencies = {
stagedJobService: StagedJobService stagedJobService: StagedJobService
@@ -118,6 +118,8 @@ export default class EventBusService
*/ */
async emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<StagedJob[] | void> async emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<StagedJob[] | void>
async emit<T>(data: EventBusTypes.Message<T>[]): Promise<StagedJob[] | void>
/** /**
* Calls all subscribers when an event occurs. * Calls all subscribers when an event occurs.
* @param {string} eventName - the name of the event to be process. * @param {string} eventName - the name of the event to be process.
@@ -133,7 +135,10 @@ export default class EventBusService
async emit< async emit<
T, T,
TInput extends string | EventBusTypes.EmitData<T>[] = string, TInput extends
| string
| EventBusTypes.EmitData<T>[]
| EventBusTypes.Message<T>[] = string,
TResult = TInput extends EventBusTypes.EmitData<T>[] TResult = TInput extends EventBusTypes.EmitData<T>[]
? StagedJob[] ? StagedJob[]
: StagedJob : StagedJob
@@ -144,16 +149,19 @@ export default class EventBusService
): Promise<TResult | void> { ): Promise<TResult | void> {
const manager = this.activeManager_ const manager = this.activeManager_
const isBulkEmit = !isString(eventNameOrData) const isBulkEmit = !isString(eventNameOrData)
const dataBody = isString(eventNameOrData)
? data ?? (data as Message<T>).body
: undefined
const events: EventBusTypes.EmitData[] = isBulkEmit const events: EventBusTypes.EmitData[] = isBulkEmit
? eventNameOrData.map((event) => ({ ? eventNameOrData.map((event) => ({
eventName: event.eventName, eventName: event.eventName,
data: event.data, data: (event as EmitData).data ?? (event as Message<T>).body.data,
options: event.options, options: event.options,
})) }))
: [ : [
{ {
eventName: eventNameOrData, eventName: eventNameOrData,
data: data, data: dataBody,
options: options, options: options,
}, },
] ]
@@ -1,32 +1,40 @@
import { EntityManager, In } from "typeorm"
import { import {
IEventBusService, IEventBusService,
IInventoryService, IInventoryService,
IStockLocationService,
InventoryItemDTO, InventoryItemDTO,
InventoryLevelDTO, InventoryLevelDTO,
IStockLocationService, RemoteQueryFunction,
ReservationItemDTO, ReservationItemDTO,
ReserveQuantityContext, ReserveQuantityContext,
} from "@medusajs/types" } 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 { LineItem, Product, ProductVariant } from "../models"
import { isDefined, MedusaError, promiseAll } from "@medusajs/utils"
import { PricedProduct, PricedVariant } from "../types/pricing" import { PricedProduct, PricedVariant } from "../types/pricing"
import { TransactionBaseService } from "../interfaces"
import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item" import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item"
import { getSetDifference } from "../utils/diff-set"
import ProductVariantService from "./product-variant" import ProductVariantService from "./product-variant"
import SalesChannelInventoryService from "./sales-channel-inventory" import SalesChannelInventoryService from "./sales-channel-inventory"
import SalesChannelLocationService from "./sales-channel-location" import SalesChannelLocationService from "./sales-channel-location"
import { TransactionBaseService } from "../interfaces"
import { getSetDifference } from "../utils/diff-set"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager
salesChannelLocationService: SalesChannelLocationService salesChannelLocationService: SalesChannelLocationService
salesChannelInventoryService: SalesChannelInventoryService salesChannelInventoryService: SalesChannelInventoryService
productVariantService: ProductVariantService productVariantService: ProductVariantService
stockLocationService: IStockLocationService
inventoryService: IInventoryService
eventBusService: IEventBusService eventBusService: IEventBusService
featureFlagRouter: FlagRouter
remoteQuery: RemoteQueryFunction
} }
type AvailabilityContext = { type AvailabilityContext = {
@@ -42,6 +50,8 @@ class ProductVariantInventoryService extends TransactionBaseService {
protected readonly salesChannelInventoryService_: SalesChannelInventoryService protected readonly salesChannelInventoryService_: SalesChannelInventoryService
protected readonly productVariantService_: ProductVariantService protected readonly productVariantService_: ProductVariantService
protected readonly eventBusService_: IEventBusService protected readonly eventBusService_: IEventBusService
protected readonly featureFlagRouter_: FlagRouter
protected readonly remoteQuery_: RemoteQueryFunction
protected get inventoryService_(): IInventoryService { protected get inventoryService_(): IInventoryService {
return this.__container__.inventoryService return this.__container__.inventoryService
@@ -56,6 +66,8 @@ class ProductVariantInventoryService extends TransactionBaseService {
salesChannelInventoryService, salesChannelInventoryService,
productVariantService, productVariantService,
eventBusService, eventBusService,
featureFlagRouter,
remoteQuery,
}: InjectedDependencies) { }: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params // eslint-disable-next-line prefer-rest-params
super(arguments[0]) super(arguments[0])
@@ -64,6 +76,8 @@ class ProductVariantInventoryService extends TransactionBaseService {
this.salesChannelInventoryService_ = salesChannelInventoryService this.salesChannelInventoryService_ = salesChannelInventoryService
this.productVariantService_ = productVariantService this.productVariantService_ = productVariantService
this.eventBusService_ = eventBusService this.eventBusService_ = eventBusService
this.featureFlagRouter_ = featureFlagRouter
this.remoteQuery_ = remoteQuery
} }
/** /**
@@ -307,16 +321,29 @@ class ProductVariantInventoryService extends TransactionBaseService {
} }
// Verify that variant exists // Verify that variant exists
const variants = await this.productVariantService_ let variants
.withTransaction(this.activeManager_) if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) {
.list( variants = await this.remoteQuery_(
{ remoteQueryObjectFromString({
id: data.map((d) => d.variantId), entryPoint: "variants",
}, variables: {
{ id: data.map((d) => d.variantId),
select: ["id"], },
} 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 foundVariantIds = new Set(variants.map((v) => v.id))
const requestedVariantIds = new Set(data.map((v) => v.variantId)) const requestedVariantIds = new Set(data.map((v) => v.variantId))
@@ -404,7 +431,11 @@ class ProductVariantInventoryService extends TransactionBaseService {
): tc is ProductVariantInventoryItem => !!tc ): tc is ProductVariantInventoryItem => !!tc
) )
return await variantInventoryRepo.save(toCreate) const createdVariantInventoryItems = await variantInventoryRepo.save(
toCreate
)
return createdVariantInventoryItems
} }
/** /**
@@ -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",
])
)
})
})
+6 -5
View File
@@ -13,6 +13,7 @@ import {
ModuleJoinerConfig, ModuleJoinerConfig,
ModuleServiceInitializeOptions, ModuleServiceInitializeOptions,
RemoteJoinerQuery, RemoteJoinerQuery,
RemoteQueryFunction,
} from "@medusajs/types" } from "@medusajs/types"
import { import {
ContainerRegistrationKeys, ContainerRegistrationKeys,
@@ -41,6 +42,7 @@ export type RunMigrationFn = (
export type MedusaModuleConfig = { export type MedusaModuleConfig = {
[key: string | Modules]: [key: string | Modules]:
| string
| boolean | boolean
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration> | Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
} }
@@ -182,10 +184,7 @@ function registerCustomJoinerConfigs(servicesConfig: ModuleJoinerConfig[]) {
export type MedusaAppOutput = { export type MedusaAppOutput = {
modules: Record<string, LoadedModule | LoadedModule[]> modules: Record<string, LoadedModule | LoadedModule[]>
link: RemoteLink | undefined link: RemoteLink | undefined
query: ( query: RemoteQueryFunction
query: string | RemoteJoinerQuery | object,
variables?: Record<string, unknown>
) => Promise<any>
entitiesMap?: Record<string, any> entitiesMap?: Record<string, any>
notFound?: Record<string, Record<string, string>> notFound?: Record<string, Record<string, string>>
runMigrations: RunMigrationFn runMigrations: RunMigrationFn
@@ -201,6 +200,7 @@ export async function MedusaApp({
linkModules, linkModules,
remoteFetchData, remoteFetchData,
injectedDependencies, injectedDependencies,
onApplicationStartCb,
}: { }: {
sharedContainer?: MedusaContainer sharedContainer?: MedusaContainer
sharedResourcesConfig?: SharedResources sharedResourcesConfig?: SharedResources
@@ -212,6 +212,7 @@ export async function MedusaApp({
linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[] linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[]
remoteFetchData?: RemoteFetchDataCallback remoteFetchData?: RemoteFetchDataCallback
injectedDependencies?: any injectedDependencies?: any
onApplicationStartCb?: () => void
} = {}): Promise<{ } = {}): Promise<{
modules: Record<string, LoadedModule | LoadedModule[]> modules: Record<string, LoadedModule | LoadedModule[]>
link: RemoteLink | undefined link: RemoteLink | undefined
@@ -355,6 +356,6 @@ export async function MedusaApp({
runMigrations, runMigrations,
} }
} finally { } finally {
await MedusaModule.onApplicationStart() MedusaModule.onApplicationStart(onApplicationStartCb)
} }
} }
+15 -4
View File
@@ -69,7 +69,8 @@ export type LinkModuleBootstrapOptions = {
} }
export class MedusaModule { export class MedusaModule {
private static instances_: Map<string, any> = new Map() private static instances_: Map<string, { [key: string]: IModuleService }> =
new Map()
private static modules_: Map<string, ModuleAlias[]> = new Map() private static modules_: Map<string, ModuleAlias[]> = new Map()
private static loading_: Map<string, Promise<any>> = new Map() private static loading_: Map<string, Promise<any>> = new Map()
private static joinerConfig_: Map<string, ModuleJoinerConfig> = new Map() private static joinerConfig_: Map<string, ModuleJoinerConfig> = 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 instances of MedusaModule.instances_.values()) {
for (const instance of Object.values(instances) as IModuleService[]) { for (const instance of Object.values(instances) as IModuleService[]) {
if (instance?.__hooks) { if (instance?.__hooks) {
instance.__hooks?.onApplicationStart instance.__hooks?.onApplicationStart
?.bind(instance)() ?.bind(instance)()
.then(() => {
onApplicationStartCb?.()
})
.catch(() => { .catch(() => {
// The module should handle this and log it // The module should handle this and log it
return void 0 return void 0
@@ -217,7 +221,9 @@ export class MedusaModule {
) )
if (MedusaModule.instances_.has(hashKey)) { if (MedusaModule.instances_.has(hashKey)) {
return MedusaModule.instances_.get(hashKey) return MedusaModule.instances_.get(hashKey)! as {
[key: string]: T
}
} }
if (MedusaModule.loading_.has(hashKey)) { 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) { if (injectedDependencies) {
for (const service in injectedDependencies) { for (const service in injectedDependencies) {
@@ -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
}
-1
View File
@@ -1,3 +1,2 @@
export * from "./clean-graphql-schema" export * from "./clean-graphql-schema"
export * from "./get-fields-and-relations"
export * from "./graphql-schema-to-fields" export * from "./graphql-schema-to-fields"
+11 -25
View File
@@ -8,6 +8,7 @@ import {
PriceSet, PriceSet,
PriceSetMoneyAmount, PriceSetMoneyAmount,
} from "@models" } from "@models"
import schema from "./schema"
export const LinkableKeys = { export const LinkableKeys = {
money_amount_id: MoneyAmount.name, money_amount_id: MoneyAmount.name,
@@ -32,45 +33,30 @@ export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.PRICING, serviceName: Modules.PRICING,
primaryKeys: ["id"], primaryKeys: ["id"],
linkableKeys: LinkableKeys, linkableKeys: LinkableKeys,
schema,
alias: [ alias: [
{ {
name: "price_set", name: ["price_set", "price_sets"],
args: {
entity: "PriceSet",
},
}, },
{ {
name: "price_sets", name: ["money_amount", "money_amounts"],
},
{
name: "money_amount",
args: { args: {
methodSuffix: "MoneyAmounts", methodSuffix: "MoneyAmounts",
entity: "MoneyAmount",
}, },
}, },
{ {
name: "money_amounts", name: ["currency", "currencies"],
args: {
methodSuffix: "MoneyAmounts",
},
},
{
name: "currency",
args: { args: {
methodSuffix: "Currencies", methodSuffix: "Currencies",
entity: "Currency",
}, },
}, },
{ {
name: "currencies", name: ["price_list", "price_lists"],
args: {
methodSuffix: "Currencies",
},
},
{
name: "price_list",
args: {
methodSuffix: "PriceLists",
},
},
{
name: "price_lists",
args: { args: {
methodSuffix: "PriceLists", methodSuffix: "PriceLists",
}, },
+17
View File
@@ -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
@@ -1,19 +1,27 @@
import { import {
EmitData,
EventBusTypes, EventBusTypes,
IEventBusModuleService, IEventBusModuleService,
Message,
Subscriber, Subscriber,
} from "@medusajs/types" } from "@medusajs/types"
export class EventBusService implements IEventBusModuleService { export class EventBusService implements IEventBusModuleService {
async emit<T>( emit<T>(
eventName: string, eventName: string,
data: T, data: T,
options: Record<string, unknown> options?: Record<string, unknown>
): Promise<void> ): Promise<void>
emit<T>(data: EmitData<T>[]): Promise<void>
emit<T>(data: Message<T>[]): Promise<void>
async emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<void> async emit<
T,
async emit<T, TInput extends string | EventBusTypes.EmitData<T>[] = string>( TInput extends
| string
| EventBusTypes.EmitData<T>[]
| EventBusTypes.Message<T>[] = string
>(
eventOrData: TInput, eventOrData: TInput,
data?: T, data?: T,
options: Record<string, unknown> = {} options: Record<string, unknown> = {}
@@ -2,6 +2,7 @@ import { MedusaModule, Modules } from "@medusajs/modules-sdk"
import { IProductModuleService } from "@medusajs/types" import { IProductModuleService } from "@medusajs/types"
import { kebabCase } from "@medusajs/utils" import { kebabCase } from "@medusajs/utils"
import { knex } from "knex" import { knex } from "knex"
import { initModules } from "medusa-test-utils"
import * as CustomRepositories from "../__fixtures__/module" import * as CustomRepositories from "../__fixtures__/module"
import { import {
buildProductAndRelationsData, buildProductAndRelationsData,
@@ -9,7 +10,6 @@ import {
} from "../__fixtures__/product" } from "../__fixtures__/product"
import { productsData } from "../__fixtures__/product/data" import { productsData } from "../__fixtures__/product/data"
import { DB_URL, TestDatabase } from "../utils" import { DB_URL, TestDatabase } from "../utils"
import { initModules } from "medusa-test-utils"
import { getInitModuleConfig } from "../utils/get-init-module-config" import { getInitModuleConfig } from "../utils/get-init-module-config"
const sharedPgConnection = knex<any, any>({ const sharedPgConnection = knex<any, any>({
@@ -238,7 +238,23 @@ describe("Product module", function () {
thumbnail: images[0], 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).toHaveLength(1)
@@ -1,12 +1,12 @@
import { MedusaModule, Modules } from "@medusajs/modules-sdk"
import { IProductModuleService, ProductTypes } from "@medusajs/types" import { IProductModuleService, ProductTypes } from "@medusajs/types"
import { Product, ProductCategory } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql" 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 { createProductCategories } from "../../../__fixtures__/product-category"
import { productCategoriesRankData } from "../../../__fixtures__/product-category/data" import { productCategoriesRankData } from "../../../__fixtures__/product-category/data"
import { EventBusService } from "../../../__fixtures__/event-bus" import { TestDatabase, getInitModuleConfig } from "../../../utils"
import { MedusaModule, Modules } from "@medusajs/modules-sdk"
import { initModules } from "medusa-test-utils/dist"
describe("ProductModuleService product categories", () => { describe("ProductModuleService product categories", () => {
let service: IProductModuleService let service: IProductModuleService
@@ -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 { 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", () => { describe("ProductModuleService product tags", () => {
let service: IProductModuleService let service: IProductModuleService
@@ -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 { 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", () => { describe("ProductModuleService product types", () => {
let service: IProductModuleService let service: IProductModuleService
@@ -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 { 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", () => { describe("ProductModuleService product variants", () => {
let service: IProductModuleService let service: IProductModuleService
@@ -13,13 +13,13 @@ import {
ProductVariant, ProductVariant,
} from "@models" } from "@models"
import { initModules } from "medusa-test-utils"
import { initialize } from "../../../../src" import { initialize } from "../../../../src"
import { EventBusService } from "../../../__fixtures__/event-bus" import { EventBusService } from "../../../__fixtures__/event-bus"
import { createCollections, createTypes } from "../../../__fixtures__/product" import { createCollections, createTypes } from "../../../__fixtures__/product"
import { createProductCategories } from "../../../__fixtures__/product-category" import { createProductCategories } from "../../../__fixtures__/product-category"
import { buildProductAndRelationsData } from "../../../__fixtures__/product/data/create-product" import { buildProductAndRelationsData } from "../../../__fixtures__/product/data/create-product"
import { DB_URL, getInitModuleConfig, TestDatabase } from "../../../utils" import { DB_URL, TestDatabase, getInitModuleConfig } from "../../../utils"
import { initModules } from "medusa-test-utils"
const beforeEach_ = async () => { const beforeEach_ = async () => {
await TestDatabase.setupDatabase() await TestDatabase.setupDatabase()
@@ -584,7 +584,23 @@ describe("ProductModuleService products", function () {
thumbnail: images[0], 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).toHaveLength(1)
expect(products[0].images).toHaveLength(1) expect(products[0].images).toHaveLength(1)
+20 -55
View File
@@ -43,92 +43,57 @@ export const joinerConfig: ModuleJoinerConfig = {
schema: moduleSchema, schema: moduleSchema,
alias: [ alias: [
{ {
name: "product", name: ["product", "products"],
},
{
name: "products",
},
{
name: "variant",
args: { args: {
entity: "Product",
},
},
{
name: ["variant", "variants"],
args: {
entity: "ProductVariant",
methodSuffix: "Variants", methodSuffix: "Variants",
}, },
}, },
{ {
name: "variants", name: ["product_option", "product_options"],
args: {
methodSuffix: "Variants",
},
},
{
name: "product_option",
args: { args: {
entity: "ProductOption",
methodSuffix: "Options", methodSuffix: "Options",
}, },
}, },
{ {
name: "product_options", name: ["product_type", "product_types"],
args: {
methodSuffix: "Options",
},
},
{
name: "product_type",
args: { args: {
entity: "ProductType",
methodSuffix: "Types", methodSuffix: "Types",
}, },
}, },
{ {
name: "product_types", name: ["product_image", "product_images"],
args: {
methodSuffix: "Types",
},
},
{
name: "product_image",
args: { args: {
entity: "ProductImage",
methodSuffix: "Images", methodSuffix: "Images",
}, },
}, },
{ {
name: "product_images", name: ["product_tag", "product_tags"],
args: {
methodSuffix: "Images",
},
},
{
name: "product_tag",
args: { args: {
entity: "ProductTag",
methodSuffix: "Tags", methodSuffix: "Tags",
}, },
}, },
{ {
name: "product_tags", name: ["product_collection", "product_collections"],
args: {
methodSuffix: "Tags",
},
},
{
name: "product_collection",
args: { args: {
entity: "ProductCollection",
methodSuffix: "Collections", methodSuffix: "Collections",
}, },
}, },
{ {
name: "product_collections", name: ["product_category", "product_categories"],
args: {
methodSuffix: "Collections",
},
},
{
name: "product_category",
args: {
methodSuffix: "Categories",
},
},
{
name: "product_categories",
args: { args: {
entity: "ProductCategory",
methodSuffix: "Categories", methodSuffix: "Categories",
}, },
}, },
@@ -7,6 +7,7 @@ import {
Index, Index,
ManyToMany, ManyToMany,
ManyToOne, ManyToOne,
OnInit,
OneToMany, OneToMany,
OptionalProps, OptionalProps,
PrimaryKey, PrimaryKey,
@@ -14,8 +15,8 @@ import {
Unique, Unique,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import Product from "./product"
import { DAL } from "@medusajs/types" import { DAL } from "@medusajs/types"
import Product from "./product"
type OptionalFields = DAL.SoftDeletableEntityDateColumns type OptionalFields = DAL.SoftDeletableEntityDateColumns
@@ -85,6 +86,11 @@ class ProductCategory {
@ManyToMany(() => Product, (product) => product.categories) @ManyToMany(() => Product, (product) => product.categories)
products = new Collection<Product>(this) products = new Collection<Product>(this)
@OnInit()
async onInit() {
this.id = generateEntityId(this.id, "pcat")
}
@BeforeCreate() @BeforeCreate()
async onCreate(args: EventArgs<ProductCategory>) { async onCreate(args: EventArgs<ProductCategory>) {
this.id = generateEntityId(this.id, "pcat") this.id = generateEntityId(this.id, "pcat")
@@ -4,6 +4,7 @@ import {
Entity, Entity,
Filter, Filter,
Index, Index,
OnInit,
OneToMany, OneToMany,
OptionalProps, OptionalProps,
PrimaryKey, PrimaryKey,
@@ -11,9 +12,9 @@ import {
Unique, Unique,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { DAL } from "@medusajs/types"
import { DALUtils, generateEntityId, kebabCase } from "@medusajs/utils" import { DALUtils, generateEntityId, kebabCase } from "@medusajs/utils"
import Product from "./product" import Product from "./product"
import { DAL } from "@medusajs/types"
type OptionalRelations = "products" type OptionalRelations = "products"
type OptionalFields = DAL.SoftDeletableEntityDateColumns type OptionalFields = DAL.SoftDeletableEntityDateColumns
@@ -61,6 +62,11 @@ class ProductCollection {
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date deleted_at?: Date
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "pcol")
}
@BeforeCreate() @BeforeCreate()
onCreate() { onCreate() {
this.id = generateEntityId(this.id, "pcol") this.id = generateEntityId(this.id, "pcol")
+7 -1
View File
@@ -5,14 +5,15 @@ import {
Filter, Filter,
Index, Index,
ManyToMany, ManyToMany,
OnInit,
OptionalProps, OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { DAL } from "@medusajs/types"
import { DALUtils, generateEntityId } from "@medusajs/utils" import { DALUtils, generateEntityId } from "@medusajs/utils"
import Product from "./product" import Product from "./product"
import { DAL } from "@medusajs/types"
type OptionalRelations = "products" type OptionalRelations = "products"
type OptionalFields = DAL.SoftDeletableEntityDateColumns type OptionalFields = DAL.SoftDeletableEntityDateColumns
@@ -54,6 +55,11 @@ class ProductImage {
@ManyToMany(() => Product, (product) => product.images) @ManyToMany(() => Product, (product) => product.images)
products = new Collection<Product>(this) products = new Collection<Product>(this)
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "img")
}
@BeforeCreate() @BeforeCreate()
onCreate() { onCreate() {
this.id = generateEntityId(this.id, "img") this.id = generateEntityId(this.id, "img")
@@ -1,16 +1,17 @@
import { DAL } from "@medusajs/types"
import { DALUtils, generateEntityId } from "@medusajs/utils"
import { import {
BeforeCreate, BeforeCreate,
Entity, Entity,
Filter, Filter,
Index, Index,
ManyToOne, ManyToOne,
OnInit,
OptionalProps, OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { ProductOption, ProductVariant } from "./index" import { ProductOption, ProductVariant } from "./index"
import { DALUtils, generateEntityId } from "@medusajs/utils"
import { DAL } from "@medusajs/types"
type OptionalFields = type OptionalFields =
| "allow_backorder" | "allow_backorder"
@@ -72,6 +73,11 @@ class ProductOptionValue {
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date deleted_at?: Date
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "optval")
}
@BeforeCreate() @BeforeCreate()
beforeCreate() { beforeCreate() {
this.id = generateEntityId(this.id, "optval") this.id = generateEntityId(this.id, "optval")
@@ -8,6 +8,7 @@ import {
Filter, Filter,
Index, Index,
ManyToOne, ManyToOne,
OnInit,
OneToMany, OneToMany,
OptionalProps, OptionalProps,
PrimaryKey, PrimaryKey,
@@ -70,6 +71,11 @@ class ProductOption {
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date deleted_at?: Date
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "opt")
}
@BeforeCreate() @BeforeCreate()
beforeCreate() { beforeCreate() {
this.id = generateEntityId(this.id, "opt") this.id = generateEntityId(this.id, "opt")
+7 -1
View File
@@ -5,14 +5,15 @@ import {
Filter, Filter,
Index, Index,
ManyToMany, ManyToMany,
OnInit,
OptionalProps, OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { DAL } from "@medusajs/types"
import { DALUtils, generateEntityId } from "@medusajs/utils" import { DALUtils, generateEntityId } from "@medusajs/utils"
import Product from "./product" import Product from "./product"
import { DAL } from "@medusajs/types"
type OptionalRelations = "products" type OptionalRelations = "products"
type OptionalFields = DAL.SoftDeletableEntityDateColumns type OptionalFields = DAL.SoftDeletableEntityDateColumns
@@ -53,6 +54,11 @@ class ProductTag {
@ManyToMany(() => Product, (product) => product.tags) @ManyToMany(() => Product, (product) => product.tags)
products = new Collection<Product>(this) products = new Collection<Product>(this)
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "ptag")
}
@BeforeCreate() @BeforeCreate()
onCreate() { onCreate() {
this.id = generateEntityId(this.id, "ptag") this.id = generateEntityId(this.id, "ptag")
+7 -1
View File
@@ -3,13 +3,14 @@ import {
Entity, Entity,
Filter, Filter,
Index, Index,
OnInit,
OptionalProps, OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { DALUtils, generateEntityId } from "@medusajs/utils"
import { DAL } from "@medusajs/types" import { DAL } from "@medusajs/types"
import { DALUtils, generateEntityId } from "@medusajs/utils"
type OptionalFields = DAL.SoftDeletableEntityDateColumns type OptionalFields = DAL.SoftDeletableEntityDateColumns
@@ -46,6 +47,11 @@ class ProductType {
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date deleted_at?: Date
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "ptyp")
}
@BeforeCreate() @BeforeCreate()
onCreate() { onCreate() {
this.id = generateEntityId(this.id, "ptyp") this.id = generateEntityId(this.id, "ptyp")
@@ -1,3 +1,4 @@
import { DAL } from "@medusajs/types"
import { import {
DALUtils, DALUtils,
generateEntityId, generateEntityId,
@@ -11,6 +12,7 @@ import {
Filter, Filter,
Index, Index,
ManyToOne, ManyToOne,
OnInit,
OneToMany, OneToMany,
OptionalProps, OptionalProps,
PrimaryKey, PrimaryKey,
@@ -19,7 +21,6 @@ import {
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { Product } from "@models" import { Product } from "@models"
import ProductOptionValue from "./product-option-value" import ProductOptionValue from "./product-option-value"
import { DAL } from "@medusajs/types"
type OptionalFields = type OptionalFields =
| "allow_backorder" | "allow_backorder"
@@ -152,6 +153,11 @@ class ProductVariant {
}) })
options = new Collection<ProductOptionValue>(this) options = new Collection<ProductOptionValue>(this)
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "variant")
}
@BeforeCreate() @BeforeCreate()
onCreate() { onCreate() {
this.id = generateEntityId(this.id, "variant") this.id = generateEntityId(this.id, "variant")
+8 -2
View File
@@ -8,12 +8,14 @@ import {
ManyToMany, ManyToMany,
ManyToOne, ManyToOne,
OneToMany, OneToMany,
OnInit,
OptionalProps, OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
Unique, Unique,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { DAL } from "@medusajs/types"
import { import {
DALUtils, DALUtils,
generateEntityId, generateEntityId,
@@ -22,12 +24,11 @@ import {
} from "@medusajs/utils" } from "@medusajs/utils"
import ProductCategory from "./product-category" import ProductCategory from "./product-category"
import ProductCollection from "./product-collection" import ProductCollection from "./product-collection"
import ProductImage from "./product-image"
import ProductOption from "./product-option" import ProductOption from "./product-option"
import ProductTag from "./product-tag" import ProductTag from "./product-tag"
import ProductType from "./product-type" import ProductType from "./product-type"
import ProductVariant from "./product-variant" import ProductVariant from "./product-variant"
import ProductImage from "./product-image"
import { DAL } from "@medusajs/types"
type OptionalRelations = "collection" | "type" type OptionalRelations = "collection" | "type"
type OptionalFields = type OptionalFields =
@@ -176,6 +177,11 @@ class Product {
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "prod")
}
@BeforeCreate() @BeforeCreate()
beforeCreate() { beforeCreate() {
this.id = generateEntityId(this.id, "prod") this.id = generateEntityId(this.id, "prod")
@@ -42,12 +42,6 @@ import {
ModulesSdkUtils, ModulesSdkUtils,
promiseAll, promiseAll,
} from "@medusajs/utils" } 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 { import {
ProductCategoryServiceTypes, ProductCategoryServiceTypes,
ProductCollectionServiceTypes, ProductCollectionServiceTypes,
@@ -55,6 +49,12 @@ import {
ProductServiceTypes, ProductServiceTypes,
ProductVariantServiceTypes, ProductVariantServiceTypes,
} from "@types" } 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 = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
@@ -371,52 +371,56 @@ export default class ProductModuleService<
async createTags( async createTags(
data: ProductTypes.CreateProductTagDTO[], data: ProductTypes.CreateProductTagDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ): Promise<ProductTypes.ProductTagDTO[]> {
const productTags = await this.productTagService_.create( const productTags = await this.productTagService_.create(
data, data,
sharedContext sharedContext
) )
return JSON.parse(JSON.stringify(productTags)) return await this.baseRepository_.serialize(productTags, { populate: true })
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
async updateTags( async updateTags(
data: ProductTypes.UpdateProductTagDTO[], data: ProductTypes.UpdateProductTagDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ): Promise<ProductTypes.ProductTagDTO[]> {
const productTags = await this.productTagService_.update( const productTags = await this.productTagService_.update(
data, data,
sharedContext sharedContext
) )
return JSON.parse(JSON.stringify(productTags)) return await this.baseRepository_.serialize(productTags, { populate: true })
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
async createTypes( async createTypes(
data: ProductTypes.CreateProductTypeDTO[], data: ProductTypes.CreateProductTypeDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ): Promise<ProductTypes.ProductTypeDTO[]> {
const productTypes = await this.productTypeService_.create( const productTypes = await this.productTypeService_.create(
data, data,
sharedContext sharedContext
) )
return JSON.parse(JSON.stringify(productTypes)) return await this.baseRepository_.serialize(productTypes, {
populate: true,
})
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
async updateTypes( async updateTypes(
data: ProductTypes.UpdateProductTypeDTO[], data: ProductTypes.UpdateProductTypeDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ): Promise<ProductTypes.ProductTypeDTO[]> {
const productTypes = await this.productTypeService_.update( const productTypes = await this.productTypeService_.update(
data, data,
sharedContext sharedContext
) )
return JSON.parse(JSON.stringify(productTypes)) return await this.baseRepository_.serialize(productTypes, {
populate: true,
})
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
@@ -476,7 +480,7 @@ export default class ProductModuleService<
async updateCollections( async updateCollections(
data: ProductTypes.UpdateProductCollectionDTO[], data: ProductTypes.UpdateProductCollectionDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ): Promise<ProductTypes.ProductCollectionDTO[]> {
const productCollections = await this.productCollectionService_.update( const productCollections = await this.productCollectionService_.update(
data, data,
sharedContext 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_") @InjectTransactionManager("baseRepository_")
async createCategory( async createCategory(
data: ProductCategoryServiceTypes.CreateProductCategoryDTO, data: ProductCategoryServiceTypes.CreateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ): Promise<ProductTypes.ProductCategoryDTO> {
const productCategory = await this.productCategoryService_.create( const productCategory = await this.productCategoryService_.create(
data, data,
sharedContext sharedContext
@@ -510,7 +516,9 @@ export default class ProductModuleService<
{ id: productCategory.id } { id: productCategory.id }
) )
return JSON.parse(JSON.stringify(productCategory)) return await this.baseRepository_.serialize(productCategory, {
populate: true,
})
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
@@ -518,7 +526,7 @@ export default class ProductModuleService<
categoryId: string, categoryId: string,
data: ProductCategoryServiceTypes.UpdateProductCategoryDTO, data: ProductCategoryServiceTypes.UpdateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ): Promise<ProductTypes.ProductCategoryDTO> {
const productCategory = await this.productCategoryService_.update( const productCategory = await this.productCategoryService_.update(
categoryId, categoryId,
data, data,
@@ -530,7 +538,9 @@ export default class ProductModuleService<
{ id: productCategory.id } { id: productCategory.id }
) )
return JSON.parse(JSON.stringify(productCategory)) return await this.baseRepository_.serialize(productCategory, {
populate: true,
})
} }
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@@ -1,10 +1,10 @@
import { Modules } from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types" import { IPromotionModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { initModules } from "medusa-test-utils"
import { createCampaigns } from "../../../__fixtures__/campaigns" import { createCampaigns } from "../../../__fixtures__/campaigns"
import { MikroOrmWrapper } from "../../../utils" import { MikroOrmWrapper } from "../../../utils"
import { getInitModuleConfig } from "../../../utils/get-init-module-config" import { getInitModuleConfig } from "../../../utils/get-init-module-config"
import { initModules } from "medusa-test-utils/dist"
import { Modules } from "@medusajs/modules-sdk"
jest.setTimeout(30000) jest.setTimeout(30000)
+46 -20
View File
@@ -1,25 +1,51 @@
export type Subscriber<T = unknown> = ( export type Subscriber<T = unknown> = (
data: T, data: T,
eventName: string eventName: string
) => Promise<void> ) => Promise<void>
export type SubscriberContext = { export type SubscriberContext = {
subscriberId: string subscriberId: string
}
export type SubscriberDescriptor = {
id: string
subscriber: Subscriber
}
export type EventHandler<T = unknown> = (
data: T,
eventName: string
) => Promise<void>
export type EmitData<T = unknown> = {
eventName: string
data: T
options?: Record<string, unknown>
}
export type MessageBody<T = unknown> = {
metadata: {
service: string
action: string
object: string
eventGroupId?: string
} }
data: T
}
export type SubscriberDescriptor = { export type Message<T = unknown> = {
id: string eventName: string
subscriber: Subscriber body: MessageBody<T>
options?: Record<string, unknown>
}
export type MessageFormat<T = unknown> = {
eventName: string
metadata: {
service: string
action: string
object: string
eventGroupId?: string
} }
data: T | T[]
export type EventHandler<T = unknown> = ( }
data: T,
eventName: string
) => Promise<void>
export type EmitData<T = unknown> = {
eventName: string
data: T
options?: Record<string, unknown>
}
@@ -1,4 +1,4 @@
import { EmitData, Subscriber, SubscriberContext } from "./common" import { EmitData, Message, Subscriber, SubscriberContext } from "./common"
export interface IEventBusModuleService { export interface IEventBusModuleService {
emit<T>( emit<T>(
@@ -7,6 +7,7 @@ export interface IEventBusModuleService {
options?: Record<string, unknown> options?: Record<string, unknown>
): Promise<void> ): Promise<void>
emit<T>(data: EmitData<T>[]): Promise<void> emit<T>(data: EmitData<T>[]): Promise<void>
emit<T>(data: Message<T>[]): Promise<void>
subscribe( subscribe(
eventName: string | symbol, eventName: string | symbol,
+5 -2
View File
@@ -1,5 +1,5 @@
import { Subscriber, SubscriberContext } from "." import { ITransactionBaseService } from "../transaction-base"
import { ITransactionBaseService } from "../transaction-base/transaction-base" import { EmitData, Message, Subscriber, SubscriberContext } from "./common"
export interface IEventBusService extends ITransactionBaseService { export interface IEventBusService extends ITransactionBaseService {
subscribe( subscribe(
@@ -13,5 +13,8 @@ export interface IEventBusService extends ITransactionBaseService {
subscriber: Subscriber, subscriber: Subscriber,
context?: SubscriberContext context?: SubscriberContext
): this ): this
emit<T>(event: string, data: T, options?: unknown): Promise<unknown | void> emit<T>(event: string, data: T, options?: unknown): Promise<unknown | void>
emit<T>(data: EmitData<T>[]): Promise<unknown | void>
emit<T>(data: Message<T>[]): Promise<unknown | void>
} }
+2 -7
View File
@@ -14,15 +14,10 @@ import {
} from "./common" } from "./common"
import { FindConfig } from "../common" import { FindConfig } from "../common"
import { ModuleJoinerConfig } from "../modules-sdk" import { IModuleService } from "../modules-sdk"
import { SharedContext } from "../shared-context" import { SharedContext } from "../shared-context"
export interface IInventoryService { export interface IInventoryService extends IModuleService {
/**
* @ignore
*/
__joinerConfig(): ModuleJoinerConfig
/** /**
* 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. * 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 {FilterableInventoryItemProps} selector - The filters to apply on the retrieved inventory items.
+2 -4
View File
@@ -1,11 +1,9 @@
import { FindConfig } from "../common" import { FindConfig } from "../common"
import { RestoreReturn, SoftDeleteReturn } from "../dal" import { RestoreReturn, SoftDeleteReturn } from "../dal"
import { ModuleJoinerConfig } from "../modules-sdk" import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context" import { Context } from "../shared-context"
export interface ILinkModule { export interface ILinkModule extends IModuleService {
__joinerConfig(): ModuleJoinerConfig
list( list(
filters?: Record<string, unknown>, filters?: Record<string, unknown>,
config?: FindConfig<unknown>, config?: FindConfig<unknown>,
+3 -8
View File
@@ -45,16 +45,11 @@ import {
} from "./common" } from "./common"
import { FindConfig } from "../common" import { FindConfig } from "../common"
import { ModuleJoinerConfig } from "../modules-sdk"
import { Context } from "../shared-context"
import { RestoreReturn, SoftDeleteReturn } from "../dal" import { RestoreReturn, SoftDeleteReturn } from "../dal"
import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context"
export interface IPricingModuleService { export interface IPricingModuleService extends IModuleService {
/**
* @ignore
*/
__joinerConfig(): ModuleJoinerConfig
/** /**
* This method is used to calculate prices based on the provided filters and context. * This method is used to calculate prices based on the provided filters and context.
* *
+4 -9
View File
@@ -1,3 +1,4 @@
import { RestoreReturn, SoftDeleteReturn } from "../dal"
import { import {
CreateProductCategoryDTO, CreateProductCategoryDTO,
CreateProductCollectionDTO, CreateProductCollectionDTO,
@@ -28,18 +29,12 @@ import {
UpdateProductTypeDTO, UpdateProductTypeDTO,
UpdateProductVariantDTO, UpdateProductVariantDTO,
} from "./common" } from "./common"
import { RestoreReturn, SoftDeleteReturn } from "../dal"
import { Context } from "../shared-context"
import { FindConfig } from "../common" import { FindConfig } from "../common"
import { ModuleJoinerConfig } from "../modules-sdk" import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context"
export interface IProductModuleService {
/**
* @ignore
*/
__joinerConfig(): ModuleJoinerConfig
export interface IProductModuleService extends IModuleService {
/** /**
* This method is used to retrieve a product by its ID * This method is used to retrieve a product by its ID
* *
+22 -1
View File
@@ -1,6 +1,8 @@
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { Message } from "./event-bus"
/** /**
* @deprecated use `Context` instead
* @interface * @interface
* *
* A context used to share resources, such as transaction manager, between the application and the module. * 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 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<string, Message[]>
clearMessages(): void
}
/** /**
* @interface * @interface
*
* A context used to share resources, such as transaction manager, between the application and the module. * A context used to share resources, such as transaction manager, between the application and the module.
*/ */
export type Context<TManager = unknown> = { export type Context<TManager = unknown> = {
@@ -39,11 +51,20 @@ export type Context<TManager = unknown> = {
* A boolean value indicating whether nested transactions are enabled. * A boolean value indicating whether nested transactions are enabled.
*/ */
enableNestedTransactions?: boolean 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. * A string indicating the ID of the current transaction.
*/ */
transactionId?: string 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. * A string indicating the ID of the current request.
*/ */
+2 -7
View File
@@ -6,15 +6,10 @@ import {
} from "./common" } from "./common"
import { FindConfig } from "../common/common" import { FindConfig } from "../common/common"
import { ModuleJoinerConfig } from "../modules-sdk" import { IModuleService } from "../modules-sdk"
import { SharedContext } from "../shared-context" import { SharedContext } from "../shared-context"
export interface IStockLocationService { export interface IStockLocationService extends IModuleService {
/**
* @ignore
*/
__joinerConfig(): ModuleJoinerConfig
/** /**
* This method is used to retrieve a paginated list of stock locations based on optional filters and configuration. * This method is used to retrieve a paginated list of stock locations based on optional filters and configuration.
* *
@@ -1,2 +1,2 @@
export const camelToSnakeCase = (string) => export const camelToSnakeCase = (string) =>
string.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) string.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase()
@@ -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 },
},
},
])
})
})
@@ -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<T>(
messageData:
| EventBusTypes.MessageFormat<T>
| EventBusTypes.MessageFormat<T>[],
options?: Record<string, unknown>
): EventBusTypes.Message<T>[] {
const messageData_ = Array.isArray(messageData) ? messageData : [messageData]
const messages: EventBusTypes.Message<T>[] = []
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<string, unknown>
}
): 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,
}
}
@@ -0,0 +1,7 @@
export enum CommonEvents {
CREATED = "created",
UPDATED = "updated",
DELETED = "deleted",
ATTACHED = "attached",
DETACHED = "detached",
}
+5
View File
@@ -22,6 +22,7 @@ export abstract class AbstractEventBusModuleService
options: Record<string, unknown> options: Record<string, unknown>
): Promise<void> ): Promise<void>
abstract emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<void> abstract emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<void>
abstract emit<T>(data: EventBusTypes.Message<T>[]): Promise<void>
protected storeSubscribers({ protected storeSubscribers({
event, event,
@@ -101,3 +102,7 @@ export abstract class AbstractEventBusModuleService
return this return this
} }
} }
export * from "./build-event-messages"
export * from "./common-events"
export * from "./message-aggregator"
@@ -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
}
}
@@ -0,0 +1,30 @@
export function InjectIntoContext(
properties: Record<string, unknown | Function>
): 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)
}
}
}