@vytches/ddd-aggregates 0.26.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/LICENSE +21 -0
- package/LLMGUIDE.md +185 -0
- package/README.md +1393 -0
- package/dist/aggregate-errors.d.ts +58 -0
- package/dist/aggregate-interfaces.d.ts +398 -0
- package/dist/capabilities/audit-capability.d.ts +95 -0
- package/dist/capabilities/event-sourcing-capability.d.ts +79 -0
- package/dist/capabilities/index.d.ts +4 -0
- package/dist/capabilities/snapshot-capability.d.ts +113 -0
- package/dist/capabilities/versioning-capability.d.ts +93 -0
- package/dist/core/aggregate-root.builder.d.ts +110 -0
- package/dist/core/aggregate-root.d.ts +201 -0
- package/dist/core/aggregate-utilities.d.ts +233 -0
- package/dist/core/entity.d.ts +76 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1283 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 VytchesDDD
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/LLMGUIDE.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# @vytches/ddd-aggregates - LLM Guide
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Provides the `AggregateRoot` base class and a capability system for composing
|
|
6
|
+
event sourcing, snapshots, audit logging, and versioning into aggregates without
|
|
7
|
+
inheritance chains.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { AggregateRoot, AggregateBuilder } from '@vytches/ddd-aggregates';
|
|
13
|
+
import { EntityId } from '@vytches/ddd-value-objects';
|
|
14
|
+
import type { IAggregateConstructorParams } from '@vytches/ddd-aggregates';
|
|
15
|
+
|
|
16
|
+
interface OrderCreatedPayload {
|
|
17
|
+
customerId: string;
|
|
18
|
+
amount: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class Order extends AggregateRoot<string> {
|
|
22
|
+
private customerId = '';
|
|
23
|
+
private amount = 0;
|
|
24
|
+
|
|
25
|
+
constructor(params: IAggregateConstructorParams<string>) {
|
|
26
|
+
super(params);
|
|
27
|
+
this.registerEventHandler<OrderCreatedPayload>('OrderCreated', payload => {
|
|
28
|
+
this.customerId = payload!.customerId;
|
|
29
|
+
this.amount = payload!.amount;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static create(customerId: string, amount: number): Order {
|
|
34
|
+
const id = EntityId.create();
|
|
35
|
+
const order = new Order({ id, version: 0 });
|
|
36
|
+
order.apply('OrderCreated', { customerId, amount });
|
|
37
|
+
return order;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Usage
|
|
42
|
+
const order = Order.create('c-1', 500);
|
|
43
|
+
const events = order.getDomainEvents(); // [{ eventName: 'OrderCreated', ... }]
|
|
44
|
+
order.commit(); // clear pending events after saving
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Key API
|
|
48
|
+
|
|
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
|
+
| `hasAllCapabilities(agg, [...])` | function | Returns `true` if aggregate has every listed capability |
|
|
63
|
+
| `getAggregateCapabilities(agg)` | function | Returns string array of capability type names |
|
|
64
|
+
| `AggregateWithSnapshotCapability<TId>` | type | Type-safe alias for snapshot-capable aggregates |
|
|
65
|
+
| `IAggregateRoot<TId>` | interface | Full contract for aggregates |
|
|
66
|
+
|
|
67
|
+
### `AggregateRoot` method reference
|
|
68
|
+
|
|
69
|
+
| Method | Description |
|
|
70
|
+
| --------------------------------------- | ------------------------------------------------------------------------- |
|
|
71
|
+
| `apply(eventType, payload?, metadata?)` | Record a domain event, increment version, call handler |
|
|
72
|
+
| `loadFromHistory(events)` | Replay historical events to restore state without accumulating new events |
|
|
73
|
+
| `getDomainEvents()` | Return readonly array of uncommitted events since last `commit()` |
|
|
74
|
+
| `commit()` | Clear uncommitted events; call after persisting to storage |
|
|
75
|
+
| `hasChanges()` | `true` if there are uncommitted events |
|
|
76
|
+
| `getVersion()` | Current version after all applied events |
|
|
77
|
+
| `getInitialVersion()` | Version when last loaded from storage (for optimistic locking) |
|
|
78
|
+
| `addCapability(cap)` | Attach a capability; calls `cap.attach(this)` |
|
|
79
|
+
| `getCapability(CapClass)` | Retrieve capability by constructor, returns `undefined` if absent |
|
|
80
|
+
| `hasCapability(CapClass)` | Type-safe capability presence check |
|
|
81
|
+
| `registerEventHandler(type, fn)` | Protected; wire event name to state mutation function |
|
|
82
|
+
|
|
83
|
+
## Patterns
|
|
84
|
+
|
|
85
|
+
### Event sourcing reconstitution
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// In your repository, after loading events from the store:
|
|
89
|
+
const order = new Order({ id: EntityId.fromUUID(rawId), version: 0 });
|
|
90
|
+
order['loadFromHistory'](storedEvents); // protected; call from within the class or a static factory
|
|
91
|
+
// If loadFromHistory must be called externally, expose a static fromEvents():
|
|
92
|
+
class Order extends AggregateRoot<string> {
|
|
93
|
+
static fromEvents(id: EntityId<string>, events: IDomainEvent[]): Order {
|
|
94
|
+
const order = new Order({ id, version: 0 });
|
|
95
|
+
order.loadFromHistory(events);
|
|
96
|
+
return order;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Using the builder with capabilities
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import {
|
|
105
|
+
AggregateBuilder,
|
|
106
|
+
SnapshotCapability,
|
|
107
|
+
AuditCapability,
|
|
108
|
+
} from '@vytches/ddd-aggregates';
|
|
109
|
+
import { EntityId } from '@vytches/ddd-value-objects';
|
|
110
|
+
|
|
111
|
+
const order = AggregateBuilder.create({ id: EntityId.create() })
|
|
112
|
+
.withSnapshots()
|
|
113
|
+
.withAudit()
|
|
114
|
+
.build(Order);
|
|
115
|
+
|
|
116
|
+
// Now safe to cast:
|
|
117
|
+
import { asSnapshotAggregate } from '@vytches/ddd-aggregates';
|
|
118
|
+
const snapshotOrder = asSnapshotAggregate(order);
|
|
119
|
+
const snap = snapshotOrder
|
|
120
|
+
.getCapability(SnapshotCapability)!
|
|
121
|
+
.createSnapshot(() => ({ amount: order.getAmount() }));
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Capability type guard before use
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import {
|
|
128
|
+
hasAllCapabilities,
|
|
129
|
+
asAuditAggregate,
|
|
130
|
+
AuditCapability,
|
|
131
|
+
} from '@vytches/ddd-aggregates';
|
|
132
|
+
|
|
133
|
+
if (hasAllCapabilities(order, [AuditCapability])) {
|
|
134
|
+
const auditable = asAuditAggregate(order);
|
|
135
|
+
const log = auditable.getCapability(AuditCapability)!.getAuditLog();
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Anti-Patterns
|
|
140
|
+
|
|
141
|
+
**Mutating state directly instead of using `apply()`.** Assigning
|
|
142
|
+
`this.status = 'paid'` bypasses event recording and version tracking. All state
|
|
143
|
+
changes must go through `apply()` so they are captured as events.
|
|
144
|
+
|
|
145
|
+
**Not registering event handlers before calling `apply()`.**
|
|
146
|
+
`registerEventHandler` must be called in the constructor before any `apply()`
|
|
147
|
+
calls. If handlers are missing, events are recorded but state is not updated —
|
|
148
|
+
no error is thrown.
|
|
149
|
+
|
|
150
|
+
**Forgetting to call `super(params)` in subclass constructor.** Without the
|
|
151
|
+
super call, `_id`, `_version`, and internal maps are never initialized.
|
|
152
|
+
TypeScript will not always catch this at compile time.
|
|
153
|
+
|
|
154
|
+
**Using `loadFromHistory` on a live aggregate that already has uncommitted
|
|
155
|
+
events.** `loadFromHistory` resets `_domainEvents` to an empty array and replays
|
|
156
|
+
from `_initialVersion`. Calling it after `apply()` discards pending events
|
|
157
|
+
silently.
|
|
158
|
+
|
|
159
|
+
**Adding capabilities after `apply()` calls.** Capabilities like
|
|
160
|
+
`AuditCapability` only audit events applied after they are attached. Add all
|
|
161
|
+
capabilities in the constructor or builder before any business methods are
|
|
162
|
+
called.
|
|
163
|
+
|
|
164
|
+
## Hidden Features
|
|
165
|
+
|
|
166
|
+
`apply()` accepts either a plain string event type or a full `IDomainEvent`
|
|
167
|
+
object — pass a class-based event instance and the prototype chain is preserved,
|
|
168
|
+
enabling `instanceof` checks on events after enrichment.
|
|
169
|
+
|
|
170
|
+
`getAggregateInfo(aggregate)` (from utilities) returns a single diagnostic
|
|
171
|
+
object with `id`, `type`, `version`, `hasChanges`, `capabilities`, and `events`
|
|
172
|
+
count — useful for logging without exposing internals.
|
|
173
|
+
|
|
174
|
+
`aggregateBuilder(...).buildWithAllCapabilities(MyClass)` attaches all four
|
|
175
|
+
built-in capabilities (snapshot, versioning, audit, event sourcing) in one call.
|
|
176
|
+
|
|
177
|
+
`_internal_setState` exists on `AggregateRoot` and is intended only for
|
|
178
|
+
capability implementations — never call it from domain code.
|
|
179
|
+
|
|
180
|
+
## Package Dependencies
|
|
181
|
+
|
|
182
|
+
**Depends on:** `@vytches/ddd-contracts`, `@vytches/ddd-logging`.
|
|
183
|
+
|
|
184
|
+
**Depended on by:** `@vytches/ddd-enterprise` (re-exports), consumer domain
|
|
185
|
+
packages.
|