diff --git a/src/core/primitives/aggregate-root.base.ts b/src/core/primitives/aggregate-root.base.ts index 2a52907..73c8efd 100644 --- a/src/core/primitives/aggregate-root.base.ts +++ b/src/core/primitives/aggregate-root.base.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import { DomainEvent } from "./domain-event.base.js"; interface AggregateRootProperties { id: string; @@ -16,6 +17,8 @@ export abstract class AggregateRoot< > { public readonly id: AggregateRootProperties["id"]; private readonly _properties: Properties; + // @TODO: Benchmark the performance on high-scale events (consider using a queue) + private readonly _domainEvents: DomainEvent[] = []; constructor({ id, properties }: CreateAggregateRootProperties) { this.id = id ?? randomUUID(); @@ -29,10 +32,24 @@ export abstract class AggregateRoot< return new this(...args); } + protected commit(event: DomainEvent) { + this._domainEvents.push(event); + } + + private clearDomainEvents() { + this._domainEvents.length = 0; + } + get properties() { return Object.freeze({ id: this.id, ...this._properties, }); } + + pullDomainEvents() { + const domainEvents = [...this._domainEvents]; + this.clearDomainEvents(); + return domainEvents; + } } diff --git a/src/core/primitives/domain-event.base.ts b/src/core/primitives/domain-event.base.ts new file mode 100644 index 0000000..4640056 --- /dev/null +++ b/src/core/primitives/domain-event.base.ts @@ -0,0 +1,24 @@ +import { randomUUID } from "node:crypto"; + +interface CreateDomainEventProps> { + aggregateId: string; + payload: Payload; +} + +export abstract class DomainEvent> { + readonly id: string; + readonly metadata: { + // @TODO: Implement 'correlationId' in the future + // correlationId: string; + emittedByAggregateId: string; + }; + readonly payload: Payload; + + constructor(properties: CreateDomainEventProps) { + this.id = randomUUID(); + this.metadata = { + emittedByAggregateId: properties.aggregateId, + }; + this.payload = properties.payload; + } +} diff --git a/src/identity-and-access/domain/account/aggregate-root.ts b/src/identity-and-access/domain/account/aggregate-root.ts index c812650..71e6c98 100644 --- a/src/identity-and-access/domain/account/aggregate-root.ts +++ b/src/identity-and-access/domain/account/aggregate-root.ts @@ -1,4 +1,5 @@ import { AggregateRoot } from "@core/primitives/aggregate-root.base.js"; +import { NewAccountRegisteredDomainEvent } from "./events/new-account-registered.js"; interface Properties { email: string; @@ -7,6 +8,15 @@ interface Properties { export class Account extends AggregateRoot { static create(properties: Properties) { - return new Account({ properties }); + const account = new Account({ properties }); + + account.commit( + new NewAccountRegisteredDomainEvent({ + aggregateId: account.id, + payload: account.properties, + }) + ); + + return account; } } diff --git a/src/identity-and-access/domain/account/events/new-account-registered.ts b/src/identity-and-access/domain/account/events/new-account-registered.ts new file mode 100644 index 0000000..5505fe6 --- /dev/null +++ b/src/identity-and-access/domain/account/events/new-account-registered.ts @@ -0,0 +1,5 @@ +import { DomainEvent } from "@core/primitives/domain-event.base.js"; + +export class NewAccountRegisteredDomainEvent extends DomainEvent<{ + email: string; +}> {} diff --git a/src/identity-and-access/domain/password-reset-request/aggregate-root.ts b/src/identity-and-access/domain/password-reset-request/aggregate-root.ts index 70de7d8..1b563cd 100644 --- a/src/identity-and-access/domain/password-reset-request/aggregate-root.ts +++ b/src/identity-and-access/domain/password-reset-request/aggregate-root.ts @@ -1,5 +1,6 @@ import { AggregateRoot } from "@core/primitives/aggregate-root.base.js"; import { DateTime } from "luxon"; +import { PasswordResetRequestedDomainEvent } from "./events/password-reset-requested.domain-event.js"; interface Properties { accountId: string; @@ -14,13 +15,21 @@ interface CreateProperties { export class PasswordResetRequest extends AggregateRoot { static create(properties: CreateProperties) { const token = Math.random().toString(36).slice(2); - - return new PasswordResetRequest({ + const passwordResetRequest = new PasswordResetRequest({ properties: { accountId: properties.accountId, token, expiresAt: DateTime.now().plus({ days: 1 }), }, }); + + passwordResetRequest.commit( + new PasswordResetRequestedDomainEvent({ + aggregateId: passwordResetRequest.id, + payload: passwordResetRequest.properties, + }) + ); + + return passwordResetRequest; } } diff --git a/src/identity-and-access/domain/password-reset-request/events/password-reset-requested.domain-event.ts b/src/identity-and-access/domain/password-reset-request/events/password-reset-requested.domain-event.ts new file mode 100644 index 0000000..54a1bdf --- /dev/null +++ b/src/identity-and-access/domain/password-reset-request/events/password-reset-requested.domain-event.ts @@ -0,0 +1,8 @@ +import { DomainEvent } from "@core/primitives/domain-event.base.js"; +import { DateTime } from "luxon"; + +export class PasswordResetRequestedDomainEvent extends DomainEvent<{ + accountId: string; + expiresAt: DateTime; + token: string; +}> {}