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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user