@vytches/ddd-aggregates 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,22 @@
1
1
  import { IAggregateCapability, IAggregateConstructorParams, IAggregateEventHandler, IAggregateRoot } from '../aggregate-interfaces';
2
- import { Capability, CapabilityConstructor, EntityId, IDomainEvent, IEventMetadata } from '../../../contracts/src/index.ts';
2
+ import { Capability, CapabilityConstructor, EntityId, IDomainEvent, IEventMetadata } from '@vytches/ddd-contracts';
3
+ /**
4
+ * VF-023 (D-2, AC5, BREAKING, CRITICAL F-H4): module-private gating token
5
+ * for {@link AggregateRoot._internal_setState}. This is exported from this
6
+ * module (`./core/aggregate-root`) — NOT from the package's public barrel
7
+ * (`index.ts`) — so it is reachable only via a deep import within this
8
+ * package's own source (e.g. `SnapshotCapability`), never via
9
+ * `@vytches/ddd-aggregates`. Consumers cannot obtain this token, and
10
+ * therefore cannot call `_internal_setState` at all: TypeScript rejects the
11
+ * call without it, and even a `as any` cast would need the exact symbol
12
+ * reference, which is not exported publicly.
13
+ *
14
+ * Previously `_internal_setState` was a plain public method reachable via
15
+ * any `as any`/duck-typed cast — no compile-time or run-time barrier
16
+ * prevented domain code from bypassing `apply()`/`loadFromHistory()` and
17
+ * corrupting the version/event-log invariant directly.
18
+ */
19
+ export declare const INTERNAL_STATE_TOKEN: unique symbol;
3
20
  /**
4
21
  * Base class for aggregate roots — the *transactional consistency boundary*
5
22
  * of a Domain-Driven Design model. Per Eric Evans (Blue Book, ch. 6):
@@ -12,7 +29,7 @@ import { Capability, CapabilityConstructor, EntityId, IDomainEvent, IEventMetada
12
29
  *
13
30
  * `AggregateRoot<TId>` provides:
14
31
  *
15
- * - **Identity** via `EntityId<TId>` (`getId()`)
32
+ * - **Identity** via `EntityId<TId>` (`getId()`, `equals()`)
16
33
  * - **Versioning** for optimistic locking (`getVersion()`,
17
34
  * `getInitialVersion()`)
18
35
  * - **Event emission** via `apply()` — append a `DomainEvent`, increment
@@ -102,6 +119,20 @@ export declare class AggregateRoot<TId = string> implements IAggregateRoot<TId>
102
119
  private _version;
103
120
  private _initialVersion;
104
121
  private _domainEvents;
122
+ /**
123
+ * VP-012b (D2, F15/F5): memoized, deep-frozen view returned by
124
+ * {@link getDomainEvents}. Rebuilt lazily on next read only when
125
+ * `_domainEventsCacheDirty` is true — see `_invalidateDomainEventsCache()`.
126
+ */
127
+ private _domainEventsCache;
128
+ /**
129
+ * VP-012b (D2): dirty flag for `_domainEventsCache`. Set true by
130
+ * `_invalidateDomainEventsCache()`, the single helper called from every
131
+ * site that mutates `_domainEvents` (apply, commit, transformDomainEvents,
132
+ * loadFromHistory, _internal_setState). Starts true so the first
133
+ * `getDomainEvents()` call always builds the cache.
134
+ */
135
+ private _domainEventsCacheDirty;
105
136
  private _eventHandlers;
106
137
  private _capabilities;
107
138
  /**
@@ -111,35 +142,141 @@ export declare class AggregateRoot<TId = string> implements IAggregateRoot<TId>
111
142
  * loops or malicious replay payloads.
112
143
  */
113
144
  private readonly _maxEvents;
145
+ /**
146
+ * VF-023 (D-4, AC6, non-breaking addition): controls what happens when
147
+ * `apply()` or `loadFromHistory()` processes an event for which no
148
+ * handler was registered via `registerEventHandler()`. Defaults to
149
+ * `"warn"` (preserves the previous silent-but-logged-nowhere behavior's
150
+ * intent without a breaking default) — the event is still recorded /
151
+ * replayed, but a warning is logged via the library's internal logger.
152
+ * Set to `"throw"` to fail fast instead, useful in tests or strict
153
+ * environments where a missing handler indicates a real bug.
154
+ */
155
+ private readonly _onMissingHandler;
114
156
  constructor(params: IAggregateConstructorParams<TId>);
115
157
  /**
116
158
  * Get the unique identifier of this aggregate
117
159
  */
118
160
  getId(): EntityId<TId>;
119
161
  /**
162
+ * Identity-based equality, matching {@link Entity.equals}. Two aggregates
163
+ * are equal iff they have the same id (compared via `EntityId.equals()`);
164
+ * attribute/version differences are ignored.
165
+ *
166
+ * VF-023 (D-5, AC10, non-breaking addition): `AggregateRoot` does not
167
+ * extend `Entity` (it implements `IAggregateRoot<TId>` directly), so this
168
+ * mirrors `Entity.equals()` rather than inheriting it.
169
+ *
170
+ * @param other - Aggregate to compare against. May be `null` or `undefined`.
171
+ * @returns `true` if both aggregates have the same id; `false` otherwise.
172
+ */
173
+ equals(other: AggregateRoot<TId> | null | undefined): boolean;
174
+ /**
175
+ * The version increments by one each time `apply()` successfully records a
176
+ * domain event; compare it against the persisted version to detect
177
+ * concurrent modification (optimistic locking).
120
178
  *
121
179
  * @returns {number} Current version number for optimistic locking
122
180
  */
123
181
  getVersion(): number;
124
182
  /**
183
+ * The version this aggregate had when it was loaded (or as of the last
184
+ * `commit()`), letting callers work out how many events have been applied
185
+ * since without recomputing from the event log.
125
186
  *
126
187
  * @returns {number} Initial version when aggregate was loaded
127
188
  */
128
189
  getInitialVersion(): number;
129
190
  /**
191
+ * Whether this aggregate has recorded domain events since its last
192
+ * `commit()` — use it to decide whether persistence/dispatch work is
193
+ * needed at all.
130
194
  *
131
195
  * @returns {boolean} True if aggregate has uncommitted domain events
132
196
  */
133
197
  hasChanges(): boolean;
134
198
  /**
199
+ * VF-023 (D-3, AC11, BREAKING): the returned array and every event object
200
+ * it contains (including nested `payload`/`metadata`) are now deep-frozen
201
+ * via `LibUtils.deepFreeze` — the same immutability guarantee applied to
202
+ * `BaseValueObject` values (see `base-value-object.ts`). Previously this
203
+ * returned only a shallow array copy: the array itself couldn't be
204
+ * mutated by callers, but the event objects (and their nested payload
205
+ * data) inside it could still be mutated in place, silently corrupting
206
+ * state shared with `_domainEvents` (the same object references are
207
+ * stored internally — events are never re-cloned after creation).
135
208
  *
209
+ * VP-012b (D2, BREAKING behavior change, non-breaking signature): the
210
+ * returned array reference is now **stable between calls** — the same
211
+ * frozen array object is returned on every call until the next mutation
212
+ * (`apply()`, `commit()`, `loadFromHistory()`, or
213
+ * `transformDomainEvents()`), instead of a fresh array being built on
214
+ * every call as before. Content equality (`toEqual`) is unaffected either
215
+ * way. Code that relies on *identity* — e.g. asserting `not.toBe`/`!==`
216
+ * between two `getDomainEvents()` calls to prove "each call returns a new
217
+ * array" — will observe the new stable-reference behavior and must be
218
+ * updated; asserting `.toBe`/`===` between two calls made with no
219
+ * intervening mutation now holds, where it previously did not. This is
220
+ * safe to alias: the returned array and its contents are frozen, so
221
+ * holding onto the reference across a mutation still reflects the state
222
+ * as of the call that produced it (the cache is not that same array
223
+ * anymore after invalidation — a stored old reference is simply stale
224
+ * data, never mutated out from under the caller).
136
225
  * @returns {ReadonlyArray<IDomainEvent>} Array of uncommitted domain events
137
226
  */
138
227
  getDomainEvents(): ReadonlyArray<IDomainEvent>;
228
+ /**
229
+ * VP-012b (D2): single invalidation point for `_domainEventsCache`,
230
+ * called from every site that mutates `_domainEvents` — `apply()`,
231
+ * `commit()`, `transformDomainEvents()`, `loadFromHistory()`, and
232
+ * `_internal_setState()`. Keeping invalidation in one helper (rather than
233
+ * setting the flag ad hoc at each call site) means a future mutation site
234
+ * cannot silently forget to invalidate.
235
+ */
236
+ private _invalidateDomainEventsCache;
139
237
  /**
140
238
  * Clear all uncommitted domain events and update initial version
141
239
  */
142
240
  commit(): void;
241
+ /**
242
+ * Rewrite the payload and/or metadata of the uncommitted domain events.
243
+ *
244
+ * This is an infrastructure boundary API, in the same family as `commit()`:
245
+ * call it from a repository immediately before persisting, never from the
246
+ * domain or application layer. An aggregate records events through `apply()`;
247
+ * it does not rewrite its own history.
248
+ *
249
+ * It exists for data that only the persistence boundary knows — a
250
+ * crypto-shredding key id resolved at save time, a correlation id assigned at
251
+ * dispatch, an encrypted payload. Doing this by hand from outside is not
252
+ * possible: `getDomainEvents()` deep-freezes what it returns, and the
253
+ * internal list is private. The transformation has to land here rather than
254
+ * on a local copy inside `save()`, because the event dispatcher reads the
255
+ * aggregate's events again — a local copy would leave the in-process event
256
+ * bus publishing the untransformed originals.
257
+ *
258
+ * Only `payload` and `metadata` can be replaced. `eventName`, identity, and
259
+ * the number and order of events are fixed: changing those would desync
260
+ * handlers on replay or break the version invariant. Return nothing to leave
261
+ * an event as it is.
262
+ *
263
+ * Identity, prototype and `instanceof` survive, and the event's constructor
264
+ * is never called, so event classes with their own constructor arguments are
265
+ * safe.
266
+ *
267
+ * @example
268
+ * // inside a repository's save(), before persisting:
269
+ * order.transformDomainEvents(event => ({
270
+ * payload: encrypt(event.payload, key),
271
+ * metadata: { userSpecificKeyId: key.id },
272
+ * }));
273
+ *
274
+ * @param transform - Called per event; returns the parts to replace, or nothing to keep it
275
+ */
276
+ transformDomainEvents<P = unknown>(transform: (event: Readonly<IDomainEvent<P>>, index: number) => {
277
+ payload?: P;
278
+ metadata?: Partial<IEventMetadata>;
279
+ } | void): void;
143
280
  /**
144
281
  * @param {string} eventType - Event type to register handler for
145
282
  * @param {IAggregateEventHandler<T>} handler - Event handler function
@@ -151,9 +288,28 @@ export declare class AggregateRoot<TId = string> implements IAggregateRoot<TId>
151
288
  * @param {string | IDomainEvent<P>} eventTypeOrEvent - Event type string or complete event object
152
289
  * @param {P} payload - Event payload data
153
290
  * @param {Partial<IEventMetadata>} metadata - Optional event metadata
291
+ * @throws {Error} when the aggregate's configured maxEvents limit would be exceeded
292
+ * @example Hitting the maxEvents guard
293
+ * ```typescript
294
+ * class Order extends AggregateRoot<string> {
295
+ * record(customerId: string, amount: number): void {
296
+ * this.apply('OrderCreated', { customerId, amount });
297
+ * }
298
+ * }
299
+ *
300
+ * const order = new Order({ id: EntityId.create(), version: 0, maxEvents: 1 });
301
+ * order.record('c-1', 100); // ok, 1 event recorded
302
+ * order.record('c-1', 100); // throws: maxEvents limit of 1 exceeded
303
+ * ```
154
304
  */
155
305
  protected apply<P = unknown>(eventTypeOrEvent: string | IDomainEvent<P>, payload?: P, metadata?: Partial<IEventMetadata>): void;
156
306
  private handleEvent;
307
+ /**
308
+ * VF-023 (D-4, AC6): called from `handleEvent()`, which is shared by both
309
+ * `apply()` (live) and `loadFromHistory()` (replay) — so the missing-
310
+ * handler policy applies identically to both paths.
311
+ */
312
+ private reportMissingHandler;
157
313
  /**
158
314
  * Reconstruct aggregate state from historical domain events
159
315
  * @param {IDomainEvent[]} events - Array of events to replay for state reconstruction
@@ -190,10 +346,20 @@ export declare class AggregateRoot<TId = string> implements IAggregateRoot<TId>
190
346
  getCapabilityTypes(): string[];
191
347
  private static sanitizeMetadata;
192
348
  /**
193
- * Internal method for capabilities to access private state
194
- * Should only be used by capabilities, not directly by domain code
349
+ * Internal method for capabilities to access private state.
350
+ *
351
+ * VF-023 (D-2, AC5, BREAKING, CRITICAL F-H4): gated by
352
+ * {@link INTERNAL_STATE_TOKEN}, a module-private `unique symbol` that is
353
+ * exported only from this module (not from the package's public barrel).
354
+ * Callers must supply the exact token reference to invoke this method —
355
+ * consumers of `@vytches/ddd-aggregates` cannot obtain it, so this method
356
+ * is unreachable from outside this package's own source, even via
357
+ * `as unknown as {...}` casts (the token value itself, not just its type,
358
+ * is required and cannot be forged).
359
+ *
360
+ * @throws {TypeError} if `token` does not match {@link INTERNAL_STATE_TOKEN}
195
361
  */
196
- _internal_setState(state: {
362
+ _internal_setState(token: typeof INTERNAL_STATE_TOKEN, state: {
197
363
  version: number;
198
364
  initialVersion: number;
199
365
  domainEvents: IDomainEvent[];
@@ -1,5 +1,5 @@
1
- import { Result } from '../../../utils/src/index.ts';
2
- import { CapabilityConstructor, Capability, IAggregateCapability, IAggregateSnapshot } from '../../../contracts/src/index.ts';
1
+ import { Result } from '@vytches/ddd-utils';
2
+ import { CapabilityConstructor, Capability, IAggregateCapability, IAggregateSnapshot } from '@vytches/ddd-contracts';
3
3
  import { AggregateError } from '../aggregate-errors';
4
4
  import { SnapshotCapability } from '../capabilities/snapshot-capability';
5
5
  import { VersioningCapability } from '../capabilities/versioning-capability';
@@ -1,4 +1,4 @@
1
- import { EntityId } from '../../../contracts/src/index.ts';
1
+ import { EntityId } from '@vytches/ddd-contracts';
2
2
  /**
3
3
  * Base class for domain entities — objects with identity that may be
4
4
  * referenced from other aggregates but are NOT themselves aggregate roots.
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=require("@vytches/ddd-contracts"),D=require("@vytches/ddd-logging"),o=require("@vytches/ddd-domain-primitives"),u=require("@vytches/ddd-utils");class d extends h.Capability{constructor(){super(...arguments),this.type="versioning",this.upcasters=new Map}static get capabilityType(){return"versioning"}attach(t){this.aggregate=t}detach(){this.aggregate=void 0,this.upcasters.clear()}registerUpcaster(t,e,i){this.upcasters.has(t)||this.upcasters.set(t,new Map),this.upcasters.get(t).set(e,i)}handleVersionedEvent(t,e){const i=t.metadata?.version||1,a=t.metadata?.targetVersion||i;let n=t;if(i<a&&this.upcasters.has(t.eventName)){const c=this.upcasters.get(t.eventName),m=D.Logger.forContext("VersioningCapability");for(let l=i;l<a;l++){const S=c.get(l);if(!S){m.warn("Missing upcaster for event version — event will be processed without upcast",{eventName:t.eventName,fromVersion:l,toVersion:l+1});continue}const V=S.upcast(n.payload,n.metadata);n={...n,payload:V,metadata:{...n.metadata,version:l+1}}}}const g=e.get(n.eventName);g&&g(n.payload,n.metadata)}getRegisteredEventTypes(){return Array.from(this.upcasters.keys())}hasUpcaster(t,e){return this.upcasters.get(t)?.has(e)||!1}getUpcastersForType(t){return this.upcasters.get(t)}clearUpcastersForType(t){this.upcasters.delete(t)}getTotalUpcasterCount(){let t=0;for(const e of this.upcasters.values())t+=e.size;return t}}class p{constructor(t){this._version=0,this._initialVersion=0,this._domainEvents=[],this._eventHandlers=new Map,this._capabilities=new h.CapabilityRegistry,this._id=t.id,this._version=t.version||0,this._initialVersion=t.version||0,this._maxEvents=t.maxEvents}getId(){return this._id}getVersion(){return this._version}getInitialVersion(){return this._initialVersion}hasChanges(){return this._domainEvents.length>0}getDomainEvents(){return[...this._domainEvents]}commit(){this._domainEvents=[],this._initialVersion=this._version}registerEventHandler(t,e){return this._eventHandlers.set(t,e),this}apply(t,e,i){this._version++;let a,n;typeof t=="string"?(a=h.createDomainEvent(t,e,i),n=a.metadata):(a=t,n=i?{...a.metadata,...i}:a.metadata);const g=p.sanitizeMetadata({...n,aggregateId:this._id.toString(),aggregateType:this.constructor.name,aggregateVersion:this._version,timestamp:n?.timestamp??new Date}),c=Object.assign(Object.create(Object.getPrototypeOf(a)),a,{metadata:g});if(this._maxEvents!==void 0&&this._domainEvents.length>=this._maxEvents)throw new Error(`Aggregate ${this.constructor.name} (id=${this._id.toString()}) exceeded maxEvents limit of ${this._maxEvents}. This usually indicates a runaway loop or replay of a corrupted event stream. Increase maxEvents in the aggregate constructor params if the limit is too restrictive for your domain.`);this._domainEvents.push(c),this.handleEvent(c)}handleEvent(t){const e=this.getCapability(d);if(e)e.handleVersionedEvent(t,this._eventHandlers);else{const i=this._eventHandlers.get(t.eventName);i&&i(t.payload,t.metadata)}}loadFromHistory(t){this._version=this._initialVersion,this._domainEvents=[];for(const e of t)this.handleEvent(e),this._version++;this._initialVersion=this._version}addCapability(t){return"attach"in t&&typeof t.attach=="function"&&t.attach(this),this._capabilities.register(t),this}getCapability(t){return this._capabilities.get(t)}hasCapability(t){return this._capabilities.has(t)}removeCapability(t){const e=this._capabilities.get(t);return e&&"detach"in e&&typeof e.detach=="function"&&e.detach(),this._capabilities.remove(t),this}getAllCapabilities(){return this._capabilities.getAll()}getCapabilityTypes(){return this._capabilities.getTypes()}static sanitizeMetadata(t){if(!Object.prototype.hasOwnProperty.call(t,"__proto__")&&!Object.prototype.hasOwnProperty.call(t,"constructor")&&!Object.prototype.hasOwnProperty.call(t,"prototype"))return t;const e={};for(const i of Object.keys(t))i==="__proto__"||i==="constructor"||i==="prototype"||(e[i]=t[i]);return e}_internal_setState(t){this._version=t.version,this._initialVersion=t.initialVersion,this._domainEvents=[...t.domainEvents]}}class f extends h.Capability{constructor(){super(...arguments),this.type="audit",this.auditLog=[]}static get capabilityType(){return"audit"}attach(t){this.aggregate=t;const e=this.aggregate;typeof e.apply=="function"&&(this.originalApply=e.apply,this.originalApply&&(this.aggregate.apply=(...i)=>{const a=this.originalApply.call(this.aggregate,...i);return this.recordAuditEvent(),a}))}detach(){this.aggregate&&this.originalApply&&(this.aggregate.apply=this.originalApply),this.aggregate=void 0,this.auditLog=[],this.originalApply=void 0}getAuditLog(){return[...this.auditLog]}clearAuditLog(){this.auditLog=[]}getAuditStatistics(){const t={};let e=0;this.auditLog.forEach((a,n)=>{if(t[a.eventName]=(t[a.eventName]||0)+1,n>0){const g=this.auditLog[n-1];if(g){const c=new Date(g.timestamp).getTime(),m=new Date(a.timestamp).getTime();e+=m-c}}});const i=this.auditLog.length>1?e/(this.auditLog.length-1):0;return{totalEvents:this.auditLog.length,eventsByType:t,averageTimeBetweenEvents:i}}recordAuditEvent(){const t=this.aggregate.getDomainEvents(),e=t[t.length-1];e&&this.recordEvent(e)}recordEvent(t){const e={eventId:t.metadata?.eventId||`audit-${Date.now()}-${Math.random()}`,eventName:t.eventName,aggregateId:this.aggregate.getId().toString(),aggregateType:this.aggregate.constructor.name,aggregateVersion:this.aggregate.getVersion(),timestamp:new Date,payload:t.payload,metadata:{...t.metadata,auditCapability:!0},actor:t.metadata?.userId};this.auditLog.push(e)}getEventsByName(t){return this.auditLog.filter(e=>e.eventName===t)}getEventsByTimeRange(t,e){return this.auditLog.filter(i=>{const a=new Date(i.timestamp).getTime();return a>=t.getTime()&&a<=e.getTime()})}getFirstEvent(){return this.auditLog[0]||null}getLastEvent(){return this.auditLog[this.auditLog.length-1]||null}}class r extends o.IDomainError{static invalidArguments(t,e){const i={code:o.DomainErrorCode.InvalidParameter,data:e};return new r(t,i)}static versionConflict(t,e,i,a){const n=`Version conflict: Aggregate ${t} with ID ${e} has version ${i}, but expected ${a}`,g={code:o.DomainErrorCode.ValidationFailed,data:{aggregateType:t,aggregateId:e,currentVersion:i,expectedVersion:a}};return new r(n,g)}static featureNotEnabled(t){const e=`Feature '${t}' is not enabled on aggregate`,i={code:o.DomainErrorCode.ValidationFailed,data:{feature:t}};return new r(e,i)}static methodNotImplemented(t,e){const i=`Method '${t}' must be implemented by ${e} to use this feature`,a={code:o.DomainErrorCode.MissingValue,data:{methodName:t,aggregateType:e}};return new r(i,a)}static invalidSnapshot(t,e){const i=`Invalid snapshot for aggregate ${t}${e?`: ${e}`:""}`,a={code:o.DomainErrorCode.InvalidFormat,data:{aggregateType:t,reason:e}};return new r(i,a)}static idMismatch(t,e){const i=`ID mismatch: Snapshot is for ID ${t}, but aggregate has ID ${e}`,a={code:o.DomainErrorCode.ValidationFailed,data:{snapshotId:t,aggregateId:e}};return new r(i,a)}static typeMismatch(t,e){const i=`Aggregate type mismatch: Snapshot is for type ${t}, but trying to load into ${e}`,a={code:o.DomainErrorCode.ValidationFailed,data:{snapshotType:t,aggregateType:e}};return new r(i,a)}static duplicateUpcaster(t,e){const i=`Upcaster for event ${t} version ${e} already exists`,a={code:o.DomainErrorCode.DuplicateEntry,data:{eventType:t,sourceVersion:e}};return new r(i,a)}static missingUpcaster(t,e,i){const a=`Missing upcaster for event ${t} from version ${e} to ${i}`,n={code:o.DomainErrorCode.MissingValue,data:{eventType:t,fromVersion:e,toVersion:i}};return new r(a,n)}static configurationError(t,e){const i=`Configuration error in ${t}: ${e}`,a={code:o.DomainErrorCode.ValidationFailed,data:{aggregateType:t,configurationError:e}};return new r(i,a)}static cannotInterceptApplyMethod(t,e){const i=`Cannot intercept apply method - method not found on aggregate ${t}`,a={code:o.DomainErrorCode.InternalError,data:{aggergateId:t,...e}};return new r(i,a)}static eventStoreNotConfigured(t){const e="Event store not configured",i={code:o.DomainErrorCode.MissingValue,data:t};return new r(e,i)}static aggregateDoesNotSupportReplay(t,e){const i=`Aggregate ${t} does not support event replay`,a={code:o.DomainErrorCode.MethodNotSupported,data:{aggregateType:t,...e}};return new r(i,a)}}class y extends h.Capability{constructor(){super(...arguments),this.type="eventSourcing",this.eventStore=null}static get capabilityType(){return"eventSourcing"}attach(t){this.aggregate=t}detach(){this.aggregate=void 0,this.eventStore=null}setEventStore(t){this.eventStore=t}getEventStore(){return this.eventStore}async loadFromEventStore(t){if(!this.eventStore)throw r.configurationError(this.aggregate.constructor.name,"Event store not configured");const e=await this.eventStore.getEvents(t);if(e.length===0)return;const i=this.aggregate;i.loadFromHistory&&i.loadFromHistory(e)}async saveToEventStore(){if(!this.eventStore)throw r.configurationError(this.aggregate.constructor.name,"Event store not configured");const t=this.aggregate.getDomainEvents();t.length!==0&&await this.eventStore.saveEvents(this.aggregate.getId().toString(),[...t],this.aggregate.getInitialVersion())}hasEventStore(){return this.eventStore!==null}getStreamName(){return`${this.aggregate.constructor.name}-${this.aggregate.getId().toString()}`}async loadFromVersion(t,e){if(!this.eventStore)throw r.configurationError(this.aggregate.constructor.name,"Event store not configured");const i=await this.eventStore.getEventsAfterVersion(t,e),a=this.aggregate;a.loadFromHistory&&i.length>0&&a.loadFromHistory(i)}}class E extends h.Capability{constructor(){super(...arguments),this.type="snapshot",this._snapshot=null}static get capabilityType(){return"snapshot"}attach(t){this.aggregate=t}detach(){this.aggregate=void 0,this._snapshot=null}createSnapshot(t,e){const i=this.aggregate.getDomainEvents(),n=(i.length>0?i[i.length-1]:null)?.metadata?.eventId,c={aggregateId:this.aggregate.getId().toString(),version:this.aggregate.getVersion(),aggregateType:this.aggregate.constructor.name,state:t(),timestamp:new Date,...n!==void 0&&{lastEventId:n}};return e&&(c.metadata=e()),c}restoreFromSnapshot(t,e,i){if(!t||!t.state)throw r.invalidSnapshot(this.aggregate.constructor.name,"Invalid snapshot");const a=this.aggregate.getId().toString();if(t.aggregateId!==a)throw r.idMismatch(t.aggregateId,a);if(t.aggregateType!==this.aggregate.constructor.name)throw r.typeMismatch(t.aggregateType,this.aggregate.constructor.name);e(t.state),t.metadata&&i&&i(t.metadata);const n=this.aggregate;n._internal_setState&&n._internal_setState({version:t.version,initialVersion:t.version,domainEvents:[]})}saveTemporaryState(t){this._snapshot=this.createSnapshot(()=>t)}getLastSnapshotTimestamp(){return this._snapshot?.timestamp||null}saveSnapshot(t,e){this._snapshot=this.createSnapshot(t,e)}getPreviousState(){const t=this._snapshot;return this._snapshot=null,t}}class v{constructor(t){this.capabilities=[],this.params=t}static create(t){const e={id:t.id instanceof h.EntityId?t.id:new h.EntityId(t.id,"text"),version:t.version||0};return new v(e)}withSnapshots(){return this.capabilities.push({capability:new E}),this}withVersioning(){return this.capabilities.push({capability:new d}),this}withAudit(){return this.capabilities.push({capability:new f}),this}withEventSourcing(t){const e=new y;return this.capabilities.push({capability:e,configure:i=>{const a=i;t?a.setEventStore(t):this.eventStore&&a.setEventStore(this.eventStore)}}),this}withCapability(t,e){return this.capabilities.push({capability:t,configure:e?i=>e(i):void 0}),this}setEventStore(t){return this.eventStore=t,this}build(t){const e=t||p,i=new e(this.params);for(const{capability:a,configure:n}of this.capabilities)i.addCapability(a),n&&n(a);return i}buildWithAllCapabilities(t){return this.withSnapshots().withVersioning().withAudit().withEventSourcing().build(t)}}function I(s){return v.create(s)}class b{constructor(t){this._id=t}getId(){return this._id}equals(t){return t==null?!1:t===this?!0:t instanceof b?this._id.equals(t._id):!1}}function w(s){return s.hasCapability(E)}function C(s){return s.hasCapability(d)}function _(s){return s.hasCapability(f)}function A(s){return s.hasCapability(y)}function $(s){if(!w(s))throw r.featureNotEnabled("snapshot");return s}function N(s){return w(s)?u.Result.ok(s):u.Result.fail(r.featureNotEnabled("snapshot"))}function L(s){if(!C(s))throw r.featureNotEnabled("versioning");return s}function M(s){return C(s)?u.Result.ok(s):u.Result.fail(r.featureNotEnabled("versioning"))}function T(s){if(!_(s))throw r.featureNotEnabled("audit");return s}function F(s){return _(s)?u.Result.ok(s):u.Result.fail(r.featureNotEnabled("audit"))}function x(s){if(!A(s))throw r.featureNotEnabled("eventSourcing");return s}function R(s){return A(s)?u.Result.ok(s):u.Result.fail(r.featureNotEnabled("eventSourcing"))}function U(s){return s.getCapabilityTypes()}function H(s,t){return t.every(e=>s.hasCapability(e))}exports.AggregateBuilder=v;exports.AggregateError=r;exports.AggregateRoot=p;exports.AuditCapability=f;exports.Entity=b;exports.EventSourcingCapability=y;exports.SnapshotCapability=E;exports.VersioningCapability=d;exports.aggregateBuilder=I;exports.asAuditAggregate=T;exports.asEventSourcingAggregate=x;exports.asSnapshotAggregate=$;exports.asVersioningAggregate=L;exports.getAggregateCapabilities=U;exports.hasAllCapabilities=H;exports.tryAsAuditAggregate=F;exports.tryAsEventSourcingAggregate=R;exports.tryAsSnapshotAggregate=N;exports.tryAsVersioningAggregate=M;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const l=require("@vytches/ddd-contracts"),S=require("@vytches/ddd-contracts/internal"),h=require("@vytches/ddd-utils"),o=require("@vytches/ddd-domain-primitives");class p extends l.Capability{constructor(){super(...arguments),this.type="versioning",this.upcasters=new Map}static get capabilityType(){return"versioning"}attach(t){this.aggregate=t}detach(){this.aggregate=void 0,this.upcasters.clear()}registerUpcaster(t,e,i){this.upcasters.has(t)||this.upcasters.set(t,new Map),this.upcasters.get(t).set(e,i)}handleVersionedEvent(t,e){const i=t.metadata?.version||1,a=t.metadata?.targetVersion||i;let s=t;if(i<a&&this.upcasters.has(t.eventName)){const u=this.upcasters.get(t.eventName);for(let c=i;c<a;c++){const b=u.get(c);if(!b){S.internalLogger.warn("VersioningCapability: Missing upcaster for event version — event will be processed without upcast",{eventName:t.eventName,fromVersion:c,toVersion:c+1});continue}const V=b.upcast(s.payload,s.metadata);s={...s,payload:V,metadata:{...s.metadata,version:c+1}}}}const g=e.get(s.eventName);g&&g(s.payload,s.metadata)}getRegisteredEventTypes(){return Array.from(this.upcasters.keys())}hasUpcaster(t,e){return this.upcasters.get(t)?.has(e)||!1}getUpcastersForType(t){return this.upcasters.get(t)}clearUpcastersForType(t){this.upcasters.delete(t)}getTotalUpcasterCount(){let t=0;for(const e of this.upcasters.values())t+=e.size;return t}}const _=Symbol("vytches-ddd-aggregate-internal-state");class d{constructor(t){this._version=0,this._initialVersion=0,this._domainEvents=[],this._domainEventsCache=Object.freeze([]),this._domainEventsCacheDirty=!0,this._eventHandlers=new Map,this._capabilities=new l.CapabilityRegistry,this._id=t.id,this._version=t.version||0,this._initialVersion=t.version||0,this._maxEvents=t.maxEvents,this._onMissingHandler=t.onMissingHandler??"warn"}getId(){return this._id}equals(t){return t==null?!1:t===this?!0:t instanceof d?this._id.equals(t._id):!1}getVersion(){return this._version}getInitialVersion(){return this._initialVersion}hasChanges(){return this._domainEvents.length>0}getDomainEvents(){if(this._domainEventsCacheDirty){const t=new WeakSet,e=this._domainEvents.map(i=>h.LibUtils.deepFreeze(i,t));this._domainEventsCache=Object.freeze(e),this._domainEventsCacheDirty=!1}return this._domainEventsCache}_invalidateDomainEventsCache(){this._domainEventsCacheDirty=!0}commit(){this._domainEvents=[],this._initialVersion=this._version,this._invalidateDomainEventsCache()}transformDomainEvents(t){this._domainEvents=this._domainEvents.map((e,i)=>{const a=t(e,i);return a?l.enrichEvent(e,a):e}),this._invalidateDomainEventsCache()}registerEventHandler(t,e){return this._eventHandlers.set(t,e),this}apply(t,e,i){if(this._maxEvents!==void 0&&this._domainEvents.length>=this._maxEvents)throw new Error(`Aggregate ${this.constructor.name} (id=${this._id.toString()}) exceeded maxEvents limit of ${this._maxEvents}. This usually indicates a runaway loop or replay of a corrupted event stream. Increase maxEvents in the aggregate constructor params if the limit is too restrictive for your domain.`);const a=this._version+1;let s,g;typeof t=="string"?(s=l.createDomainEvent(t,e,i),g=s.metadata):(s=t,g=i?{...s.metadata,...i}:s.metadata);const u=d.sanitizeMetadata({...g,aggregateId:this._id.toString(),aggregateType:this.constructor.name,aggregateVersion:a,timestamp:g?.timestamp??new Date}),c=Object.assign(Object.create(Object.getPrototypeOf(s)),s,{metadata:u});this._version=a,this._domainEvents.push(c),this._invalidateDomainEventsCache(),this.handleEvent(c)}handleEvent(t){const e=this.getCapability(p);if(e)e.handleVersionedEvent(t,this._eventHandlers);else{const i=this._eventHandlers.get(t.eventName);i?i(t.payload,t.metadata):this.reportMissingHandler(t.eventName)}}reportMissingHandler(t){const e=`Aggregate ${this.constructor.name} (id=${this._id.toString()}) has no registered handler for event "${t}". State will not be updated for this event. Register a handler via registerEventHandler('${t}', ...) before applying/replaying this event, or pass onMissingHandler: "warn" explicitly to silence this in the constructor params.`;if(this._onMissingHandler==="throw")throw new Error(e);S.internalLogger.warn(e,{aggregateType:this.constructor.name,aggregateId:this._id.toString(),eventName:t})}loadFromHistory(t){this._version=this._initialVersion,this._domainEvents=[],this._invalidateDomainEventsCache();for(const e of t)this.handleEvent(e),this._version++;this._initialVersion=this._version}addCapability(t){return"attach"in t&&typeof t.attach=="function"&&t.attach(this),this._capabilities.register(t),this}getCapability(t){return this._capabilities.get(t)}hasCapability(t){return this._capabilities.has(t)}removeCapability(t){const e=this._capabilities.get(t);return e&&"detach"in e&&typeof e.detach=="function"&&e.detach(),this._capabilities.remove(t),this}getAllCapabilities(){return this._capabilities.getAll()}getCapabilityTypes(){return this._capabilities.getTypes()}static sanitizeMetadata(t){if(!Object.prototype.hasOwnProperty.call(t,"__proto__")&&!Object.prototype.hasOwnProperty.call(t,"constructor")&&!Object.prototype.hasOwnProperty.call(t,"prototype"))return t;const e={};for(const i of Object.keys(t))i==="__proto__"||i==="constructor"||i==="prototype"||(e[i]=t[i]);return e}_internal_setState(t,e){if(t!==_)throw new TypeError("_internal_setState: invalid or missing internal state token. This method is restricted to capabilities within @vytches/ddd-aggregates.");this._version=e.version,this._initialVersion=e.initialVersion,this._domainEvents=[...e.domainEvents],this._invalidateDomainEventsCache()}}class m extends l.Capability{constructor(){super(...arguments),this.type="audit",this.auditLog=[]}static get capabilityType(){return"audit"}attach(t){this.aggregate=t;const e=this.aggregate;typeof e.apply=="function"&&(this.originalApply=e.apply,this.originalApply&&(this.aggregate.apply=(...i)=>{const a=this.originalApply.call(this.aggregate,...i);return this.recordAuditEvent(),a}))}detach(){this.aggregate&&this.originalApply&&(this.aggregate.apply=this.originalApply),this.aggregate=void 0,this.auditLog=[],this.originalApply=void 0}getAuditLog(){return[...this.auditLog]}clearAuditLog(){this.auditLog=[]}getAuditStatistics(){const t={};let e=0;this.auditLog.forEach((a,s)=>{if(t[a.eventName]=(t[a.eventName]||0)+1,s>0){const g=this.auditLog[s-1];if(g){const u=new Date(g.timestamp).getTime(),c=new Date(a.timestamp).getTime();e+=c-u}}});const i=this.auditLog.length>1?e/(this.auditLog.length-1):0;return{totalEvents:this.auditLog.length,eventsByType:t,averageTimeBetweenEvents:i}}recordAuditEvent(){const t=this.aggregate.getDomainEvents(),e=t[t.length-1];e&&this.recordEvent(e)}recordEvent(t){const e={eventId:t.metadata?.eventId||`audit-${h.LibUtils.getUUID()}`,eventName:t.eventName,aggregateId:this.aggregate.getId().toString(),aggregateType:this.aggregate.constructor.name,aggregateVersion:this.aggregate.getVersion(),timestamp:new Date,payload:t.payload,metadata:{...t.metadata,auditCapability:!0},actor:t.metadata?.userId};this.auditLog.push(e)}getEventsByName(t){return this.auditLog.filter(e=>e.eventName===t)}getEventsByTimeRange(t,e){return this.auditLog.filter(i=>{const a=new Date(i.timestamp).getTime();return a>=t.getTime()&&a<=e.getTime()})}getFirstEvent(){return this.auditLog[0]||null}getLastEvent(){return this.auditLog[this.auditLog.length-1]||null}}class r extends o.IDomainError{static invalidArguments(t,e){const i={code:o.DomainErrorCode.InvalidParameter,data:e};return new r(t,i)}static versionConflict(t,e,i,a){const s=`Version conflict: Aggregate ${t} with ID ${e} has version ${i}, but expected ${a}`,g={code:o.DomainErrorCode.ValidationFailed,data:{aggregateType:t,aggregateId:e,currentVersion:i,expectedVersion:a}};return new r(s,g)}static featureNotEnabled(t){const e=`Feature '${t}' is not enabled on aggregate`,i={code:o.DomainErrorCode.ValidationFailed,data:{feature:t}};return new r(e,i)}static methodNotImplemented(t,e){const i=`Method '${t}' must be implemented by ${e} to use this feature`,a={code:o.DomainErrorCode.MissingValue,data:{methodName:t,aggregateType:e}};return new r(i,a)}static invalidSnapshot(t,e){const i=`Invalid snapshot for aggregate ${t}${e?`: ${e}`:""}`,a={code:o.DomainErrorCode.InvalidFormat,data:{aggregateType:t,reason:e}};return new r(i,a)}static idMismatch(t,e){const i=`ID mismatch: Snapshot is for ID ${t}, but aggregate has ID ${e}`,a={code:o.DomainErrorCode.ValidationFailed,data:{snapshotId:t,aggregateId:e}};return new r(i,a)}static typeMismatch(t,e){const i=`Aggregate type mismatch: Snapshot is for type ${t}, but trying to load into ${e}`,a={code:o.DomainErrorCode.ValidationFailed,data:{snapshotType:t,aggregateType:e}};return new r(i,a)}static duplicateUpcaster(t,e){const i=`Upcaster for event ${t} version ${e} already exists`,a={code:o.DomainErrorCode.DuplicateEntry,data:{eventType:t,sourceVersion:e}};return new r(i,a)}static missingUpcaster(t,e,i){const a=`Missing upcaster for event ${t} from version ${e} to ${i}`,s={code:o.DomainErrorCode.MissingValue,data:{eventType:t,fromVersion:e,toVersion:i}};return new r(a,s)}static configurationError(t,e){const i=`Configuration error in ${t}: ${e}`,a={code:o.DomainErrorCode.ValidationFailed,data:{aggregateType:t,configurationError:e}};return new r(i,a)}static cannotInterceptApplyMethod(t,e){const i=`Cannot intercept apply method - method not found on aggregate ${t}`,a={code:o.DomainErrorCode.InternalError,data:{aggergateId:t,...e}};return new r(i,a)}static eventStoreNotConfigured(t){const e="Event store not configured",i={code:o.DomainErrorCode.MissingValue,data:t};return new r(e,i)}static aggregateDoesNotSupportReplay(t,e){const i=`Aggregate ${t} does not support event replay`,a={code:o.DomainErrorCode.MethodNotSupported,data:{aggregateType:t,...e}};return new r(i,a)}}class f extends l.Capability{constructor(){super(...arguments),this.type="eventSourcing",this.eventStore=null}static get capabilityType(){return"eventSourcing"}attach(t){this.aggregate=t}detach(){this.aggregate=void 0,this.eventStore=null}setEventStore(t){this.eventStore=t}getEventStore(){return this.eventStore}async loadFromEventStore(t){if(!this.eventStore)throw r.configurationError(this.aggregate.constructor.name,"Event store not configured");const e=await this.eventStore.getEvents(t);if(e.length===0)return;const i=this.aggregate;i.loadFromHistory&&i.loadFromHistory(e)}async saveToEventStore(){if(!this.eventStore)throw r.configurationError(this.aggregate.constructor.name,"Event store not configured");const t=this.aggregate.getDomainEvents();t.length!==0&&await this.eventStore.saveEvents(this.aggregate.getId().toString(),[...t],this.aggregate.getInitialVersion())}hasEventStore(){return this.eventStore!==null}getStreamName(){return`${this.aggregate.constructor.name}-${this.aggregate.getId().toString()}`}async loadFromVersion(t,e){if(!this.eventStore)throw r.configurationError(this.aggregate.constructor.name,"Event store not configured");const i=await this.eventStore.getEventsAfterVersion(t,e),a=this.aggregate;a.loadFromHistory&&i.length>0&&a.loadFromHistory(i)}}class y extends l.Capability{constructor(){super(...arguments),this.type="snapshot",this._snapshot=null}static get capabilityType(){return"snapshot"}attach(t){this.aggregate=t}detach(){this.aggregate=void 0,this._snapshot=null}createSnapshot(t,e){const i=this.aggregate.getDomainEvents(),s=(i.length>0?i[i.length-1]:null)?.metadata?.eventId,u={aggregateId:this.aggregate.getId().toString(),version:this.aggregate.getVersion(),aggregateType:this.aggregate.constructor.name,state:t(),timestamp:new Date,...s!==void 0&&{lastEventId:s}};return e&&(u.metadata=e()),u}restoreFromSnapshot(t,e,i){if(!t||!t.state)throw r.invalidSnapshot(this.aggregate.constructor.name,"Invalid snapshot");const a=this.aggregate.getId().toString();if(t.aggregateId!==a)throw r.idMismatch(t.aggregateId,a);if(t.aggregateType!==this.aggregate.constructor.name)throw r.typeMismatch(t.aggregateType,this.aggregate.constructor.name);e(t.state),t.metadata&&i&&i(t.metadata),this.aggregate._internal_setState(_,{version:t.version,initialVersion:t.version,domainEvents:[]})}saveTemporaryState(t){this._snapshot=this.createSnapshot(()=>t)}getLastSnapshotTimestamp(){return this._snapshot?.timestamp||null}saveSnapshot(t,e){this._snapshot=this.createSnapshot(t,e)}getPreviousState(){const t=this._snapshot;return this._snapshot=null,t}}class v{constructor(t){this.capabilities=[],this.params=t}static create(t){const e={id:t.id instanceof l.EntityId?t.id:new l.EntityId(t.id,"text"),version:t.version||0};return new v(e)}withSnapshots(){return this.capabilities.push({capability:new y}),this}withVersioning(){return this.capabilities.push({capability:new p}),this}withAudit(){return this.capabilities.push({capability:new m}),this}withEventSourcing(t){const e=new f;return this.capabilities.push({capability:e,configure:i=>{const a=i;t?a.setEventStore(t):this.eventStore&&a.setEventStore(this.eventStore)}}),this}withCapability(t,e){return this.capabilities.push({capability:t,configure:e?i=>e(i):void 0}),this}setEventStore(t){return this.eventStore=t,this}build(t){const e=t||d,i=new e(this.params);for(const{capability:a,configure:s}of this.capabilities)i.addCapability(a),s&&s(a);return i}buildWithAllCapabilities(t){return this.withSnapshots().withVersioning().withAudit().withEventSourcing().build(t)}}function I(n){return v.create(n)}class E{constructor(t){this._id=t}getId(){return this._id}equals(t){return t==null?!1:t===this?!0:t instanceof E?this._id.equals(t._id):!1}}function w(n){return n.hasCapability(y)}function C(n){return n.hasCapability(p)}function A(n){return n.hasCapability(m)}function D(n){return n.hasCapability(f)}function $(n){if(!w(n))throw r.featureNotEnabled("snapshot");return n}function M(n){return w(n)?h.Result.ok(n):h.Result.fail(r.featureNotEnabled("snapshot"))}function N(n){if(!C(n))throw r.featureNotEnabled("versioning");return n}function T(n){return C(n)?h.Result.ok(n):h.Result.fail(r.featureNotEnabled("versioning"))}function L(n){if(!A(n))throw r.featureNotEnabled("audit");return n}function F(n){return A(n)?h.Result.ok(n):h.Result.fail(r.featureNotEnabled("audit"))}function H(n){if(!D(n))throw r.featureNotEnabled("eventSourcing");return n}function x(n){return D(n)?h.Result.ok(n):h.Result.fail(r.featureNotEnabled("eventSourcing"))}function U(n){return n.getCapabilityTypes()}function O(n,t){return t.every(e=>n.hasCapability(e))}exports.AggregateBuilder=v;exports.AggregateError=r;exports.AggregateRoot=d;exports.AuditCapability=m;exports.Entity=E;exports.EventSourcingCapability=f;exports.SnapshotCapability=y;exports.VersioningCapability=p;exports.aggregateBuilder=I;exports.asAuditAggregate=L;exports.asEventSourcingAggregate=H;exports.asSnapshotAggregate=$;exports.asVersioningAggregate=N;exports.getAggregateCapabilities=U;exports.hasAllCapabilities=O;exports.tryAsAuditAggregate=F;exports.tryAsEventSourcingAggregate=x;exports.tryAsSnapshotAggregate=M;exports.tryAsVersioningAggregate=T;
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { AuditCapability } from './capabilities/audit-capability';
5
5
  export { EventSourcingCapability } from './capabilities/event-sourcing-capability';
6
6
  export { SnapshotCapability } from './capabilities/snapshot-capability';
7
7
  export { VersioningCapability } from './capabilities/versioning-capability';
8
- export type { IAggregateRoot, IAggregateCapability, IAggregateConstructorParams, IAggregateEventHandler, IAggregateBuilder, } from './aggregate-interfaces';
8
+ export type { IAggregateRoot, IAggregateCapability, IAggregateConstructorParams, IAggregateEventHandler, } from './aggregate-interfaces';
9
9
  export { AggregateError } from './aggregate-errors';
10
10
  export type { AggregateWithSnapshotCapability, AggregateWithVersioningCapability, AggregateWithAuditCapability, AggregateWithEventSourcingCapability, } from './core/aggregate-utilities';
11
11
  export { asSnapshotAggregate, tryAsSnapshotAggregate, asVersioningAggregate, tryAsVersioningAggregate, asAuditAggregate, tryAsAuditAggregate, asEventSourcingAggregate, tryAsEventSourcingAggregate, getAggregateCapabilities, hasAllCapabilities, } from './core/aggregate-utilities';