@vytches/ddd-aggregates 0.30.0 → 0.31.0-alpha.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.
- package/LLMGUIDE.md +84 -17
- package/README.md +5 -1
- package/dist/aggregate-interfaces.d.ts +16 -184
- package/dist/capabilities/audit-capability.d.ts +1 -1
- package/dist/capabilities/event-sourcing-capability.d.ts +1 -1
- package/dist/capabilities/snapshot-capability.d.ts +1 -1
- package/dist/capabilities/versioning-capability.d.ts +15 -2
- package/dist/core/aggregate-root.builder.d.ts +1 -1
- package/dist/core/aggregate-root.d.ts +93 -6
- package/dist/core/aggregate-utilities.d.ts +2 -2
- package/dist/core/entity.d.ts +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +255 -168
- package/package.json +8 -7
package/LLMGUIDE.md
CHANGED
|
@@ -46,23 +46,39 @@ order.commit(); // clear pending events after saving
|
|
|
46
46
|
|
|
47
47
|
## Key API
|
|
48
48
|
|
|
49
|
-
| Export
|
|
50
|
-
|
|
|
51
|
-
| `AggregateRoot<TId>`
|
|
52
|
-
| `Entity<TId>`
|
|
53
|
-
| `AggregateBuilder<TId>`
|
|
54
|
-
| `aggregateBuilder(params)`
|
|
55
|
-
| `AggregateError`
|
|
56
|
-
| `SnapshotCapability`
|
|
57
|
-
| `AuditCapability`
|
|
58
|
-
| `VersioningCapability`
|
|
59
|
-
| `EventSourcingCapability`
|
|
60
|
-
| `asSnapshotAggregate(agg)`
|
|
61
|
-
| `asAuditAggregate(agg)`
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
49
|
+
| Export | Kind | Description |
|
|
50
|
+
| ------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `AggregateRoot<TId>` | class | Base aggregate root; manages version, events, capabilities — the _transactional_ root |
|
|
52
|
+
| `Entity<TId>` | abstract class | Base for **non-root** domain entities (VF-CANON-001) — identity-based equality, no version/event machinery. Use for `OrderLine`, inner aggregate entities |
|
|
53
|
+
| `AggregateBuilder<TId>` | class | Fluent builder for constructing aggregates with capabilities |
|
|
54
|
+
| `aggregateBuilder(params)` | function | Shorthand factory for `AggregateBuilder.create(params)` |
|
|
55
|
+
| `AggregateError` | class | Domain error for aggregate-level failures |
|
|
56
|
+
| `SnapshotCapability` | class | Enables snapshot creation and restoration |
|
|
57
|
+
| `AuditCapability` | class | Attaches an audit log to every event applied |
|
|
58
|
+
| `VersioningCapability` | class | Registers event upcasters for schema evolution |
|
|
59
|
+
| `EventSourcingCapability` | class | Integrates with `IEventStore` for persistence |
|
|
60
|
+
| `asSnapshotAggregate(agg)` | function | Casts aggregate, throws if no `SnapshotCapability` |
|
|
61
|
+
| `asAuditAggregate(agg)` | function | Casts aggregate, throws if no `AuditCapability` |
|
|
62
|
+
| `asVersioningAggregate(agg)` | function | Casts aggregate, throws if no `VersioningCapability` |
|
|
63
|
+
| `asEventSourcingAggregate(agg)` | function | Casts aggregate, throws if no `EventSourcingCapability` |
|
|
64
|
+
| `tryAsSnapshotAggregate(agg)` | function | Non-throwing sibling of `asSnapshotAggregate`; returns `Result.ok(aggregate)` or `Result.fail(AggregateError)` instead of throwing |
|
|
65
|
+
| `tryAsAuditAggregate(agg)` | function | Non-throwing sibling of `asAuditAggregate`; returns a `Result<AggregateWithAuditCapability<TId>, AggregateError>` |
|
|
66
|
+
| `tryAsVersioningAggregate(agg)` | function | Non-throwing sibling of `asVersioningAggregate`; returns a `Result<AggregateWithVersioningCapability<TId>, AggregateError>` |
|
|
67
|
+
| `tryAsEventSourcingAggregate(agg)` | function | Non-throwing sibling of `asEventSourcingAggregate`; returns a `Result<AggregateWithEventSourcingCapability<TId>, AggregateError>` |
|
|
68
|
+
| `hasAllCapabilities(agg, [...])` | function | Returns `true` if aggregate has every listed capability |
|
|
69
|
+
| `getAggregateCapabilities(agg)` | function | Returns string array of capability type names |
|
|
70
|
+
| `AggregateWithSnapshotCapability<TId>` | type | Type-safe alias for snapshot-capable aggregates; return type of `asSnapshotAggregate`/`tryAsSnapshotAggregate` |
|
|
71
|
+
| `AggregateWithAuditCapability<TId>` | type | Type-safe alias for audit-capable aggregates; return type of `asAuditAggregate`/`tryAsAuditAggregate` |
|
|
72
|
+
| `AggregateWithVersioningCapability<TId>` | type | Type-safe alias for versioning-capable aggregates; return type of `asVersioningAggregate`/`tryAsVersioningAggregate` |
|
|
73
|
+
| `AggregateWithEventSourcingCapability<TId>` | type | Type-safe alias for event-sourcing-capable aggregates; return type of `asEventSourcingAggregate`/`tryAsEventSourcingAggregate` |
|
|
74
|
+
| `IAggregateRoot<TId>` | interface | Full contract for aggregates |
|
|
75
|
+
| `IAggregateCapability` | interface | Base contract every capability implements: `attach(aggregate)` (required) + `detach?()` (optional). `SnapshotCapability`, `AuditCapability`, `VersioningCapability`, `EventSourcingCapability` all satisfy it |
|
|
76
|
+
| `IAggregateEventHandler<T>` | interface | Callable shape `(payload: T, metadata?) => void` — the handler function type registered via `registerEventHandler(type, fn)` |
|
|
77
|
+
|
|
78
|
+
> **BREAKING (v0.31.0):** `IAggregateBuilder<TId>` was removed — it was exported
|
|
79
|
+
> but shape-incompatible with the concrete `AggregateBuilder` class. Use
|
|
80
|
+
> `AggregateBuilder` directly for `withSnapshots()`, `withVersioning()`,
|
|
81
|
+
> `withEventSourcing()`, `withAudit()`, `withCustomCapability()`, `build()`.
|
|
66
82
|
|
|
67
83
|
### `AggregateRoot` method reference
|
|
68
84
|
|
|
@@ -136,6 +152,57 @@ if (hasAllCapabilities(order, [AuditCapability])) {
|
|
|
136
152
|
}
|
|
137
153
|
```
|
|
138
154
|
|
|
155
|
+
### Composing multiple capabilities and narrowing with `tryAs*` helpers
|
|
156
|
+
|
|
157
|
+
Build one aggregate with two capabilities (versioning + event sourcing), then
|
|
158
|
+
narrow it back to each capability-specific type with the non-throwing `tryAs*`
|
|
159
|
+
variants — useful in application-layer code where a missing capability should be
|
|
160
|
+
handled as a `Result`, not an exception.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import {
|
|
164
|
+
AggregateBuilder,
|
|
165
|
+
VersioningCapability,
|
|
166
|
+
EventSourcingCapability,
|
|
167
|
+
tryAsVersioningAggregate,
|
|
168
|
+
tryAsEventSourcingAggregate,
|
|
169
|
+
type AggregateWithVersioningCapability,
|
|
170
|
+
type AggregateWithEventSourcingCapability,
|
|
171
|
+
} from '@vytches/ddd-aggregates';
|
|
172
|
+
import { EntityId } from '@vytches/ddd-value-objects';
|
|
173
|
+
|
|
174
|
+
const order = AggregateBuilder.create({ id: EntityId.create() })
|
|
175
|
+
.withVersioning()
|
|
176
|
+
.withEventSourcing()
|
|
177
|
+
.build(Order);
|
|
178
|
+
|
|
179
|
+
// Narrow to versioning-capable: register an upcaster for an old event shape.
|
|
180
|
+
const versioningResult = tryAsVersioningAggregate(order);
|
|
181
|
+
if (versioningResult.isSuccess) {
|
|
182
|
+
const versioned: AggregateWithVersioningCapability<string> =
|
|
183
|
+
versioningResult.value;
|
|
184
|
+
versioned
|
|
185
|
+
.getCapability(VersioningCapability)!
|
|
186
|
+
.registerUpcaster('OrderCreated', 1, {
|
|
187
|
+
upcast: (payload: { amount: number }) => ({
|
|
188
|
+
...payload,
|
|
189
|
+
currency: 'USD',
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Narrow to event-sourcing-capable: persist to the configured event store.
|
|
195
|
+
const eventSourcingResult = tryAsEventSourcingAggregate(order);
|
|
196
|
+
if (eventSourcingResult.isSuccess) {
|
|
197
|
+
const sourced: AggregateWithEventSourcingCapability<string> =
|
|
198
|
+
eventSourcingResult.value;
|
|
199
|
+
await sourced.getCapability(EventSourcingCapability)!.saveToEventStore();
|
|
200
|
+
} else {
|
|
201
|
+
// eventSourcingResult.error is an AggregateError — handle without throwing.
|
|
202
|
+
console.error(eventSourcingResult.error.message);
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
139
206
|
## Anti-Patterns
|
|
140
207
|
|
|
141
208
|
**Mutating state directly instead of using `apply()`.** Assigning
|
package/README.md
CHANGED
|
@@ -181,7 +181,11 @@ class OrderLine extends Entity<string> {
|
|
|
181
181
|
| `IAggregateCapability` | interface | Base capability contract |
|
|
182
182
|
| `IAggregateConstructorParams<TId>` | interface | Constructor parameter shape for aggregates |
|
|
183
183
|
| `IAggregateEventHandler` | interface | Event handler registration contract |
|
|
184
|
-
|
|
184
|
+
|
|
185
|
+
> **BREAKING (v0.31.0):** `IAggregateBuilder` was removed. It was
|
|
186
|
+
> shape-incompatible with the concrete `AggregateBuilder` class (a broken public
|
|
187
|
+
> interface is worse than none). Use the `AggregateBuilder` class directly for
|
|
188
|
+
> the builder's fluent contract.
|
|
185
189
|
|
|
186
190
|
### Utility Types
|
|
187
191
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Capability, CapabilityConstructor, EntityId, IAggregateSnapshot, IAuditEvent, IDomainEvent, IEventMetadata,
|
|
1
|
+
import { Capability, CapabilityConstructor, EntityId, IAggregateSnapshot, IAuditEvent, IDomainEvent, IEventMetadata, IEventUpcaster } from '@vytches/ddd-contracts';
|
|
2
2
|
/**
|
|
3
3
|
* Core interface for aggregate root functionality.
|
|
4
4
|
* Provides the foundation for domain aggregates with event sourcing, versioning, and capability management.
|
|
@@ -80,6 +80,21 @@ export interface IAggregateConstructorParams<TId = string> {
|
|
|
80
80
|
* Leave undefined for "no limit" (preserves backward compatibility).
|
|
81
81
|
*/
|
|
82
82
|
maxEvents?: number;
|
|
83
|
+
/**
|
|
84
|
+
* VF-023 (D-4, AC6, non-breaking addition): controls what happens when
|
|
85
|
+
* `apply()` (live) or `loadFromHistory()` (replay) processes an event for
|
|
86
|
+
* which no handler was registered via `registerEventHandler()`.
|
|
87
|
+
*
|
|
88
|
+
* - `"warn"` (default): the event is still recorded/replayed, but a
|
|
89
|
+
* warning is logged via the library's internal logger. Preserves the
|
|
90
|
+
* previous behavior's intent (missing handlers did not throw) without
|
|
91
|
+
* silently swallowing the condition.
|
|
92
|
+
* - `"throw"`: fail fast instead — useful in tests or strict environments
|
|
93
|
+
* where a missing handler indicates a real bug.
|
|
94
|
+
*
|
|
95
|
+
* Leave undefined for the default `"warn"` behavior (backward compatible).
|
|
96
|
+
*/
|
|
97
|
+
onMissingHandler?: 'warn' | 'throw';
|
|
83
98
|
}
|
|
84
99
|
/**
|
|
85
100
|
* Base interface for all aggregate capabilities.
|
|
@@ -174,36 +189,6 @@ export interface IMiddlewareCapability extends IAggregateCapability {
|
|
|
174
189
|
*/
|
|
175
190
|
use(middleware: EventAggregateMiddleware): this;
|
|
176
191
|
}
|
|
177
|
-
/**
|
|
178
|
-
* Builder interface for creating aggregates with capabilities.
|
|
179
|
-
* Provides fluent API for configuring aggregate features before construction.
|
|
180
|
-
*/
|
|
181
|
-
export interface IAggregateBuilder<TId> {
|
|
182
|
-
/**
|
|
183
|
-
* Adds snapshot capability
|
|
184
|
-
*/
|
|
185
|
-
withSnapshots<TState = unknown, TMeta = unknown>(): this;
|
|
186
|
-
/**
|
|
187
|
-
* Adds versioning capability
|
|
188
|
-
*/
|
|
189
|
-
withVersioning(): this;
|
|
190
|
-
/**
|
|
191
|
-
* Adds event sourcing capability
|
|
192
|
-
*/
|
|
193
|
-
withEventSourcing(eventStore?: IEventStore): this;
|
|
194
|
-
/**
|
|
195
|
-
* Adds audit capability
|
|
196
|
-
*/
|
|
197
|
-
withAudit(): this;
|
|
198
|
-
/**
|
|
199
|
-
* Adds custom capability
|
|
200
|
-
*/
|
|
201
|
-
withCustomCapability<T extends IAggregateCapability>(name: string, capability: T): this;
|
|
202
|
-
/**
|
|
203
|
-
* Builds the aggregate with all configured capabilities
|
|
204
|
-
*/
|
|
205
|
-
build(): IAggregateRoot<TId>;
|
|
206
|
-
}
|
|
207
192
|
/**
|
|
208
193
|
* Handler function for processing aggregate events.
|
|
209
194
|
* Used internally by aggregates to handle domain events.
|
|
@@ -243,156 +228,3 @@ export type AggregateIdType<T> = T extends IAggregateRoot<infer TId> ? TId : nev
|
|
|
243
228
|
export type AggregateWithCapabilities<TId, TCapabilities extends CapabilityName[]> = IAggregateRoot<TId> & {
|
|
244
229
|
[K in TCapabilities[number] as `getCapability`]: (name: K) => K extends 'snapshot' ? ISnapshotCapability : K extends 'versioning' ? IVersioningCapability : K extends 'eventSourcing' ? IEventSourcingCapability : K extends 'audit' ? IAuditCapability : K extends 'middleware' ? IMiddlewareCapability : IAggregateCapability;
|
|
245
230
|
};
|
|
246
|
-
/**
|
|
247
|
-
* Factory interface for creating and rebuilding aggregates.
|
|
248
|
-
* Provides standard methods for aggregate lifecycle management.
|
|
249
|
-
* @template TId - The type of the aggregate ID value (string, number, etc.)
|
|
250
|
-
*/
|
|
251
|
-
export interface IAggregateFactory<TId = string, TAggregate extends IAggregateRoot<TId> = IAggregateRoot<TId>> {
|
|
252
|
-
/**
|
|
253
|
-
* Creates a new aggregate instance
|
|
254
|
-
*/
|
|
255
|
-
create(id: EntityId<TId>, ...args: unknown[]): TAggregate;
|
|
256
|
-
/**
|
|
257
|
-
* Rebuilds aggregate from events
|
|
258
|
-
*/
|
|
259
|
-
fromEvents(id: EntityId<TId>, events: IDomainEvent[]): TAggregate;
|
|
260
|
-
/**
|
|
261
|
-
* Rebuilds aggregate from snapshot and subsequent events
|
|
262
|
-
*/
|
|
263
|
-
fromSnapshot<TState, TMeta>(snapshot: IAggregateSnapshot<TState, TMeta>, events: IDomainEvent[]): TAggregate;
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Interface for validating aggregate state and operations.
|
|
267
|
-
* Enables business rule validation at the aggregate level.
|
|
268
|
-
*/
|
|
269
|
-
export interface IAggregateValidator<TAggregate extends IAggregateRoot<unknown>> {
|
|
270
|
-
/**
|
|
271
|
-
* Validates aggregate state
|
|
272
|
-
*/
|
|
273
|
-
validate(aggregate: TAggregate): ValidationResult;
|
|
274
|
-
/**
|
|
275
|
-
* Validates aggregate before applying event
|
|
276
|
-
*/
|
|
277
|
-
validateBeforeEvent(aggregate: TAggregate, event: IDomainEvent): ValidationResult;
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Result of aggregate validation operation.
|
|
281
|
-
* Contains validation status and any error details.
|
|
282
|
-
*/
|
|
283
|
-
export interface ValidationResult {
|
|
284
|
-
/** Whether the validation passed */
|
|
285
|
-
isValid: boolean;
|
|
286
|
-
/** List of validation errors if any */
|
|
287
|
-
errors: IValidationError[];
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Represents a single validation error.
|
|
291
|
-
* Provides details about what failed validation and why.
|
|
292
|
-
*/
|
|
293
|
-
export interface IValidationError {
|
|
294
|
-
/** The field or property that failed validation */
|
|
295
|
-
field: string;
|
|
296
|
-
/** Human-readable error message */
|
|
297
|
-
message: string;
|
|
298
|
-
/** Optional error code for programmatic handling */
|
|
299
|
-
code?: string;
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Capability for caching aggregate data.
|
|
303
|
-
* Provides key-value storage with TTL support for performance optimization.
|
|
304
|
-
*/
|
|
305
|
-
export interface ICachingCapability extends IAggregateCapability {
|
|
306
|
-
/**
|
|
307
|
-
* Gets cached value
|
|
308
|
-
*/
|
|
309
|
-
get<T>(key: string): T | undefined;
|
|
310
|
-
/**
|
|
311
|
-
* Sets cached value
|
|
312
|
-
*/
|
|
313
|
-
set<T>(key: string, value: T, ttl?: number): void;
|
|
314
|
-
/**
|
|
315
|
-
* Clears all cached values
|
|
316
|
-
*/
|
|
317
|
-
clear(): void;
|
|
318
|
-
/**
|
|
319
|
-
* Invalidates specific cached value
|
|
320
|
-
*/
|
|
321
|
-
invalidate(key: string): void;
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Capability for collecting aggregate metrics.
|
|
325
|
-
* Enables monitoring and observability of aggregate operations.
|
|
326
|
-
*/
|
|
327
|
-
export interface IMetricsCapability extends IAggregateCapability {
|
|
328
|
-
/**
|
|
329
|
-
* Records a metric
|
|
330
|
-
*/
|
|
331
|
-
record(name: string, value: number, tags?: Record<string, string>): void;
|
|
332
|
-
/**
|
|
333
|
-
* Increments a counter
|
|
334
|
-
*/
|
|
335
|
-
increment(name: string, tags?: Record<string, string>): void;
|
|
336
|
-
/**
|
|
337
|
-
* Records timing information
|
|
338
|
-
*/
|
|
339
|
-
timing(name: string, duration: number, tags?: Record<string, string>): void;
|
|
340
|
-
/**
|
|
341
|
-
* Gets recorded metrics
|
|
342
|
-
*/
|
|
343
|
-
getMetrics(): MetricData[];
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Represents a single metric data point.
|
|
347
|
-
* Contains metric information with optional tags and timestamp.
|
|
348
|
-
*/
|
|
349
|
-
export interface MetricData {
|
|
350
|
-
/** Metric name */
|
|
351
|
-
name: string;
|
|
352
|
-
/** Metric value */
|
|
353
|
-
value: number;
|
|
354
|
-
/** Type of metric */
|
|
355
|
-
type: 'counter' | 'gauge' | 'timing';
|
|
356
|
-
/** Optional tags for metric categorization */
|
|
357
|
-
tags?: Record<string, string>;
|
|
358
|
-
/** When the metric was recorded */
|
|
359
|
-
timestamp: Date;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Capability for security features like encryption and authorization.
|
|
363
|
-
* Provides security controls for sensitive aggregate operations.
|
|
364
|
-
*/
|
|
365
|
-
export interface ISecurityCapability extends IAggregateCapability {
|
|
366
|
-
/**
|
|
367
|
-
* Encrypts sensitive data
|
|
368
|
-
*/
|
|
369
|
-
encrypt(data: unknown): string;
|
|
370
|
-
/**
|
|
371
|
-
* Decrypts sensitive data
|
|
372
|
-
*/
|
|
373
|
-
decrypt(encryptedData: string): unknown;
|
|
374
|
-
/**
|
|
375
|
-
* Checks if user has permission to perform action
|
|
376
|
-
*/
|
|
377
|
-
hasPermission(action: string, user?: unknown): boolean;
|
|
378
|
-
/**
|
|
379
|
-
* Logs security events
|
|
380
|
-
*/
|
|
381
|
-
logSecurityEvent(event: SecurityEvent): void;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Represents a security-related event.
|
|
385
|
-
* Used for audit trails and security monitoring.
|
|
386
|
-
*/
|
|
387
|
-
export interface SecurityEvent {
|
|
388
|
-
/** Type of security event */
|
|
389
|
-
type: 'access' | 'modification' | 'failure';
|
|
390
|
-
/** Action that was performed */
|
|
391
|
-
action: string;
|
|
392
|
-
/** User who performed the action */
|
|
393
|
-
user?: unknown;
|
|
394
|
-
/** When the event occurred */
|
|
395
|
-
timestamp: Date;
|
|
396
|
-
/** Additional event metadata */
|
|
397
|
-
metadata?: Record<string, unknown>;
|
|
398
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Capability, IAuditCapability, IAuditEvent, IDomainEvent } from '
|
|
1
|
+
import { Capability, IAuditCapability, IAuditEvent, IDomainEvent } from '@vytches/ddd-contracts';
|
|
2
2
|
/**
|
|
3
3
|
* Capability that records every domain event applied to the aggregate as
|
|
4
4
|
* an `IAuditEvent`. Useful for compliance, debugging, behavior analytics,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IEventSourcingCapability, IEventStore, Capability } from '
|
|
1
|
+
import { IEventSourcingCapability, IEventStore, Capability } from '@vytches/ddd-contracts';
|
|
2
2
|
/**
|
|
3
3
|
* Capability that integrates an aggregate with an external `IEventStore` —
|
|
4
4
|
* the persistence boundary for an event-sourced system. Allows the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IAggregateSnapshot, ISnapshotCapability, Capability } from '
|
|
1
|
+
import { IAggregateSnapshot, ISnapshotCapability, Capability } from '@vytches/ddd-contracts';
|
|
2
2
|
/**
|
|
3
3
|
* Capability that enables snapshot creation and restoration on an aggregate
|
|
4
4
|
* — a performance optimization for event-sourced aggregates with long
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Capability, IVersioningCapability, IDomainEvent, IEventUpcaster } from '
|
|
1
|
+
import { Capability, IVersioningCapability, IDomainEvent, IEventUpcaster } from '@vytches/ddd-contracts';
|
|
2
2
|
import { IAggregateEventHandler } from '../aggregate-interfaces';
|
|
3
3
|
/**
|
|
4
4
|
* Capability that handles **event schema evolution** — when a stored event
|
|
@@ -63,7 +63,20 @@ export declare class VersioningCapability extends Capability<'versioning'> imple
|
|
|
63
63
|
*/
|
|
64
64
|
registerUpcaster<TFrom = unknown, TTo = unknown>(eventType: string, sourceVersion: number, upcaster: IEventUpcaster<TFrom, TTo>): void;
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
66
|
+
* Applies registered upcasters to bring `event` from its stored version up
|
|
67
|
+
* to the aggregate's current version, then invokes the matching handler.
|
|
68
|
+
*
|
|
69
|
+
* **Upcasting only activates if `event.metadata.targetVersion` is set.**
|
|
70
|
+
* This is NOT inferred automatically from registered upcasters or from
|
|
71
|
+
* any aggregate-level "current version" — the caller (typically your
|
|
72
|
+
* event-sourcing replay/load path) must explicitly stamp each historical
|
|
73
|
+
* event's metadata with `targetVersion` before calling this method. If
|
|
74
|
+
* `targetVersion` is absent, `currentVersion` falls back to `eventVersion`
|
|
75
|
+
* (i.e. no upcasting happens — the event is passed through unchanged even
|
|
76
|
+
* if upcasters are registered for it).
|
|
77
|
+
*
|
|
78
|
+
* @param {IDomainEvent} event - Event to process with version handling;
|
|
79
|
+
* set `event.metadata.targetVersion` to enable upcasting
|
|
67
80
|
* @param {Map<string, IAggregateEventHandler>} handlers - Map of event handlers
|
|
68
81
|
*/
|
|
69
82
|
handleVersionedEvent(event: IDomainEvent, handlers: Map<string, IAggregateEventHandler>): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Capability, IEventStore, EntityId } from '
|
|
1
|
+
import { Capability, IEventStore, EntityId } from '@vytches/ddd-contracts';
|
|
2
2
|
import { IAggregateCapability, IAggregateConstructorParams } from '../aggregate-interfaces';
|
|
3
3
|
import { AggregateRoot } from './aggregate-root';
|
|
4
4
|
/**
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { IAggregateCapability, IAggregateConstructorParams, IAggregateEventHandler, IAggregateRoot } from '../aggregate-interfaces';
|
|
2
|
-
import { Capability, CapabilityConstructor, EntityId, IDomainEvent, IEventMetadata } from '
|
|
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
|
|
@@ -111,28 +128,69 @@ export declare class AggregateRoot<TId = string> implements IAggregateRoot<TId>
|
|
|
111
128
|
* loops or malicious replay payloads.
|
|
112
129
|
*/
|
|
113
130
|
private readonly _maxEvents;
|
|
131
|
+
/**
|
|
132
|
+
* VF-023 (D-4, AC6, non-breaking addition): controls what happens when
|
|
133
|
+
* `apply()` or `loadFromHistory()` processes an event for which no
|
|
134
|
+
* handler was registered via `registerEventHandler()`. Defaults to
|
|
135
|
+
* `"warn"` (preserves the previous silent-but-logged-nowhere behavior's
|
|
136
|
+
* intent without a breaking default) — the event is still recorded /
|
|
137
|
+
* replayed, but a warning is logged via the library's internal logger.
|
|
138
|
+
* Set to `"throw"` to fail fast instead, useful in tests or strict
|
|
139
|
+
* environments where a missing handler indicates a real bug.
|
|
140
|
+
*/
|
|
141
|
+
private readonly _onMissingHandler;
|
|
114
142
|
constructor(params: IAggregateConstructorParams<TId>);
|
|
115
143
|
/**
|
|
116
144
|
* Get the unique identifier of this aggregate
|
|
117
145
|
*/
|
|
118
146
|
getId(): EntityId<TId>;
|
|
119
147
|
/**
|
|
148
|
+
* Identity-based equality, matching {@link Entity.equals}. Two aggregates
|
|
149
|
+
* are equal iff they have the same id (compared via `EntityId.equals()`);
|
|
150
|
+
* attribute/version differences are ignored.
|
|
151
|
+
*
|
|
152
|
+
* VF-023 (D-5, AC10, non-breaking addition): `AggregateRoot` does not
|
|
153
|
+
* extend `Entity` (it implements `IAggregateRoot<TId>` directly), so this
|
|
154
|
+
* mirrors `Entity.equals()` rather than inheriting it.
|
|
155
|
+
*
|
|
156
|
+
* @param other - Aggregate to compare against. May be `null` or `undefined`.
|
|
157
|
+
* @returns `true` if both aggregates have the same id; `false` otherwise.
|
|
158
|
+
*/
|
|
159
|
+
equals(other: AggregateRoot<TId> | null | undefined): boolean;
|
|
160
|
+
/**
|
|
161
|
+
* The version increments by one each time `apply()` successfully records a
|
|
162
|
+
* domain event; compare it against the persisted version to detect
|
|
163
|
+
* concurrent modification (optimistic locking).
|
|
120
164
|
*
|
|
121
165
|
* @returns {number} Current version number for optimistic locking
|
|
122
166
|
*/
|
|
123
167
|
getVersion(): number;
|
|
124
168
|
/**
|
|
169
|
+
* The version this aggregate had when it was loaded (or as of the last
|
|
170
|
+
* `commit()`), letting callers work out how many events have been applied
|
|
171
|
+
* since without recomputing from the event log.
|
|
125
172
|
*
|
|
126
173
|
* @returns {number} Initial version when aggregate was loaded
|
|
127
174
|
*/
|
|
128
175
|
getInitialVersion(): number;
|
|
129
176
|
/**
|
|
177
|
+
* Whether this aggregate has recorded domain events since its last
|
|
178
|
+
* `commit()` — use it to decide whether persistence/dispatch work is
|
|
179
|
+
* needed at all.
|
|
130
180
|
*
|
|
131
181
|
* @returns {boolean} True if aggregate has uncommitted domain events
|
|
132
182
|
*/
|
|
133
183
|
hasChanges(): boolean;
|
|
134
184
|
/**
|
|
135
|
-
*
|
|
185
|
+
* VF-023 (D-3, AC11, BREAKING): the returned array and every event object
|
|
186
|
+
* it contains (including nested `payload`/`metadata`) are now deep-frozen
|
|
187
|
+
* via `LibUtils.deepFreeze` — the same immutability guarantee applied to
|
|
188
|
+
* `BaseValueObject` values (see `base-value-object.ts`). Previously this
|
|
189
|
+
* returned only a shallow array copy: the array itself couldn't be
|
|
190
|
+
* mutated by callers, but the event objects (and their nested payload
|
|
191
|
+
* data) inside it could still be mutated in place, silently corrupting
|
|
192
|
+
* state shared with `_domainEvents` (the same object references are
|
|
193
|
+
* stored internally — events are never re-cloned after creation).
|
|
136
194
|
* @returns {ReadonlyArray<IDomainEvent>} Array of uncommitted domain events
|
|
137
195
|
*/
|
|
138
196
|
getDomainEvents(): ReadonlyArray<IDomainEvent>;
|
|
@@ -151,9 +209,28 @@ export declare class AggregateRoot<TId = string> implements IAggregateRoot<TId>
|
|
|
151
209
|
* @param {string | IDomainEvent<P>} eventTypeOrEvent - Event type string or complete event object
|
|
152
210
|
* @param {P} payload - Event payload data
|
|
153
211
|
* @param {Partial<IEventMetadata>} metadata - Optional event metadata
|
|
212
|
+
* @throws {Error} when the aggregate's configured maxEvents limit would be exceeded
|
|
213
|
+
* @example Hitting the maxEvents guard
|
|
214
|
+
* ```typescript
|
|
215
|
+
* class Order extends AggregateRoot<string> {
|
|
216
|
+
* record(customerId: string, amount: number): void {
|
|
217
|
+
* this.apply('OrderCreated', { customerId, amount });
|
|
218
|
+
* }
|
|
219
|
+
* }
|
|
220
|
+
*
|
|
221
|
+
* const order = new Order({ id: EntityId.create(), version: 0, maxEvents: 1 });
|
|
222
|
+
* order.record('c-1', 100); // ok, 1 event recorded
|
|
223
|
+
* order.record('c-1', 100); // throws: maxEvents limit of 1 exceeded
|
|
224
|
+
* ```
|
|
154
225
|
*/
|
|
155
226
|
protected apply<P = unknown>(eventTypeOrEvent: string | IDomainEvent<P>, payload?: P, metadata?: Partial<IEventMetadata>): void;
|
|
156
227
|
private handleEvent;
|
|
228
|
+
/**
|
|
229
|
+
* VF-023 (D-4, AC6): called from `handleEvent()`, which is shared by both
|
|
230
|
+
* `apply()` (live) and `loadFromHistory()` (replay) — so the missing-
|
|
231
|
+
* handler policy applies identically to both paths.
|
|
232
|
+
*/
|
|
233
|
+
private reportMissingHandler;
|
|
157
234
|
/**
|
|
158
235
|
* Reconstruct aggregate state from historical domain events
|
|
159
236
|
* @param {IDomainEvent[]} events - Array of events to replay for state reconstruction
|
|
@@ -190,10 +267,20 @@ export declare class AggregateRoot<TId = string> implements IAggregateRoot<TId>
|
|
|
190
267
|
getCapabilityTypes(): string[];
|
|
191
268
|
private static sanitizeMetadata;
|
|
192
269
|
/**
|
|
193
|
-
* Internal method for capabilities to access private state
|
|
194
|
-
*
|
|
270
|
+
* Internal method for capabilities to access private state.
|
|
271
|
+
*
|
|
272
|
+
* VF-023 (D-2, AC5, BREAKING, CRITICAL F-H4): gated by
|
|
273
|
+
* {@link INTERNAL_STATE_TOKEN}, a module-private `unique symbol` that is
|
|
274
|
+
* exported only from this module (not from the package's public barrel).
|
|
275
|
+
* Callers must supply the exact token reference to invoke this method —
|
|
276
|
+
* consumers of `@vytches/ddd-aggregates` cannot obtain it, so this method
|
|
277
|
+
* is unreachable from outside this package's own source, even via
|
|
278
|
+
* `as unknown as {...}` casts (the token value itself, not just its type,
|
|
279
|
+
* is required and cannot be forged).
|
|
280
|
+
*
|
|
281
|
+
* @throws {TypeError} if `token` does not match {@link INTERNAL_STATE_TOKEN}
|
|
195
282
|
*/
|
|
196
|
-
_internal_setState(state: {
|
|
283
|
+
_internal_setState(token: typeof INTERNAL_STATE_TOKEN, state: {
|
|
197
284
|
version: number;
|
|
198
285
|
initialVersion: number;
|
|
199
286
|
domainEvents: IDomainEvent[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Result } from '
|
|
2
|
-
import { CapabilityConstructor, Capability, IAggregateCapability, IAggregateSnapshot } from '
|
|
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';
|
package/dist/core/entity.d.ts
CHANGED