chore(): start moving some packages to the core directory (#7215)
This commit is contained in:
@@ -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,87 @@
|
||||
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
|
||||
* @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,8 @@
|
||||
export enum CommonEvents {
|
||||
CREATED = "created",
|
||||
UPDATED = "updated",
|
||||
DELETED = "deleted",
|
||||
RESTORED = "restored",
|
||||
ATTACHED = "attached",
|
||||
DETACHED = "detached",
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { EventBusTypes } from "@medusajs/types"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
export abstract class AbstractEventBusModuleService
|
||||
implements EventBusTypes.IEventBusModuleService
|
||||
{
|
||||
protected eventToSubscribersMap_: Map<
|
||||
string | symbol,
|
||||
EventBusTypes.SubscriberDescriptor[]
|
||||
> = new Map()
|
||||
|
||||
public get eventToSubscribersMap(): Map<
|
||||
string | symbol,
|
||||
EventBusTypes.SubscriberDescriptor[]
|
||||
> {
|
||||
return this.eventToSubscribersMap_
|
||||
}
|
||||
|
||||
abstract emit<T>(
|
||||
eventName: string,
|
||||
data: T,
|
||||
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,
|
||||
subscriberId,
|
||||
subscriber,
|
||||
}: {
|
||||
event: string | symbol
|
||||
subscriberId: string
|
||||
subscriber: EventBusTypes.Subscriber
|
||||
}) {
|
||||
const newSubscriberDescriptor = { subscriber, id: subscriberId }
|
||||
|
||||
const existingSubscribers = this.eventToSubscribersMap_.get(event) ?? []
|
||||
|
||||
const subscriberAlreadyExists = existingSubscribers.find(
|
||||
(sub) => sub.id === subscriberId
|
||||
)
|
||||
|
||||
if (subscriberAlreadyExists) {
|
||||
throw Error(`Subscriber with id ${subscriberId} already exists`)
|
||||
}
|
||||
|
||||
this.eventToSubscribersMap_.set(event, [
|
||||
...existingSubscribers,
|
||||
newSubscriberDescriptor,
|
||||
])
|
||||
}
|
||||
|
||||
public subscribe(
|
||||
eventName: string | symbol,
|
||||
subscriber: EventBusTypes.Subscriber,
|
||||
context?: EventBusTypes.SubscriberContext
|
||||
): this {
|
||||
if (typeof subscriber !== `function`) {
|
||||
throw new Error("Subscriber must be a function")
|
||||
}
|
||||
/**
|
||||
* If context is provided, we use the subscriberId from it
|
||||
* otherwise we generate a random using a ulid
|
||||
*/
|
||||
|
||||
const randId = ulid()
|
||||
const event = eventName.toString()
|
||||
|
||||
this.storeSubscribers({
|
||||
event,
|
||||
subscriberId: context?.subscriberId ?? `${event}-${randId}`,
|
||||
subscriber,
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
unsubscribe(
|
||||
eventName: string | symbol,
|
||||
subscriber: EventBusTypes.Subscriber,
|
||||
context: EventBusTypes.SubscriberContext
|
||||
): this {
|
||||
if (typeof subscriber !== `function`) {
|
||||
throw new Error("Subscriber must be a function")
|
||||
}
|
||||
|
||||
const existingSubscribers = this.eventToSubscribersMap_.get(eventName)
|
||||
|
||||
if (existingSubscribers?.length) {
|
||||
const subIndex = existingSubscribers?.findIndex(
|
||||
(sub) => sub.id === context?.subscriberId
|
||||
)
|
||||
|
||||
if (subIndex !== -1) {
|
||||
this.eventToSubscribersMap_
|
||||
.get(eventName)
|
||||
?.splice(subIndex as number, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export * from "./build-event-messages"
|
||||
export * from "./common-events"
|
||||
export * from "./message-aggregator"
|
||||
export * from "./utils"
|
||||
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
EventBusTypes,
|
||||
IMessageAggregator,
|
||||
Message,
|
||||
MessageAggregatorFormat,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { buildEventMessages } from "./build-event-messages"
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
saveRawMessageData<T>(
|
||||
messageData:
|
||||
| EventBusTypes.MessageFormat<T>
|
||||
| EventBusTypes.MessageFormat<T>[],
|
||||
options?: Record<string, unknown>
|
||||
): void {
|
||||
this.save(buildEventMessages(messageData, options))
|
||||
}
|
||||
|
||||
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,86 @@
|
||||
import { camelToSnakeCase, kebabCase, lowerCaseFirst } from "../common"
|
||||
import { CommonEvents } from "./common-events"
|
||||
import { KebabCase, SnakeCase } from "@medusajs/types"
|
||||
|
||||
type ReturnType<TNames extends string[]> = TNames extends [
|
||||
infer TFirstName,
|
||||
...infer TRest
|
||||
]
|
||||
? {
|
||||
[K in Lowercase<CommonEvents>]: `${KebabCase<TFirstName & string>}.${K}`
|
||||
} & {
|
||||
[K in TRest[number] as `${SnakeCase<K & string>}_created`]: `${KebabCase<
|
||||
K & string
|
||||
>}.created`
|
||||
} & {
|
||||
[K in TRest[number] as `${SnakeCase<K & string>}_updated`]: `${KebabCase<
|
||||
K & string
|
||||
>}.updated`
|
||||
} & {
|
||||
[K in TRest[number] as `${SnakeCase<K & string>}_deleted`]: `${KebabCase<
|
||||
K & string
|
||||
>}.deleted`
|
||||
} & {
|
||||
[K in TRest[number] as `${SnakeCase<K & string>}_restored`]: `${KebabCase<
|
||||
K & string
|
||||
>}.restored`
|
||||
} & {
|
||||
[K in TRest[number] as `${SnakeCase<K & string>}_attached`]: `${KebabCase<
|
||||
K & string
|
||||
>}.attached`
|
||||
} & {
|
||||
[K in TRest[number] as `${SnakeCase<K & string>}_detached`]: `${KebabCase<
|
||||
K & string
|
||||
>}.detached`
|
||||
}
|
||||
: {}
|
||||
|
||||
/**
|
||||
* From the given strings it will produce the event names accordingly.
|
||||
* the result will look like:
|
||||
* input: 'serviceZone'
|
||||
* output: {
|
||||
* created: 'fulfillment-set.created',
|
||||
* updated: 'fulfillment-set.updated',
|
||||
* deleted: 'fulfillment-set.deleted',
|
||||
* restored: 'fulfillment-set.restored',
|
||||
* attached: 'fulfillment-set.attached',
|
||||
* detached: 'fulfillment-set.detached',
|
||||
* service_zone_created: 'service-zone.created',
|
||||
* service_zone_updated: 'service-zone.updated',
|
||||
* service_zone_deleted: 'service-zone.deleted',
|
||||
* service_zone_restored: 'service-zone.restored',
|
||||
* service_zone_attached: 'service-zone.attached',
|
||||
* service_zone_detached: 'service-zone.detached',
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* @param names
|
||||
*/
|
||||
export function buildEventNamesFromEntityName<TNames extends string[]>(
|
||||
names: TNames
|
||||
): ReturnType<TNames> {
|
||||
const events = {}
|
||||
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const name = names[i]
|
||||
const snakedCaseName = lowerCaseFirst(camelToSnakeCase(name))
|
||||
const kebabCaseName = lowerCaseFirst(kebabCase(name))
|
||||
|
||||
if (i === 0) {
|
||||
for (const event of Object.values(CommonEvents) as string[]) {
|
||||
events[event] = `${kebabCaseName}.${event}`
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for (const event of Object.values(CommonEvents) as string[]) {
|
||||
events[`${snakedCaseName}_${event}`] =
|
||||
`${kebabCaseName}.${event}` as `${KebabCase<
|
||||
typeof name
|
||||
>}.${typeof event}`
|
||||
}
|
||||
}
|
||||
|
||||
return events as ReturnType<TNames>
|
||||
}
|
||||
Reference in New Issue
Block a user