From a6aa2b4272e37f51a098e0fe768dace35b40fb2c Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 19:23:49 -0300 Subject: [PATCH 01/18] chore: ignore idea config --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2b1c829..7ba018d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ types.d.ts.map types.js.map !tests/* -!lib/* \ No newline at end of file +!lib/* +.idea/ \ No newline at end of file From d25e067a330ac4fbf8f945c9f560ad625ce070cd Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 19:24:26 -0300 Subject: [PATCH 02/18] feat: added new events class as core --- lib/core/events.v2.ts | 142 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 lib/core/events.v2.ts diff --git a/lib/core/events.v2.ts b/lib/core/events.v2.ts new file mode 100644 index 0000000..3775fbb --- /dev/null +++ b/lib/core/events.v2.ts @@ -0,0 +1,142 @@ +import { Metrics, Event, Options, Handler, PromiseHandler } from "../types"; + +export default class TsEvents { + private _metrics: Metrics; + private _events: Event[]; + private totalDispatched: number; + + /** + * Creates an instance of Events for aggregate. + */ + constructor(private readonly aggregate: T) { + this._events = []; + this.totalDispatched = 0; + this._metrics = { + totalDispatched: (): number => this.totalDispatched, + totalEvents: (): number => this._events.length + } + } + + /** + * Getter method for accessing metrics related to events. + * @returns {Metrics} Metrics related to events. + */ + get metrics(): Metrics { + return this._metrics; + } + + /** + * Gets the priority based on the number of events. + * @returns The priority value. + */ + private getPriority(): number { + const totalEvents = this._events.length; + if (totalEvents <= 1) return 2; + return totalEvents; + } + + /** + * Gets the default options. + * @returns The default options. + */ + private getDefaultOptions(): Options { + const priority = this.getPriority(); + return { + priority + }; + } + + /** + * Adds a new event. + * @param eventName - The name of the event. + * @param handler - The event handler function. + * @param options - The options for the event. + */ + addEvent(eventName: string, handler: Handler, options?: Options): void { + const defaultOptions = this.getDefaultOptions(); + const opt = options ? options : defaultOptions; + this.validateEventName(eventName); + this.validateHandler(handler, eventName); + this.removeEvent(eventName); + this._events.push({ eventName, handler, options: opt }); + } + + /** + * Validates the event handler. + * @param handler - The event handler function. + * @param eventName - The name of the event. + */ + private validateHandler(handler: Handler, eventName: string): void { + if (typeof handler !== 'function') { + const message = `addEvent: handler for ${eventName} is not a function`; + throw new Error(message); + }; + } + + /** + * Validates the event name. + * @param eventName - The name of the event. + */ + private validateEventName(eventName: string): void { + if (typeof eventName !== 'string' || String(eventName).length < 3) { + const message = `addEvent: invalid event name ${eventName}`; + throw new Error(message); + } + } + + /** + * Clears all events. + */ + clearEvents(): void { + this._events = [] + } + + /** + * Removes an event by name. + * @param eventName - The name of the event to remove. + */ + removeEvent(eventName: string): void { + this._events = this._events.filter( + (event): boolean => event.eventName !== eventName + ); + } + + /** + * Dispatches an event. + * @param eventName - The name of the event to dispatch. + * @param args - Any param user wants provide as argument. + * @returns The result of the event handler function. + */ + dispatchEvent(eventName: string, ...args: any[]): void | Promise { + const _event = this._events.find( + (evt): boolean => evt.eventName === eventName + ); + if (!_event) { + const message = `dispatchEvent: ${eventName} event not found`; + return console.error(message); + }; + this.totalDispatched = this.totalDispatched + 1; + _event.handler(this.aggregate, [_event, ...args]); + this.removeEvent(eventName); + } + + /** + * Dispatches all events. + * @returns A promise that resolves when all promise-based events are completed. + */ + async dispatchEvents(): Promise { + const promisesEvents: PromiseHandler[] = []; + const sorted = this._events.sort( + (a, b): number => a.options.priority - b.options.priority + ); + sorted.forEach((_event): void => { + this.totalDispatched = this.totalDispatched + 1; + const fn = _event.handler(this.aggregate, [_event]); + if (fn instanceof Promise) { + promisesEvents.push(fn as unknown as PromiseHandler); + } + }); + await Promise.all(promisesEvents).catch(console.error); + this.clearEvents(); + } +} From f1369f77a40ffdf49275170e246f1e9ca4c28395 Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 19:24:53 -0300 Subject: [PATCH 03/18] feat: added types for events class --- lib/types.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/lib/types.ts b/lib/types.ts index 0484a60..f233d09 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -298,3 +298,52 @@ export interface EventMetrics { total: number; dispatch: number; } + +/** + * Interface representing an event. + */ +export interface Event { + eventName: string; + handler: Handler; + options: Options; +} + +/** + * Represents a promise-based event handler. + */ +export type PromiseHandler = (...args: [T, [Event, ...any[]]]) => Promise; + +/** + * Represents a normal event handler. + */ +export type NormalHandler = (...args: [T, [Event, ...any[]]]) => void; + +/** + * Represents an event handler, which can be either a promise-based handler or a normal handler. + */ +export type Handler = PromiseHandler | NormalHandler; + +/** + * Interface representing options for an event. + */ +export interface Options { + priority: number; +} + +/** + * Interface representing metrics related to events. + * @interface Metrics + */ +export interface Metrics { + /** + * A function that returns the total number of events. + * @returns {number} The total number of events. + */ + totalEvents: () => number; + + /** + * A function that returns the total number of dispatched events. + * @returns {number} The total number of dispatched events. + */ + totalDispatched: () => number; +} From 0fc3dc4be964c940ab5fe47c982c427ea625e866 Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 19:25:16 -0300 Subject: [PATCH 04/18] feat: export events --- lib/core/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/core/index.ts b/lib/core/index.ts index ae8c9e5..3d6d848 100644 --- a/lib/core/index.ts +++ b/lib/core/index.ts @@ -12,3 +12,4 @@ export * from './create-many-domain-instance'; export * from './crypto'; export * from './ok'; export * from './fail'; +export * from './events.v2'; From 4c3a6d097ba78c3894e49a72725c2ae92c6a5a6a Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 19:25:30 -0300 Subject: [PATCH 05/18] docs: update changelog --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5e6ee..102d83f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,45 @@ All notable changes to this project will be documented in this file. --- +### [1.20.0] - 2024-03-17 + +### Changed + +- change: modify the to add and dispatch event on aggregate +- added: Implemented a new instance of the event management class, allowing for better event handling and organization. + +**Details:** +Introduced a new method to create an event management instance for the aggregate. +Improved event handling and organization, enhancing the overall performance and efficiency of event management. +Enabled easier integration and usage of event management features in various applications. + +```ts +/** + * Create a new instance of the event management class + * Pass the aggregate as a parameter + * Return an event management instance for the aggregate + */ +const events = new Events(aggregate); + +events.addEvent('eventName', (...args) => { + console.log('executing event...'); + console.log(args); +}); + +await events.dispatchEvents(); + +// OR + +events.dispatchEvent('eventName'); + +``` + +--- + +## Released + +--- + ### [1.19.2] - 2024-03-15 ### Changed From b4811d1e806a1032696100efeb1e8d450bac1093 Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 19:25:54 -0300 Subject: [PATCH 06/18] test: ensure events methods --- tests/core/events.v2.spec.ts | 167 +++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 tests/core/events.v2.spec.ts diff --git a/tests/core/events.v2.spec.ts b/tests/core/events.v2.spec.ts new file mode 100644 index 0000000..3d731b9 --- /dev/null +++ b/tests/core/events.v2.spec.ts @@ -0,0 +1,167 @@ +import TsEvents from '../../lib/core/events.v2'; + +describe('events.v2', () => { + + it('should create instance with success', () => { + const instance = new TsEvents({ name: 'Jane', age: 21 }); + expect(instance.metrics.totalDispatched()).toBe(0); + expect(instance.metrics.totalEvents()).toBe(0); + }); + + it('should do nothing if event does not exists', () => { + const instance = new TsEvents({ name: 'Jane', age: 21 }); + instance.dispatchEvent('not_exists'); + expect(instance.metrics.totalDispatched()).toBe(0); + expect(instance.metrics.totalEvents()).toBe(0); + }); + + it('should throw if provide empty string as event name', () => { + const instance = new TsEvents({ name: 'Jane', age: 21 }); + const spy = () => instance.addEvent('', () => { }); + expect(spy).toThrowErrorMatchingInlineSnapshot(`"addEvent: invalid event name "`); + }); + + it('should throw if not provide function as callback', () => { + const instance = new TsEvents({ name: 'Jane', age: 21 }); + const spy = () => instance.addEvent('not_callback', { execute: () => { } } as any); + expect(spy).toThrowErrorMatchingInlineSnapshot(`"addEvent: handler for not_callback is not a function"`); + }); + + it('should add and call event with success', () => { + const instance = new TsEvents({ name: 'Jane', age: 21 }); + let params = null; + const callback = (...args: any) => params = args; + + instance.addEvent('start', callback); + expect(instance.metrics.totalDispatched()).toBe(0); + expect(instance.metrics.totalEvents()).toBe(1); + + instance.dispatchEvent('start'); + + expect(instance.metrics.totalDispatched()).toBe(1); + expect(instance.metrics.totalEvents()).toBe(0); + expect(params).toMatchObject([{ "age": 21, "name": "Jane" }, [ + { + "eventName": "start", + "handler": callback, + "options": { + "priority": 2, + }, + } + ]]); + }); + + it('should add and call event with success', () => { + const instance = new TsEvents({ name: 'Jane', age: 21 }); + let params = null; + const callback = (...args: any) => params = args; + + instance.addEvent('start', callback); + expect(instance.metrics.totalDispatched()).toBe(0); + expect(instance.metrics.totalEvents()).toBe(1); + + instance.dispatchEvent('start', { dto: { some: 'info' } }); + + expect(instance.metrics.totalDispatched()).toBe(1); + expect(instance.metrics.totalEvents()).toBe(0); + expect(params).toMatchObject([{ "age": 21, "name": "Jane" }, [ + { + "eventName": "start", + "handler": callback, + "options": { + "priority": 2, + }, + }, + { + dto: { some: 'info' } + } + ]]); + }); + + it('should add many events and call event priority 1 as first', async () => { + const instance = new TsEvents({ name: 'Jane', age: 21 }); + const callback = jest.fn(); + + instance.addEvent('first', callback); + instance.addEvent('second', callback); + instance.addEvent('third', callback, { priority: 1 }); + + expect(instance.metrics.totalDispatched()).toBe(0); + expect(instance.metrics.totalEvents()).toBe(3); + + await instance.dispatchEvents(); + + expect(instance.metrics.totalDispatched()).toBe(3); + expect(instance.metrics.totalEvents()).toBe(0); + expect(callback).toHaveBeenNthCalledWith(1, { + "age": 21, + "name": "Jane" + }, [ + { + "eventName": "third", + "handler": callback, + "options": { "priority": 1 } + } + ]); + }); + + it('should clear all events', () => { + const instance = new TsEvents({ name: 'Jane', age: 21 }); + const callback = jest.fn(); + + instance.addEvent('first', callback); + instance.addEvent('second', callback); + instance.addEvent('third', callback, { priority: 1 }); + + expect(instance.metrics.totalDispatched()).toBe(0); + expect(instance.metrics.totalEvents()).toBe(3); + + instance.clearEvents(); + expect(instance.metrics.totalDispatched()).toBe(0); + expect(instance.metrics.totalEvents()).toBe(0); + }); + + it('should remove an event by name', () => { + const instance = new TsEvents({ name: 'Jane', age: 21 }); + const callback = jest.fn(); + + instance.addEvent('first', callback); + instance.addEvent('second', callback); + instance.addEvent('third', callback, { priority: 1 }); + + expect(instance.metrics.totalDispatched()).toBe(0); + expect(instance.metrics.totalEvents()).toBe(3); + + instance.removeEvent('second'); + expect(instance.metrics.totalDispatched()).toBe(0); + expect(instance.metrics.totalEvents()).toBe(2); + }); + + it('should replace an existing with the same name', () => { + const instance = new TsEvents({ name: 'Jane', age: 21 }); + const callback = jest.fn(); + + instance.addEvent('first', callback); + instance.addEvent('first', callback); + instance.addEvent('first', callback, { priority: 1 }); + + expect(instance.metrics.totalDispatched()).toBe(0); + expect(instance.metrics.totalEvents()).toBe(1); + }); + + it('should resolve many promises', async () => { + const instance = new TsEvents({ name: 'Jane', age: 21 }); + const callback = () => new Promise((resolve) => resolve(1)); + + instance.addEvent('first', callback); + instance.addEvent('second', callback); + instance.addEvent('third', callback, { priority: 1 }); + + expect(instance.metrics.totalDispatched()).toBe(0); + expect(instance.metrics.totalEvents()).toBe(3); + await instance.dispatchEvents(); + + expect(instance.metrics.totalDispatched()).toBe(3); + expect(instance.metrics.totalEvents()).toBe(0); + }); +}); From 731b2d8a9ded726344b3f36436c8036a9fbc207e Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 22:00:09 -0300 Subject: [PATCH 07/18] chore: update deps --- yarn.lock | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 8119b4c..e79f48e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -703,13 +703,20 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/node@*", "@types/node@^20.8.0": +"@types/node@*": version "20.11.25" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.25.tgz#0f50d62f274e54dd7a49f7704cc16bfbcccaf49f" integrity sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw== dependencies: undici-types "~5.26.4" +"@types/node@^20.8.0": + version "20.11.28" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.28.tgz#4fd5b2daff2e580c12316e457473d68f15ee6f66" + integrity sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA== + dependencies: + undici-types "~5.26.4" + "@types/prettier@^2.1.5": version "2.7.0" resolved "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.0.tgz" From 3fedb35ef4d7b421ab8c07d5893b6803d3867ff5 Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 22:01:13 -0300 Subject: [PATCH 08/18] refactor: rename events file --- lib/core/events.ts | 218 ++++++++++++++++++++++++++---------------- lib/core/events.v2.ts | 142 --------------------------- 2 files changed, 136 insertions(+), 224 deletions(-) delete mode 100644 lib/core/events.v2.ts diff --git a/lib/core/events.ts b/lib/core/events.ts index 465aebf..3775fbb 100644 --- a/lib/core/events.ts +++ b/lib/core/events.ts @@ -1,88 +1,142 @@ -import { EventHandler, IAggregate, IDispatchOptions, IDomainEvent, IEvent, IIterator, UID } from "../types"; -import Iterator from "./iterator"; +import { Metrics, Event, Options, Handler, PromiseHandler } from "../types"; -/** - * @description Domain Events manager for global events. - * @global events for aggregates. - * @ignore events added in instance of aggregates directly. - */ - export abstract class DomainEvents { - public static events: IIterator>> = Iterator.create(); +export default class TsEvents { + private _metrics: Metrics; + private _events: Event[]; + private totalDispatched: number; - /** - * @description Add event to state. - * @param param event to be added. - */ - public static addEvent({ event, replace }: IEvent>) { - const target = Reflect.getPrototypeOf(event.callback); - const eventName = event.callback?.eventName ?? target?.constructor.name as string; - if (!!replace) DomainEvents.deleteEvent({ eventName, id: event.aggregate.id }); - event.callback.eventName = eventName; - DomainEvents.events.addToEnd(event); - } + /** + * Creates an instance of Events for aggregate. + */ + constructor(private readonly aggregate: T) { + this._events = []; + this.totalDispatched = 0; + this._metrics = { + totalDispatched: (): number => this.totalDispatched, + totalEvents: (): number => this._events.length + } + } - /** - * @description Dispatch event for a provided name and an aggregate id. - * @param options params to find event to dispatch it. - * @returns promise void. - */ - public static async dispatch(options: IDispatchOptions, handler?: EventHandler, void>): Promise { - const log = (): void => console.log('None handler provided'); - const callback: EventHandler, void> = handler ? handler : ({ execute: (): void => { log(); } }); - const eventsToDispatch: Array>> = []; - const events = DomainEvents.events.toArray(); - let position = 0; - while (events[position]) { - const event = events[position]; - if (event.aggregate.id.equal(options.id) && event.callback.eventName === options.eventName) { - eventsToDispatch.push(event); - DomainEvents.events.removeItem(event); - } - position = position + 1; - } - eventsToDispatch.forEach((agg): void | Promise => agg.callback.dispatch(agg, callback)); - } + /** + * Getter method for accessing metrics related to events. + * @returns {Metrics} Metrics related to events. + */ + get metrics(): Metrics { + return this._metrics; + } - /** - * @description Dispatch event for a provided name and an aggregate id. - * @param id aggregate id. - * @returns promise void. - */ - public static async dispatchAll(id: UID, handler?: EventHandler, void>): Promise { - const log = (): void => console.log('None handler provided'); - const callback: EventHandler, void> = handler ? handler : ({ execute: (): void => { log(); } }); - const eventsToDispatch: Array>> = []; - const events = DomainEvents.events.toArray(); - let position = 0; - while (events[position]) { - const event = events[position]; - if (event.aggregate.id.equal(id)) { - eventsToDispatch.push(event); - DomainEvents.events.removeItem(event); - } - position = position + 1; - } - eventsToDispatch.forEach((agg): void | Promise => agg.callback.dispatch(agg, callback)); - } + /** + * Gets the priority based on the number of events. + * @returns The priority value. + */ + private getPriority(): number { + const totalEvents = this._events.length; + if (totalEvents <= 1) return 2; + return totalEvents; + } - /** - * @description Delete an event from state. - * @param options to find event to be deleted. - */ - public static deleteEvent(options: IDispatchOptions): void { - const events = DomainEvents.events.toArray(); - let position = 0; - while (events[position]) { - const event = events[position]; - const target = Reflect.getPrototypeOf(event.callback); - const eventName = event.callback?.eventName ?? target?.constructor.name; - - if (event.aggregate.id.equal(options.id) && options.eventName === eventName) { - DomainEvents.events.removeItem(event); - } - position = position + 1; - } - } -} + /** + * Gets the default options. + * @returns The default options. + */ + private getDefaultOptions(): Options { + const priority = this.getPriority(); + return { + priority + }; + } + + /** + * Adds a new event. + * @param eventName - The name of the event. + * @param handler - The event handler function. + * @param options - The options for the event. + */ + addEvent(eventName: string, handler: Handler, options?: Options): void { + const defaultOptions = this.getDefaultOptions(); + const opt = options ? options : defaultOptions; + this.validateEventName(eventName); + this.validateHandler(handler, eventName); + this.removeEvent(eventName); + this._events.push({ eventName, handler, options: opt }); + } + + /** + * Validates the event handler. + * @param handler - The event handler function. + * @param eventName - The name of the event. + */ + private validateHandler(handler: Handler, eventName: string): void { + if (typeof handler !== 'function') { + const message = `addEvent: handler for ${eventName} is not a function`; + throw new Error(message); + }; + } + + /** + * Validates the event name. + * @param eventName - The name of the event. + */ + private validateEventName(eventName: string): void { + if (typeof eventName !== 'string' || String(eventName).length < 3) { + const message = `addEvent: invalid event name ${eventName}`; + throw new Error(message); + } + } -export default DomainEvents; + /** + * Clears all events. + */ + clearEvents(): void { + this._events = [] + } + + /** + * Removes an event by name. + * @param eventName - The name of the event to remove. + */ + removeEvent(eventName: string): void { + this._events = this._events.filter( + (event): boolean => event.eventName !== eventName + ); + } + + /** + * Dispatches an event. + * @param eventName - The name of the event to dispatch. + * @param args - Any param user wants provide as argument. + * @returns The result of the event handler function. + */ + dispatchEvent(eventName: string, ...args: any[]): void | Promise { + const _event = this._events.find( + (evt): boolean => evt.eventName === eventName + ); + if (!_event) { + const message = `dispatchEvent: ${eventName} event not found`; + return console.error(message); + }; + this.totalDispatched = this.totalDispatched + 1; + _event.handler(this.aggregate, [_event, ...args]); + this.removeEvent(eventName); + } + + /** + * Dispatches all events. + * @returns A promise that resolves when all promise-based events are completed. + */ + async dispatchEvents(): Promise { + const promisesEvents: PromiseHandler[] = []; + const sorted = this._events.sort( + (a, b): number => a.options.priority - b.options.priority + ); + sorted.forEach((_event): void => { + this.totalDispatched = this.totalDispatched + 1; + const fn = _event.handler(this.aggregate, [_event]); + if (fn instanceof Promise) { + promisesEvents.push(fn as unknown as PromiseHandler); + } + }); + await Promise.all(promisesEvents).catch(console.error); + this.clearEvents(); + } +} diff --git a/lib/core/events.v2.ts b/lib/core/events.v2.ts deleted file mode 100644 index 3775fbb..0000000 --- a/lib/core/events.v2.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { Metrics, Event, Options, Handler, PromiseHandler } from "../types"; - -export default class TsEvents { - private _metrics: Metrics; - private _events: Event[]; - private totalDispatched: number; - - /** - * Creates an instance of Events for aggregate. - */ - constructor(private readonly aggregate: T) { - this._events = []; - this.totalDispatched = 0; - this._metrics = { - totalDispatched: (): number => this.totalDispatched, - totalEvents: (): number => this._events.length - } - } - - /** - * Getter method for accessing metrics related to events. - * @returns {Metrics} Metrics related to events. - */ - get metrics(): Metrics { - return this._metrics; - } - - /** - * Gets the priority based on the number of events. - * @returns The priority value. - */ - private getPriority(): number { - const totalEvents = this._events.length; - if (totalEvents <= 1) return 2; - return totalEvents; - } - - /** - * Gets the default options. - * @returns The default options. - */ - private getDefaultOptions(): Options { - const priority = this.getPriority(); - return { - priority - }; - } - - /** - * Adds a new event. - * @param eventName - The name of the event. - * @param handler - The event handler function. - * @param options - The options for the event. - */ - addEvent(eventName: string, handler: Handler, options?: Options): void { - const defaultOptions = this.getDefaultOptions(); - const opt = options ? options : defaultOptions; - this.validateEventName(eventName); - this.validateHandler(handler, eventName); - this.removeEvent(eventName); - this._events.push({ eventName, handler, options: opt }); - } - - /** - * Validates the event handler. - * @param handler - The event handler function. - * @param eventName - The name of the event. - */ - private validateHandler(handler: Handler, eventName: string): void { - if (typeof handler !== 'function') { - const message = `addEvent: handler for ${eventName} is not a function`; - throw new Error(message); - }; - } - - /** - * Validates the event name. - * @param eventName - The name of the event. - */ - private validateEventName(eventName: string): void { - if (typeof eventName !== 'string' || String(eventName).length < 3) { - const message = `addEvent: invalid event name ${eventName}`; - throw new Error(message); - } - } - - /** - * Clears all events. - */ - clearEvents(): void { - this._events = [] - } - - /** - * Removes an event by name. - * @param eventName - The name of the event to remove. - */ - removeEvent(eventName: string): void { - this._events = this._events.filter( - (event): boolean => event.eventName !== eventName - ); - } - - /** - * Dispatches an event. - * @param eventName - The name of the event to dispatch. - * @param args - Any param user wants provide as argument. - * @returns The result of the event handler function. - */ - dispatchEvent(eventName: string, ...args: any[]): void | Promise { - const _event = this._events.find( - (evt): boolean => evt.eventName === eventName - ); - if (!_event) { - const message = `dispatchEvent: ${eventName} event not found`; - return console.error(message); - }; - this.totalDispatched = this.totalDispatched + 1; - _event.handler(this.aggregate, [_event, ...args]); - this.removeEvent(eventName); - } - - /** - * Dispatches all events. - * @returns A promise that resolves when all promise-based events are completed. - */ - async dispatchEvents(): Promise { - const promisesEvents: PromiseHandler[] = []; - const sorted = this._events.sort( - (a, b): number => a.options.priority - b.options.priority - ); - sorted.forEach((_event): void => { - this.totalDispatched = this.totalDispatched + 1; - const fn = _event.handler(this.aggregate, [_event]); - if (fn instanceof Promise) { - promisesEvents.push(fn as unknown as PromiseHandler); - } - }); - await Promise.all(promisesEvents).catch(console.error); - this.clearEvents(); - } -} From 32457680cb5fdecc06300f8ceea996753a66e837 Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 22:02:40 -0300 Subject: [PATCH 09/18] refactor: change domain event and types --- lib/core/domain-event.ts | 17 --------------- lib/types.ts | 47 +--------------------------------------- 2 files changed, 1 insertion(+), 63 deletions(-) delete mode 100644 lib/core/domain-event.ts diff --git a/lib/core/domain-event.ts b/lib/core/domain-event.ts deleted file mode 100644 index 31ad784..0000000 --- a/lib/core/domain-event.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IDomainEvent, IHandle } from "../types"; - -/** - * @description Domain Event with state. - */ - export class DomainEvent implements IDomainEvent { - public aggregate!: T; - public createdAt!: Date; - public callback: IHandle; - constructor(aggregate: T, callback: IHandle){ - this.aggregate = aggregate; - this.createdAt = new Date(); - this.callback = callback; - } -} - -export default DomainEvent; diff --git a/lib/types.ts b/lib/types.ts index f233d09..e285572 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -179,51 +179,6 @@ export interface IPublicHistory { export type IPropsValidation = { [P in keyof Required]: (value: T[P]) => boolean }; -/** - * @description Domain Events Params - * @param aggregate the entity to add the event. - * @param createdAt the current date time the event was created. - * @param callback the event handler to be executed on dispatch. - */ -export interface IDomainEvent { - aggregate: T; - createdAt: Date; - callback: IHandle; -} - -/** - * @description Define a handler to be executed on dispatch an event. - * @var eventName is optional value as string or undefine. - * @method dispatch is the method to be executed on dispatch the event. - */ -export interface IHandle { - /** - * @description eventName is optional value. Default is className - */ - eventName?: string; - dispatch(event: IDomainEvent, handler: EventHandler): Promise | void; -} - -/** - * @description Options to dispatch some event. - * @param eventName is the value defined on handler. Default is the class name. - * @param id is the ID to identify the aggregate on state. - */ -export interface IDispatchOptions { - eventName: string; - id: UID; -} - -/** - * @description Event options - */ -export interface IEvent { - event: IDomainEvent; - replace?: boolean; -} - -export type IReplaceOptions = 'REPLACE_DUPLICATED' | 'UPDATE' | 'KEEP'; - export interface IAdapter { build(target: F): IResult; } @@ -256,7 +211,7 @@ export interface IAggregate { hashCode(): UID; isNew(): boolean; clone(): IEntity; - addEvent(event: IHandle>, replace?: IReplaceOptions): void; + addEvent(eventName: string, handler: Handler, options?: Options): void; deleteEvent(eventName: string): void; } From 19a9653dada614b6496558c2060840ffa89f4d4b Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 22:03:02 -0300 Subject: [PATCH 10/18] refactor: change domain event on aggregate --- lib/core/aggregate.ts | 83 ++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 52 deletions(-) diff --git a/lib/core/aggregate.ts b/lib/core/aggregate.ts index df1615e..ad5ea29 100644 --- a/lib/core/aggregate.ts +++ b/lib/core/aggregate.ts @@ -1,5 +1,6 @@ -import { EntityProps, EventHandler, EventMetrics, IAggregate, IDomainEvent, IHandle, IReplaceOptions, IResult, ISettings, UID } from "../types"; -import DomainEvent from "./domain-event"; +import { IResult, ISettings, Options, UID } from "../types"; +import { EntityProps, EventMetrics, Handler, IAggregate } from "../types"; +import TsEvent from "./events"; import Entity from "./entity"; import ID from "./id"; import Result from "./result"; @@ -8,13 +9,14 @@ import Result from "./result"; * @description Aggregate identified by an id */ export class Aggregate extends Entity implements IAggregate { - private _domainEvents: Array>>; + private _domainEvents: TsEvent; private _dispatchEventsAmount: number; - constructor(props: Props, config?: ISettings, events?: Array>>) { + constructor(props: Props, config?: ISettings, events?: TsEvent>) { super(props, config); this._dispatchEventsAmount = 0; - this._domainEvents = Array.isArray(events) ? events : []; + this._domainEvents = new TsEvent(this); + if(events) this._domainEvents = events as unknown as TsEvent; } /** @@ -38,8 +40,8 @@ export class Aggregate extends Entity implemen */ get eventsMetrics(): EventMetrics { return { - current: this._domainEvents.length, - total: this._domainEvents.length + this._dispatchEventsAmount, + current: this._domainEvents.metrics.totalEvents(), + total: this._domainEvents.metrics.totalEvents() + this._dispatchEventsAmount, dispatch: this._dispatchEventsAmount } } @@ -53,7 +55,7 @@ export class Aggregate extends Entity implemen */ clone(props?: Partial & { copyEvents?: boolean }): this { const _props = props ? { ...this.props, ...props } : { ...this.props }; - const _events = (props && !!props.copyEvents) ? this._domainEvents : []; + const _events = (props && !!props.copyEvents) ? this._domainEvents : null; const instance = Reflect.getPrototypeOf(this); const args = [_props, this.config, _events]; const aggregate = Reflect.construct(instance!.constructor, args); @@ -62,20 +64,12 @@ export class Aggregate extends Entity implemen /** * @description Dispatch event added to aggregate instance - * @param eventName optional event name as string. If provided only event match name is called. + * @param eventName optional event name as string. * @returns Promise void as executed event */ - dispatchEvent(eventName?: string, handler?: EventHandler, void>): void { - if (!eventName) return this.dispatchAll(handler); - - const callback = handler || ({ execute: (): void => { } }); - for (const event of this._domainEvents) { - if (event.aggregate.id.equal(this.id) && event.callback.eventName === eventName) { - this._dispatchEventsAmount++; - event.callback.dispatch(event, callback); - this.deleteEvent(eventName!); - } - } + dispatchEvent(eventName: string): void | Promise { + this._domainEvents.dispatchEvent(eventName); + this._dispatchEventsAmount++; } /** @@ -83,15 +77,10 @@ export class Aggregate extends Entity implemen * @param handler as EventHandler. * @returns promise void. */ - dispatchAll(handler?: EventHandler) { - const callback = handler || ({ execute: (): void => { } }); - for (const event of this._domainEvents) { - if (event.aggregate.id.equal(this.id)) { - this._dispatchEventsAmount++; - event.callback.dispatch(event, callback); - } - } - this._domainEvents = []; + async dispatchAll(): Promise { + const current = this._domainEvents.metrics.totalEvents(); + await this._domainEvents.dispatchEvents(); + this._dispatchEventsAmount = this._dispatchEventsAmount + current; }; /** @@ -100,24 +89,18 @@ export class Aggregate extends Entity implemen * @returns void. */ clearEvents(config = { resetMetrics: false }): void { - this._dispatchEventsAmount = config.resetMetrics ? 0 : this._dispatchEventsAmount; - this._domainEvents = []; + if (config.resetMetrics) this._dispatchEventsAmount = 0; + this._domainEvents.clearEvents(); }; - /** - * @description Add event to aggregate instance. - * @param eventToAdd Event to be dispatched. - * @param replace 'REPLACE_DUPLICATED' option to remove old event with the same name and id. - * @emits dispatch to aggregate instance. Do not use event using global event manager as DomainEvent.dispatch - */ - addEvent(eventToAdd: IHandle, replace?: IReplaceOptions): void { - const doReplace = replace === 'REPLACE_DUPLICATED'; - const event = new DomainEvent(this, eventToAdd); - const target = Reflect.getPrototypeOf(event.callback); - const eventName = event.callback?.eventName ?? target?.constructor.name as string; - event.callback.eventName = eventName; - if (!!doReplace) this.deleteEvent(eventName); - this._domainEvents.push(event); + /** + * Adds a new event. + * @param eventName - The name of the event. + * @param handler - The event handler function. + * @param options - The options for the event. + */ + addEvent(eventName: string, handler: Handler, options?: Options): void { + this._domainEvents.addEvent(eventName, handler, options); } /** @@ -126,13 +109,9 @@ export class Aggregate extends Entity implemen * @returns number of deleted events */ deleteEvent(eventName: string): number { - let deletedEventsAmount = this._domainEvents.length; - - this._domainEvents = this._domainEvents.filter( - domainEvent => (domainEvent.callback.eventName !== eventName) - ); - - return deletedEventsAmount - this._domainEvents.length; + const totalBefore = this._domainEvents.metrics.totalEvents(); + this._domainEvents.removeEvent(eventName); + return totalBefore - this._domainEvents.metrics.totalEvents(); } public static create(props: any): IResult; From 5806bad44beafdad9adc76abfdc087cf18fe4e4f Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 22:03:53 -0300 Subject: [PATCH 11/18] fix: delete domain events from validator --- lib/utils/validator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/utils/validator.ts b/lib/utils/validator.ts index 26c81b6..605f66f 100644 --- a/lib/utils/validator.ts +++ b/lib/utils/validator.ts @@ -34,6 +34,7 @@ export class Validator { return props instanceof Date; } isObject(props: any): boolean { + delete props?._domainEvents; const isObj = typeof props === 'object'; if (!isObj || props === null) return false; if (JSON.stringify(props) === JSON.stringify({})) return true; From fda6181b29295d7cc22dccc8369bb15745218377 Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 22:04:14 -0300 Subject: [PATCH 12/18] feat: export events --- lib/core/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/core/index.ts b/lib/core/index.ts index 3d6d848..3f432d4 100644 --- a/lib/core/index.ts +++ b/lib/core/index.ts @@ -1,6 +1,5 @@ export * from './aggregate'; export * from './auto-mapper'; -export * from './domain-event'; export * from './entity'; export * from './events'; export * from './getters-and-setters'; @@ -12,4 +11,3 @@ export * from './create-many-domain-instance'; export * from './crypto'; export * from './ok'; export * from './fail'; -export * from './events.v2'; From 68afca92113f388a89cb54f30f61d9009b10752b Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 22:04:36 -0300 Subject: [PATCH 13/18] test: ensure event implementation --- tests/core/aggragate.spec.ts | 156 +++++------------- tests/core/domain-events.spec.ts | 69 -------- .../{events.v2.spec.ts => events.spec.ts} | 4 +- 3 files changed, 44 insertions(+), 185 deletions(-) delete mode 100644 tests/core/domain-events.spec.ts rename tests/core/{events.v2.spec.ts => events.spec.ts} (98%) diff --git a/tests/core/aggragate.spec.ts b/tests/core/aggragate.spec.ts index 3424c06..a3f903a 100644 --- a/tests/core/aggragate.spec.ts +++ b/tests/core/aggragate.spec.ts @@ -1,5 +1,5 @@ import { Aggregate, ID, Ok, Result, ValueObject } from "../../lib/core"; -import { IDomainEvent, IHandle, IResult, ISettings, UID } from "../../lib/types"; +import { IResult, ISettings, UID } from "../../lib/types"; describe('aggregate', () => { @@ -217,39 +217,24 @@ describe('aggregate', () => { }); it('should add domain event [3]', async () => { - - class Handler implements IHandle { - eventName: string = 'Handler Event'; - dispatch(event: IDomainEvent): void | Promise { - console.log(event.aggregate.toObject()); - } - } - const agg = UserAgg.create({ name: 'Jane' }).value(); - agg.addEvent(new Handler(), 'REPLACE_DUPLICATED'); + agg.addEvent('someEvent', () => { + console.log('event'); + }); expect(agg.eventsMetrics.current).toBe(1); - agg.deleteEvent('Handler Event'); + agg.deleteEvent('someEvent'); expect(agg.eventsMetrics.current).toBe(0); }); it('should dispatch domain event from aggregate', async () => { - - class Handler implements IHandle { - public eventName?: string | undefined; - constructor() { - this.eventName = "hello" - } - dispatch(event: IDomainEvent): void | Promise { - console.log(event.aggregate.toObject()); - } - } - const agg = UserAgg.create({ name: 'Jane' }).value(); - agg.addEvent(new Handler()); + agg.addEvent('hello', (agg) => { + console.log(agg.get('name')); + }); expect(agg.eventsMetrics.total).toBe(1); @@ -258,76 +243,29 @@ describe('aggregate', () => { expect(agg.eventsMetrics.current).toBe(0); }); - it('should dispatch all domain events from aggregate', () => { - - class HandlerA implements IHandle { - public eventName?: string | undefined; - constructor() { - this.eventName = "helloA" - } - dispatch(event: IDomainEvent): void | Promise { - console.log(event.aggregate.toObject()); - } - } - class HandlerB implements IHandle { - public eventName?: string | undefined; - constructor() { - this.eventName = "helloB" - } - dispatch(event: IDomainEvent): void | Promise { - console.log(event.aggregate.toObject()); - } - } - + it('should dispatch all domain events from aggregate', async () => { const agg = UserAgg.create({ name: 'Jane' }).value(); - agg.addEvent(new HandlerA()); - agg.addEvent(new HandlerB()); + agg.addEvent('event1', () => {}); + agg.addEvent('event2', () => {}); expect(agg.eventsMetrics.current).toBe(2); - agg.dispatchEvent(); - - expect(agg.eventsMetrics.current).toBe(0); - }); - - it('should add domain event [2]', async () => { - - class Handler implements IHandle { - eventName: string = 'Handler Event'; - dispatch(event: IDomainEvent): void | Promise { - console.log(event.aggregate.toObject()); - } - } - - const agg = UserAgg.create({ name: 'Jane' }).value(); - - agg.addEvent(new Handler(), 'REPLACE_DUPLICATED'); - - expect(agg.eventsMetrics.current).toBe(1); - - agg.dispatchEvent('Handler Event') + await agg.dispatchAll(); expect(agg.eventsMetrics.current).toBe(0); + expect(agg.eventsMetrics.dispatch).toBe(2); }); - it('should add domain event [1]', async () => { - - class Handler implements IHandle { - eventName = undefined; - dispatch(event: IDomainEvent): void | Promise { - console.log(event.aggregate.toObject()); - } - } + it('should add domain event [1] with the same name', async () => { const agg = UserAgg.create({ name: 'Jane' }).value(); - agg.addEvent(new Handler(), 'REPLACE_DUPLICATED'); + agg.addEvent('unique', () => {}); + agg.addEvent('unique', () => {}); expect(agg.eventsMetrics.current).toBe(1); - - agg.dispatchEvent(Handler.name) - + await agg.dispatchAll(); expect(agg.eventsMetrics.current).toBe(0); }); @@ -408,41 +346,28 @@ describe('aggregate', () => { it('should dispatch all events once', async () => { class Agg extends Aggregate<{ key: string }> { }; - class Event implements IHandle { - dispatch(): Promise { - return Promise.resolve(); - } - } - - const event = new Event(); - const eventSpy = jest.spyOn(event, 'dispatch'); + const spy = jest.fn(); const agg = Agg.create({ key: 'some' }).value(); - agg.addEvent(event); + agg.addEvent('event', spy); expect(agg.eventsMetrics.current).toBe(1); expect(agg.eventsMetrics.dispatch).toBe(0); - agg.dispatchAll(); + await agg.dispatchAll(); expect(agg.eventsMetrics.current).toBe(0); expect(agg.eventsMetrics.dispatch).toBe(1); - expect(eventSpy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); }); it('should clear events and metrics', async () => { class Agg extends Aggregate<{ key: string }> { }; - class Event implements IHandle { - dispatch(): Promise { - return Promise.resolve(); - } - } - const event = new Event(); const agg = Agg.create({ key: 'some' }).value(); - agg.addEvent(event); + agg.addEvent('event', () => {}, { priority: 1 }); expect(agg.eventsMetrics.current).toBe(1); expect(agg.eventsMetrics.dispatch).toBe(0); @@ -453,10 +378,12 @@ describe('aggregate', () => { expect(agg.eventsMetrics.dispatch).toBe(0); const aggB = Agg.create({ key: 'some' }).value(); - aggB.addEvent(event); - aggB.dispatchEvent(Event.name); + + aggB.addEvent('event1', () => {}); + aggB.dispatchEvent('event1'); expect(aggB.eventsMetrics.dispatch).toBe(1); - aggB.addEvent(event); + + aggB.addEvent('event2', () => {}); expect(aggB.eventsMetrics.current).toBe(1); expect(aggB.eventsMetrics.dispatch).toBe(1); @@ -475,21 +402,12 @@ describe('aggregate', () => { } }; - class Event implements IHandle { - eventName: string; - constructor(name: string) { - this.eventName = name; - } - dispatch(): Promise { - return Promise.resolve(); - } - } - const eventA = new Event('eventA'); - const eventB = new Event('eventA'); + const agg = Agg.create({ key: 'some' }).value(); - agg.addEvent(eventA); - agg.addEvent(eventB); + + agg.addEvent('eventA', () => {}); + agg.addEvent('eventB', () => {}); expect(agg.eventsMetrics.current).toBe(2); expect(agg.eventsMetrics.dispatch).toBe(0); @@ -511,7 +429,7 @@ describe('aggregate', () => { }); describe('toObject', () => { - it('should infer types to aggregate on toObject method', () => { + it('should infer types to aggregate on toObject method', async () => { class Name extends ValueObject<{ value: string }>{ private constructor(props: { value: string }) { @@ -545,6 +463,16 @@ describe('aggregate', () => { const props: Props = { name, additionalInfo: ['from brazil'], price: 10 }; const orange = Product.create(props).value(); + orange.addEvent('create', () => { + console.log('make a juice'); + }) + + orange.addEvent('save', () => { + console.log('make a juice'); + }, { priority: 1 }) + + await orange.dispatchAll(); + const object = orange.toObject(); expect(object.additionalInfo).toEqual(['from brazil']); expect(object.name).toBe('orange'); diff --git a/tests/core/domain-events.spec.ts b/tests/core/domain-events.spec.ts deleted file mode 100644 index 58dc178..0000000 --- a/tests/core/domain-events.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Aggregate, DomainEvent, DomainEvents } from '../../lib/core'; -import { EventHandler, IDomainEvent, IHandle } from '../../lib/types'; - -describe('domain-events', () => { - - interface Props { - id?: string; - name: string; - } - - class User extends Aggregate{ - private constructor(props: Props) { - super(props) - } - } - - class Handle implements IHandle { - eventName: string = 'Handler'; - dispatch(event: IDomainEvent, handler: EventHandler): void | Promise { - console.log(event.aggregate.hashCode().value()); - const { aggregate } = event; - const eventName = this.eventName; - handler.execute({ aggregate, eventName }); - } - } - - const user = User.create({ name: 'Jane' }).value(); - - const event = new DomainEvent(user, new Handle()); - - it('should add domain event with success', () => { - - DomainEvents.addEvent({ event, replace: true }); - - DomainEvents.addEvent({ event, replace: true }); - - expect(DomainEvents.events.total()).toBe(1); - - DomainEvents.dispatch({ eventName: 'Handler', id: user.id }); - - expect(DomainEvents.events.total()).toBe(0); - }); - - it('should add domain and do not replace event with success', () => { - - DomainEvents.addEvent({ event, replace: false }); - - DomainEvents.addEvent({ event, replace: false }); - - expect(DomainEvents.events.total()).toBe(2); - - DomainEvents.dispatch({ eventName: 'Handler', id: user.id }); - - expect(DomainEvents.events.total()).toBe(0); - }); - - it('should dispatch all by id', () => { - - DomainEvents.addEvent({ event, replace: false }); - - DomainEvents.addEvent({ event, replace: false }); - - expect(DomainEvents.events.total()).toBe(2); - - DomainEvents.dispatchAll(user.id); - - expect(DomainEvents.events.total()).toBe(0); - }); -}); diff --git a/tests/core/events.v2.spec.ts b/tests/core/events.spec.ts similarity index 98% rename from tests/core/events.v2.spec.ts rename to tests/core/events.spec.ts index 3d731b9..ebc21fe 100644 --- a/tests/core/events.v2.spec.ts +++ b/tests/core/events.spec.ts @@ -1,6 +1,6 @@ -import TsEvents from '../../lib/core/events.v2'; +import TsEvents from '../../lib/core/events'; -describe('events.v2', () => { +describe('events', () => { it('should create instance with success', () => { const instance = new TsEvents({ name: 'Jane', age: 21 }); From 05789e3dac7b9094b350c98c051d4ff95474425a Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 22:34:42 -0300 Subject: [PATCH 14/18] docs: update readme --- README.md | 46 +++++----------------------------------------- 1 file changed, 5 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 560d455..086d98f 100644 --- a/README.md +++ b/README.md @@ -1673,28 +1673,6 @@ export default Product; #### Domain Event Let's create an aggregate instance and see how to add domain event to it. - -```ts - -export class ProductCreatedEvent implements IHandle{ - public eventName: string; - - constructor() { - this.eventName = 'ProductCreated'; - } - - dispatch(event: IDomainEvent): void { - // your action here - const { aggregate } = event; - console.log(`EVENT DISPATCH: ${aggregate.hashCode().value()}`); - } -} - -export default ProductCreatedEvent; - -``` - -Now let's add the event to a product instance.
Events are stored in memory and are deleted after being triggered. ```ts @@ -1703,27 +1681,13 @@ const result = Product.create({ name, price }); const product = result.value(); -const event = new ProductCreatedEvent(); +product.addEvent('eventName', (product) => { + console.log(product.toObject()) +}); -product.addEvent(event); - -``` - -Now we can dispatch the event whenever we want. - -```ts - -DomainEvents.dispatch({ id: product.id, eventName: "ProductCreated" }); - -> "EVENT DISPATCH: [Aggregate@Product]:6039756f-d3bc-452e-932a-ec89ff536dda" - -// OR you can dispatch all events in aggregate instance - -product.dispatchEvent(); - -// OR +// dispatch from aggregate instance -product.dispatchEvent("ProductCreated"); +product.dispatchEvent("eventName"); ``` --- From 88555edf75ad1df41f60af070c7c4c71d8210020 Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Sun, 17 Mar 2024 22:38:02 -0300 Subject: [PATCH 15/18] ci: set version --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 102d83f..e45e5e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. --- -### [1.20.0] - 2024-03-17 +### [1.20.0-beta] - 2024-03-17 ### Changed diff --git a/package.json b/package.json index e4c7307..8e026ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rich-domain", - "version": "1.19.2", + "version": "1.20.0-beta", "description": "This package provide utils file and interfaces to assistant build a complex application with domain driving design", "main": "index.js", "types": "index.d.ts", From 41b54a999afac7d3d69d779ca837134926ed6a70 Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Mon, 18 Mar 2024 01:31:32 -0300 Subject: [PATCH 16/18] feat: added domain event handler #120 --- CHANGELOG.md | 39 ++++++++++++++++++ lib/core/aggregate.ts | 40 ++++++++++++------ lib/core/events.ts | 4 +- lib/types.ts | 79 +++++++++++++++++++++++++++++------- tests/core/aggragate.spec.ts | 65 ++++++++++++++++++++++++----- tests/core/events.spec.ts | 41 +++++++++++++++++++ 6 files changed, 228 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e45e5e3..adc56a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,45 @@ All notable changes to this project will be documented in this file. ## Unreleased +--- +### [1.20.1-beta] - 2024-03-18 + +### Updates + +Implemented a new event handling mechanism using the Handler class. +Example: + +```ts + +// implement extending to EventHandler +class Handler extends EventHandler { + constructor() { super({ eventName: 'sample' }) }; + + dispatch(product: Product, args_1: [DEvent, any[]]): void | Promise { + const model = product.toObject(); + const [event, args] = args_1; + + console.log(model); + console.log(event); + console.log(event.eventName); + console.log(event.options); + // custom params provided on call dispatchEvent + console.log(args); + } +} + +const event = new Handler(); +orange.addEvent(event); + +await orange.dispatchEvent('sample', { custom: 'params' }); + +``` + +### Bug Fixes +Fixed an issue with the event dispatching mechanism. +Notes +This version introduces significant changes to the event handling system, enhancing the flexibility and usability of the Aggregate class. + --- ### [1.20.0-beta] - 2024-03-17 diff --git a/lib/core/aggregate.ts b/lib/core/aggregate.ts index ad5ea29..bc094da 100644 --- a/lib/core/aggregate.ts +++ b/lib/core/aggregate.ts @@ -1,4 +1,4 @@ -import { IResult, ISettings, Options, UID } from "../types"; +import { EventHandler, IResult, ISettings, Options, UID } from "../types"; import { EntityProps, EventMetrics, Handler, IAggregate } from "../types"; import TsEvent from "./events"; import Entity from "./entity"; @@ -16,7 +16,7 @@ export class Aggregate extends Entity implemen super(props, config); this._dispatchEventsAmount = 0; this._domainEvents = new TsEvent(this); - if(events) this._domainEvents = events as unknown as TsEvent; + if (events) this._domainEvents = events as unknown as TsEvent; } /** @@ -67,8 +67,8 @@ export class Aggregate extends Entity implemen * @param eventName optional event name as string. * @returns Promise void as executed event */ - dispatchEvent(eventName: string): void | Promise { - this._domainEvents.dispatchEvent(eventName); + dispatchEvent(eventName: string, ...args: any[]): void | Promise { + this._domainEvents.dispatchEvent(eventName, args); this._dispatchEventsAmount++; } @@ -93,16 +93,30 @@ export class Aggregate extends Entity implemen this._domainEvents.clearEvents(); }; - /** - * Adds a new event. - * @param eventName - The name of the event. - * @param handler - The event handler function. - * @param options - The options for the event. - */ - addEvent(eventName: string, handler: Handler, options?: Options): void { - this._domainEvents.addEvent(eventName, handler, options); - } + /** + * Adds a new event. + * @param event - The event object containing the event name, handler, and options. + */ + addEvent(event: EventHandler): void; + + /** + * Adds a new event. + * @param eventName - The name of the event. + * @param handler - The event handler function. + * @param options - The options for the event. + */ + addEvent(eventName: string, handler: Handler, options?: Options): void; + addEvent(eventNameOrEvent: string | EventHandler, handler?: Handler, options?: Options): void { + if (typeof eventNameOrEvent === 'string' && handler) { + this._domainEvents.addEvent(eventNameOrEvent, handler! ?? null, options); + return; + } + const _options = (eventNameOrEvent as EventHandler)?.params?.options; + const eventName = (eventNameOrEvent as EventHandler)?.params?.eventName; + const eventHandler = (eventNameOrEvent as EventHandler)?.dispatch; + this._domainEvents.addEvent(eventName, eventHandler! ?? null, _options); + } /** * @description Delete event match with provided name * @param eventName event name as string diff --git a/lib/core/events.ts b/lib/core/events.ts index 3775fbb..b6ee62c 100644 --- a/lib/core/events.ts +++ b/lib/core/events.ts @@ -1,8 +1,8 @@ -import { Metrics, Event, Options, Handler, PromiseHandler } from "../types"; +import { Metrics, DEvent, Options, Handler, PromiseHandler } from "../types"; export default class TsEvents { private _metrics: Metrics; - private _events: Event[]; + private _events: DEvent[]; private totalDispatched: number; /** diff --git a/lib/types.ts b/lib/types.ts index e285572..05246fc 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -40,7 +40,6 @@ export interface IResult { */ export type Payload = IResult; export type HandlerPayload = { aggregate: T, eventName: string }; -export type EventHandler = ICommand, Promise | B>; /** * @@ -211,7 +210,6 @@ export interface IAggregate { hashCode(): UID; isNew(): boolean; clone(): IEntity; - addEvent(eventName: string, handler: Handler, options?: Options): void; deleteEvent(eventName: string): void; } @@ -257,21 +255,23 @@ export interface EventMetrics { /** * Interface representing an event. */ -export interface Event { - eventName: string; - handler: Handler; - options: Options; +export interface DEvent { + eventName: string; + handler: Handler; + options: Options; } +export type HandlerArgs = [T, [DEvent, ...any[]]] + /** * Represents a promise-based event handler. */ -export type PromiseHandler = (...args: [T, [Event, ...any[]]]) => Promise; +export type PromiseHandler = (...args: HandlerArgs) => Promise; /** * Represents a normal event handler. */ -export type NormalHandler = (...args: [T, [Event, ...any[]]]) => void; +export type NormalHandler = (...args: HandlerArgs) => void; /** * Represents an event handler, which can be either a promise-based handler or a normal handler. @@ -282,7 +282,7 @@ export type Handler = PromiseHandler | NormalHandler; * Interface representing options for an event. */ export interface Options { - priority: number; + priority: number; } /** @@ -290,15 +290,64 @@ export interface Options { * @interface Metrics */ export interface Metrics { + /** + * A function that returns the total number of events. + * @returns {number} The total number of events. + */ + totalEvents: () => number; + + /** + * A function that returns the total number of dispatched events. + * @returns {number} The total number of dispatched events. + */ + totalDispatched: () => number; +} + +/** + * Parameters for defining an event. + */ +export interface EventParams { + /** + * The name of the event. + */ + eventName: string; + /** + * Additional options for the event. + */ + options?: Options; +} + + +/** + * Abstract class representing an event handler. + * @template T - The type of aggregate this event handler is associated with. + */ +export abstract class EventHandler { + /** + * Creates an instance of EventHandler. + * @param {EventParams} params - Parameters for the event handler. + * @throws {Error} If params.eventName is not provided as a string. + */ + constructor(public readonly params: EventParams) { + if (typeof params?.eventName !== 'string') { + throw new Error('params.eventName is required as string'); + } + } + /** - * A function that returns the total number of events. - * @returns {number} The total number of events. + * Dispatches the event with the provided arguments. + * @abstract + * @param {T} aggregate - The aggregate associated with the event. + * @param {[DEvent, any[]]} args - Arguments for the event dispatch. + * @returns {Promise | void} A Promise if the event dispatch is asynchronous, otherwise void. */ - totalEvents: () => number; + abstract dispatch(aggregate: T, args: [DEvent, any[]]): Promise | void; /** - * A function that returns the total number of dispatched events. - * @returns {number} The total number of dispatched events. + * Dispatches the event with the provided arguments. + * @abstract + * @param {...HandlerArgs} args - Arguments for the event dispatch. + * @returns {Promise | void} A Promise if the event dispatch is asynchronous, otherwise void. */ - totalDispatched: () => number; + abstract dispatch(...args: HandlerArgs): Promise | void; } diff --git a/tests/core/aggragate.spec.ts b/tests/core/aggragate.spec.ts index a3f903a..9ce6a20 100644 --- a/tests/core/aggragate.spec.ts +++ b/tests/core/aggragate.spec.ts @@ -1,5 +1,5 @@ import { Aggregate, ID, Ok, Result, ValueObject } from "../../lib/core"; -import { IResult, ISettings, UID } from "../../lib/types"; +import { DEvent, EventHandler, IResult, ISettings, UID } from "../../lib/types"; describe('aggregate', () => { @@ -246,8 +246,8 @@ describe('aggregate', () => { it('should dispatch all domain events from aggregate', async () => { const agg = UserAgg.create({ name: 'Jane' }).value(); - agg.addEvent('event1', () => {}); - agg.addEvent('event2', () => {}); + agg.addEvent('event1', () => { }); + agg.addEvent('event2', () => { }); expect(agg.eventsMetrics.current).toBe(2); @@ -261,8 +261,8 @@ describe('aggregate', () => { const agg = UserAgg.create({ name: 'Jane' }).value(); - agg.addEvent('unique', () => {}); - agg.addEvent('unique', () => {}); + agg.addEvent('unique', () => { }); + agg.addEvent('unique', () => { }); expect(agg.eventsMetrics.current).toBe(1); await agg.dispatchAll(); @@ -367,7 +367,7 @@ describe('aggregate', () => { class Agg extends Aggregate<{ key: string }> { }; const agg = Agg.create({ key: 'some' }).value(); - agg.addEvent('event', () => {}, { priority: 1 }); + agg.addEvent('event', () => { }, { priority: 1 }); expect(agg.eventsMetrics.current).toBe(1); expect(agg.eventsMetrics.dispatch).toBe(0); @@ -379,11 +379,11 @@ describe('aggregate', () => { const aggB = Agg.create({ key: 'some' }).value(); - aggB.addEvent('event1', () => {}); + aggB.addEvent('event1', () => { }); aggB.dispatchEvent('event1'); expect(aggB.eventsMetrics.dispatch).toBe(1); - aggB.addEvent('event2', () => {}); + aggB.addEvent('event2', () => { }); expect(aggB.eventsMetrics.current).toBe(1); expect(aggB.eventsMetrics.dispatch).toBe(1); @@ -406,8 +406,8 @@ describe('aggregate', () => { const agg = Agg.create({ key: 'some' }).value(); - agg.addEvent('eventA', () => {}); - agg.addEvent('eventB', () => {}); + agg.addEvent('eventA', () => { }); + agg.addEvent('eventB', () => { }); expect(agg.eventsMetrics.current).toBe(2); expect(agg.eventsMetrics.dispatch).toBe(0); @@ -479,4 +479,49 @@ describe('aggregate', () => { expect(object.price).toBe(10); }); }); + + it('should add event as class', async () => { + interface Props { + id?: UID; + price: number; + name: string; + createdAt?: Date; + updatedAt?: Date; + }; + + class Product extends Aggregate{ + private constructor(props: Props) { + super(props) + } + public static create(props: Props): Result { + return Ok(new Product(props)); + } + } + + const props: Props = { name: 'Orange', price: 1.21 }; + const orange = Product.create(props).value(); + + class Handler extends EventHandler { + constructor() { super({ eventName: 'event' }) }; + + dispatch(product: Product, args_1: [DEvent, any[]]): void | Promise { + const model = product.toObject(); + const [event, args] = args_1; + + console.log(model); + console.log(event); + console.log(event.eventName); + console.log(event.options); + console.log(args); + } + + } + + const event = new Handler(); + orange.addEvent(event); + + await orange.dispatchEvent('event', { custom: 'params' }); + expect(orange.eventsMetrics.dispatch).toBe(1); + + }); }); diff --git a/tests/core/events.spec.ts b/tests/core/events.spec.ts index ebc21fe..4eba424 100644 --- a/tests/core/events.spec.ts +++ b/tests/core/events.spec.ts @@ -1,4 +1,5 @@ import TsEvents from '../../lib/core/events'; +import { EventHandler } from '../../lib/types'; describe('events', () => { @@ -164,4 +165,44 @@ describe('events', () => { expect(instance.metrics.totalDispatched()).toBe(3); expect(instance.metrics.totalEvents()).toBe(0); }); + + it('should call event adding a class handler', () => { + + let params = null; + + class Handler extends EventHandler<{ name: string, age: number }> { + + constructor() { + super({ eventName: 'test' }) + } + + dispatch(...args: any): void | Promise { + params = args; + } + } + + const instance = new TsEvents({ name: 'Jane', age: 21 }); + const event = new Handler(); + + instance.addEvent(event.params.eventName, event.dispatch); + + instance.dispatchEvent('test'); + expect(params).toMatchObject( + [ + { + "age": 21, + "name": "Jane", + }, + [ + { + "eventName": "test", + "handler": event.dispatch, + "options": { + "priority": 2, + }, + }, + ], + ] + ); + }) }); From 8f58b1fc5153f426d21246d7f8529d0a7a77e49e Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Mon, 18 Mar 2024 01:36:18 -0300 Subject: [PATCH 17/18] docs: update readme --- README.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 086d98f..96e1ade 100644 --- a/README.md +++ b/README.md @@ -1685,6 +1685,20 @@ product.addEvent('eventName', (product) => { console.log(product.toObject()) }); +// or alternatively you can create a event handler + +class Handler extends EventHandler { + constructor() { super({ eventName: 'eventName' }) }; + + dispatch(product: Product): void { + const model = product.toObject(); + console.log(model); + } +} + +// add instance to aggregate +product.addEvent(new Handler()); + // dispatch from aggregate instance product.dispatchEvent("eventName"); diff --git a/package.json b/package.json index 8e026ee..d599cca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rich-domain", - "version": "1.20.0-beta", + "version": "1.20.1-beta", "description": "This package provide utils file and interfaces to assistant build a complex application with domain driving design", "main": "index.js", "types": "index.d.ts", From 09a26d6d8e740d8c46ce72de90e750c8a1037b5e Mon Sep 17 00:00:00 2001 From: 4lessandrodev Date: Mon, 18 Mar 2024 09:34:40 -0300 Subject: [PATCH 18/18] docs: update readme #120 --- CHANGELOG.md | 98 +++++++++++++++++++++++++++++++++++----------------- package.json | 2 +- 2 files changed, 67 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adc56a3..dc0e4e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,42 @@ All notable changes to this project will be documented in this file. ## Unreleased --- -### [1.20.1-beta] - 2024-03-18 -### Updates +### [1.20.0] - 2024-03-17 + +### Changed + +- change: modify the to add and dispatch event on aggregate +- added: Implemented a new instance of the event management class, allowing for better event handling and organization. + +**Details:** +Introduced a new method to create an event management instance for the aggregate. +Improved event handling and organization, enhancing the overall performance and efficiency of event management. +Enabled easier integration and usage of event management features in various applications. + +```ts + +import { TsEvents } from 'rich-domain'; + +/** + * Create a new instance of the event management class + * Pass the aggregate as a parameter + * Return an event management instance for the aggregate + */ +const events = new TsEvents(aggregate); + +events.addEvent('eventName', (...args) => { + console.log(args); +}); + +// dispatch all events +await events.dispatchEvents(); + +// OR dispatch a specific event +events.dispatchEvent('eventName'); + +``` + Implemented a new event handling mechanism using the Handler class. Example: @@ -16,25 +49,28 @@ Example: // implement extending to EventHandler class Handler extends EventHandler { - constructor() { super({ eventName: 'sample' }) }; + + constructor() { + super({ eventName: 'sample' }); + }; - dispatch(product: Product, args_1: [DEvent, any[]]): void | Promise { + dispatch(product: Product, params: [DEvent, any[]]): void | Promise { const model = product.toObject(); - const [event, args] = args_1; + const [event, args] = params; console.log(model); console.log(event); - console.log(event.eventName); - console.log(event.options); + // custom params provided on call dispatchEvent console.log(args); } } const event = new Handler(); -orange.addEvent(event); -await orange.dispatchEvent('sample', { custom: 'params' }); +aggregate.addEvent(event); + +aggregate.dispatchEvent('sample', { custom: 'params' }); ``` @@ -43,40 +79,38 @@ Fixed an issue with the event dispatching mechanism. Notes This version introduces significant changes to the event handling system, enhancing the flexibility and usability of the Aggregate class. ---- +### Migrating from v1.19.x -### [1.20.0-beta] - 2024-03-17 +Change event handler implementation -### Changed -- change: modify the to add and dispatch event on aggregate -- added: Implemented a new instance of the event management class, allowing for better event handling and organization. +```ts +// use extends to EventHandler +class Handler extends EventHandler { + + constructor() { + super({ eventName: 'sample' }); + }; + + // aggregate as first param + dispatch(product: Product): void | Promise { + // your implementation + } +} -**Details:** -Introduced a new method to create an event management instance for the aggregate. -Improved event handling and organization, enhancing the overall performance and efficiency of event management. -Enabled easier integration and usage of event management features in various applications. +``` + +Remove imports `DomainEvents` and use events from aggregate instance ```ts -/** - * Create a new instance of the event management class - * Pass the aggregate as a parameter - * Return an event management instance for the aggregate - */ -const events = new Events(aggregate); -events.addEvent('eventName', (...args) => { - console.log('executing event...'); - console.log(args); -}); +aggregate.addEvent(event); -await events.dispatchEvents(); +aggregate.dispatchEvent('eventName'); -// OR +``` -events.dispatchEvent('eventName'); -``` --- diff --git a/package.json b/package.json index d599cca..4dff536 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rich-domain", - "version": "1.20.1-beta", + "version": "1.20.0", "description": "This package provide utils file and interfaces to assistant build a complex application with domain driving design", "main": "index.js", "types": "index.d.ts",