feat(fulfillment): Events management (#6730)
**What** This pr includes some cleanup and refactoring around the abstract event emitter method for the module service to not rely on a named class property but instead on resolution naming. Includes fulfillment set events on creation. The idea is that if that pr allows us to align and agreed on the approach as well as including the cleanup for other pr to use, If this gets merged I ll continue with another pr to do the rest of the event management partially fix CORE-1735
This commit is contained in:
committed by
GitHub
parent
06f22bb48a
commit
c658bd0233
@@ -0,0 +1,24 @@
|
||||
import { EventBusTypes } from "@medusajs/types"
|
||||
|
||||
export function buildExpectedEventMessageShape(options: {
|
||||
eventName: string
|
||||
action: string
|
||||
object: string
|
||||
eventGroupId?: string
|
||||
data: any
|
||||
options?: Record<string, unknown>
|
||||
}): EventBusTypes.Message {
|
||||
return {
|
||||
eventName: options.eventName,
|
||||
body: {
|
||||
metadata: {
|
||||
action: options.action,
|
||||
eventGroupId: options.eventGroupId,
|
||||
service: "fulfillment",
|
||||
object: options.object,
|
||||
},
|
||||
data: options.data,
|
||||
},
|
||||
options: options.options,
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { IFulfillmentModuleService } from "@medusajs/types"
|
||||
|
||||
export * from "./shipping-options"
|
||||
export * from "./fulfillment"
|
||||
export * from "./events"
|
||||
|
||||
export async function createFullDataStructure(
|
||||
service: IFulfillmentModuleService,
|
||||
|
||||
@@ -6,14 +6,26 @@ import {
|
||||
ServiceZoneDTO,
|
||||
UpdateFulfillmentSetDTO,
|
||||
} from "@medusajs/types"
|
||||
import { GeoZoneType } from "@medusajs/utils"
|
||||
import { FulfillmentEvents, GeoZoneType } from "@medusajs/utils"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { MockEventBusService } from "medusa-test-utils/dist"
|
||||
import { buildExpectedEventMessageShape } from "../../__fixtures__"
|
||||
|
||||
jest.setTimeout(100000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.FULFILLMENT,
|
||||
testSuite: ({ service }: SuiteOptions<IFulfillmentModuleService>) => {
|
||||
let eventBusEmitSpy
|
||||
|
||||
beforeEach(() => {
|
||||
eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("Fulfillment Module Service", () => {
|
||||
describe("read", () => {
|
||||
it("should list fulfillment sets with a filter", async function () {
|
||||
@@ -153,6 +165,15 @@ moduleIntegrationTestRunner({
|
||||
type: data.type,
|
||||
})
|
||||
)
|
||||
|
||||
expect(eventBusEmitSpy).toHaveBeenCalledWith([
|
||||
buildExpectedEventMessageShape({
|
||||
eventName: FulfillmentEvents.created,
|
||||
action: "created",
|
||||
object: "fulfillment_set",
|
||||
data: { id: fulfillmentSet.id },
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should create a collection of fulfillment sets", async function () {
|
||||
@@ -180,6 +201,18 @@ moduleIntegrationTestRunner({
|
||||
type: data_.type,
|
||||
})
|
||||
)
|
||||
|
||||
expect(eventBusEmitSpy).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
buildExpectedEventMessageShape({
|
||||
eventName: FulfillmentEvents.created,
|
||||
action: "created",
|
||||
object: "fulfillment_set",
|
||||
data: { id: fulfillmentSets[i].id },
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
++i
|
||||
}
|
||||
})
|
||||
@@ -210,6 +243,21 @@ moduleIntegrationTestRunner({
|
||||
]),
|
||||
})
|
||||
)
|
||||
|
||||
expect(eventBusEmitSpy).toHaveBeenCalledWith([
|
||||
buildExpectedEventMessageShape({
|
||||
eventName: FulfillmentEvents.created,
|
||||
action: "created",
|
||||
object: "fulfillment_set",
|
||||
data: { id: fulfillmentSet.id },
|
||||
}),
|
||||
buildExpectedEventMessageShape({
|
||||
eventName: FulfillmentEvents.service_zone_created,
|
||||
action: "created",
|
||||
object: "service_zone",
|
||||
data: { id: fulfillmentSet.service_zones[0].id },
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should create a collection of fulfillment sets with new service zones", async function () {
|
||||
@@ -262,6 +310,24 @@ moduleIntegrationTestRunner({
|
||||
]),
|
||||
})
|
||||
)
|
||||
|
||||
expect(eventBusEmitSpy).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
buildExpectedEventMessageShape({
|
||||
eventName: FulfillmentEvents.created,
|
||||
action: "created",
|
||||
object: "fulfillment_set",
|
||||
data: { id: fulfillmentSets[i].id },
|
||||
}),
|
||||
buildExpectedEventMessageShape({
|
||||
eventName: FulfillmentEvents.service_zone_created,
|
||||
action: "created",
|
||||
object: "service_zone",
|
||||
data: { id: fulfillmentSets[i].service_zones[0].id },
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
++i
|
||||
}
|
||||
})
|
||||
@@ -305,6 +371,27 @@ moduleIntegrationTestRunner({
|
||||
]),
|
||||
})
|
||||
)
|
||||
|
||||
expect(eventBusEmitSpy).toHaveBeenCalledWith([
|
||||
buildExpectedEventMessageShape({
|
||||
eventName: FulfillmentEvents.created,
|
||||
action: "created",
|
||||
object: "fulfillment_set",
|
||||
data: { id: fulfillmentSet.id },
|
||||
}),
|
||||
buildExpectedEventMessageShape({
|
||||
eventName: FulfillmentEvents.service_zone_created,
|
||||
action: "created",
|
||||
object: "service_zone",
|
||||
data: { id: fulfillmentSet.service_zones[0].id },
|
||||
}),
|
||||
buildExpectedEventMessageShape({
|
||||
eventName: FulfillmentEvents.geo_zone_created,
|
||||
action: "created",
|
||||
object: "geo_zone",
|
||||
data: { id: fulfillmentSet.service_zones[0].geo_zones[0].id },
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should create a collection of fulfillment sets with new service zones and new geo zones", async function () {
|
||||
@@ -385,6 +472,32 @@ moduleIntegrationTestRunner({
|
||||
]),
|
||||
})
|
||||
)
|
||||
|
||||
expect(eventBusEmitSpy).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
buildExpectedEventMessageShape({
|
||||
eventName: FulfillmentEvents.created,
|
||||
action: "created",
|
||||
object: "fulfillment_set",
|
||||
data: { id: fulfillmentSets[i].id },
|
||||
}),
|
||||
buildExpectedEventMessageShape({
|
||||
eventName: FulfillmentEvents.service_zone_created,
|
||||
action: "created",
|
||||
object: "service_zone",
|
||||
data: { id: fulfillmentSets[i].service_zones[0].id },
|
||||
}),
|
||||
buildExpectedEventMessageShape({
|
||||
eventName: FulfillmentEvents.geo_zone_created,
|
||||
action: "created",
|
||||
object: "geo_zone",
|
||||
data: {
|
||||
id: fulfillmentSets[i].service_zones[0].geo_zones[0].id,
|
||||
},
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
++i
|
||||
}
|
||||
})
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
arrayDifference,
|
||||
EmitEvents,
|
||||
FulfillmentUtils,
|
||||
getSetDifference,
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
@@ -36,6 +38,7 @@ import {
|
||||
import { isContextValid, validateRules } from "@utils"
|
||||
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
||||
import FulfillmentProviderService from "./fulfillment-provider"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
|
||||
const generateMethodForModels = [
|
||||
ServiceZone,
|
||||
@@ -242,6 +245,7 @@ export default class FulfillmentModuleService<
|
||||
): Promise<FulfillmentTypes.FulfillmentSetDTO>
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
@EmitEvents()
|
||||
async create(
|
||||
data:
|
||||
| FulfillmentTypes.CreateFulfillmentSetDTO
|
||||
@@ -287,6 +291,11 @@ export default class FulfillmentModuleService<
|
||||
sharedContext
|
||||
)
|
||||
|
||||
this.aggregateFulfillmentSetCreatedEvents(
|
||||
createdFulfillmentSets,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return Array.isArray(data)
|
||||
? createdFulfillmentSets
|
||||
: createdFulfillmentSets[0]
|
||||
@@ -1498,7 +1507,7 @@ export default class FulfillmentModuleService<
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
private static buildGeoZoneConstraintsFromAddress(
|
||||
protected static buildGeoZoneConstraintsFromAddress(
|
||||
address: FulfillmentTypes.FilterableShippingOptionForContextProps["address"]
|
||||
) {
|
||||
/**
|
||||
@@ -1557,4 +1566,60 @@ export default class FulfillmentModuleService<
|
||||
|
||||
return geoZoneConstraints
|
||||
}
|
||||
|
||||
protected aggregateFulfillmentSetCreatedEvents(
|
||||
createdFulfillmentSets: TEntity[],
|
||||
sharedContext: Context
|
||||
): void {
|
||||
const buildMessage = ({
|
||||
eventName,
|
||||
id,
|
||||
object,
|
||||
}: {
|
||||
eventName: string
|
||||
id: string
|
||||
object: string
|
||||
}) => {
|
||||
return {
|
||||
eventName,
|
||||
metadata: {
|
||||
object,
|
||||
service: Modules.FULFILLMENT,
|
||||
action: "created",
|
||||
eventGroupId: sharedContext.eventGroupId,
|
||||
},
|
||||
data: { id },
|
||||
}
|
||||
}
|
||||
|
||||
for (const fulfillmentSet of createdFulfillmentSets) {
|
||||
sharedContext.messageAggregator!.saveRawMessageData(
|
||||
buildMessage({
|
||||
eventName: FulfillmentUtils.FulfillmentEvents.created,
|
||||
id: fulfillmentSet.id,
|
||||
object: "fulfillment_set",
|
||||
})
|
||||
)
|
||||
|
||||
for (const serviceZone of fulfillmentSet.service_zones ?? []) {
|
||||
sharedContext.messageAggregator!.saveRawMessageData(
|
||||
buildMessage({
|
||||
eventName: FulfillmentUtils.FulfillmentEvents.service_zone_created,
|
||||
id: serviceZone.id,
|
||||
object: "service_zone",
|
||||
})
|
||||
)
|
||||
|
||||
for (const geoZone of serviceZone.geo_zones ?? []) {
|
||||
sharedContext.messageAggregator!.saveRawMessageData(
|
||||
buildMessage({
|
||||
eventName: FulfillmentUtils.FulfillmentEvents.geo_zone_created,
|
||||
id: geoZone.id,
|
||||
object: "geo_zone",
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,3 +531,17 @@ export type Pluralize<Singular extends string> = Singular extends `${infer R}y`
|
||||
| `${infer R}o`
|
||||
? `${Singular}es`
|
||||
: `${Singular}s`
|
||||
|
||||
export type SnakeCase<S extends string> =
|
||||
S extends `${infer T}${infer U}${infer V}`
|
||||
? U extends Uppercase<U>
|
||||
? `${Lowercase<T>}_${SnakeCase<`${Lowercase<U>}${V}`>}`
|
||||
: `${T}${SnakeCase<`${U}${V}`>}`
|
||||
: S
|
||||
|
||||
export type KebabCase<S extends string> =
|
||||
S extends `${infer T}${infer U}${infer V}`
|
||||
? U extends Uppercase<U>
|
||||
? `${Lowercase<T>}-${KebabCase<`${Lowercase<U>}${V}`>}`
|
||||
: `${T}${KebabCase<`${U}${V}`>}`
|
||||
: S
|
||||
|
||||
@@ -13,19 +13,21 @@ export function buildEventMessages<T>(
|
||||
options?: Record<string, unknown>
|
||||
): EventBusTypes.Message<T>[] {
|
||||
const messageData_ = Array.isArray(messageData) ? messageData : [messageData]
|
||||
const messages: EventBusTypes.Message<T>[] = []
|
||||
const messages: EventBusTypes.Message<any>[] = []
|
||||
|
||||
messageData_.map((data) => {
|
||||
const data_ = Array.isArray(data.data) ? data.data : [data.data]
|
||||
data_.forEach((bodyData) => {
|
||||
const message = {
|
||||
eventName: data.eventName,
|
||||
body: {
|
||||
metadata: data.metadata,
|
||||
data: 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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -106,3 +106,4 @@ export abstract class AbstractEventBusModuleService
|
||||
export * from "./build-event-messages"
|
||||
export * from "./common-events"
|
||||
export * from "./message-aggregator"
|
||||
export * from "./utils"
|
||||
|
||||
86
packages/utils/src/event-bus/utils.ts
Normal file
86
packages/utils/src/event-bus/utils.ts
Normal file
@@ -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>
|
||||
}
|
||||
21
packages/utils/src/fulfillment/events.ts
Normal file
21
packages/utils/src/fulfillment/events.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { buildEventNamesFromEntityName } from "../event-bus"
|
||||
|
||||
const eventBaseNames: [
|
||||
"fulfillmentSet",
|
||||
"serviceZone",
|
||||
"geoZone",
|
||||
"shippingOption",
|
||||
"shippingProfile",
|
||||
"shippingOptionRule",
|
||||
"fulfillment"
|
||||
] = [
|
||||
"fulfillmentSet",
|
||||
"serviceZone",
|
||||
"geoZone",
|
||||
"shippingOption",
|
||||
"shippingProfile",
|
||||
"shippingOptionRule",
|
||||
"fulfillment",
|
||||
]
|
||||
|
||||
export const FulfillmentEvents = buildEventNamesFromEntityName(eventBaseNames)
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./geo-zone"
|
||||
export * from "./shipping-options"
|
||||
export * from "./provider"
|
||||
export * from "./events"
|
||||
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
SoftDeleteReturn,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
MapToConfig,
|
||||
isString,
|
||||
kebabCase,
|
||||
lowerCaseFirst,
|
||||
mapObjectTo,
|
||||
MapToConfig,
|
||||
pluralize,
|
||||
upperCaseFirst,
|
||||
} from "../common"
|
||||
@@ -467,11 +467,19 @@ export function abstractModuleServiceFactory<
|
||||
this.__container__ = container
|
||||
this.baseRepository_ = container.baseRepository
|
||||
|
||||
try {
|
||||
this.eventBusModuleService_ = container.eventBusModuleService
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const hasEventBusModuleService = Object.keys(this.__container__).find(
|
||||
// TODO: Should use ModuleRegistrationName.EVENT_BUS but it would require to move it to the utils package to prevent circular dependencies
|
||||
(key) => key === "eventBusModuleService"
|
||||
)
|
||||
const hasEventBusService = Object.keys(this.__container__).find(
|
||||
(key) => key === "eventBusService"
|
||||
)
|
||||
|
||||
this.eventBusModuleService_ = hasEventBusService
|
||||
? this.__container__.eventBusService
|
||||
: hasEventBusModuleService
|
||||
? this.__container__.eventBusModuleService
|
||||
: undefined
|
||||
}
|
||||
|
||||
protected async emitEvents_(groupedEvents) {
|
||||
@@ -481,7 +489,7 @@ export function abstractModuleServiceFactory<
|
||||
|
||||
const promises: Promise<void>[] = []
|
||||
for (const group of Object.keys(groupedEvents)) {
|
||||
promises.push(this.eventBusModuleService_?.emit(groupedEvents[group]))
|
||||
promises.push(this.eventBusModuleService_.emit(groupedEvents[group]))
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { MessageAggregator } from "../../event-bus"
|
||||
import { InjectIntoContext } from "./inject-into-context"
|
||||
import {MessageAggregatorFormat} from "@medusajs/types";
|
||||
|
||||
export function EmitEvents() {
|
||||
export function EmitEvents(options: MessageAggregatorFormat = {} as MessageAggregatorFormat) {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string | symbol,
|
||||
@@ -17,7 +18,7 @@ export function EmitEvents() {
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const result = await original.apply(this, args)
|
||||
|
||||
await target.emitEvents_.apply(this, [aggregator.getMessages()])
|
||||
await target.emitEvents_.apply(this, [aggregator.getMessages(options)])
|
||||
|
||||
aggregator.clearMessages()
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user