@shirudo/ddd-kit 2.2.0 → 3.0.0-rc.4
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/README.md +206 -55
- package/dist/chunks/deep-equal-except.js +639 -0
- package/dist/chunks/deep-equal-except.js.map +1 -0
- package/dist/chunks/errors.d.ts +785 -0
- package/dist/chunks/errors.js +822 -0
- package/dist/chunks/errors.js.map +1 -0
- package/dist/chunks/ports.js +891 -0
- package/dist/chunks/ports.js.map +1 -0
- package/dist/chunks/snapshot-store.d.ts +2808 -0
- package/dist/chunks/utils.d.ts +110 -0
- package/dist/http.d.ts +64 -51
- package/dist/http.js +54 -20
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +2351 -2650
- package/dist/index.js +6140 -3915
- package/dist/index.js.map +1 -1
- package/dist/money.d.ts +376 -0
- package/dist/money.js +578 -0
- package/dist/money.js.map +1 -0
- package/dist/presentation.d.ts +86 -37
- package/dist/presentation.js +208 -39
- package/dist/presentation.js.map +1 -1
- package/dist/testing.d.ts +517 -335
- package/dist/testing.js +2396 -1184
- package/dist/testing.js.map +1 -1
- package/dist/utils.d.ts +2 -106
- package/dist/utils.js +2 -530
- package/package.json +35 -18
- package/dist/aggregate-DFi6HlEh.d.ts +0 -771
- package/dist/utils.js.map +0 -1
package/dist/testing.d.ts
CHANGED
|
@@ -1,374 +1,556 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import '@shirudo/result';
|
|
3
|
-
import '@shirudo/base-error';
|
|
1
|
+
import { At as AnyDomainEvent, Et as IAggregateRoot, G as DispatchTrackingOutbox, L as CommandOutboxCommitCandidate, U as CommittedDomainEvent, Z as Outbox, a as StreamReadResult, b as IdempotencyStore, c as ProjectionCheckpointStore, en as Id, f as DeadlineStore, h as IdempotencyClaimHandle, i as ReadStreamOptions, it as PublishedCommand, n as EventStore, q as EventCommitCandidate, t as SnapshotStore, wt as AggregateAddress, z as CommandOutboxWriter } from "./chunks/snapshot-store.js";
|
|
4
2
|
|
|
3
|
+
//#region src/testing/contract-assertions.d.ts
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* so the suite does not require one.
|
|
5
|
+
* One entry of a contract test suite. Every suite (repository,
|
|
6
|
+
* event-sourced repository, outbox, idempotency store) returns a list
|
|
7
|
+
* of these; bind them with
|
|
8
|
+
* `(test.skipped ? it.skip : it)(test.name, test.run)`.
|
|
11
9
|
*/
|
|
12
|
-
interface
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
interface ContractTest {
|
|
11
|
+
name: string;
|
|
12
|
+
run: () => Promise<void>;
|
|
13
|
+
/** Present when the harness lacks the capability this test needs. */
|
|
14
|
+
skipped?: {
|
|
15
|
+
capability: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/testing/command-outbox-contract.d.ts
|
|
20
|
+
interface CommandOutboxContractEnvironment<C extends PublishedCommand> {
|
|
21
|
+
readonly outbox: CommandOutboxWriter<C>;
|
|
22
|
+
readonly addCommitted: (commits: ReadonlyArray<CommandOutboxCommitCandidate<C>>) => Promise<void>;
|
|
23
|
+
readonly addRolledBack?: (commits: ReadonlyArray<CommandOutboxCommitCandidate<C>>) => Promise<void>;
|
|
24
|
+
readonly readAll: () => Promise<ReadonlyArray<CommandOutboxCommitCandidate<C>>>;
|
|
25
|
+
readonly teardown?: () => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
interface CommandOutboxContractHarness<C extends PublishedCommand> {
|
|
28
|
+
readonly createEnvironment: () => Promise<CommandOutboxContractEnvironment<C>>;
|
|
29
|
+
/**
|
|
30
|
+
* Builds one command for the given seed. The suite derives conflicting
|
|
31
|
+
* commits from different seeds, so `createCommand` MUST return distinct
|
|
32
|
+
* command content per seed (put the seed in the payload). A constant
|
|
33
|
+
* command makes a manufactured conflict deep-equal to its original, and
|
|
34
|
+
* the conflict tests then fail a compliant adapter that deduplicates
|
|
35
|
+
* the exact retry.
|
|
36
|
+
*/
|
|
37
|
+
readonly createCommand: (seed: number) => C;
|
|
38
|
+
readonly providesRolledBackAdds?: boolean;
|
|
39
|
+
}
|
|
40
|
+
type CommandOutboxContractTest = ContractTest;
|
|
41
|
+
declare function createCommandOutboxContractTests<C extends PublishedCommand>(harness: CommandOutboxContractHarness<C>): ReadonlyArray<CommandOutboxContractTest>;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/testing/deadline-store-contract.d.ts
|
|
44
|
+
/** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
|
|
45
|
+
type DeadlineStoreContractTest = ContractTest;
|
|
46
|
+
/** The plain-data payload shape the suite round-trips. */
|
|
47
|
+
interface SuitePayload {
|
|
48
|
+
kind: string;
|
|
49
|
+
step?: number;
|
|
15
50
|
}
|
|
16
51
|
/**
|
|
17
|
-
* One isolated test environment: fresh
|
|
18
|
-
*
|
|
52
|
+
* One isolated test environment: a fresh deadline store. The suite
|
|
53
|
+
* creates one per test and tears it down afterwards.
|
|
19
54
|
*/
|
|
20
|
-
interface
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* `EventStore.readStream` so the suite's ordering and slicing
|
|
40
|
-
* assertions exercise your real read path. A rolled-back
|
|
41
|
-
* transaction's events must not appear here.
|
|
42
|
-
*/
|
|
43
|
-
committedStreamEvents(id: TAgg["id"], fromVersion?: number): Promise<ReadonlyArray<Evt>>;
|
|
44
|
-
/** Release connections, drop schemas, etc. Called in a finally. */
|
|
45
|
-
teardown?(): Promise<void>;
|
|
55
|
+
interface DeadlineStoreContractEnvironment {
|
|
56
|
+
/** The adapter under test. */
|
|
57
|
+
store: DeadlineStore<SuitePayload>;
|
|
58
|
+
/**
|
|
59
|
+
* Runs `work` (schedule/cancel calls) the way production does:
|
|
60
|
+
* inside a transaction that COMMITS. For a non-transactional store
|
|
61
|
+
* this simply invokes `work`.
|
|
62
|
+
*/
|
|
63
|
+
run<R>(work: () => Promise<R>): Promise<R>;
|
|
64
|
+
/**
|
|
65
|
+
* Optional capability: runs `work` inside a transaction that ROLLS
|
|
66
|
+
* BACK. Enables the rollback tests: a rolled-back schedule must not
|
|
67
|
+
* leave a deadline behind (a ghost input for a state change that
|
|
68
|
+
* never happened), and a rolled-back cancel must not have removed
|
|
69
|
+
* one. Transactional adapters should always provide this.
|
|
70
|
+
*/
|
|
71
|
+
runRolledBack?<R>(work: () => Promise<R>): Promise<R>;
|
|
72
|
+
/** Release connections, drop schemas, etc. Called in a finally. */
|
|
73
|
+
teardown?(): Promise<void>;
|
|
46
74
|
}
|
|
47
75
|
/**
|
|
48
|
-
* What an adapter supplies to run the
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
76
|
+
* What an adapter supplies to run the deadline-store contract suite.
|
|
77
|
+
* For SQL adapters, run against a real database (testcontainers or
|
|
78
|
+
* equivalent): the rollback tests prove YOUR transaction wiring, and
|
|
79
|
+
* schedule/cancel joining the write transaction is the port's central
|
|
80
|
+
* correctness rule.
|
|
81
|
+
*/
|
|
82
|
+
interface DeadlineStoreContractHarness {
|
|
83
|
+
createEnvironment(): Promise<DeadlineStoreContractEnvironment>;
|
|
84
|
+
/**
|
|
85
|
+
* The adapter's attempt ceiling: how many `markFailed` reports move
|
|
86
|
+
* a deadline to the dead-letter set. Must be at least 2 so the
|
|
87
|
+
* attempts-surfacing test can observe a survivor.
|
|
88
|
+
*/
|
|
89
|
+
failuresToDeadLetter: number;
|
|
90
|
+
/**
|
|
91
|
+
* Declare `true` when environments provide {@link
|
|
92
|
+
* DeadlineStoreContractEnvironment.runRolledBack}. Without it, the
|
|
93
|
+
* rollback tests are marked skipped: the honest state of an
|
|
94
|
+
* in-memory fake, and a loud gap for a transactional adapter.
|
|
95
|
+
*/
|
|
96
|
+
providesRolledBackRuns?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Declare `true` when the adapter's `due` CLAIMS the returned
|
|
99
|
+
* records for competing pollers (lease, visibility timeout), as the
|
|
100
|
+
* port sanctions. Tests that re-poll records an earlier poll
|
|
101
|
+
* returned without resolving them (attempts surfacing, neighbor
|
|
102
|
+
* flow after a dead-letter, successor visibility during a
|
|
103
|
+
* reschedule race) assume a non-claiming read and are marked
|
|
104
|
+
* skipped for claiming adapters; prove your claim/expiry semantics
|
|
105
|
+
* in your own suite.
|
|
106
|
+
*/
|
|
107
|
+
claimsOnDue?: boolean;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The deadline-store contract test suite: the proof that an adapter
|
|
111
|
+
* delivers the schedule/cancel/due/acknowledge semantics the port
|
|
112
|
+
* documents. Store semantics are an **adapter contract, not a kit
|
|
113
|
+
* guarantee**; this suite is how an adapter demonstrates them.
|
|
54
114
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* applied creation event (version 1, `persistedVersion === undefined`).
|
|
58
|
-
* - `mutate()` applies exactly ONE event (+1 version).
|
|
115
|
+
* Framework-agnostic: bind with
|
|
116
|
+
* `(test.skipped ? it.skip : it)(test.name, test.run)`.
|
|
59
117
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
* compared with deep equality. Enables the state assertions in the
|
|
78
|
-
* mandatory and replay-equality tests. Must be roundtrip-stable
|
|
79
|
-
* across your store (see the state-stored harness JSDoc for the
|
|
80
|
-
* normalization checklist).
|
|
81
|
-
*/
|
|
82
|
-
snapshotState?(aggregate: TAgg): unknown;
|
|
118
|
+
declare function createDeadlineStoreContractTests(harness: DeadlineStoreContractHarness): DeadlineStoreContractTest[];
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/testing/es-repository-contract.d.ts
|
|
121
|
+
/** Event-sourced repositories normally expose no physical removal. */
|
|
122
|
+
interface EsContractRepository<TAggregate extends IAggregateRoot<Id<string>, AnyDomainEvent>> {
|
|
123
|
+
findById(id: TAggregate["id"]): Promise<TAggregate | undefined>;
|
|
124
|
+
add(aggregate: TAggregate): void;
|
|
125
|
+
update(aggregate: TAggregate): void;
|
|
126
|
+
}
|
|
127
|
+
interface EsRepositoryContractEnvironment<TAggregate extends IAggregateRoot<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
|
|
128
|
+
run<R>(work: (context: {
|
|
129
|
+
repository: EsContractRepository<TAggregate>;
|
|
130
|
+
}) => Promise<R>): Promise<R>;
|
|
131
|
+
committedOutboxEvents(): Promise<ReadonlyArray<CommittedDomainEvent<TEvent>>>;
|
|
132
|
+
failNextOutboxWrite(error: Error): void;
|
|
133
|
+
committedStreamEvents(stream: AggregateAddress<TAggregate["id"]>, options: ReadStreamOptions): Promise<StreamReadResult<TEvent>>;
|
|
134
|
+
teardown?(): Promise<void>;
|
|
83
135
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
136
|
+
interface EsRepositoryContractHarness<TAggregate extends IAggregateRoot<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
|
|
137
|
+
createEnvironment(): Promise<EsRepositoryContractEnvironment<TAggregate, TEvent>>;
|
|
138
|
+
/** Fresh aggregate with exactly one recorded creation event. */
|
|
139
|
+
createAggregate(): TAggregate;
|
|
140
|
+
createAggregateWithId?(id: TAggregate["id"]): TAggregate;
|
|
141
|
+
streamKeyFor(id: TAggregate["id"]): AggregateAddress<TAggregate["id"]>;
|
|
142
|
+
/** Applies exactly one event and advances the aggregate version by one. */
|
|
143
|
+
mutate(aggregate: TAggregate): void;
|
|
144
|
+
snapshotState?(aggregate: TAggregate): unknown;
|
|
92
145
|
}
|
|
146
|
+
type EsRepositoryContractTest = ContractTest;
|
|
93
147
|
/**
|
|
94
|
-
*
|
|
95
|
-
* adapter delivers what the kit's `EventStore` port and Unit of Work
|
|
96
|
-
* document. The kit is store-agnostic: the expectedVersion guard lives
|
|
97
|
-
* in YOUR adapter's append. That makes stream OCC a **repository
|
|
98
|
-
* contract, not a kit guarantee**; an adapter that has not passed this
|
|
99
|
-
* suite (against a real store, for SQL-backed adapters) has not
|
|
100
|
-
* demonstrated it.
|
|
101
|
-
*
|
|
102
|
-
* What each test proves:
|
|
103
|
-
* - The MANDATORY two-writer test proves your append's expectedVersion
|
|
104
|
-
* guard and its atomicity.
|
|
105
|
-
* - The replay/lifecycle tests prove your read path (fold order,
|
|
106
|
-
* identity map wiring) and the commit lifecycle (outbox harvest,
|
|
107
|
-
* `markPersisted`, rollback purity).
|
|
108
|
-
* - The duplicate-create and fromVersion tests prove the create race
|
|
109
|
-
* and the snapshot catch-up read.
|
|
148
|
+
* Contract suite for event-stream adapters using v3 Unit-of-Work receipts.
|
|
110
149
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* for (const test of createEsRepositoryContractTests(harness)) {
|
|
116
|
-
* (test.skipped ? it.skip : it)(test.name, test.run);
|
|
117
|
-
* }
|
|
118
|
-
* ```
|
|
119
|
-
*
|
|
120
|
-
* **Known limitation:** like the state-stored suite, the two-writer
|
|
121
|
-
* test is sequential-deterministic; lock interaction and raw
|
|
122
|
-
* serialization failures (Postgres 40001) need adapter-specific tests
|
|
123
|
-
* on top.
|
|
150
|
+
* `add` and `update` register intent only. At commit, the adapter appends the
|
|
151
|
+
* receipt's exact event batch with the Unit of Work's expected version, in the
|
|
152
|
+
* same transaction as the outbox. `run` must support overlapping calls so the
|
|
153
|
+
* mandatory stale-writer proof exercises a real stream OCC predicate.
|
|
124
154
|
*/
|
|
125
|
-
declare function createEsRepositoryContractTests<
|
|
126
|
-
|
|
155
|
+
declare function createEsRepositoryContractTests<TAggregate extends IAggregateRoot<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent>(harness: EsRepositoryContractHarness<TAggregate, TEvent>): EsRepositoryContractTest[];
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/testing/event-store-contract.d.ts
|
|
158
|
+
/** One named contract test for an EventStore adapter. */
|
|
159
|
+
type EventStoreContractTest = ContractTest;
|
|
160
|
+
/** One isolated adapter instance. The suite creates one per test. */
|
|
161
|
+
interface EventStoreContractEnvironment<Evt extends AnyDomainEvent> {
|
|
162
|
+
readonly store: EventStore<Evt>;
|
|
163
|
+
teardown?(): Promise<void>;
|
|
164
|
+
}
|
|
127
165
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
166
|
+
* Inputs needed to prove the EventStore's observable port contract.
|
|
167
|
+
*
|
|
168
|
+
* `createCollidingStreamKeys` must return two valid stream keys with the same
|
|
169
|
+
* raw aggregate id and different aggregate types. `createEvent` must return an
|
|
170
|
+
* event addressed to the supplied key; different sequence values must produce
|
|
171
|
+
* different event ids.
|
|
134
172
|
*/
|
|
135
|
-
interface
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
173
|
+
interface EventStoreContractHarness<Evt extends AnyDomainEvent> {
|
|
174
|
+
createEnvironment(): Promise<EventStoreContractEnvironment<Evt>>;
|
|
175
|
+
createCollidingStreamKeys(): readonly [AggregateAddress, AggregateAddress];
|
|
176
|
+
createEvent(stream: AggregateAddress, sequence: number): Evt;
|
|
139
177
|
}
|
|
140
178
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
179
|
+
* Reusable proof of an EventStore adapter's portable semantics: qualified
|
|
180
|
+
* value identity, ordered reads and slicing, OCC error mapping and atomicity,
|
|
181
|
+
* no-op empty appends, and detached return arrays. Physical-position
|
|
182
|
+
* corruption needs adapter-specific fixture support and is tested there.
|
|
183
|
+
*/
|
|
184
|
+
declare function createEventStoreContractTests<Evt extends AnyDomainEvent>(harness: EventStoreContractHarness<Evt>): EventStoreContractTest[];
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/testing/idempotency-store-contract.d.ts
|
|
187
|
+
/** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
|
|
188
|
+
type IdempotencyStoreContractTest = ContractTest;
|
|
189
|
+
/**
|
|
190
|
+
* One isolated test environment: a fresh idempotency store. The suite
|
|
191
|
+
* creates one per test and tears it down afterwards.
|
|
145
192
|
*/
|
|
146
|
-
interface
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
193
|
+
interface IdempotencyStoreContractEnvironment<TCtx> {
|
|
194
|
+
/** The adapter under test. */
|
|
195
|
+
store: IdempotencyStore<TCtx>;
|
|
196
|
+
/**
|
|
197
|
+
* Runs `work` inside a transaction that COMMITS, handing it the
|
|
198
|
+
* transaction context the store methods expect, the way
|
|
199
|
+
* `withIdempotentCommit` calls them in production. For a
|
|
200
|
+
* non-transactional store this simply invokes `work` with a dummy
|
|
201
|
+
* context.
|
|
202
|
+
*/
|
|
203
|
+
run<R>(work: (ctx: TCtx) => Promise<R>): Promise<R>;
|
|
204
|
+
/**
|
|
205
|
+
* For the `"transactional"` family: runs `work` inside a transaction
|
|
206
|
+
* that ROLLS BACK. Required there; the rollback-releases-the-claim
|
|
207
|
+
* test is that family's core proof. Irrelevant for the
|
|
208
|
+
* `"non-transactional"` family.
|
|
209
|
+
*/
|
|
210
|
+
runRolledBack?<R>(work: (ctx: TCtx) => Promise<R>): Promise<R>;
|
|
211
|
+
/**
|
|
212
|
+
* For the `"non-transactional"` family: advances or edits adapter test
|
|
213
|
+
* state so this exact claim's CURRENT lease is expired. Required there;
|
|
214
|
+
* a fake clock or test-only row update keeps the suite deterministic.
|
|
215
|
+
*/
|
|
216
|
+
expireLease?(claim: IdempotencyClaimHandle): Promise<void>;
|
|
217
|
+
/**
|
|
218
|
+
* Moves the adapter's test clock to an exact instant. Required by the
|
|
219
|
+
* non-transactional family so renewal is proved without wall-clock sleeps.
|
|
220
|
+
*/
|
|
221
|
+
advanceTimeTo?(instant: Date): Promise<void>;
|
|
222
|
+
/** Release connections, drop schemas, etc. Called in a finally. */
|
|
223
|
+
teardown?(): Promise<void>;
|
|
165
224
|
}
|
|
166
225
|
/**
|
|
167
|
-
* What an adapter supplies to run the contract suite.
|
|
226
|
+
* What an adapter supplies to run the idempotency-store contract suite.
|
|
168
227
|
*
|
|
169
|
-
* The
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* database** (testcontainers or equivalent), not an in-memory fake:
|
|
173
|
-
* the mandatory two-writer test proves YOUR `WHERE version = ?`
|
|
174
|
-
* predicate, and an in-memory stand-in proves only itself.
|
|
228
|
+
* The port (`IdempotencyStore`) deliberately supports two adapter
|
|
229
|
+
* families with different lifecycle semantics, and the suite follows
|
|
230
|
+
* the declared family instead of forcing one onto the other:
|
|
175
231
|
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
232
|
+
* - `"transactional"` (the single-transaction pattern): the record
|
|
233
|
+
* lives in the same database as the aggregates; a committed
|
|
234
|
+
* `complete` is final and replayable, a rollback releases everything,
|
|
235
|
+
* and `renew`/`confirm`/`abandon`/`reconcile` are no-ops. The family's core proof is the
|
|
236
|
+
* rollback test, so environments MUST provide `runRolledBack`, and
|
|
237
|
+
* run against a real database for SQL adapters.
|
|
238
|
+
* - `"non-transactional"` (the leased two-phase pattern, e.g. the
|
|
239
|
+
* in-memory reference): the store cannot see commits, so `complete` only
|
|
240
|
+
* STAGES the outcome, `confirm` finalizes it post-commit, `abandon`
|
|
241
|
+
* compensates failed attempts, and expired staged records require
|
|
242
|
+
* reconciliation. Environments MUST provide deterministic `expireLease` and
|
|
243
|
+
* `advanceTimeTo` controls. The rollback test is skipped.
|
|
183
244
|
*/
|
|
184
|
-
interface
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
* `persistedVersion === undefined`).
|
|
189
|
-
*/
|
|
190
|
-
createAggregate(): TAgg;
|
|
191
|
-
/**
|
|
192
|
-
* Apply exactly ONE version-bumping domain mutation that records at
|
|
193
|
-
* least one domain event (a `commit()`-style state change). The
|
|
194
|
-
* suite relies on the +1-per-call arithmetic and on the event for
|
|
195
|
-
* its outbox assertions.
|
|
196
|
-
*/
|
|
197
|
-
mutate(aggregate: TAgg): void;
|
|
198
|
-
/**
|
|
199
|
-
* Optional: a version-bumping mutation whose state is deep-equal to
|
|
200
|
-
* the previous state (`setState({...state}, true)`). Enables the
|
|
201
|
-
* version-only-change-still-persists test: the skip-save/OCC-desync
|
|
202
|
-
* trap.
|
|
203
|
-
*/
|
|
204
|
-
mutateVersionOnly?(aggregate: TAgg): void;
|
|
205
|
-
/**
|
|
206
|
-
* Optional: a mutation that changes ONLY a child collection (a
|
|
207
|
-
* non-root-row `changedKeys` entry). Enables the
|
|
208
|
-
* child-change-bumps-root-version test for partial-write
|
|
209
|
-
* repositories.
|
|
210
|
-
*/
|
|
211
|
-
mutateChildCollection?(aggregate: TAgg): void;
|
|
212
|
-
/**
|
|
213
|
-
* Optional: construct a NEW (never-persisted) aggregate instance
|
|
214
|
-
* carrying a SPECIFIC id. Enables TWO tests: deletion-is-final-
|
|
215
|
-
* across-instances (resurrection via a factory after delete) and
|
|
216
|
-
* the duplicate-insert test (see
|
|
217
|
-
* {@link insertsAreDuplicateChecked} to opt out of the latter
|
|
218
|
-
* independently).
|
|
219
|
-
*/
|
|
220
|
-
createAggregateWithId?(id: TAgg["id"]): TAgg;
|
|
221
|
-
/**
|
|
222
|
-
* Semantic opt-OUT (default `true`): whether `save()`'s INSERT path
|
|
223
|
-
* rejects an existing id with `DuplicateAggregateError` (mapping the
|
|
224
|
-
* driver's unique-violation: Postgres `23505`, MySQL `1062`, SQLite
|
|
225
|
-
* `SQLITE_CONSTRAINT_UNIQUE`). This is the near-mandatory contract:
|
|
226
|
-
* `save()` is insert-or-update, never upsert. Create-idempotency
|
|
227
|
-
* belongs in the USE CASE (load, then decide), not in the save path.
|
|
228
|
-
* Set `false` ONLY for a deliberately upserting adapter
|
|
229
|
-
* (idempotent-create design); the duplicate-insert test is then
|
|
230
|
-
* reported as skipped under this capability name, without costing
|
|
231
|
-
* the deletion-finality coverage that `createAggregateWithId` also
|
|
232
|
-
* gates.
|
|
233
|
-
*/
|
|
234
|
-
insertsAreDuplicateChecked?: boolean;
|
|
235
|
-
/**
|
|
236
|
-
* Optional: a plain-data projection of the aggregate's persisted
|
|
237
|
-
* state, compared with deep equality. Enables the mandatory test's
|
|
238
|
-
* state assertion (without it, only the version and the outbox are
|
|
239
|
-
* compared, and an adapter whose predicate guards the version write
|
|
240
|
-
* but not the state write would slip through).
|
|
241
|
-
*
|
|
242
|
-
* **The projection must be roundtrip-stable**: it compares a
|
|
243
|
-
* DB-reloaded aggregate against an in-memory one, so normalize
|
|
244
|
-
* anything your store changes in transit: dates to ISO strings at
|
|
245
|
-
* your store's precision (MySQL DATETIME truncates millis), no
|
|
246
|
-
* `undefined`-valued keys (JSON columns drop them), decimals/bigints
|
|
247
|
-
* to one consistent representation. A mismatch here fails the
|
|
248
|
-
* mandatory test; the message names the projection as a suspect.
|
|
249
|
-
*/
|
|
250
|
-
snapshotState?(aggregate: TAgg): unknown;
|
|
251
|
-
/**
|
|
252
|
-
* Optional flag: declare it when your `delete(aggregate)` runs an
|
|
253
|
-
* OCC predicate (`DELETE … WHERE id = ? AND version = ?`). Enables
|
|
254
|
-
* the stale-delete conflict test. Unpredicated deletes are
|
|
255
|
-
* last-write-wins by construction: acceptable for GC-style
|
|
256
|
-
* cleanup, rarely for user-initiated deletion of contended
|
|
257
|
-
* aggregates (see the repository guide).
|
|
258
|
-
*/
|
|
259
|
-
deletesAreVersionChecked?: boolean;
|
|
245
|
+
interface IdempotencyStoreContractHarness<TCtx> {
|
|
246
|
+
createEnvironment(): Promise<IdempotencyStoreContractEnvironment<TCtx>>;
|
|
247
|
+
/** Which lifecycle family the adapter implements; see above. */
|
|
248
|
+
family: "transactional" | "non-transactional";
|
|
260
249
|
}
|
|
261
250
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
251
|
+
* The idempotency-store contract test suite: the proof that an adapter
|
|
252
|
+
* delivers the claim/renew/complete/confirm/abandon/reconcile lifecycle
|
|
253
|
+
* `withIdempotentCommit` documents, for its declared family. Store
|
|
254
|
+
* semantics are an **adapter contract, not a kit guarantee**; this
|
|
255
|
+
* suite is how an adapter demonstrates them.
|
|
256
|
+
*
|
|
257
|
+
* Framework-agnostic: bind with
|
|
258
|
+
* `(test.skipped ? it.skip : it)(test.name, test.run)`.
|
|
270
259
|
*/
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
260
|
+
declare function createIdempotencyStoreContractTests<TCtx>(harness: IdempotencyStoreContractHarness<TCtx>): IdempotencyStoreContractTest[];
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/testing/outbox-contract.d.ts
|
|
263
|
+
/** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
|
|
264
|
+
type OutboxContractTest = ContractTest;
|
|
265
|
+
/**
|
|
266
|
+
* One isolated test environment: a fresh outbox store. The suite
|
|
267
|
+
* creates one per test and tears it down afterwards.
|
|
268
|
+
*/
|
|
269
|
+
interface OutboxContractEnvironment<Evt extends AnyDomainEvent> {
|
|
270
|
+
/** The adapter under test. */
|
|
271
|
+
outbox: Outbox<Evt> | DispatchTrackingOutbox<Evt>;
|
|
272
|
+
/**
|
|
273
|
+
* Runs `outbox.add(candidates)` inside a transaction that COMMITS, the
|
|
274
|
+
* way `withCommit` calls it in production. The suite supplies complete
|
|
275
|
+
* candidates with explicit source, aggregate version, zero-based commit
|
|
276
|
+
* sequence, and commit size. For a non-transactional store this is simply
|
|
277
|
+
* `outbox.add(candidates)`.
|
|
278
|
+
*/
|
|
279
|
+
addCommitted(events: ReadonlyArray<EventCommitCandidate<Evt>>): Promise<void>;
|
|
280
|
+
/**
|
|
281
|
+
* Optional capability: runs `outbox.add(candidates)` inside a
|
|
282
|
+
* transaction that ROLLS BACK. Enables the rollback-purity test: a
|
|
283
|
+
* rolled-back add must leave nothing behind. Transactional adapters
|
|
284
|
+
* should always provide this; it is the half of the outbox promise
|
|
285
|
+
* that in-memory fakes cannot keep.
|
|
286
|
+
*/
|
|
287
|
+
addRolledBack?(events: ReadonlyArray<EventCommitCandidate<Evt>>): Promise<void>;
|
|
288
|
+
/** Release connections, drop schemas, etc. Called in a finally. */
|
|
289
|
+
teardown?(): Promise<void>;
|
|
278
290
|
}
|
|
279
291
|
/**
|
|
280
|
-
*
|
|
281
|
-
* actually delivers the guarantees the kit's Unit of Work documents.
|
|
282
|
-
*
|
|
283
|
-
* The kit is ORM-agnostic: the OCC version predicate lives in YOUR
|
|
284
|
-
* repository's SQL. That makes optimistic concurrency a **repository
|
|
285
|
-
* contract, not a kit guarantee**: the kit ships the boundary, the
|
|
286
|
-
* `persistedVersion` baseline, `ConcurrencyConflictError`, and this
|
|
287
|
-
* suite; your adapter must pass it. An adapter that has not passed the
|
|
288
|
-
* suite (against a real database, for SQL adapters) has not
|
|
289
|
-
* demonstrated OCC.
|
|
292
|
+
* What an adapter supplies to run the outbox contract suite.
|
|
290
293
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
294
|
+
* For SQL adapters, run against a real database (testcontainers or
|
|
295
|
+
* equivalent): the commit-order and rollback tests prove YOUR schema
|
|
296
|
+
* and transaction wiring, not the kit's.
|
|
293
297
|
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
298
|
+
* Note on claiming: multi-instance safety (`getPending` claiming via
|
|
299
|
+
* `FOR UPDATE SKIP LOCKED` or equivalent) is part of the port contract
|
|
300
|
+
* for competing dispatchers but is not covered here; concurrency
|
|
301
|
+
* cannot be proven portably by a generic suite. Test it in your
|
|
302
|
+
* adapter's own suite if you run more than one dispatcher.
|
|
303
|
+
*/
|
|
304
|
+
interface OutboxContractHarness<Evt extends AnyDomainEvent> {
|
|
305
|
+
createEnvironment(): Promise<OutboxContractEnvironment<Evt>>;
|
|
306
|
+
/**
|
|
307
|
+
* Deterministic event factory: the same `seed` yields an event with
|
|
308
|
+
* the SAME `eventId` (the suite uses this for the dedupe test), and
|
|
309
|
+
* different seeds yield distinct `eventId`s.
|
|
310
|
+
*/
|
|
311
|
+
createEvent(seed: number): Evt;
|
|
312
|
+
/**
|
|
313
|
+
* For a `DispatchTrackingOutbox` adapter: how many `markFailed`
|
|
314
|
+
* reports move a record to the dead-letter set (the adapter's
|
|
315
|
+
* configured attempt ceiling). Omit for plain `Outbox` adapters;
|
|
316
|
+
* the dispatch-tracking tests are then marked skipped. With a
|
|
317
|
+
* ceiling of 1 the attempts-surfacing test is marked skipped too:
|
|
318
|
+
* observing attempts on a PENDING record needs a record that
|
|
319
|
+
* survives one failure.
|
|
320
|
+
*/
|
|
321
|
+
failuresToDeadLetter?: number;
|
|
322
|
+
/**
|
|
323
|
+
* Declare `true` when environments provide {@link
|
|
324
|
+
* OutboxContractEnvironment.addRolledBack}. Without it, the
|
|
325
|
+
* rollback-purity test is marked skipped: the honest state of an
|
|
326
|
+
* in-memory fake, and a loud gap for a transactional adapter.
|
|
327
|
+
*/
|
|
328
|
+
providesRolledBackAdds?: boolean;
|
|
329
|
+
/**
|
|
330
|
+
* Declare `true` when the adapter's `getPending` CLAIMS the returned
|
|
331
|
+
* records for competing dispatchers (lease, visibility timeout,
|
|
332
|
+
* `FOR UPDATE SKIP LOCKED`), as the port sanctions. Every test that
|
|
333
|
+
* re-polls records a previous poll returned without resolving them
|
|
334
|
+
* (head stability, re-ack non-disturbance, attempts surfacing)
|
|
335
|
+
* assumes a non-claiming read and is marked skipped for claiming
|
|
336
|
+
* adapters; prove your claim/expiry semantics in your own suite.
|
|
337
|
+
*/
|
|
338
|
+
claimsOnGetPending?: boolean;
|
|
339
|
+
/**
|
|
340
|
+
* Declare `true` when `add()` dedupes on `eventId` (the unique-key
|
|
341
|
+
* constraint the port RECOMMENDS). The dedupe test is gated on this:
|
|
342
|
+
* an adapter without the constraint satisfies the port's normative
|
|
343
|
+
* requirements and must not fail the suite, but the skip stays
|
|
344
|
+
* visible as the unproven recommendation it is.
|
|
345
|
+
*/
|
|
346
|
+
dedupesOnEventId?: boolean;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The outbox contract test suite: the proof that an adapter delivers
|
|
350
|
+
* the guarantees `withCommit` and `OutboxDispatcher` document. The kit
|
|
351
|
+
* is store-agnostic, so commit-order reads, qualified source-position
|
|
352
|
+
* identity, eventful-predecessor linkage, idempotent acks, and rollback
|
|
353
|
+
* purity are an **adapter contract, not a kit guarantee**; this suite is how
|
|
354
|
+
* an adapter demonstrates them. Its source-law tests also prove that colliding
|
|
355
|
+
* raw ids stay isolated by aggregate type and aggregate id.
|
|
297
356
|
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
357
|
+
* Framework-agnostic: bind with
|
|
358
|
+
* `(test.skipped ? it.skip : it)(test.name, test.run)`.
|
|
359
|
+
*/
|
|
360
|
+
declare function createOutboxContractTests<Evt extends AnyDomainEvent>(harness: OutboxContractHarness<Evt>): OutboxContractTest[];
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/testing/projection-checkpoint-contract.d.ts
|
|
363
|
+
/** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
|
|
364
|
+
type ProjectionCheckpointStoreContractTest = ContractTest;
|
|
365
|
+
/**
|
|
366
|
+
* One isolated test environment: a fresh checkpoint store. The suite
|
|
367
|
+
* creates one per test and tears it down afterwards.
|
|
368
|
+
*/
|
|
369
|
+
interface ProjectionCheckpointStoreContractEnvironment<TCtx> {
|
|
370
|
+
/** The adapter under test. */
|
|
371
|
+
store: ProjectionCheckpointStore<TCtx>;
|
|
372
|
+
/**
|
|
373
|
+
* Runs `work` inside a transaction that COMMITS, handing it the
|
|
374
|
+
* transaction context the store methods expect, the way the
|
|
375
|
+
* `Projector` calls them in production. For a non-transactional
|
|
376
|
+
* store this simply invokes `work` with a dummy context.
|
|
377
|
+
*/
|
|
378
|
+
run<R>(work: (ctx: TCtx) => Promise<R>): Promise<R>;
|
|
379
|
+
/**
|
|
380
|
+
* Optional capability: starts every supplied transaction independently and
|
|
381
|
+
* concurrently. Each callback must receive its own transaction context for
|
|
382
|
+
* database adapters (normally a separate pooled connection). Enables the
|
|
383
|
+
* missing-key/existing-key exclusion test; implementing this as sequential
|
|
384
|
+
* calls would make that proof meaningless.
|
|
385
|
+
*/
|
|
386
|
+
runConcurrently?<R>(works: ReadonlyArray<(ctx: TCtx) => Promise<R>>): Promise<R[]>;
|
|
387
|
+
/**
|
|
388
|
+
* Optional capability: runs `work` inside a transaction that ROLLS
|
|
389
|
+
* BACK. Enables the rollback test: a rolled-back save must leave no
|
|
390
|
+
* checkpoint behind, the half of the atomic update+checkpoint
|
|
391
|
+
* promise the store contributes. Transactional adapters should
|
|
392
|
+
* always provide this; in-memory fakes cannot.
|
|
393
|
+
*/
|
|
394
|
+
runRolledBack?<R>(work: (ctx: TCtx) => Promise<R>): Promise<R>;
|
|
395
|
+
/** Release connections, drop schemas, etc. Called in a finally. */
|
|
396
|
+
teardown?(): Promise<void>;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* What an adapter supplies to run the projection-checkpoint-store
|
|
400
|
+
* contract suite. For SQL adapters, run against a real database
|
|
401
|
+
* (testcontainers or equivalent): concurrent runs prove missing-key locking,
|
|
402
|
+
* the rollback test proves YOUR transaction wiring, and the checkpoint table
|
|
403
|
+
* must live in the same database as the read models it accounts for.
|
|
404
|
+
*/
|
|
405
|
+
interface ProjectionCheckpointStoreContractHarness<TCtx> {
|
|
406
|
+
createEnvironment(): Promise<ProjectionCheckpointStoreContractEnvironment<TCtx>>;
|
|
407
|
+
/**
|
|
408
|
+
* Declare `true` when environments provide {@link
|
|
409
|
+
* ProjectionCheckpointStoreContractEnvironment.runRolledBack}.
|
|
410
|
+
* Without it, the rollback test is marked skipped: the honest state
|
|
411
|
+
* of an in-memory fake, and a loud gap for a transactional adapter.
|
|
412
|
+
*/
|
|
413
|
+
providesRolledBackRuns?: boolean;
|
|
414
|
+
/**
|
|
415
|
+
* Declare `true` when environments provide {@link
|
|
416
|
+
* ProjectionCheckpointStoreContractEnvironment.runConcurrently}. Without
|
|
417
|
+
* it, missing-key lock safety is marked skipped and remains an explicitly
|
|
418
|
+
* unproven adapter guarantee.
|
|
419
|
+
*/
|
|
420
|
+
providesConcurrentRuns?: boolean;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The projection-checkpoint-store contract test suite: the proof that
|
|
424
|
+
* an adapter delivers the watermark semantics the `Projector`
|
|
425
|
+
* documents. Checkpoint semantics are an **adapter contract, not a
|
|
426
|
+
* kit guarantee**; this suite is how an adapter demonstrates them. Enable its
|
|
427
|
+
* concurrent-runs capability to prove genesis-safe exclusion rather than
|
|
428
|
+
* leaving that guarantee visibly skipped.
|
|
323
429
|
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
430
|
+
* The concurrent test exercises commit visibility, but cannot deterministically
|
|
431
|
+
* hold an adapter between return from `withCheckpointLocks` and its surrounding
|
|
432
|
+
* transaction commit. It can therefore expose an early lock release only when
|
|
433
|
+
* a waiter enters during that window; holding database locks through commit or
|
|
434
|
+
* rollback remains an explicit adapter responsibility, not a complete proof
|
|
435
|
+
* supplied by this suite.
|
|
330
436
|
*
|
|
331
|
-
*
|
|
332
|
-
* (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
437
|
+
* Framework-agnostic: bind with
|
|
438
|
+
* `(test.skipped ? it.skip : it)(test.name, test.run)`.
|
|
439
|
+
*/
|
|
440
|
+
declare function createProjectionCheckpointStoreContractTests<TCtx>(harness: ProjectionCheckpointStoreContractHarness<TCtx>): ProjectionCheckpointStoreContractTest[];
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region src/testing/repository-contract.d.ts
|
|
443
|
+
/** Application-facing state-stored repository exercised by the suite. */
|
|
444
|
+
interface ContractRepository<TAggregate extends IAggregateRoot<Id<string>, AnyDomainEvent>> {
|
|
445
|
+
findById(id: TAggregate["id"]): Promise<TAggregate | undefined>;
|
|
446
|
+
add(aggregate: TAggregate): void;
|
|
447
|
+
update(aggregate: TAggregate): void;
|
|
448
|
+
/** Physical removal is an optional persistence capability. */
|
|
449
|
+
remove?(aggregate: TAggregate): void;
|
|
450
|
+
}
|
|
451
|
+
/** One isolated real-adapter environment. `run` must permit overlapping calls. */
|
|
452
|
+
interface RepositoryContractEnvironment<TAggregate extends IAggregateRoot<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
|
|
453
|
+
run<R>(work: (context: {
|
|
454
|
+
repository: ContractRepository<TAggregate>;
|
|
455
|
+
}) => Promise<R>): Promise<R>;
|
|
456
|
+
committedOutboxEvents(): Promise<ReadonlyArray<CommittedDomainEvent<TEvent>>>;
|
|
457
|
+
/** Makes the next transactional outbox write fail for atomicity proof. */
|
|
458
|
+
failNextOutboxWrite(error: Error): void;
|
|
459
|
+
teardown?(): Promise<void>;
|
|
460
|
+
}
|
|
461
|
+
/** Fixtures and observable projections supplied by an adapter package. */
|
|
462
|
+
interface RepositoryContractHarness<TAggregate extends IAggregateRoot<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent> {
|
|
463
|
+
createEnvironment(): Promise<RepositoryContractEnvironment<TAggregate, TEvent>>;
|
|
464
|
+
/** A fresh aggregate with a unique id. */
|
|
465
|
+
createAggregate(): TAggregate;
|
|
466
|
+
/** One version-bumping decision that records at least one event. */
|
|
467
|
+
mutate(aggregate: TAggregate): void;
|
|
468
|
+
/** Required for duplicate-add and same-UoW deletion-finality proofs. */
|
|
469
|
+
createAggregateWithId?(id: TAggregate["id"]): TAggregate;
|
|
470
|
+
/**
|
|
471
|
+
* A version-bumping decision with no event whose resulting state is
|
|
472
|
+
* deep-equal to the previous state (`setState({ ...state })`). The
|
|
473
|
+
* deep-equal requirement is load-bearing: it forces a diff-based
|
|
474
|
+
* `PersistenceModel` to derive an EMPTY change set, so the suite proves
|
|
475
|
+
* that an adapter persists the bumped version even when
|
|
476
|
+
* `changes.empty` is true. Skipping that write desyncs the persisted
|
|
477
|
+
* version and produces false concurrency conflicts later.
|
|
478
|
+
*/
|
|
479
|
+
mutateVersionOnly?(aggregate: TAggregate): void;
|
|
480
|
+
/** A decision that changes a nested collection. */
|
|
481
|
+
mutateChildCollection?(aggregate: TAggregate): void;
|
|
482
|
+
/** Round-trip-stable adapter persistence projection. */
|
|
483
|
+
snapshotState?(aggregate: TAggregate): unknown;
|
|
484
|
+
/** Opt out only for an intentionally upserting add implementation. */
|
|
485
|
+
insertsAreDuplicateChecked?: boolean;
|
|
486
|
+
/** Enables physical-remove behavior and stale-remove OCC tests. */
|
|
487
|
+
removesAreSupported?: boolean;
|
|
488
|
+
/** The remove flush predicates on the version captured at load. */
|
|
489
|
+
removesAreVersionChecked?: boolean;
|
|
490
|
+
}
|
|
491
|
+
type RepositoryContractTest = ContractTest;
|
|
492
|
+
/**
|
|
493
|
+
* Contract suite for the v3 explicit-intent, commit-time-flush protocol.
|
|
340
494
|
*
|
|
341
|
-
*
|
|
342
|
-
* `
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* anywhere in the chain is the contract.
|
|
495
|
+
* The harness must use the public `UnitOfWork` with a real adapter. In
|
|
496
|
+
* particular, `run` must create a fresh Unit of Work and transaction for each
|
|
497
|
+
* call and allow two calls to overlap; the mandatory stale-writer proof keeps
|
|
498
|
+
* writer B open while writer A commits. SQL/ORM adapters therefore need a
|
|
499
|
+
* real database and connection pool. An in-memory harness proves only itself.
|
|
347
500
|
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
501
|
+
* Writes are synchronous registrations. Durable adapter I/O happens after the
|
|
502
|
+
* callback returns, while the transaction is still open. A test that passes
|
|
503
|
+
* because `add` or `update` writes early is not a conforming implementation.
|
|
504
|
+
*/
|
|
505
|
+
declare function createRepositoryContractTests<TAggregate extends IAggregateRoot<Id<string>, TEvent>, TEvent extends AnyDomainEvent = AnyDomainEvent>(harness: RepositoryContractHarness<TAggregate, TEvent>): RepositoryContractTest[];
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/testing/snapshot-store-contract.d.ts
|
|
508
|
+
/** One contract test; bind with `(test.skipped ? it.skip : it)(test.name, test.run)`. */
|
|
509
|
+
type SnapshotStoreContractTest = ContractTest;
|
|
510
|
+
/** The plain-data state shape the suite round-trips. */
|
|
511
|
+
interface SuiteState {
|
|
512
|
+
total: number;
|
|
513
|
+
items: Array<{
|
|
514
|
+
sku: string;
|
|
515
|
+
qty: number;
|
|
516
|
+
}>;
|
|
517
|
+
note?: string;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* One isolated test environment: a fresh snapshot store. The suite
|
|
521
|
+
* creates one per test and tears it down afterwards. No transaction
|
|
522
|
+
* wrapper: the port is transaction-free by design (snapshots are
|
|
523
|
+
* derived data written after the commit; see `SnapshotStore`).
|
|
524
|
+
*/
|
|
525
|
+
interface SnapshotStoreContractEnvironment {
|
|
526
|
+
/** The adapter under test. */
|
|
527
|
+
store: SnapshotStore<SuiteState>;
|
|
528
|
+
/** Release connections, drop schemas, etc. Called in a finally. */
|
|
529
|
+
teardown?(): Promise<void>;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* What an adapter supplies to run the snapshot-store contract suite.
|
|
533
|
+
* For SQL adapters, run against a real database (testcontainers or
|
|
534
|
+
* equivalent). Note the fidelity demands the suite enforces:
|
|
535
|
+
* `snapshotAt` must survive with millisecond precision (store it as
|
|
536
|
+
* ISO-8601 text or epoch milliseconds; MySQL `DATETIME` without
|
|
537
|
+
* fractional seconds truncates), and an ABSENT `schemaVersion` must
|
|
538
|
+
* come back absent, not as `0` or `null`-coerced.
|
|
539
|
+
*/
|
|
540
|
+
interface SnapshotStoreContractHarness {
|
|
541
|
+
createEnvironment(): Promise<SnapshotStoreContractEnvironment>;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* The snapshot-store contract test suite: the proof that an adapter
|
|
545
|
+
* delivers the round-trip and isolation semantics the
|
|
546
|
+
* snapshot-plus-recent-events load path relies on. Store semantics are
|
|
547
|
+
* an **adapter contract, not a kit guarantee**; this suite is how an
|
|
548
|
+
* adapter demonstrates them.
|
|
358
549
|
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
* loads, writer A loads/mutates/commits, then B commits its stale
|
|
362
|
-
* instance. The stale `persistedVersion` baseline travels with B's
|
|
363
|
-
* instance, so the version predicate is exercised exactly as in a true
|
|
364
|
-
* race, without depending on lock timing, pool sizes, or
|
|
365
|
-
* engine-specific blocking. The flip side: lock interaction is NOT
|
|
366
|
-
* covered. A `SELECT … FOR UPDATE`-style repository that blocks
|
|
367
|
-
* instead of conflicting, or a SERIALIZABLE engine surfacing raw
|
|
368
|
-
* serialization failures (Postgres 40001) your adapter must map to
|
|
369
|
-
* `ConcurrencyConflictError`, needs adapter-specific tests on top of
|
|
370
|
-
* this suite.
|
|
550
|
+
* Framework-agnostic: bind with
|
|
551
|
+
* `(test.skipped ? it.skip : it)(test.name, test.run)`.
|
|
371
552
|
*/
|
|
372
|
-
declare function
|
|
373
|
-
|
|
374
|
-
export { type ContractRepository, type EsContractRepository, type EsRepositoryContractEnvironment, type EsRepositoryContractHarness, type EsRepositoryContractTest, type RepositoryContractEnvironment, type RepositoryContractHarness, type RepositoryContractTest, createEsRepositoryContractTests, createRepositoryContractTests };
|
|
553
|
+
declare function createSnapshotStoreContractTests(harness: SnapshotStoreContractHarness): SnapshotStoreContractTest[];
|
|
554
|
+
//#endregion
|
|
555
|
+
export { type CommandOutboxContractEnvironment, type CommandOutboxContractHarness, type CommandOutboxContractTest, type ContractRepository, type DeadlineStoreContractEnvironment, type DeadlineStoreContractHarness, type DeadlineStoreContractTest, type EsContractRepository, type EsRepositoryContractEnvironment, type EsRepositoryContractHarness, type EsRepositoryContractTest, type EventStoreContractEnvironment, type EventStoreContractHarness, type EventStoreContractTest, type IdempotencyStoreContractEnvironment, type IdempotencyStoreContractHarness, type IdempotencyStoreContractTest, type OutboxContractEnvironment, type OutboxContractHarness, type OutboxContractTest, type ProjectionCheckpointStoreContractEnvironment, type ProjectionCheckpointStoreContractHarness, type ProjectionCheckpointStoreContractTest, type RepositoryContractEnvironment, type RepositoryContractHarness, type RepositoryContractTest, type SnapshotStoreContractEnvironment, type SnapshotStoreContractHarness, type SnapshotStoreContractTest, createCommandOutboxContractTests, createDeadlineStoreContractTests, createEsRepositoryContractTests, createEventStoreContractTests, createIdempotencyStoreContractTests, createOutboxContractTests, createProjectionCheckpointStoreContractTests, createRepositoryContractTests, createSnapshotStoreContractTests };
|
|
556
|
+
//# sourceMappingURL=testing.d.ts.map
|