@cratis/chronicle 6.22.1 → 6.23.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 +2 -27
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/createAppendResult.d.ts +13 -0
- package/dist/eventSequences/createAppendResult.d.ts.map +1 -0
- package/dist/eventSequences/createAppendResult.js +31 -0
- package/dist/eventSequences/createAppendResult.js.map +1 -0
- package/dist/events/constraints/Constraints.d.ts +8 -3
- package/dist/events/constraints/Constraints.d.ts.map +1 -1
- package/dist/events/constraints/Constraints.js +135 -125
- package/dist/events/constraints/Constraints.js.map +1 -1
- package/dist/testing/EventScenario.d.ts.map +1 -1
- package/dist/testing/EventScenario.js +70 -10
- package/dist/testing/EventScenario.js.map +1 -1
- package/dist/testing/InProcessConstraints.d.ts +15 -0
- package/dist/testing/InProcessConstraints.d.ts.map +1 -0
- package/dist/testing/InProcessConstraints.js +124 -0
- package/dist/testing/InProcessConstraints.js.map +1 -0
- package/dist/testing/InProcessEventSequence.d.ts +5 -1
- package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
- package/dist/testing/InProcessEventSequence.js +34 -15
- package/dist/testing/InProcessEventSequence.js.map +1 -1
- package/dist/testing/ReactorScenario.d.ts.map +1 -1
- package/dist/testing/ReactorScenario.js +3 -2
- package/dist/testing/ReactorScenario.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/EventSequence.ts +3 -32
- package/eventSequences/createAppendResult.ts +44 -0
- package/events/constraints/Constraints.ts +57 -47
- package/events/constraints/for_Constraints/when_resolving_violation_messages.spec.ts +28 -0
- package/package.json +1 -1
- package/testing/EventScenario.constraint-discovery.spec.ts +69 -0
- package/testing/EventScenario.constraints.spec.ts +414 -0
- package/testing/EventScenario.fixtures.spec.ts +5 -3
- package/testing/EventScenario.rejections.spec.ts +10 -3
- package/testing/EventScenario.ts +72 -11
- package/testing/InProcessConstraints.ts +127 -0
- package/testing/InProcessEventSequence.ts +32 -14
- package/testing/ReactorScenario.ts +3 -1
package/testing/EventScenario.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { DefaultClientArtifactsProvider } from '../artifacts/DefaultClientArtifactsProvider.js';
|
|
5
5
|
import { TypeDiscoverer } from '../types/TypeDiscoverer.js';
|
|
6
|
+
import { TypeIntrospector } from '../types/TypeIntrospector.js';
|
|
6
7
|
import type { AppendOptions } from '../eventSequences/AppendOptions.js';
|
|
7
8
|
import type { AppendResult } from '../eventSequences/AppendResult.js';
|
|
8
9
|
import type { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
|
|
@@ -10,8 +11,12 @@ import type { AppendedEvent } from '../events/AppendedEvent.js';
|
|
|
10
11
|
import type { IEventSequence } from '../eventSequences/IEventSequence.js';
|
|
11
12
|
import type { IEventLog } from '../eventSequences/IEventLog.js';
|
|
12
13
|
import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
|
|
13
|
-
import { getEventTypeMetadata } from '../events/eventTypeDecorator.js';
|
|
14
|
+
import { getEventTypeFor, getEventTypeMetadata } from '../events/eventTypeDecorator.js';
|
|
15
|
+
import { compileConstraints } from '../events/constraints/Constraints.js';
|
|
14
16
|
import { getUniqueEventMetadata, getUniquePropertyMetadata } from '../events/constraints/unique.js';
|
|
17
|
+
import { getRemovedConstraintNames } from '../events/constraints/removeConstraint.js';
|
|
18
|
+
import { getConstraintMetadata } from '../events/constraints/constraint.js';
|
|
19
|
+
import { InProcessConstraints } from './InProcessConstraints.js';
|
|
15
20
|
import type { EventScenarioOptions } from './EventScenarioOptions.js';
|
|
16
21
|
import { EventScenarioGivenBuilder } from './EventScenarioGivenBuilder.js';
|
|
17
22
|
import { EventScenarioWhenBuilder } from './EventScenarioWhenBuilder.js';
|
|
@@ -19,6 +24,11 @@ import { InProcessEventSequence } from './InProcessEventSequence.js';
|
|
|
19
24
|
import { registerScenarioSeed } from './EventScenarioSeed.js';
|
|
20
25
|
import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
|
|
21
26
|
|
|
27
|
+
function hasConstraintDecorators(type: Function): boolean {
|
|
28
|
+
return getUniqueEventMetadata(type) !== undefined || getRemovedConstraintNames(type).length > 0 ||
|
|
29
|
+
TypeIntrospector.getTrackedProperties(type).some(property => getUniquePropertyMetadata(type, property) !== undefined);
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
/** A kernel-free, fixture-bounded append scenario. No observer runs automatically. */
|
|
23
33
|
export class EventScenario {
|
|
24
34
|
readonly eventSequence: IEventSequence;
|
|
@@ -26,25 +36,76 @@ export class EventScenario {
|
|
|
26
36
|
readonly when: EventScenarioWhenBuilder;
|
|
27
37
|
|
|
28
38
|
constructor(options: EventScenarioOptions = {}) {
|
|
29
|
-
const
|
|
39
|
+
const selectedArtifacts = options.artifacts;
|
|
40
|
+
const selectedConstraints = selectedArtifacts?.constraints;
|
|
41
|
+
const artifacts = selectedArtifacts ?? new DefaultClientArtifactsProvider(TypeDiscoverer.default);
|
|
30
42
|
const eventTypes = [...artifacts.eventTypes];
|
|
31
|
-
if (
|
|
43
|
+
if (selectedConstraints?.length === 0 && options.constraints !== 'disabled') {
|
|
32
44
|
throw new UnsupportedEventSequenceOperation('artifacts.constraints', 'empty constraint catalog',
|
|
33
45
|
'An empty selected catalog cannot silently disable constraint discovery; set constraints: disabled explicitly.');
|
|
34
46
|
}
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
const discoveredArtifacts = selectedArtifacts && selectedConstraints === undefined
|
|
48
|
+
? new DefaultClientArtifactsProvider(TypeDiscoverer.default) : undefined;
|
|
49
|
+
const discovered = selectedConstraints ?? discoveredArtifacts?.constraints ?? artifacts.constraints;
|
|
50
|
+
let constraints: InProcessConstraints | undefined;
|
|
51
|
+
if (options.constraints !== 'disabled') {
|
|
52
|
+
for (const type of discovered ?? []) {
|
|
53
|
+
if (!getConstraintMetadata(type)) throw new UnsupportedEventSequenceOperation('artifacts.constraints', type.name,
|
|
54
|
+
'Every selected constraint must have @constraint metadata.');
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
// Keep global decorator contributions. A shadowed ID is accepted only when neither
|
|
58
|
+
// constructor has constraint decorators, so either one compiles to the same constraints.
|
|
59
|
+
const compiledEventTypes = [...new Set([...(discoveredArtifacts?.eventTypes ?? []), ...eventTypes])];
|
|
60
|
+
const constructorsById = new Map<string, (typeof eventTypes)[number]>();
|
|
61
|
+
for (const type of compiledEventTypes) {
|
|
62
|
+
const id = getEventTypeFor(type).id.value;
|
|
63
|
+
const previous = constructorsById.get(id);
|
|
64
|
+
if (previous && previous !== type && (hasConstraintDecorators(previous) || hasConstraintDecorators(type))) {
|
|
65
|
+
throw new UnsupportedEventSequenceOperation('artifacts.eventTypes', id,
|
|
66
|
+
'Conflicting constructors share an event type ID; constraint discovery cannot choose one.');
|
|
67
|
+
}
|
|
68
|
+
constructorsById.set(id, type);
|
|
69
|
+
}
|
|
70
|
+
const compiled = compileConstraints({ eventTypes: [...constructorsById.values()], constraints: discovered ?? [] });
|
|
71
|
+
const selectedIds = new Set(eventTypes.map(type => getEventTypeFor(type).id.value));
|
|
72
|
+
const definitions = discoveredArtifacts ? new Map([...compiled].filter(([, capture]) => {
|
|
73
|
+
const constrained = capture.uniqueConstraint?.eventDefinitions.map(entry => entry.eventTypeId) ??
|
|
74
|
+
capture.uniqueEventType?.eventTypeIds ?? [capture.uniqueEventType?.eventTypeId];
|
|
75
|
+
const removedWith = [...(capture.uniqueConstraint?.removedWithEventTypeIds ?? []),
|
|
76
|
+
capture.uniqueConstraint?.removedWithEventTypeId,
|
|
77
|
+
...(capture.uniqueEventType?.removedWithEventTypeIds ?? [])];
|
|
78
|
+
return [...constrained, ...removedWith].some(id => id !== undefined && selectedIds.has(id));
|
|
79
|
+
})) : compiled;
|
|
80
|
+
constraints = new InProcessConstraints(definitions);
|
|
81
|
+
for (const [name, capture] of definitions) {
|
|
82
|
+
const ids = capture.uniqueConstraint?.eventDefinitions.map(entry => entry.eventTypeId) ??
|
|
83
|
+
capture.uniqueEventType?.eventTypeIds ?? [capture.uniqueEventType?.eventTypeId];
|
|
84
|
+
const removedWith = [...(capture.uniqueConstraint?.removedWithEventTypeIds ?? []),
|
|
85
|
+
capture.uniqueConstraint?.removedWithEventTypeId,
|
|
86
|
+
...(capture.uniqueEventType?.removedWithEventTypeIds ?? [])];
|
|
87
|
+
if ([...ids, ...removedWith].some(id => id !== undefined && !selectedIds.has(id))) {
|
|
88
|
+
throw new UnsupportedEventSequenceOperation('artifacts.constraints', name,
|
|
89
|
+
'Every constrained event type must be in the selected catalog.');
|
|
90
|
+
}
|
|
91
|
+
for (const entry of capture.uniqueConstraint?.eventDefinitions ?? []) {
|
|
92
|
+
const type = eventTypes.find(type => getEventTypeFor(type).id.value === entry.eventTypeId)!;
|
|
93
|
+
if (getEventTypeMetadata(type)?.schema.properties?.[entry.properties[0]]?.type !== 'string') {
|
|
94
|
+
throw new UnsupportedEventSequenceOperation('artifacts.constraints', name,
|
|
95
|
+
'The constrained property must be a schema-backed string.');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error instanceof UnsupportedEventSequenceOperation) throw error;
|
|
101
|
+
throw new UnsupportedEventSequenceOperation('artifacts.constraints', 'compiler', `Invalid constraint definition: ${String(error)}.`);
|
|
102
|
+
}
|
|
42
103
|
}
|
|
43
104
|
if (artifacts.eventTypeMigrations?.length) {
|
|
44
105
|
throw new UnsupportedEventSequenceOperation('artifacts.eventTypeMigrations', artifacts.eventTypeMigrations.map(type => type.name).join(', '),
|
|
45
106
|
'Migrations are not fixture-backed.');
|
|
46
107
|
}
|
|
47
|
-
const sequence = new InProcessEventSequence(options, eventTypes);
|
|
108
|
+
const sequence = new InProcessEventSequence(options, eventTypes, constraints);
|
|
48
109
|
this.eventSequence = sequence;
|
|
49
110
|
registerScenarioSeed(this, (source, events) => sequence.seed(source, events));
|
|
50
111
|
this.given = new EventScenarioGivenBuilder(this);
|
|
@@ -0,0 +1,127 @@
|
|
|
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 type { ConstraintViolation as ContractsConstraintViolation } from '@cratis/chronicle.contracts';
|
|
6
|
+
import type { AppendedEvent } from '../events/AppendedEvent.js';
|
|
7
|
+
import type { ConstraintCapture } from '../events/constraints/ConstraintBuilder.js';
|
|
8
|
+
import { resolveConstraintMessage } from '../events/constraints/Constraints.js';
|
|
9
|
+
import type { ConstraintViolation } from '../eventSequences/ConstraintViolation.js';
|
|
10
|
+
import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
|
|
11
|
+
|
|
12
|
+
export type WireConstraintViolation = ContractsConstraintViolation;
|
|
13
|
+
|
|
14
|
+
const unavailable = '18446744073709551615';
|
|
15
|
+
|
|
16
|
+
/** Narrow, fixture-backed unscoped constraint validation over serialized event snapshots. */
|
|
17
|
+
export class InProcessConstraints {
|
|
18
|
+
constructor(private readonly _definitions: ReadonlyMap<string, ConstraintCapture>) {
|
|
19
|
+
const coveredTypes = new Set<string>();
|
|
20
|
+
for (const [name, capture] of _definitions) {
|
|
21
|
+
if (capture.uniqueConstraint && capture.uniqueEventType) {
|
|
22
|
+
throw this.unsupported(name, 'A definition with both constraint kinds is not fixture-backed.');
|
|
23
|
+
}
|
|
24
|
+
if (capture.scope.perEventSourceType || capture.scope.perEventStreamType || capture.scope.perEventStreamId) {
|
|
25
|
+
throw this.unsupported(name, 'Scoped constraints are not fixture-backed.');
|
|
26
|
+
}
|
|
27
|
+
if (capture.uniqueConstraint) {
|
|
28
|
+
const unique = capture.uniqueConstraint;
|
|
29
|
+
if (unique.ignoreCasing || unique.removedWithEventTypeId || unique.removedWithEventTypeIds?.length ||
|
|
30
|
+
unique.eventDefinitions.length === 0 || unique.eventDefinitions.some(entry => entry.properties.length !== 1 ||
|
|
31
|
+
!/^[a-z][a-zA-Z0-9]*$/.test(entry.properties[0]))) {
|
|
32
|
+
throw this.unsupported(name, 'Case folding, removal and composite or nested keys are not fixture-backed.');
|
|
33
|
+
}
|
|
34
|
+
} else if (capture.uniqueEventType) {
|
|
35
|
+
const unique = capture.uniqueEventType;
|
|
36
|
+
if (unique.removedWithEventTypeIds?.length || (unique.eventTypeIds?.length ?? 1) !== 1) {
|
|
37
|
+
throw this.unsupported(name, 'Removal and multi-type unique event cycles are not fixture-backed.');
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
throw this.unsupported(name, 'Unknown constraint definition.');
|
|
41
|
+
}
|
|
42
|
+
const ids = capture.uniqueConstraint?.eventDefinitions.map(entry => entry.eventTypeId) ??
|
|
43
|
+
capture.uniqueEventType?.eventTypeIds ?? [capture.uniqueEventType!.eventTypeId];
|
|
44
|
+
for (const id of ids) {
|
|
45
|
+
if (coveredTypes.has(id)) throw this.unsupported(name, 'Overlapping constraints are not fixture-backed.');
|
|
46
|
+
coveredTypes.add(id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Validate against committed history and all earlier entries of this append, without changing state. */
|
|
52
|
+
validate(history: readonly AppendedEvent[], incoming: readonly AppendedEvent[]): WireConstraintViolation[] {
|
|
53
|
+
const violations: WireConstraintViolation[] = [];
|
|
54
|
+
const stagedKeys = new Map<string, string>();
|
|
55
|
+
const stagedTypes = new Set<string>();
|
|
56
|
+
for (const event of incoming) {
|
|
57
|
+
const type = event.eventType.id.value;
|
|
58
|
+
const source = event.context.eventSourceId;
|
|
59
|
+
for (const [name, definition] of this._definitions) {
|
|
60
|
+
const property = definition.uniqueConstraint?.eventDefinitions.find(entry => entry.eventTypeId === type)?.properties[0];
|
|
61
|
+
if (property !== undefined) {
|
|
62
|
+
const raw = event.content[property];
|
|
63
|
+
// The oracle proves ordinal ASCII casing and significant spaces; no .NET Unicode folding or conversion is inferred.
|
|
64
|
+
if (typeof raw !== 'string' || !/^[A-Za-z ]+$/.test(raw)) {
|
|
65
|
+
throw this.unsupported(name, 'Only nonempty ASCII letter and space string keys are fixture-backed.');
|
|
66
|
+
}
|
|
67
|
+
const key = createHash('sha256').update(raw).digest('hex');
|
|
68
|
+
const claims = new Map<string, { source: string; sequence: string }>();
|
|
69
|
+
const owners = new Map<string, string>();
|
|
70
|
+
for (const prior of history) {
|
|
71
|
+
const priorProperty = definition.uniqueConstraint!.eventDefinitions.find(entry =>
|
|
72
|
+
entry.eventTypeId === prior.eventType.id.value)?.properties[0];
|
|
73
|
+
if (priorProperty !== undefined) {
|
|
74
|
+
const priorValue = prior.content[priorProperty];
|
|
75
|
+
if (typeof priorValue !== 'string') throw this.unsupported(name, 'History has an unproven key.');
|
|
76
|
+
owners.set(prior.context.eventSourceId, createHash('sha256').update(priorValue).digest('hex'));
|
|
77
|
+
claims.set(createHash('sha256').update(priorValue).digest('hex'), {
|
|
78
|
+
source: prior.context.eventSourceId, sequence: prior.context.sequenceNumber.toString()
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const ownerKey = owners.get(source);
|
|
83
|
+
if (ownerKey !== undefined && ownerKey !== key) {
|
|
84
|
+
throw this.unsupported(name, 'Replacing a unique key is not fixture-backed.');
|
|
85
|
+
}
|
|
86
|
+
const stagedOwner = stagedKeys.get(`${name}\u0000${source}`);
|
|
87
|
+
if (stagedOwner !== undefined && stagedOwner !== key) {
|
|
88
|
+
throw this.unsupported(name, 'Replacing a key within one batch is not fixture-backed.');
|
|
89
|
+
}
|
|
90
|
+
const existing = claims.get(key);
|
|
91
|
+
const batchOwner = stagedKeys.get(`${name}\u0001${key}`);
|
|
92
|
+
if (existing && existing.source !== source || batchOwner && batchOwner !== source) {
|
|
93
|
+
const details = { PropertyName: property, PropertyValue: raw };
|
|
94
|
+
violations.push({ EventTypeId: type, SequenceNumber: BigInt(existing?.sequence ?? unavailable), ConstraintType: 1,
|
|
95
|
+
ConstraintName: name,
|
|
96
|
+
Message: `Event '${type}' on member '${property}' violated a unique constraint on sequence number ${existing?.sequence ?? unavailable}`,
|
|
97
|
+
Details: details });
|
|
98
|
+
} else {
|
|
99
|
+
stagedKeys.set(`${name}\u0000${source}`, key);
|
|
100
|
+
stagedKeys.set(`${name}\u0001${key}`, source);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const uniqueType = definition.uniqueEventType;
|
|
104
|
+
if (uniqueType && (uniqueType.eventTypeIds ?? [uniqueType.eventTypeId]).includes(type)) {
|
|
105
|
+
const first = history.find(prior => prior.context.eventSourceId === source &&
|
|
106
|
+
(uniqueType.eventTypeIds ?? [uniqueType.eventTypeId]).includes(prior.eventType.id.value));
|
|
107
|
+
const staged = stagedTypes.has(`${name}\u0000${source}`);
|
|
108
|
+
if (first || staged) {
|
|
109
|
+
violations.push({ EventTypeId: type, SequenceNumber: first?.context.sequenceNumber ?? BigInt(unavailable), ConstraintType: 2,
|
|
110
|
+
ConstraintName: name,
|
|
111
|
+
Message: `Event '${type}' violated a unique event type constraint on sequence number ${first?.context.sequenceNumber.toString() ?? unavailable}`,
|
|
112
|
+
Details: {} });
|
|
113
|
+
} else stagedTypes.add(`${name}\u0000${source}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return violations;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
resolveMessage(violation: ConstraintViolation): ConstraintViolation {
|
|
121
|
+
return resolveConstraintMessage(this._definitions, violation);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private unsupported(name: string, reason: string): UnsupportedEventSequenceOperation {
|
|
125
|
+
return new UnsupportedEventSequenceOperation('artifacts.constraints', name, reason);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -6,6 +6,7 @@ import { EventObservationState } from '@cratis/chronicle.contracts';
|
|
|
6
6
|
import { AppendOperationsBroadcaster } from '../eventSequences/AppendOperationsBroadcaster.js';
|
|
7
7
|
import { prepareBatchAppend } from '../eventSequences/prepareBatchAppend.js';
|
|
8
8
|
import { createAppendNotification, mapAppendNotificationCausation } from '../eventSequences/createAppendNotification.js';
|
|
9
|
+
import { createAppendResult } from '../eventSequences/createAppendResult.js';
|
|
9
10
|
import { Guid, type Constructor } from '@cratis/fundamentals';
|
|
10
11
|
import { getEventTypeMetadata, getEventTypeFor } from '../events/eventTypeDecorator.js';
|
|
11
12
|
import { getTagsFor } from '../events/tagDecorator.js';
|
|
@@ -26,8 +27,8 @@ import type { ITransactionalEventSequence } from '../eventSequences/ITransaction
|
|
|
26
27
|
import type { CompleteStreamResult } from '../eventSequences/CompleteStreamResult.js';
|
|
27
28
|
import { prepareSingleAppend } from '../eventSequences/prepareSingleAppend.js';
|
|
28
29
|
import { toContractsGuid } from '../connection/Guid.js';
|
|
29
|
-
import { getUniqueEventMetadata, getUniquePropertyMetadata } from '../events/constraints/unique.js';
|
|
30
30
|
import { getRemovedConstraintNames } from '../events/constraints/removeConstraint.js';
|
|
31
|
+
import { InProcessConstraints, type WireConstraintViolation } from './InProcessConstraints.js';
|
|
31
32
|
import type { EventScenarioOptions } from './EventScenarioOptions.js';
|
|
32
33
|
import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
|
|
33
34
|
|
|
@@ -64,7 +65,7 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
64
65
|
private readonly _results: AppendResult[] = [];
|
|
65
66
|
readonly appendOperations = new AppendOperationsBroadcaster<AppendedEventWithResult[]>();
|
|
66
67
|
|
|
67
|
-
constructor(options: EventScenarioOptions, eventTypes: Constructor[]) {
|
|
68
|
+
constructor(options: EventScenarioOptions, eventTypes: Constructor[], private readonly _constraints?: InProcessConstraints) {
|
|
68
69
|
if (options.eventSequenceId && options.eventSequenceId.value !== EventSequenceId.eventLog.value) {
|
|
69
70
|
throw this.unsupported('options.eventSequenceId', options.eventSequenceId.value, 'Custom sequences are not fixture-backed.');
|
|
70
71
|
}
|
|
@@ -93,9 +94,8 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
93
94
|
if (eventType.generation.value !== 1 || eventType.tombstone) {
|
|
94
95
|
throw this.unsupported('artifacts.eventTypes', type.name, 'Only generation 1, non-tombstone events are proven.');
|
|
95
96
|
}
|
|
96
|
-
if (options.constraints !== 'disabled' &&
|
|
97
|
-
|
|
98
|
-
throw this.unsupported('artifacts.eventTypes.constraints', type.name, 'Constraint metadata is not supported in this increment.');
|
|
97
|
+
if (options.constraints !== 'disabled' && getRemovedConstraintNames(type).length) {
|
|
98
|
+
throw this.unsupported('artifacts.eventTypes.constraints', type.name, 'Constraint removal is not fixture-backed.');
|
|
99
99
|
}
|
|
100
100
|
if (getTagsFor(type).length) throw this.unsupported('artifacts.eventTypes.tags', type.name, 'Tagged events are not fixture-backed.');
|
|
101
101
|
// Validate the complete schema, not only properties present on a particular event.
|
|
@@ -132,7 +132,8 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
132
132
|
try { pending = this.append(source, event); }
|
|
133
133
|
finally { this._allowSeedAppend = false; }
|
|
134
134
|
const result = await pending;
|
|
135
|
-
if (!result.isSuccess) throw new Error(`EventScenario given setup failed: ${JSON.stringify(result)
|
|
135
|
+
if (!result.isSuccess) throw new Error(`EventScenario given setup failed: ${JSON.stringify(result, (_, value) =>
|
|
136
|
+
typeof value === 'bigint' ? value.toString() : value)}`);
|
|
136
137
|
}
|
|
137
138
|
for (const [index, stored] of this._stagedSeeds.entries()) {
|
|
138
139
|
this._history.push(stored);
|
|
@@ -212,9 +213,12 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
212
213
|
tags: prepared.tags.map(tag => new Tag(tag))
|
|
213
214
|
}
|
|
214
215
|
};
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
const violations = this.validateConstraints([...this._history, ...(this._stagedSeeds ?? [])], [stored]);
|
|
217
|
+
const result = violations.length ? this.rejected(violations) : this.success(sequenceNumber, event.constructor.name);
|
|
218
|
+
if (result.isSuccess) {
|
|
219
|
+
if (this._stagedSeeds) this._stagedSeeds.push(this.snapshot(stored));
|
|
220
|
+
else this._history.push(this.snapshot(stored));
|
|
221
|
+
}
|
|
218
222
|
if (!this._setup) this._results.push(result);
|
|
219
223
|
if (this._stagedNotifications || this.appendOperations.hasSubscribers) {
|
|
220
224
|
const notification = [createAppendNotification(eventSourceId, event, result,
|
|
@@ -309,15 +313,16 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
309
313
|
} };
|
|
310
314
|
return { stored, result: this.success(sequenceNumber, event.constructor.name) };
|
|
311
315
|
});
|
|
312
|
-
//
|
|
313
|
-
this._history
|
|
314
|
-
const results = staged.map(item => item.result);
|
|
316
|
+
// The kernel validates the entire batch before persisting any events or allocating sequences.
|
|
317
|
+
const violations = this.validateConstraints(this._history, staged.map(item => item.stored));
|
|
318
|
+
const results = violations.length ? staged.map(() => this.rejected(violations)) : staged.map(item => item.result);
|
|
319
|
+
if (!violations.length) this._history.push(...staged.map(item => this.snapshot(item.stored)));
|
|
315
320
|
this._results.push(...results);
|
|
316
321
|
if (this.appendOperations.hasSubscribers) {
|
|
317
322
|
const occurredAt = new Date();
|
|
318
323
|
const causationEntries = mapAppendNotificationCausation(batchCausationChain);
|
|
319
|
-
this.appendOperations.publish(staged.map((
|
|
320
|
-
createAppendNotification(eventsForEventSourceIds[index].eventSourceId, eventsForEventSourceIds[index].event,
|
|
324
|
+
this.appendOperations.publish(staged.map((_, index) =>
|
|
325
|
+
createAppendNotification(eventsForEventSourceIds[index].eventSourceId, eventsForEventSourceIds[index].event, results[index],
|
|
321
326
|
correlationId.toString(), causationEntries, eventsToAppend[index].Tags, occurredAt)));
|
|
322
327
|
}
|
|
323
328
|
return results;
|
|
@@ -441,6 +446,19 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
441
446
|
return content;
|
|
442
447
|
}
|
|
443
448
|
|
|
449
|
+
private validateConstraints(history: readonly AppendedEvent[], incoming: readonly AppendedEvent[]): WireConstraintViolation[] {
|
|
450
|
+
try { return this._constraints?.validate(history, incoming) ?? []; }
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (error instanceof UnsupportedEventSequenceOperation) unsupportedObservers.get(this)?.(error);
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private rejected(violations: WireConstraintViolation[]): AppendResult {
|
|
458
|
+
return createAppendResult(18446744073709551615n, violations, [], undefined,
|
|
459
|
+
violation => this._constraints!.resolveMessage(violation), async () => ({ isSuccess: true, failedPartitions: [] }));
|
|
460
|
+
}
|
|
461
|
+
|
|
444
462
|
private success(sequenceNumber: EventSequenceNumber, artifact: string): AppendResult {
|
|
445
463
|
return Object.freeze({ sequenceNumber: Object.freeze(sequenceNumber), constraintViolations: Object.freeze([]),
|
|
446
464
|
errors: Object.freeze([]), isSuccess: true,
|
|
@@ -178,7 +178,9 @@ export class ReactorScenario {
|
|
|
178
178
|
await this._events.given.forEventSource(sourceId).events(...input);
|
|
179
179
|
} else {
|
|
180
180
|
const results = await this._events.when.forEventSource(sourceId).events(...input);
|
|
181
|
-
|
|
181
|
+
const failures = results.filter(result => !result.isSuccess);
|
|
182
|
+
if (failures.length) throw new Error(`ReactorScenario action append failed: ${JSON.stringify(failures, (_, value) =>
|
|
183
|
+
typeof value === 'bigint' ? value.toString() : value)}`);
|
|
182
184
|
}
|
|
183
185
|
const events = this._events.appendedEvents.slice(before);
|
|
184
186
|
if (events.length) await this.process(sourceId, events);
|