@cratis/chronicle 6.19.0 → 6.21.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/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +8 -109
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/createAppendNotification.d.ts +15 -0
- package/dist/eventSequences/createAppendNotification.d.ts.map +1 -0
- package/dist/eventSequences/createAppendNotification.js +25 -0
- package/dist/eventSequences/createAppendNotification.js.map +1 -0
- package/dist/eventSequences/prepareBatchAppend.d.ts +31 -0
- package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -0
- package/dist/eventSequences/prepareBatchAppend.js +54 -0
- package/dist/eventSequences/prepareBatchAppend.js.map +1 -0
- package/dist/eventSequences/prepareSingleAppend.d.ts +12 -0
- package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -0
- package/dist/eventSequences/prepareSingleAppend.js +22 -0
- package/dist/eventSequences/prepareSingleAppend.js.map +1 -0
- package/dist/testing/EventScenario.d.ts +29 -0
- package/dist/testing/EventScenario.d.ts.map +1 -0
- package/dist/testing/EventScenario.js +63 -0
- package/dist/testing/EventScenario.js.map +1 -0
- package/dist/testing/EventScenarioGiven.d.ts +9 -0
- package/dist/testing/EventScenarioGiven.d.ts.map +1 -0
- package/dist/testing/EventScenarioGiven.js +16 -0
- package/dist/testing/EventScenarioGiven.js.map +1 -0
- package/dist/testing/EventScenarioGivenBuilder.d.ts +9 -0
- package/dist/testing/EventScenarioGivenBuilder.d.ts.map +1 -0
- package/dist/testing/EventScenarioGivenBuilder.js +12 -0
- package/dist/testing/EventScenarioGivenBuilder.js.map +1 -0
- package/dist/testing/EventScenarioOptions.d.ts +21 -0
- package/dist/testing/EventScenarioOptions.d.ts.map +1 -0
- package/dist/testing/EventScenarioOptions.js +4 -0
- package/dist/testing/EventScenarioOptions.js.map +1 -0
- package/dist/testing/EventScenarioSeed.d.ts +5 -0
- package/dist/testing/EventScenarioSeed.d.ts.map +1 -0
- package/dist/testing/EventScenarioSeed.js +14 -0
- package/dist/testing/EventScenarioSeed.js.map +1 -0
- package/dist/testing/EventScenarioWhen.d.ts +11 -0
- package/dist/testing/EventScenarioWhen.d.ts.map +1 -0
- package/dist/testing/EventScenarioWhen.js +16 -0
- package/dist/testing/EventScenarioWhen.js.map +1 -0
- package/dist/testing/EventScenarioWhenBuilder.d.ts +9 -0
- package/dist/testing/EventScenarioWhenBuilder.d.ts.map +1 -0
- package/dist/testing/EventScenarioWhenBuilder.js +12 -0
- package/dist/testing/EventScenarioWhenBuilder.js.map +1 -0
- package/dist/testing/InProcessEventSequence.d.ts +60 -0
- package/dist/testing/InProcessEventSequence.d.ts.map +1 -0
- package/dist/testing/InProcessEventSequence.js +462 -0
- package/dist/testing/InProcessEventSequence.js.map +1 -0
- package/dist/testing/UnsupportedEventSequenceOperation.d.ts +5 -0
- package/dist/testing/UnsupportedEventSequenceOperation.d.ts.map +1 -0
- package/dist/testing/UnsupportedEventSequenceOperation.js +10 -0
- package/dist/testing/UnsupportedEventSequenceOperation.js.map +1 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +6 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/EventSequence.ts +12 -119
- package/eventSequences/createAppendNotification.ts +35 -0
- package/eventSequences/prepareBatchAppend.ts +63 -0
- package/eventSequences/prepareSingleAppend.ts +25 -0
- package/package.json +1 -1
- package/testing/EventScenario.batches.spec.ts +537 -0
- package/testing/EventScenario.fixtures.spec.ts +260 -0
- package/testing/EventScenario.rejections.spec.ts +273 -0
- package/testing/EventScenario.ts +79 -0
- package/testing/EventScenarioGiven.ts +13 -0
- package/testing/EventScenarioGivenBuilder.ts +11 -0
- package/testing/EventScenarioOptions.ts +20 -0
- package/testing/EventScenarioSeed.ts +17 -0
- package/testing/EventScenarioWhen.ts +14 -0
- package/testing/EventScenarioWhenBuilder.ts +11 -0
- package/testing/InProcessEventSequence.ts +460 -0
- package/testing/UnsupportedEventSequenceOperation.ts +10 -0
- package/testing/index.ts +7 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { Constructor } from '@cratis/fundamentals';
|
|
5
|
+
import type { EventSequenceId } from '../eventSequences/EventSequenceId.js';
|
|
6
|
+
|
|
7
|
+
/** Configuration captured once when constructing an event scenario. */
|
|
8
|
+
export interface EventScenarioOptions {
|
|
9
|
+
/** An isolated catalog; omitting it discovers process-wide event and constraint artifacts. */
|
|
10
|
+
artifacts?: { eventTypes: Constructor[]; constraints?: Constructor[]; eventTypeMigrations?: Constructor[] };
|
|
11
|
+
/** Constraint discovery is the default. Explicitly disable it only for tests without constraints. */
|
|
12
|
+
constraints?: 'disabled';
|
|
13
|
+
/** Scenario-local identifiers; only the fixture-backed defaults are supported in this increment. */
|
|
14
|
+
eventStore?: string;
|
|
15
|
+
namespace?: string;
|
|
16
|
+
eventSequenceId?: EventSequenceId;
|
|
17
|
+
/** Deterministic sources for metadata otherwise provided by the environment. */
|
|
18
|
+
clock?: () => Date;
|
|
19
|
+
correlationId?: () => string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { EventScenario } from './EventScenario.js';
|
|
5
|
+
|
|
6
|
+
const seeders = new WeakMap<EventScenario, (source: string, events: object[]) => Promise<void>>();
|
|
7
|
+
|
|
8
|
+
/** Internal builder wiring; not exported by the testing package. */
|
|
9
|
+
export function registerScenarioSeed(scenario: EventScenario, seed: (source: string, events: object[]) => Promise<void>): void {
|
|
10
|
+
seeders.set(scenario, seed);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function seedScenario(scenario: EventScenario, source: string, events: object[]): Promise<void> {
|
|
14
|
+
const seed = seeders.get(scenario);
|
|
15
|
+
if (!seed) throw new Error('EventScenario setup requires a constructed scenario.');
|
|
16
|
+
return seed(source, events);
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { AppendResult } from '../eventSequences/AppendResult.js';
|
|
5
|
+
import type { EventScenario } from './EventScenario.js';
|
|
6
|
+
|
|
7
|
+
/** A single act-phase append; the plural form is reserved for atomic batches. */
|
|
8
|
+
export class EventScenarioWhen {
|
|
9
|
+
constructor(private readonly _scenario: EventScenario, private readonly _id: string) {}
|
|
10
|
+
async event(event: object): Promise<AppendResult> { return this._scenario.append(this._id, event); }
|
|
11
|
+
async events(...events: object[]): Promise<readonly AppendResult[]> {
|
|
12
|
+
return this._scenario.appendMany(this._id, events);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { EventScenario } from './EventScenario.js';
|
|
5
|
+
import { EventScenarioWhen } from './EventScenarioWhen.js';
|
|
6
|
+
|
|
7
|
+
/** Selects the source for an act-phase append. */
|
|
8
|
+
export class EventScenarioWhenBuilder {
|
|
9
|
+
constructor(private readonly _scenario: EventScenario) {}
|
|
10
|
+
forEventSource(id: string): EventScenarioWhen { return new EventScenarioWhen(this._scenario, id); }
|
|
11
|
+
}
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { EventObservationState } from '@cratis/chronicle.contracts';
|
|
6
|
+
import { AppendOperationsBroadcaster } from '../eventSequences/AppendOperationsBroadcaster.js';
|
|
7
|
+
import { prepareBatchAppend } from '../eventSequences/prepareBatchAppend.js';
|
|
8
|
+
import { createAppendNotification, mapAppendNotificationCausation } from '../eventSequences/createAppendNotification.js';
|
|
9
|
+
import { Guid, type Constructor } from '@cratis/fundamentals';
|
|
10
|
+
import { getEventTypeMetadata, getEventTypeFor } from '../events/eventTypeDecorator.js';
|
|
11
|
+
import { getTagsFor } from '../events/tagDecorator.js';
|
|
12
|
+
import type { AppendedEvent } from '../events/AppendedEvent.js';
|
|
13
|
+
import { Tag } from '../events/Tag.js';
|
|
14
|
+
import { EventType } from '../events/EventType.js';
|
|
15
|
+
import { EventTypeId } from '../events/EventTypeId.js';
|
|
16
|
+
import { EventTypeGeneration } from '../events/EventTypeGeneration.js';
|
|
17
|
+
import { Identity } from '../identity/Identity.js';
|
|
18
|
+
import type { AppendOptions } from '../eventSequences/AppendOptions.js';
|
|
19
|
+
import type { AppendResult } from '../eventSequences/AppendResult.js';
|
|
20
|
+
import type { AppendedEventWithResult } from '../eventSequences/AppendedEventWithResult.js';
|
|
21
|
+
import type { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
|
|
22
|
+
import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
|
|
23
|
+
import { EventSequenceNumber } from '../eventSequences/EventSequenceNumber.js';
|
|
24
|
+
import type { IEventSequence } from '../eventSequences/IEventSequence.js';
|
|
25
|
+
import type { ITransactionalEventSequence } from '../eventSequences/ITransactionalEventSequence.js';
|
|
26
|
+
import type { CompleteStreamResult } from '../eventSequences/CompleteStreamResult.js';
|
|
27
|
+
import { prepareSingleAppend } from '../eventSequences/prepareSingleAppend.js';
|
|
28
|
+
import { toContractsGuid } from '../connection/Guid.js';
|
|
29
|
+
import { getUniqueEventMetadata, getUniquePropertyMetadata } from '../events/constraints/unique.js';
|
|
30
|
+
import { getRemovedConstraintNames } from '../events/constraints/removeConstraint.js';
|
|
31
|
+
import type { EventScenarioOptions } from './EventScenarioOptions.js';
|
|
32
|
+
import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
|
|
33
|
+
|
|
34
|
+
// JS trim() omits U+0085, which the kernel trims. Reject unproven non-ASCII whitespace and controls.
|
|
35
|
+
const unprovenFilterCharacters = /[\u007f-\u009f]|(?=[^\x00-\x7f])\p{White_Space}/u;
|
|
36
|
+
|
|
37
|
+
/** Fixture-backed, scenario-local append sequence; no kernel or observer scheduler is started. */
|
|
38
|
+
export class InProcessEventSequence implements IEventSequence {
|
|
39
|
+
readonly id: EventSequenceId;
|
|
40
|
+
private readonly _catalog = new Map<Function, ReturnType<typeof getEventTypeMetadata>>();
|
|
41
|
+
private readonly _history: AppendedEvent[] = [];
|
|
42
|
+
private readonly _store: string;
|
|
43
|
+
private readonly _namespace: string;
|
|
44
|
+
private readonly _clock: () => Date;
|
|
45
|
+
private readonly _correlationId?: () => string;
|
|
46
|
+
private _setup = false;
|
|
47
|
+
private _allowSeedAppend = false;
|
|
48
|
+
private _stagedSeeds?: AppendedEvent[];
|
|
49
|
+
private _stagedNotifications?: AppendedEventWithResult[][];
|
|
50
|
+
private _busy = false;
|
|
51
|
+
private readonly _results: AppendResult[] = [];
|
|
52
|
+
readonly appendOperations = new AppendOperationsBroadcaster<AppendedEventWithResult[]>();
|
|
53
|
+
|
|
54
|
+
constructor(options: EventScenarioOptions, eventTypes: Constructor[]) {
|
|
55
|
+
if (options.eventSequenceId && options.eventSequenceId.value !== EventSequenceId.eventLog.value) {
|
|
56
|
+
throw this.unsupported('options.eventSequenceId', options.eventSequenceId.value, 'Custom sequences are not fixture-backed.');
|
|
57
|
+
}
|
|
58
|
+
this.id = options.eventSequenceId ?? EventSequenceId.eventLog;
|
|
59
|
+
if (options.eventStore !== undefined && options.eventStore !== 'test-event-store') {
|
|
60
|
+
throw this.unsupported('options.eventStore', options.eventStore, 'Custom event stores are not fixture-backed.');
|
|
61
|
+
}
|
|
62
|
+
if (options.namespace !== undefined && options.namespace !== 'default') {
|
|
63
|
+
throw this.unsupported('options.namespace', options.namespace, 'Custom namespaces are not fixture-backed.');
|
|
64
|
+
}
|
|
65
|
+
this._store = options.eventStore ?? 'test-event-store';
|
|
66
|
+
this._namespace = options.namespace ?? 'default';
|
|
67
|
+
this._clock = options.clock ?? (() => new Date());
|
|
68
|
+
this._correlationId = options.correlationId;
|
|
69
|
+
const ids = new Set<string>();
|
|
70
|
+
for (const type of eventTypes) {
|
|
71
|
+
const metadata = getEventTypeMetadata(type);
|
|
72
|
+
if (!metadata) throw this.unsupported('artifacts.eventTypes', type.name, 'Event type has no @eventType metadata.');
|
|
73
|
+
const eventType = metadata.eventType;
|
|
74
|
+
if (!eventType.id.value.trim() || eventType.id.value !== eventType.id.value.trim() ||
|
|
75
|
+
unprovenFilterCharacters.test(eventType.id.value) || eventType.id.value.includes(',')) {
|
|
76
|
+
throw this.unsupported('artifacts.eventTypes.id', eventType.id.value, 'Comma-separated or padded event IDs have unproven filter behavior.');
|
|
77
|
+
}
|
|
78
|
+
if (ids.has(eventType.id.value)) throw this.unsupported('artifacts.eventTypes', type.name, 'Duplicate event ID or generation history.');
|
|
79
|
+
ids.add(eventType.id.value);
|
|
80
|
+
if (eventType.generation.value !== 1 || eventType.tombstone) {
|
|
81
|
+
throw this.unsupported('artifacts.eventTypes', type.name, 'Only generation 1, non-tombstone events are proven.');
|
|
82
|
+
}
|
|
83
|
+
if (options.constraints !== 'disabled' && (getUniqueEventMetadata(type) || getRemovedConstraintNames(type).length ||
|
|
84
|
+
[...metadata.members.keys()].some(key => getUniquePropertyMetadata(type, key)))) {
|
|
85
|
+
throw this.unsupported('artifacts.eventTypes.constraints', type.name, 'Constraint metadata is not supported in this increment.');
|
|
86
|
+
}
|
|
87
|
+
if (getTagsFor(type).length) throw this.unsupported('artifacts.eventTypes.tags', type.name, 'Tagged events are not fixture-backed.');
|
|
88
|
+
// Validate the complete schema, not only properties present on a particular event.
|
|
89
|
+
const schema = metadata.schema;
|
|
90
|
+
if (schema.type !== 'object' || schema.compliance?.length || schema.security?.length ||
|
|
91
|
+
schema.enum !== undefined || schema.items !== undefined || schema.format !== undefined ||
|
|
92
|
+
schema.additionalProperties !== false ||
|
|
93
|
+
schema.required?.length !== Object.keys(schema.properties ?? {}).length ||
|
|
94
|
+
!schema.properties || Object.keys(schema.properties).length === 0 ||
|
|
95
|
+
Object.entries(schema.properties).some(([key, property]) =>
|
|
96
|
+
!/^[a-z][a-zA-Z0-9]*$/.test(key) || !['string', 'boolean'].includes(property.type ?? '') ||
|
|
97
|
+
property.format !== undefined || property.compliance?.length || property.security?.length ||
|
|
98
|
+
property.items !== undefined || property.properties !== undefined || property.enum !== undefined ||
|
|
99
|
+
property.additionalProperties !== undefined)) {
|
|
100
|
+
throw this.unsupported('artifacts.eventTypes.schema', type.name, 'Only flat, unclassified string/boolean fields are fixture-backed.');
|
|
101
|
+
}
|
|
102
|
+
this._catalog.set(type, metadata);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get results(): readonly AppendResult[] { return Object.freeze([...this._results]); }
|
|
107
|
+
get appendedEvents(): readonly AppendedEvent[] { return Object.freeze(this._history.map(event => this.snapshot(event))); }
|
|
108
|
+
|
|
109
|
+
/** Validate and stage all setup appends, then commit and notify in single-append order. */
|
|
110
|
+
async seed(source: string, events: object[]): Promise<void> {
|
|
111
|
+
if (this._setup || this._busy) throw this.unsupported('given.events', this.id.value, 'Overlapping setup calls are not fixture-backed.');
|
|
112
|
+
this._setup = true;
|
|
113
|
+
this._stagedSeeds = [];
|
|
114
|
+
this._stagedNotifications = [];
|
|
115
|
+
try {
|
|
116
|
+
for (const event of events) {
|
|
117
|
+
this._allowSeedAppend = true;
|
|
118
|
+
let pending: Promise<AppendResult>;
|
|
119
|
+
try { pending = this.append(source, event); }
|
|
120
|
+
finally { this._allowSeedAppend = false; }
|
|
121
|
+
const result = await pending;
|
|
122
|
+
if (!result.isSuccess) throw new Error(`EventScenario given setup failed: ${JSON.stringify(result)}`);
|
|
123
|
+
}
|
|
124
|
+
for (const [index, stored] of this._stagedSeeds.entries()) {
|
|
125
|
+
this._history.push(stored);
|
|
126
|
+
this.appendOperations.publish(this._stagedNotifications[index]);
|
|
127
|
+
// Let an async subscriber observe this append before the next enters history.
|
|
128
|
+
await Promise.resolve();
|
|
129
|
+
}
|
|
130
|
+
} finally {
|
|
131
|
+
this._stagedSeeds = undefined;
|
|
132
|
+
this._stagedNotifications = undefined;
|
|
133
|
+
this._setup = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get transactional(): ITransactionalEventSequence { throw this.unsupported('transactional', this.id.value, 'Transactions require a kernel.'); }
|
|
138
|
+
|
|
139
|
+
async append(eventSourceId: string, event: object, options?: AppendOptions): Promise<AppendResult> {
|
|
140
|
+
if (this._busy || (this._setup && !this._allowSeedAppend)) {
|
|
141
|
+
throw this.unsupported('append', this.id.value, 'Overlapping calls do not have proven ordering.');
|
|
142
|
+
}
|
|
143
|
+
this._busy = true;
|
|
144
|
+
try {
|
|
145
|
+
if (!event || typeof event !== 'object') throw this.unsupported('append.event', this.id.value, 'Only registered event instances are supported.');
|
|
146
|
+
if (!/^[A-Za-z0-9_-]+$/.test(eventSourceId)) throw this.unsupported('append.source', eventSourceId, 'Only simple source identifiers are fixture-backed.');
|
|
147
|
+
if (options && (Reflect.ownKeys(options).length || options.correlationId !== undefined || options.sourceType !== undefined ||
|
|
148
|
+
options.streamType !== undefined || options.streamId !== undefined || options.subject !== undefined ||
|
|
149
|
+
options.occurred !== undefined || options.eventSourceId !== undefined || options.concurrencyScope !== undefined ||
|
|
150
|
+
options.tags !== undefined || options.concurrencyScopes !== undefined)) {
|
|
151
|
+
throw this.unsupported('append.options', event.constructor.name, 'Append metadata, routing and concurrency are not fixture-backed.');
|
|
152
|
+
}
|
|
153
|
+
const metadata = this._catalog.get(event.constructor);
|
|
154
|
+
if (!metadata) throw this.unsupported('append.event', event.constructor.name, 'Event is not in the selected, validated catalog.');
|
|
155
|
+
let prepared: ReturnType<typeof prepareSingleAppend>;
|
|
156
|
+
let content: Record<string, unknown>;
|
|
157
|
+
try {
|
|
158
|
+
prepared = prepareSingleAppend(event, this._correlationId ? { correlationId: this._correlationId() } : undefined);
|
|
159
|
+
content = JSON.parse(prepared.content) as Record<string, unknown>;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw this.unsupported('append.serialization', event.constructor.name, `Payload or metadata could not be serialized: ${String(error)}.`);
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
toContractsGuid(prepared.correlationId);
|
|
165
|
+
if (!/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(prepared.correlationId.toString())) throw new Error('Invalid correlation ID.');
|
|
166
|
+
} catch {
|
|
167
|
+
throw this.unsupported('append.correlationId', event.constructor.name, 'Correlation ID must be a valid GUID.');
|
|
168
|
+
}
|
|
169
|
+
if (prepared.identity !== Identity.system || prepared.causationChain.length !== 2 ||
|
|
170
|
+
Object.keys(prepared.causationChain[0].properties).length !== 0) {
|
|
171
|
+
throw this.unsupported('append.context', event.constructor.name, 'Ambient identity or causation metadata is not fixture-backed.');
|
|
172
|
+
}
|
|
173
|
+
const properties = metadata.schema.properties!;
|
|
174
|
+
if (!content || typeof content !== 'object' || Array.isArray(content) ||
|
|
175
|
+
Object.keys(content).length !== Object.keys(properties).length ||
|
|
176
|
+
Object.entries(properties).some(([key, property]) => typeof content[key] !== property.type ||
|
|
177
|
+
(property.type === 'string' && !/^[\x20-\x21\x23-\x5b\x5d-\x7e\u00e9]*$/.test(content[key] as string)))) {
|
|
178
|
+
throw this.unsupported('append.content', event.constructor.name, 'Content differs from the flat scalar schema; null, missing and extra values are not proven.');
|
|
179
|
+
}
|
|
180
|
+
const occurred = this._clock();
|
|
181
|
+
// Date has millisecond precision; only the UTC year range representable by DateTimeOffset is supported.
|
|
182
|
+
if (!(occurred instanceof Date) || Number.isNaN(occurred.getTime()) ||
|
|
183
|
+
occurred.getUTCFullYear() < 1 || occurred.getUTCFullYear() > 9999) {
|
|
184
|
+
throw this.unsupported('append.clock', event.constructor.name, 'Clock must return a valid Date within UTC years 1–9999 (millisecond precision).');
|
|
185
|
+
}
|
|
186
|
+
const canonical = Object.fromEntries(Object.entries(content).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
187
|
+
const hash = createHash('sha256').update(`${prepared.eventType.id.value}|${eventSourceId}|${JSON.stringify(canonical)}`).digest('base64');
|
|
188
|
+
const sequenceNumber = new EventSequenceNumber(BigInt(this._history.length + (this._stagedSeeds?.length ?? 0)));
|
|
189
|
+
const stored: AppendedEvent = {
|
|
190
|
+
eventType: prepared.eventType,
|
|
191
|
+
content,
|
|
192
|
+
context: {
|
|
193
|
+
eventStore: this._store, namespace: this._namespace,
|
|
194
|
+
sequenceNumber: sequenceNumber.value, eventSourceId,
|
|
195
|
+
eventSourceType: 'Default', eventStreamType: 'All', eventStreamId: 'Default',
|
|
196
|
+
subject: eventSourceId, hash, causedBy: prepared.identity, observationState: EventObservationState.Initial,
|
|
197
|
+
eventType: prepared.eventType, occurred, correlationId: prepared.correlationId.toString(),
|
|
198
|
+
causation: prepared.causationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
|
|
199
|
+
tags: prepared.tags.map(tag => new Tag(tag))
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
if (this._stagedSeeds) this._stagedSeeds.push(this.snapshot(stored));
|
|
203
|
+
else this._history.push(this.snapshot(stored));
|
|
204
|
+
const result = this.success(sequenceNumber, event.constructor.name);
|
|
205
|
+
if (!this._setup) this._results.push(result);
|
|
206
|
+
if (this._stagedNotifications || this.appendOperations.hasSubscribers) {
|
|
207
|
+
const notification = [createAppendNotification(eventSourceId, event, result,
|
|
208
|
+
prepared.correlationId.toString(), mapAppendNotificationCausation(prepared.causationChain), prepared.tags)];
|
|
209
|
+
if (this._stagedNotifications) this._stagedNotifications.push(notification);
|
|
210
|
+
else this.appendOperations.publish(notification);
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
} finally {
|
|
214
|
+
// Keep the append in flight until its promise crosses an async boundary.
|
|
215
|
+
await Promise.resolve();
|
|
216
|
+
this._busy = false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
appendMany(eventSourceId: string, events: object[], options?: AppendOptions): Promise<AppendResult[]>;
|
|
221
|
+
appendMany(events: EventForEventSourceId[], options?: AppendOptions): Promise<AppendResult[]>;
|
|
222
|
+
async appendMany(sourceOrEvents: string | EventForEventSourceId[], eventsOrOptions?: object[] | AppendOptions, options?: AppendOptions): Promise<AppendResult[]> {
|
|
223
|
+
if (this._busy || this._setup) throw this.unsupported('appendMany', this.id.value, 'Overlapping calls do not have proven ordering.');
|
|
224
|
+
this._busy = true;
|
|
225
|
+
try {
|
|
226
|
+
const entries = typeof sourceOrEvents === 'string' ? eventsOrOptions as object[] : sourceOrEvents;
|
|
227
|
+
const shared = typeof sourceOrEvents === 'string' ? options : eventsOrOptions as AppendOptions | undefined;
|
|
228
|
+
if (!Array.isArray(entries)) throw this.unsupported('appendMany.arguments', this.id.value, 'Events must be an array.');
|
|
229
|
+
if (!entries.length) throw this.unsupported('appendMany.empty', this.id.value, 'The kernel rejects an empty batch.');
|
|
230
|
+
this.validateBatchOptions(shared);
|
|
231
|
+
if (shared?.occurred !== undefined) this.checkedDate(shared.occurred, 'appendMany.occurred');
|
|
232
|
+
for (const entry of entries) {
|
|
233
|
+
if (typeof sourceOrEvents === 'string') break;
|
|
234
|
+
if (!entry || typeof entry !== 'object' || Reflect.ownKeys(entry).some(key =>
|
|
235
|
+
!['eventSourceId', 'event', 'eventSourceType', 'eventStreamType', 'eventStreamId', 'subject', 'occurred', 'tags'].includes(String(key)))) {
|
|
236
|
+
throw this.unsupported('appendMany.entry', this.id.value, 'Unrecognized per-entry metadata.');
|
|
237
|
+
}
|
|
238
|
+
const individual = entry as EventForEventSourceId;
|
|
239
|
+
if (individual.occurred !== undefined) this.checkedDate(individual.occurred, 'appendMany.occurred');
|
|
240
|
+
const tags = individual.tags;
|
|
241
|
+
if (tags && (!Array.isArray(tags) || tags.some(tag => typeof tag !== 'string'))) {
|
|
242
|
+
throw this.unsupported('appendMany.tags', this.id.value, 'Only plain string tags are fixture-backed.');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
let prepared: ReturnType<typeof prepareBatchAppend>;
|
|
246
|
+
try {
|
|
247
|
+
const batchOptions: AppendOptions | undefined = shared?.correlationId === undefined && this._correlationId
|
|
248
|
+
? { correlationId: this._correlationId(), sourceType: shared?.sourceType, streamType: shared?.streamType,
|
|
249
|
+
streamId: shared?.streamId, subject: shared?.subject, occurred: shared?.occurred, tags: shared?.tags }
|
|
250
|
+
: shared;
|
|
251
|
+
if (batchOptions?.correlationId !== undefined) this.validateGuid(batchOptions.correlationId, 'appendMany.correlationId');
|
|
252
|
+
prepared = typeof sourceOrEvents === 'string'
|
|
253
|
+
? prepareBatchAppend(sourceOrEvents, entries as object[], batchOptions)
|
|
254
|
+
: prepareBatchAppend(sourceOrEvents, batchOptions);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error instanceof UnsupportedEventSequenceOperation) throw error;
|
|
257
|
+
throw this.unsupported('appendMany.serialization', this.id.value, `Batch preparation failed: ${String(error)}.`);
|
|
258
|
+
}
|
|
259
|
+
const { eventsForEventSourceIds, eventsToAppend, correlationId, batchCausationChain, identity } = prepared;
|
|
260
|
+
this.validateGuid(correlationId, 'appendMany.correlationId');
|
|
261
|
+
if (identity !== Identity.system || batchCausationChain.length !== 2 ||
|
|
262
|
+
Object.keys(batchCausationChain[0].properties).length !== 0 ||
|
|
263
|
+
batchCausationChain[1].type.name !== 'TypeScriptClient.AppendMany' ||
|
|
264
|
+
batchCausationChain[1].properties.count !== String(entries.length)) {
|
|
265
|
+
throw this.unsupported('appendMany.context', this.id.value, 'Ambient identity or causation metadata is not fixture-backed.');
|
|
266
|
+
}
|
|
267
|
+
const staged = eventsForEventSourceIds.map(({ eventSourceId, event }, index) => {
|
|
268
|
+
if (!event || typeof event !== 'object' || !this._catalog.has(event.constructor)) {
|
|
269
|
+
throw this.unsupported('appendMany.event', event?.constructor?.name ?? this.id.value, 'Event is not in the selected catalog.');
|
|
270
|
+
}
|
|
271
|
+
this.validateSource(eventSourceId, 'appendMany.source');
|
|
272
|
+
const wire = eventsToAppend[index];
|
|
273
|
+
for (const [name, value] of Object.entries({ sourceType: wire.EventSourceType, streamType: wire.EventStreamType,
|
|
274
|
+
streamId: wire.EventStreamId, subject: wire.Subject })) {
|
|
275
|
+
if (value !== undefined && (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value))) {
|
|
276
|
+
throw this.unsupported(`appendMany.${name}`, String(value), 'Only simple metadata identifiers are fixture-backed.');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (wire.Tags.some(tag => !/^[A-Za-z0-9_-]+$/.test(tag))) {
|
|
280
|
+
throw this.unsupported('appendMany.tags', event.constructor.name, 'Only simple string tags are fixture-backed.');
|
|
281
|
+
}
|
|
282
|
+
const occurred = wire.Occurred ? this.checkedDate(new Date(wire.Occurred.Value), 'appendMany.occurred')
|
|
283
|
+
: this.checkedDate(this._clock(), 'appendMany.clock');
|
|
284
|
+
const content = this.checkedContent(event, wire.Content, 'appendMany.content');
|
|
285
|
+
const canonical = Object.fromEntries(Object.entries(content).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
286
|
+
const hash = createHash('sha256').update(`${wire.EventType.Id}|${eventSourceId}|${JSON.stringify(canonical)}`).digest('base64');
|
|
287
|
+
const sequenceNumber = new EventSequenceNumber(BigInt(this._history.length + index));
|
|
288
|
+
const eventType = getEventTypeFor(event.constructor);
|
|
289
|
+
const stored: AppendedEvent = { eventType, content, context: {
|
|
290
|
+
eventStore: this._store, namespace: this._namespace, sequenceNumber: sequenceNumber.value, eventSourceId,
|
|
291
|
+
eventSourceType: wire.EventSourceType || 'Default', eventStreamType: wire.EventStreamType || 'All',
|
|
292
|
+
eventStreamId: wire.EventStreamId || 'Default', subject: wire.Subject, hash, causedBy: identity,
|
|
293
|
+
observationState: EventObservationState.Initial, eventType, occurred, correlationId: correlationId.toString(),
|
|
294
|
+
causation: batchCausationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
|
|
295
|
+
tags: wire.Tags.map(tag => new Tag(tag))
|
|
296
|
+
} };
|
|
297
|
+
return { stored, result: this.success(sequenceNumber, event.constructor.name) };
|
|
298
|
+
});
|
|
299
|
+
// Stage and validate every entry before committing any event or result.
|
|
300
|
+
this._history.push(...staged.map(item => this.snapshot(item.stored)));
|
|
301
|
+
const results = staged.map(item => item.result);
|
|
302
|
+
this._results.push(...results);
|
|
303
|
+
if (this.appendOperations.hasSubscribers) {
|
|
304
|
+
const occurredAt = new Date();
|
|
305
|
+
const causationEntries = mapAppendNotificationCausation(batchCausationChain);
|
|
306
|
+
this.appendOperations.publish(staged.map(({ result }, index) =>
|
|
307
|
+
createAppendNotification(eventsForEventSourceIds[index].eventSourceId, eventsForEventSourceIds[index].event, result,
|
|
308
|
+
correlationId.toString(), causationEntries, eventsToAppend[index].Tags, occurredAt)));
|
|
309
|
+
}
|
|
310
|
+
return results;
|
|
311
|
+
} finally {
|
|
312
|
+
await Promise.resolve();
|
|
313
|
+
this._busy = false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async getNextSequenceNumber(): Promise<EventSequenceNumber> { return new EventSequenceNumber(BigInt(this._history.length)); }
|
|
318
|
+
|
|
319
|
+
async getTailSequenceNumber(source?: string, sourceType?: string, streamType?: string, streamId?: string, types?: Constructor[]): Promise<EventSequenceNumber> {
|
|
320
|
+
if (source !== undefined) this.validateReadSource('getTailSequenceNumber.source', source);
|
|
321
|
+
this.validateRouteFilters('getTailSequenceNumber.filters', sourceType, streamType, streamId);
|
|
322
|
+
const ids = this.filterIds(types);
|
|
323
|
+
const last = [...this._history].reverse().find(event => (source === undefined || event.context.eventSourceId === source) &&
|
|
324
|
+
(sourceType === undefined || sourceType === 'Default' || event.context.eventSourceType === sourceType) &&
|
|
325
|
+
(streamType === undefined || streamType === 'All' || event.context.eventStreamType === streamType) &&
|
|
326
|
+
(streamId === undefined || streamId === 'Default' || event.context.eventStreamId === streamId) &&
|
|
327
|
+
(!ids.length || ids.includes(event.eventType.id.value)));
|
|
328
|
+
return last ? new EventSequenceNumber(last.context.sequenceNumber) : EventSequenceNumber.unset;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async getTailSequenceNumberForObserver(observer: Constructor): Promise<EventSequenceNumber> {
|
|
332
|
+
throw this.unsupported('getTailSequenceNumberForObserver', observer.name, 'Observer discovery is not fixture-backed.');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async hasEventsFor(source: string): Promise<boolean> {
|
|
336
|
+
this.validateReadSource('hasEventsFor.source', source);
|
|
337
|
+
return this._history.some(event => event.context.eventSourceId === source);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async getForEventSourceIdAndEventTypes(source: string, types: Constructor[], streamType?: string, streamId?: string, sourceType?: string): Promise<AppendedEvent[]> {
|
|
341
|
+
if (!types.length) throw this.unsupported('getForEventSourceIdAndEventTypes.filters', source, 'Explicit event types are required.');
|
|
342
|
+
this.validateReadSource('getForEventSourceIdAndEventTypes.source', source);
|
|
343
|
+
this.validateRouteFilters('getForEventSourceIdAndEventTypes.filters', sourceType, streamType, streamId);
|
|
344
|
+
const ids = this.filterIds(types);
|
|
345
|
+
return this._history.filter(event => event.context.eventSourceId === source && ids.includes(event.eventType.id.value) &&
|
|
346
|
+
(sourceType === undefined || sourceType === 'Default' || event.context.eventSourceType === sourceType) &&
|
|
347
|
+
(streamType === undefined || streamType === 'All' || event.context.eventStreamType === streamType) &&
|
|
348
|
+
(streamId === undefined || streamId === 'Default' || event.context.eventStreamId === streamId)).map(event => this.snapshot(event));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async getFromSequenceNumber(sequence: EventSequenceNumber, source?: string, types?: Constructor[]): Promise<AppendedEvent[]> {
|
|
352
|
+
if (sequence.value < 0n || sequence.value > EventSequenceNumber.unset.value) {
|
|
353
|
+
throw this.unsupported('getFromSequenceNumber.sequenceNumber', sequence.value.toString(), 'Sequence numbers must fit unsigned 64-bit wire values.');
|
|
354
|
+
}
|
|
355
|
+
if (source !== undefined) this.validateReadSource('getFromSequenceNumber.source', source);
|
|
356
|
+
const ids = this.filterIds(types);
|
|
357
|
+
return this._history.filter(event => event.context.sequenceNumber >= sequence.value &&
|
|
358
|
+
(source === undefined || event.context.eventSourceId === source) &&
|
|
359
|
+
(!ids.length || ids.includes(event.eventType.id.value))).map(event => this.snapshot(event));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async redact(): Promise<void> { throw this.unsupported('redact', this.id.value, 'Redaction requires kernel storage.'); }
|
|
363
|
+
async redactForEventSource(): Promise<void> { throw this.unsupported('redactForEventSource', this.id.value, 'Redaction requires kernel storage.'); }
|
|
364
|
+
async completeStream(): Promise<CompleteStreamResult> { throw this.unsupported('completeStream', this.id.value, 'Stream completion requires a kernel.'); }
|
|
365
|
+
|
|
366
|
+
private filterIds(types?: Constructor[]): string[] {
|
|
367
|
+
return (types ?? []).map(type => {
|
|
368
|
+
const metadata = this._catalog.get(type);
|
|
369
|
+
if (!metadata) throw this.unsupported('read.eventTypes', type.name, 'Event type is not in the selected catalog.');
|
|
370
|
+
return metadata.eventType.id.value;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private validateRouteFilters(operation: string, ...filters: (string | undefined)[]): void {
|
|
375
|
+
for (const filter of filters) {
|
|
376
|
+
if (filter !== undefined && (typeof filter !== 'string' || !/^[A-Za-z0-9_-]+$/.test(filter))) {
|
|
377
|
+
throw this.unsupported(operation, String(filter), 'Only simple, nonblank route filters are fixture-backed.');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private validateBatchOptions(options?: AppendOptions): void {
|
|
383
|
+
if (options === undefined) return;
|
|
384
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
385
|
+
throw this.unsupported('appendMany.options', this.id.value, 'Append options must be an object.');
|
|
386
|
+
}
|
|
387
|
+
const allowed = ['correlationId', 'sourceType', 'streamType', 'streamId', 'subject', 'occurred', 'tags'];
|
|
388
|
+
if (Reflect.ownKeys(options).some(key => !allowed.includes(String(key))) ||
|
|
389
|
+
options.concurrencyScope !== undefined || options.concurrencyScopes !== undefined || options.eventSourceId !== undefined) {
|
|
390
|
+
throw this.unsupported('appendMany.options', this.id.value, 'Concurrency and unrecognized metadata are not fixture-backed.');
|
|
391
|
+
}
|
|
392
|
+
if (options.tags && (!Array.isArray(options.tags) || options.tags.some(tag => typeof tag !== 'string'))) {
|
|
393
|
+
throw this.unsupported('appendMany.tags', this.id.value, 'Only plain string tags are fixture-backed.');
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private validateGuid(value: string | Guid, operation: string): void {
|
|
398
|
+
try {
|
|
399
|
+
toContractsGuid(Guid.as(value));
|
|
400
|
+
if (!/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(value.toString())) throw new Error('Invalid GUID.');
|
|
401
|
+
} catch { throw this.unsupported(operation, this.id.value, 'Correlation ID must be a valid GUID.'); }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private checkedDate(value: Date, operation: string): Date {
|
|
405
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime()) || value.getUTCFullYear() < 1 || value.getUTCFullYear() > 9999) {
|
|
406
|
+
throw this.unsupported(operation, this.id.value, 'Occurrence must be within UTC years 1–9999 at millisecond precision.');
|
|
407
|
+
}
|
|
408
|
+
return new Date(value);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private validateSource(source: string, operation: string): void {
|
|
412
|
+
if (typeof source !== 'string' || !/^[A-Za-z0-9_-]+$/.test(source)) {
|
|
413
|
+
throw this.unsupported(operation, String(source), 'Only simple source identifiers are fixture-backed.');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private checkedContent(event: object, serialized: string, operation: string): Record<string, unknown> {
|
|
418
|
+
let content: Record<string, unknown>;
|
|
419
|
+
try { content = JSON.parse(serialized) as Record<string, unknown>; }
|
|
420
|
+
catch { throw this.unsupported(operation, event.constructor.name, 'Content must be valid JSON.'); }
|
|
421
|
+
const properties = this._catalog.get(event.constructor)!.schema.properties!;
|
|
422
|
+
if (!content || typeof content !== 'object' || Array.isArray(content) ||
|
|
423
|
+
Object.keys(content).length !== Object.keys(properties).length ||
|
|
424
|
+
Object.entries(properties).some(([key, property]) => typeof content[key] !== property.type ||
|
|
425
|
+
(property.type === 'string' && !/^[\x20-\x21\x23-\x5b\x5d-\x7e\u00e9]*$/.test(content[key] as string)))) {
|
|
426
|
+
throw this.unsupported(operation, event.constructor.name, 'Content differs from the fixture-backed scalar schema.');
|
|
427
|
+
}
|
|
428
|
+
return content;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private success(sequenceNumber: EventSequenceNumber, artifact: string): AppendResult {
|
|
432
|
+
return Object.freeze({ sequenceNumber: Object.freeze(sequenceNumber), constraintViolations: Object.freeze([]),
|
|
433
|
+
errors: Object.freeze([]), isSuccess: true,
|
|
434
|
+
waitForCompletion: async () => { throw this.unsupported('waitForCompletion', artifact, 'No observers run in process.'); } });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private snapshot(event: AppendedEvent): AppendedEvent {
|
|
438
|
+
const eventType = new EventType(new EventTypeId(event.eventType.id.value),
|
|
439
|
+
new EventTypeGeneration(event.eventType.generation.value), event.eventType.tombstone);
|
|
440
|
+
const causedBy = event.context.causedBy;
|
|
441
|
+
return {
|
|
442
|
+
eventType,
|
|
443
|
+
content: JSON.parse(JSON.stringify(event.content)) as Record<string, unknown>,
|
|
444
|
+
context: { ...event.context, eventType, occurred: new Date(event.context.occurred),
|
|
445
|
+
causedBy: causedBy && new Identity(causedBy.subject, causedBy.name, causedBy.userName, causedBy.onBehalfOf),
|
|
446
|
+
causation: event.context.causation.map(item => ({ ...item, occurred: item.occurred && new Date(item.occurred), properties: { ...item.properties } })),
|
|
447
|
+
tags: event.context.tags.map(item => new Tag(item.value)) }
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private validateReadSource(operation: string, source: string): void {
|
|
452
|
+
if (!source.trim() || source !== source.trim() || unprovenFilterCharacters.test(source)) {
|
|
453
|
+
throw this.unsupported(operation, source, 'Blank and padded source filters have unproven kernel normalization.');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private unsupported(operation: string, artifact: string, reason: string): UnsupportedEventSequenceOperation {
|
|
458
|
+
return new UnsupportedEventSequenceOperation(operation, artifact, reason);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
/** A kernel operation or definition outside the fixture-backed in-process boundary. */
|
|
5
|
+
export class UnsupportedEventSequenceOperation extends Error {
|
|
6
|
+
constructor(operation: string, artifact: string, reason: string) {
|
|
7
|
+
super(`UnsupportedEventSequenceOperation: ${operation} (${artifact}): ${reason} Use a kernel-backed test.`);
|
|
8
|
+
this.name = 'UnsupportedEventSequenceOperation';
|
|
9
|
+
}
|
|
10
|
+
}
|
package/testing/index.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
+
export { EventScenario } from './EventScenario.js';
|
|
5
|
+
export type { EventScenarioOptions } from './EventScenarioOptions.js';
|
|
6
|
+
export { EventScenarioGivenBuilder } from './EventScenarioGivenBuilder.js';
|
|
7
|
+
export { EventScenarioGiven } from './EventScenarioGiven.js';
|
|
8
|
+
export { EventScenarioWhenBuilder } from './EventScenarioWhenBuilder.js';
|
|
9
|
+
export { EventScenarioWhen } from './EventScenarioWhen.js';
|
|
10
|
+
export { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
|
|
4
11
|
export { ReadModelScenario } from './ReadModelScenario.js';
|
|
5
12
|
export { ReadModelScenarioGiven } from './ReadModelScenarioGiven.js';
|
|
6
13
|
export { ReadModelScenarioGivenBuilder } from './ReadModelScenarioGivenBuilder.js';
|