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
+1079 -421

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