chore(): start moving some packages to the core directory (#7215)

This commit is contained in:
Adrien de Peretti
2024-05-03 13:37:41 +02:00
committed by GitHub
parent fdee748eed
commit bbccd6481d
1436 changed files with 275 additions and 756 deletions
@@ -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",
}
+109
View File
@@ -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>
}