feat: Event emitting part 1/N (Fulfillment) (#7391)

**What**
Add support for event emitting in the fulfillment module

**NOTE**
It does not include the review of the events for the abstract module factory if the method is not implemented in the module itself and rely on the default implementation
This commit is contained in:
Adrien de Peretti
2024-06-03 10:29:35 +02:00
committed by GitHub
parent 28d2a5347a
commit 9608bf06ef
29 changed files with 1716 additions and 348 deletions
@@ -1,39 +1,4 @@
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<any>[] = []
messageData_.map((data) => {
const data_ = Array.isArray(data.data) ? data.data : [data.data]
data_.forEach((bodyData) => {
const message = composeMessage(data.eventName, {
data: bodyData,
service: data.metadata.service,
entity: data.metadata.object,
action: data.metadata.action,
context: {
eventGroupId: data.metadata.eventGroupId,
} as Context,
options,
})
messages.push(message)
})
})
return messages
}
/**
* Helper function to compose and normalize a Message to be emitted by EventBus Module
@@ -48,27 +13,29 @@ export function composeMessage(
{
data,
service,
entity,
object,
action,
context = {},
options,
}: {
data: unknown
data: any
service: string
entity: string
object: string
action?: string
context?: Context
options?: Record<string, unknown>
options?: Record<string, any>
}
): EventBusTypes.Message {
const act = action || eventName.split(".").pop()
if (!action && !Object.values(CommonEvents).includes(act as CommonEvents)) {
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,
object,
action: act!,
}
@@ -1,11 +1,12 @@
import {
Context,
EventBusTypes,
IMessageAggregator,
Message,
MessageAggregatorFormat,
} from "@medusajs/types"
import { buildEventMessages } from "./build-event-messages"
import { composeMessage } from "./build-event-messages"
export class MessageAggregator implements IMessageAggregator {
private messages: Message[]
@@ -28,11 +29,25 @@ export class MessageAggregator implements IMessageAggregator {
saveRawMessageData<T>(
messageData:
| EventBusTypes.MessageFormat<T>
| EventBusTypes.MessageFormat<T>[],
options?: Record<string, unknown>
| EventBusTypes.RawMessageFormat<T>
| EventBusTypes.RawMessageFormat<T>[],
{
options,
sharedContext,
}: { options?: Record<string, unknown>; sharedContext?: Context } = {}
): void {
this.save(buildEventMessages(messageData, options))
const messages = Array.isArray(messageData) ? messageData : [messageData]
const composedMessages = messages.map((message) => {
return composeMessage(message.eventName, {
data: message.data,
service: message.service,
object: message.object,
action: message.action,
options,
context: sharedContext,
})
})
this.save(composedMessages)
}
getMessages(format?: MessageAggregatorFormat): {
+6 -6
View File
@@ -58,7 +58,8 @@ type ReturnType<TNames extends string[]> = TNames extends [
* @param names
*/
export function buildEventNamesFromEntityName<TNames extends string[]>(
names: TNames
names: TNames,
prefix?: string
): ReturnType<TNames> {
const events = {}
@@ -69,16 +70,15 @@ export function buildEventNamesFromEntityName<TNames extends string[]>(
if (i === 0) {
for (const event of Object.values(CommonEvents) as string[]) {
events[event] = `${kebabCaseName}.${event}`
events[event] = `${prefix ? prefix + "." : ""}${kebabCaseName}.${event}`
}
continue
}
for (const event of Object.values(CommonEvents) as string[]) {
events[`${snakedCaseName}_${event}`] =
`${kebabCaseName}.${event}` as `${KebabCase<
typeof name
>}.${typeof event}`
events[`${snakedCaseName}_${event}`] = `${
prefix ? prefix + "." : ""
}${kebabCaseName}.${event}` as `${KebabCase<typeof name>}.${typeof event}`
}
}
+14 -2
View File
@@ -1,21 +1,33 @@
import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
const eventBaseNames: [
"fulfillmentSet",
"serviceZone",
"geoZone",
"shippingOption",
"shippingOptionType",
"shippingProfile",
"shippingOptionRule",
"fulfillment"
"fulfillment",
"fulfillmentAddress",
"fulfillmentItem",
"fulfillmentLabel"
] = [
"fulfillmentSet",
"serviceZone",
"geoZone",
"shippingOption",
"shippingOptionType",
"shippingProfile",
"shippingOptionRule",
"fulfillment",
"fulfillmentAddress",
"fulfillmentItem",
"fulfillmentLabel",
]
export const FulfillmentEvents = buildEventNamesFromEntityName(eventBaseNames)
export const FulfillmentEvents = buildEventNamesFromEntityName(
eventBaseNames,
Modules.FULFILLMENT
)
+12 -13
View File
@@ -1,14 +1,13 @@
import { CommonEvents } from "../event-bus"
import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
export const InventoryEvents = {
created: "inventory-item." + CommonEvents.CREATED,
updated: "inventory-item." + CommonEvents.UPDATED,
deleted: "inventory-item." + CommonEvents.DELETED,
restored: "inventory-item." + CommonEvents.RESTORED,
reservation_item_created: "reservation-item." + CommonEvents.CREATED,
reservation_item_updated: "reservation-item." + CommonEvents.UPDATED,
reservation_item_deleted: "reservation-item." + CommonEvents.DELETED,
inventory_level_deleted: "inventory-level." + CommonEvents.DELETED,
inventory_level_created: "inventory-level." + CommonEvents.CREATED,
inventory_level_updated: "inventory-level." + CommonEvents.UPDATED,
}
const eventBaseNames: ["inventoryItem", "reservationItem", "inventoryLevel"] = [
"inventoryItem",
"reservationItem",
"inventoryLevel",
]
export const InventoryEvents = buildEventNamesFromEntityName(
eventBaseNames,
Modules.INVENTORY
)
@@ -1,16 +1,17 @@
import { MessageAggregator } from "../../event-bus"
import { InjectIntoContext } from "./inject-into-context"
import {MessageAggregatorFormat} from "@medusajs/types";
import { MessageAggregatorFormat } from "@medusajs/types"
export function EmitEvents(options: MessageAggregatorFormat = {} as MessageAggregatorFormat) {
export function EmitEvents(
options: MessageAggregatorFormat = {} as MessageAggregatorFormat
) {
return function (
target: any,
propertyKey: string | symbol,
descriptor: any
): void {
const aggregator = new MessageAggregator()
InjectIntoContext({
messageAggregator: () => aggregator,
messageAggregator: () => new MessageAggregator(),
})(target, propertyKey, descriptor)
const original = descriptor.value
@@ -18,6 +19,17 @@ export function EmitEvents(options: MessageAggregatorFormat = {} as MessageAggre
descriptor.value = async function (...args: any[]) {
const result = await original.apply(this, args)
if (!target.emitEvents_) {
const logger = Object.keys(this.__container__ ?? {}).includes("logger")
? this.__container__.logger
: console
logger.warn(
`No emitEvents_ method found on ${target.constructor.name}. No events emitted. To be able to use the @EmitEvents() you need to have the emitEvents_ method implemented in the class.`
)
}
const argIndex = target.MedusaContextIndex_[propertyKey]
const aggregator = args[argIndex].messageAggregator as MessageAggregator
await target.emitEvents_.apply(this, [aggregator.getMessages(options)])
aggregator.clearMessages()
@@ -1,5 +1,3 @@
import { MessageAggregator } from "../../event-bus"
export function InjectIntoContext(
properties: Record<string, unknown | Function>
): MethodDecorator {
@@ -0,0 +1,67 @@
import { Context, EventBusTypes } from "@medusajs/types"
/**
* Factory function to create event builders for different entities
*
* @example
* const createdFulfillment = eventBuilderFactory({
* service: Modules.FULFILLMENT,
* action: CommonEvents.CREATED,
* object: "fulfillment",
* eventsEnum: FulfillmentEvents,
* })
*
* createdFulfillment({
* data,
* sharedContext,
* })
*
* @param isMainEntity
* @param action
* @param object
* @param eventsEnum
* @param service
*/
export function eventBuilderFactory({
isMainEntity,
action,
object,
eventsEnum,
service,
}: {
isMainEntity?: boolean
action: string
object: string
eventsEnum: Record<string, string>
service: string
}) {
return function ({
data,
sharedContext,
}: {
data: { id: string }[]
sharedContext: Context
}) {
if (!data.length) {
return
}
const aggregator = sharedContext.messageAggregator!
const messages: EventBusTypes.RawMessageFormat[] = []
data.forEach((dataItem) => {
messages.push({
service,
action,
context: sharedContext,
data: { id: dataItem.id },
eventName: isMainEntity
? eventsEnum[action]
: eventsEnum[`${object}_${action}`],
object,
})
})
aggregator.saveRawMessageData(messages)
}
}
@@ -9,3 +9,4 @@ export * from "./migration-scripts"
export * from "./internal-module-service-factory"
export * from "./abstract-module-service-factory"
export * from "./definition"
export * from "./event-builder-factory"
+6 -6
View File
@@ -1,9 +1,9 @@
import { CommonEvents } from "../event-bus"
import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
const eventBaseNames: ["user", "invite"] = ["user", "invite"]
export const UserEvents = {
created: "user." + CommonEvents.CREATED,
updated: "user." + CommonEvents.UPDATED,
invite_created: "invite." + CommonEvents.CREATED,
invite_updated: "invite." + CommonEvents.UPDATED,
invite_token_generated: "invite.token_generated",
...buildEventNamesFromEntityName(eventBaseNames, Modules.USER),
invite_token_generated: `${Modules.USER}.user.invite.token_generated`,
}