chore(): Handle medusa service base methods events (#9179)

* chore(): Handle medusa service base methods events

* cleanup

* cleanup

* fix import

* fix decorator order

* fixes

* apply default event emition

* fix binding

* fix binding

* align tests with new event emition
This commit is contained in:
Adrien de Peretti
2024-09-18 17:57:00 +02:00
committed by GitHub
parent 81d3ae01c7
commit a734184538
13 changed files with 290 additions and 116 deletions
@@ -1,5 +1,7 @@
import { MedusaService } from "../medusa-service"
import { model } from "../../dml"
import { MessageAggregator } from "../../event-bus"
import { ModuleJoinerConfig } from "@medusajs/types"
const baseRepoMock = {
serialize: jest.fn().mockImplementation((item) => item),
@@ -7,10 +9,15 @@ const baseRepoMock = {
getFreshManager: jest.fn().mockReturnThis(),
}
const defaultContext = { __type: "MedusaContext", manager: baseRepoMock }
const defaultContext = {
__type: "MedusaContext",
manager: baseRepoMock,
messageAggregator: new MessageAggregator(),
}
const defaultTransactionContext = {
__type: "MedusaContext",
manager: baseRepoMock,
messageAggregator: new MessageAggregator(),
}
describe("Abstract Module Service Factory", () => {
@@ -58,6 +65,9 @@ describe("Abstract Module Service Factory", () => {
beforeEach(() => {
jest.clearAllMocks()
instance = new medusaService(containerMock)
;(instance as any).__joinerConfig = {
serviceName: "serviceName",
} as ModuleJoinerConfig
})
it("should have retrieve method", async () => {
@@ -120,6 +130,9 @@ describe("Abstract Module Service Factory", () => {
beforeEach(() => {
jest.clearAllMocks()
instance = new medusaService(containerMock)
;(instance as any).__joinerConfig = {
serviceName: "serviceName",
}
})
it("should have retrieve method for other models", async () => {
@@ -36,9 +36,12 @@ export function EmitEvents(
const argIndex = target.MedusaContextIndex_[propertyKey]
const aggregator = args[argIndex].messageAggregator as MessageAggregator
await target.emitEvents_.apply(this, [aggregator.getMessages(options)])
aggregator.clearMessages()
if (aggregator.count() > 0) {
await target.emitEvents_.apply(this, [aggregator.getMessages(options)])
aggregator.clearMessages()
}
return result
}
}
@@ -1,13 +1,14 @@
import { Context, EventBusTypes } from "@medusajs/types"
import { buildModuleResourceEventName } from "../event-bus"
// TODO should that move closer to the event bus? and maybe be rename to moduleEventBuilderFactory
// TODO should that move closer to the event bus? and maybe be rename to modulemoduleEventBuilderFactory
/**
*
* Factory function to create event builders for different entities
*
* @example
* const createdFulfillment = eventBuilderFactory({
* const createdFulfillment = moduleEventBuilderFactory({
* source: Modules.FULFILLMENT,
* action: CommonEvents.CREATED,
* object: "fulfillment",
@@ -24,24 +25,31 @@ import { Context, EventBusTypes } from "@medusajs/types"
* @param eventsEnum
* @param service
*/
export function eventBuilderFactory({
export function moduleEventBuilderFactory({
action,
object,
eventsEnum,
eventName,
source,
}: {
action: string
object: string
eventsEnum: Record<string, string>
/**
* @deprecated use eventName instead
*/
eventsEnum?: Record<string, string>
eventName?: string
source: string
}) {
return function ({
data,
sharedContext,
}: {
data: { id: string }[]
data: { id: string } | { id: string }[]
sharedContext: Context
}) {
data = Array.isArray(data) ? data : [data]
if (!data.length) {
return
}
@@ -51,8 +59,17 @@ export function eventBuilderFactory({
// The event enums contains event formatted like so [object]_[action] e.g. PRODUCT_CREATED
// We expect the keys of events to be fully uppercased
const eventName =
eventsEnum[`${object.toUpperCase()}_${action.toUpperCase()}`]
let eventName_ = eventsEnum
? eventsEnum[`${object.toUpperCase()}_${action.toUpperCase()}`]
: eventName
if (!eventName_) {
eventName_ = buildModuleResourceEventName({
prefix: source,
objectName: object,
action,
})
}
data.forEach((dataItem) => {
messages.push({
@@ -60,7 +77,7 @@ export function eventBuilderFactory({
action,
context: sharedContext,
data: { id: dataItem.id },
eventName,
eventName: eventName_,
object,
})
})
@@ -11,16 +11,16 @@ import {
SoftDeleteReturn,
} from "@medusajs/types"
import {
MapToConfig,
camelToSnakeCase,
isString,
kebabCase,
lowerCaseFirst,
mapObjectTo,
MapToConfig,
pluralize,
upperCaseFirst,
} from "../common"
import { DmlEntity } from "../dml"
import { InjectManager, MedusaContext } from "./decorators"
import { EmitEvents, InjectManager, MedusaContext } from "./decorators"
import { Modules } from "./definition"
import { buildModelsNameToLinkableKeysMap } from "./joiner-config-builder"
import {
@@ -31,6 +31,8 @@ import {
ModelEntries,
ModelsConfigTemplate,
} from "./types/medusa-service"
import { CommonEvents } from "../event-bus"
import { moduleEventBuilderFactory } from "./event-builder-factory"
const readMethods = ["retrieve", "list", "listAndCount"] as BaseMethods[]
const writeMethods = [
@@ -150,13 +152,10 @@ export function MedusaService<
value: klassPrototype[methodName],
}
// The order of the decorators is important, do not change it
MedusaContext()(klassPrototype, methodName, contextIndex)
InjectManager("baseRepository_")(
klassPrototype,
methodName,
descriptorMockRef
)
EmitEvents()(klassPrototype, methodName, descriptorMockRef)
InjectManager()(klassPrototype, methodName, descriptorMockRef)
klassPrototype[methodName] = descriptorMockRef.value
}
@@ -194,6 +193,13 @@ export function MedusaService<
const models = await service.create(serviceData, sharedContext)
const response = Array.isArray(data) ? models : models[0]
klassPrototype.aggregatedEvents.bind(this)({
action: CommonEvents.CREATED,
object: camelToSnakeCase(modelName).toLowerCase(),
data: response,
context: sharedContext,
})
return await this.baseRepository_.serialize<T | T[]>(response)
}
@@ -211,6 +217,13 @@ export function MedusaService<
const models = await service.update(serviceData, sharedContext)
const response = Array.isArray(data) ? models : models[0]
klassPrototype.aggregatedEvents.bind(this)({
action: CommonEvents.UPDATED,
object: camelToSnakeCase(modelName).toLowerCase(),
data: response,
context: sharedContext,
})
return await this.baseRepository_.serialize<T | T[]>(response)
}
@@ -259,22 +272,21 @@ export function MedusaService<
const primaryKeyValues_ = Array.isArray(primaryKeyValues)
? primaryKeyValues
: [primaryKeyValues]
await this.__container__[serviceRegistrationName].delete(
primaryKeyValues_,
sharedContext
)
await this.eventBusModuleService_?.emit(
primaryKeyValues_.map((primaryKeyValue) => ({
name: `${kebabCase(modelName)}.deleted`,
primaryKeyValues_.map((primaryKeyValue) =>
klassPrototype.aggregatedEvents.bind(this)({
action: CommonEvents.DELETED,
object: camelToSnakeCase(modelName).toLowerCase(),
data: isString(primaryKeyValue)
? { id: primaryKeyValue }
: primaryKeyValue,
metadata: { source: "", action: "", object: "" },
})),
{
internal: true,
}
context: sharedContext,
})
)
}
@@ -292,25 +304,10 @@ export function MedusaService<
? primaryKeyValues
: [primaryKeyValues]
const [models, cascadedModelsMap] = await this.__container__[
const [, cascadedModelsMap] = await this.__container__[
serviceRegistrationName
].softDelete(primaryKeyValues_, sharedContext)
const softDeletedModels = await this.baseRepository_.serialize<T[]>(
models
)
await this.eventBusModuleService_?.emit(
softDeletedModels.map(({ id }) => ({
name: `${kebabCase(modelName)}.deleted`,
metadata: { source: "", action: "", object: "" },
data: { id },
})),
{
internal: true,
}
)
// Map internal table/column names to their respective external linkable keys
// eg: product.id = product_id, variant.id = variant_id
const mappedCascadedModelsMap = mapObjectTo(
@@ -321,6 +318,31 @@ export function MedusaService<
}
)
if (mappedCascadedModelsMap) {
const joinerConfig = (
typeof this.__joinerConfig === "function"
? this.__joinerConfig()
: this.__joinerConfig
) as ModuleJoinerConfig
Object.entries(mappedCascadedModelsMap).forEach(
([linkableKey, ids]) => {
const entity = joinerConfig.linkableKeys?.[linkableKey]!
if (entity) {
const linkableKeyEntity =
camelToSnakeCase(entity).toLowerCase()
klassPrototype.aggregatedEvents.bind(this)({
action: CommonEvents.DELETED,
object: linkableKeyEntity,
data: { id: ids },
context: sharedContext,
})
}
}
)
}
return mappedCascadedModelsMap ? mappedCascadedModelsMap : void 0
}
@@ -338,7 +360,7 @@ export function MedusaService<
? primaryKeyValues
: [primaryKeyValues]
const [_, cascadedModelsMap] = await this.__container__[
const [, cascadedModelsMap] = await this.__container__[
serviceRegistrationName
].restore(primaryKeyValues_, sharedContext)
@@ -353,6 +375,30 @@ export function MedusaService<
}
)
if (mappedCascadedModelsMap) {
const joinerConfig = (
typeof this.__joinerConfig === "function"
? this.__joinerConfig()
: this.__joinerConfig
) as ModuleJoinerConfig
Object.entries(mappedCascadedModelsMap).forEach(
([linkableKey, ids]) => {
const entity = joinerConfig.linkableKeys?.[linkableKey]!
if (entity) {
const linkableKeyEntity =
camelToSnakeCase(entity).toLowerCase()
klassPrototype.aggregatedEvents.bind(this)({
action: CommonEvents.CREATED,
object: linkableKeyEntity,
data: { id: ids },
context: sharedContext,
})
}
}
)
}
return mappedCascadedModelsMap ? mappedCascadedModelsMap : void 0
}
@@ -398,6 +444,50 @@ export function MedusaService<
)
}
/**
* helper function to aggregate events. Will format the message properly and store in
* the message aggregator from the context. The method must be decorated with `@EmitEvents`
* @param action
* @param object
* @param eventName optional, can be inferred from the module joiner config + action + object
* @param source optional, can be inferred from the module joiner config
* @param data
* @param context
*/
protected aggregatedEvents({
action,
object,
eventName,
source,
data,
context,
}: {
action: string
object: string
eventName?: string
source?: string
data: { id: any } | { id: any }[]
context: Context
}) {
const __joinerConfig = (
typeof this.__joinerConfig === "function"
? this.__joinerConfig()
: this.__joinerConfig
) as ModuleJoinerConfig
const eventBuilder = moduleEventBuilderFactory({
action,
object,
source: source || __joinerConfig.serviceName!,
eventName,
})
eventBuilder({
data,
sharedContext: context,
})
}
/**
* @internal this method is not meant to be used except by the internal team for now
* @param groupedEvents
@@ -265,4 +265,29 @@ export type MedusaServiceReturnType<ModelsConfig extends Record<string, any>> =
{
new (...args: any[]): AbstractModuleService<ModelsConfig>
$modelObjects: InferModelFromConfig<ModelsConfig>
/**
* helper function to aggregate events. Will format the message properly and store in
* the message aggregator in the context
* @param action
* @param object
* @param eventName optional, can be inferred from the module joiner config + action + object
* @param source optional, can be inferred from the module joiner config
* @param data
* @param context
*/
aggregatedEvents({
action,
object,
eventName,
source,
data,
context,
}: {
action: string
object: string
eventName: string
source?: string
data: { id: any } | { id: any }[]
context: Context
}): void
}