@cratis/chronicle 6.22.0 → 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.spec.ts +37 -2
- package/eventSequences/EventSequence.ts +19 -43
- package/eventSequences/createAppendResult.ts +44 -0
- package/events/constraints/Constraints.ts +57 -47
- package/events/constraints/for_Constraints/when_resolving_violation_messages.spec.ts +37 -4
- 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
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import { ConstraintType } from '@cratis/chronicle.contracts';
|
|
5
5
|
import type { ConstraintViolation } from '../../eventSequences/ConstraintViolation.js';
|
|
6
|
-
import { IClientArtifactsProvider } from '../../artifacts/index.js';
|
|
7
|
-
import { ChronicleConnection } from '../../connection/index.js';
|
|
6
|
+
import type { IClientArtifactsProvider } from '../../artifacts/index.js';
|
|
7
|
+
import type { ChronicleConnection } from '../../connection/index.js';
|
|
8
8
|
import { ConstraintId } from './ConstraintId.js';
|
|
9
9
|
import { IConstraint } from './IConstraint.js';
|
|
10
10
|
import { IConstraints } from './IConstraints.js';
|
|
@@ -35,38 +35,22 @@ function assertMatchingScope(name: string, left: ConstraintScopeCapture, right:
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* @param _eventStore - The name of the event store these constraints belong to.
|
|
45
|
-
* @param _connection - The connection used to communicate with the Kernel.
|
|
46
|
-
* @param _clientArtifacts - Provider for discovered client artifact types.
|
|
47
|
-
*/
|
|
48
|
-
constructor(
|
|
49
|
-
private readonly _eventStore: string,
|
|
50
|
-
private readonly _connection: ChronicleConnection,
|
|
51
|
-
private readonly _clientArtifacts: IClientArtifactsProvider
|
|
52
|
-
) {}
|
|
53
|
-
|
|
54
|
-
/** @inheritdoc */
|
|
55
|
-
async discover(): Promise<void> {
|
|
56
|
-
this._captures.clear();
|
|
57
|
-
const fluentIds = new Set<string>();
|
|
58
|
-
for (const type of this._clientArtifacts.constraints) {
|
|
59
|
-
const metadata = getConstraintMetadata(type);
|
|
38
|
+
/** Compile decorator and fluent constraints using the production registration rules. */
|
|
39
|
+
export function compileConstraints(provider: Pick<IClientArtifactsProvider, 'eventTypes' | 'constraints'>): Map<string, ConstraintCapture> {
|
|
40
|
+
const captures = new Map<string, ConstraintCapture>();
|
|
41
|
+
const fluentIds = new Set<string>();
|
|
42
|
+
for (const definition of provider.constraints) {
|
|
43
|
+
const metadata = getConstraintMetadata(definition);
|
|
60
44
|
if (!metadata) continue;
|
|
61
|
-
|
|
62
|
-
fluentIds.
|
|
63
|
-
|
|
64
|
-
const builder = new ConstraintBuilder(
|
|
65
|
-
const instance = new (
|
|
45
|
+
const id = metadata.id.value;
|
|
46
|
+
if (fluentIds.has(id)) throw new Error(`Duplicate constraint id '${id}'.`);
|
|
47
|
+
fluentIds.add(id);
|
|
48
|
+
const builder = new ConstraintBuilder(id);
|
|
49
|
+
const instance = new (definition as new () => IConstraint)();
|
|
66
50
|
instance.define(builder);
|
|
67
51
|
const capture = builder.capture;
|
|
68
52
|
const name = wireNameOf(capture);
|
|
69
|
-
const existing =
|
|
53
|
+
const existing = captures.get(name);
|
|
70
54
|
if (existing?.uniqueEventType && capture.uniqueEventType) {
|
|
71
55
|
assertMatchingScope(name, existing.scope, capture.scope);
|
|
72
56
|
const merged = existing.uniqueEventType;
|
|
@@ -82,12 +66,12 @@ export class Constraints implements IConstraints {
|
|
|
82
66
|
} else if (existing) {
|
|
83
67
|
throw new Error(`Duplicate constraint name '${name}'.`);
|
|
84
68
|
} else {
|
|
85
|
-
|
|
69
|
+
captures.set(name, capture);
|
|
86
70
|
}
|
|
87
71
|
}
|
|
88
72
|
|
|
89
73
|
const removalEvents = new Map<string, Function[]>();
|
|
90
|
-
for (const eventType of
|
|
74
|
+
for (const eventType of provider.eventTypes) {
|
|
91
75
|
for (const name of getRemovedConstraintNames(eventType)) {
|
|
92
76
|
const events = removalEvents.get(name) ?? [];
|
|
93
77
|
events.push(eventType);
|
|
@@ -95,16 +79,16 @@ export class Constraints implements IConstraints {
|
|
|
95
79
|
}
|
|
96
80
|
}
|
|
97
81
|
|
|
98
|
-
for (const eventType of
|
|
82
|
+
for (const eventType of provider.eventTypes) {
|
|
99
83
|
const eventMetadata = getUniqueEventMetadata(eventType);
|
|
100
84
|
if (eventMetadata) {
|
|
101
85
|
const name = eventMetadata.name ?? eventType.name;
|
|
102
|
-
let capture =
|
|
86
|
+
let capture = captures.get(name);
|
|
103
87
|
if (!capture) {
|
|
104
88
|
const builder = new ConstraintBuilder(name);
|
|
105
89
|
builder.uniqueFor(eventType, eventMetadata.message, name);
|
|
106
90
|
capture = builder.capture;
|
|
107
|
-
|
|
91
|
+
captures.set(name, capture);
|
|
108
92
|
} else if (!capture.uniqueEventType) {
|
|
109
93
|
throw new Error(`Constraint '${name}' is not a unique event type constraint.`);
|
|
110
94
|
} else {
|
|
@@ -120,12 +104,12 @@ export class Constraints implements IConstraints {
|
|
|
120
104
|
const metadata = getUniquePropertyMetadata(eventType, property);
|
|
121
105
|
if (!metadata) continue;
|
|
122
106
|
const name = metadata.name ?? property;
|
|
123
|
-
let capture =
|
|
107
|
+
let capture = captures.get(name);
|
|
124
108
|
if (!capture) {
|
|
125
109
|
const builder = new ConstraintBuilder(name);
|
|
126
110
|
builder.unique(() => {});
|
|
127
111
|
capture = builder.capture;
|
|
128
|
-
|
|
112
|
+
captures.set(name, capture);
|
|
129
113
|
}
|
|
130
114
|
if (!capture.uniqueConstraint) throw new Error(`Constraint '${name}' is not a unique property constraint.`);
|
|
131
115
|
assertMatchingScope(name, capture.scope, decoratorScope);
|
|
@@ -142,7 +126,7 @@ export class Constraints implements IConstraints {
|
|
|
142
126
|
}
|
|
143
127
|
|
|
144
128
|
for (const [name, eventTypes] of removalEvents) {
|
|
145
|
-
const capture =
|
|
129
|
+
const capture = captures.get(name);
|
|
146
130
|
if (capture) {
|
|
147
131
|
if (capture.uniqueConstraint) {
|
|
148
132
|
const unique = new UniqueConstraintBuilder(capture.uniqueConstraint);
|
|
@@ -156,6 +140,40 @@ export class Constraints implements IConstraints {
|
|
|
156
140
|
}
|
|
157
141
|
}
|
|
158
142
|
}
|
|
143
|
+
return captures;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Resolve the registered message and detail substitutions without a connection. */
|
|
147
|
+
export function resolveConstraintMessage(captures: ReadonlyMap<string, ConstraintCapture>, violation: ConstraintViolation): ConstraintViolation {
|
|
148
|
+
const capture = captures.get(violation.constraintId);
|
|
149
|
+
const message = capture?.uniqueConstraint?.message ?? capture?.uniqueEventType?.message;
|
|
150
|
+
if (!message) return violation;
|
|
151
|
+
let resolved = message;
|
|
152
|
+
for (const [key, value] of Object.entries(violation.details)) {
|
|
153
|
+
resolved = resolved.replaceAll(`{${key}}`, () => value);
|
|
154
|
+
}
|
|
155
|
+
return { ...violation, message: resolved };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Manages discovery and registration of constraints with the Chronicle Kernel. */
|
|
159
|
+
export class Constraints implements IConstraints {
|
|
160
|
+
private _captures = new Map<string, ConstraintCapture>();
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Creates a new {@link Constraints} instance.
|
|
164
|
+
* @param _eventStore - The name of the event store these constraints belong to.
|
|
165
|
+
* @param _connection - The connection used to communicate with the Kernel.
|
|
166
|
+
* @param _clientArtifacts - Provider for discovered client artifact types.
|
|
167
|
+
*/
|
|
168
|
+
constructor(
|
|
169
|
+
private readonly _eventStore: string,
|
|
170
|
+
private readonly _connection: ChronicleConnection,
|
|
171
|
+
private readonly _clientArtifacts: IClientArtifactsProvider
|
|
172
|
+
) {}
|
|
173
|
+
|
|
174
|
+
/** @inheritdoc */
|
|
175
|
+
async discover(): Promise<void> {
|
|
176
|
+
this._captures = compileConstraints(this._clientArtifacts);
|
|
159
177
|
}
|
|
160
178
|
|
|
161
179
|
/** @inheritdoc */
|
|
@@ -242,14 +260,6 @@ export class Constraints implements IConstraints {
|
|
|
242
260
|
* @returns The violation with its configured message and substituted details, if available.
|
|
243
261
|
*/
|
|
244
262
|
resolveMessageFor(violation: ConstraintViolation): ConstraintViolation {
|
|
245
|
-
|
|
246
|
-
const message = capture?.uniqueConstraint?.message ?? capture?.uniqueEventType?.message;
|
|
247
|
-
if (!message) return violation;
|
|
248
|
-
|
|
249
|
-
let resolved = message;
|
|
250
|
-
for (const [key, value] of Object.entries(violation.details)) {
|
|
251
|
-
resolved = resolved.replaceAll(`{${key}}`, () => value);
|
|
252
|
-
}
|
|
253
|
-
return { ...violation, message: resolved };
|
|
263
|
+
return resolveConstraintMessage(this._captures, violation);
|
|
254
264
|
}
|
|
255
265
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
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
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { field } from '@cratis/fundamentals';
|
|
6
|
+
import { ConstraintType, type ConstraintViolation as ContractsConstraintViolation } from '@cratis/chronicle.contracts';
|
|
4
7
|
import { chai, describe, it, vi } from 'vitest';
|
|
5
8
|
import type { ChronicleConnection } from '../../../connection/ChronicleConnection.js';
|
|
6
9
|
import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifactsProvider.js';
|
|
@@ -57,16 +60,44 @@ describe('when resolving violation messages', () => {
|
|
|
57
60
|
const connection = {
|
|
58
61
|
eventSequences: { append: vi.fn().mockResolvedValue({ Response: {
|
|
59
62
|
SequenceNumber: 0n, Errors: [], ConstraintViolations: [{
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
EventTypeId: 'message-registered', SequenceNumber: 0n, ConstraintType: ConstraintType.Unique,
|
|
64
|
+
ConstraintName: 'SharedAddress', Message: 'Kernel default', Details: { email: 'a$b' }
|
|
65
|
+
} satisfies ContractsConstraintViolation]
|
|
62
66
|
} }) }
|
|
63
67
|
} as unknown as ChronicleConnection;
|
|
64
68
|
const sequence = new EventSequence(EventSequenceId.eventLog, 'store', 'namespace', connection,
|
|
65
69
|
{} as IUnitOfWorkManager, discovered.resolveMessageFor.bind(discovered));
|
|
66
70
|
const result = await sequence.append('source', new Registered());
|
|
71
|
+
result.constraintViolations[0].constraintId.should.equal('SharedAddress');
|
|
67
72
|
result.constraintViolations[0].message.should.equal('Address a$b already registered');
|
|
68
73
|
});
|
|
69
74
|
|
|
75
|
+
it('should map a kernel wire violation name and resolve its configured message on a connected append', async () => {
|
|
76
|
+
class OracleKeyClaimed { @field(String) @unique('OracleKey', 'Already claimed: {PropertyName}={PropertyValue}') key = 'Alpha'; }
|
|
77
|
+
eventType('OracleKeyClaimed')(OracleKeyClaimed);
|
|
78
|
+
const discovered = constraints([OracleKeyClaimed]);
|
|
79
|
+
await discovered.discover();
|
|
80
|
+
const fixture = JSON.parse(readFileSync(new URL('../../../testing/fixtures/constraints.json', import.meta.url), 'utf8')) as {
|
|
81
|
+
expected: { outcomes: Array<{ wireViolations: Array<{ SequenceNumber: string; ConstraintName: string;
|
|
82
|
+
EventTypeId: string; ConstraintType: number; Message: string; Details: Record<string, string> }>;
|
|
83
|
+
violations: Array<{ message: string }> }> }
|
|
84
|
+
};
|
|
85
|
+
const wire = fixture.expected.outcomes[2].wireViolations[0];
|
|
86
|
+
const connection = {
|
|
87
|
+
eventSequences: { append: vi.fn().mockResolvedValue({ Response: {
|
|
88
|
+
SequenceNumber: 18446744073709551615n, Errors: [],
|
|
89
|
+
ConstraintViolations: [{ ...wire, SequenceNumber: BigInt(wire.SequenceNumber) }]
|
|
90
|
+
} }) }
|
|
91
|
+
} as unknown as ChronicleConnection;
|
|
92
|
+
const sequence = new EventSequence(EventSequenceId.eventLog, 'store', 'namespace', connection,
|
|
93
|
+
{} as IUnitOfWorkManager, discovered.resolveMessageFor.bind(discovered));
|
|
94
|
+
const result = await sequence.append('B', new OracleKeyClaimed());
|
|
95
|
+
result.isSuccess.should.be.false;
|
|
96
|
+
result.constraintViolations[0].constraintId.should.equal(wire.ConstraintName);
|
|
97
|
+
result.constraintViolations[0].message.should.equal(fixture.expected.outcomes[2].violations[0].message);
|
|
98
|
+
result.constraintViolations[0].message.should.not.equal(wire.Message);
|
|
99
|
+
});
|
|
100
|
+
|
|
70
101
|
it('should resolve a named fluent event type with a different class id', async () => {
|
|
71
102
|
const discovered = constraints([], [FluentRegistration]);
|
|
72
103
|
await discovered.discover();
|
|
@@ -80,13 +111,15 @@ describe('when resolving violation messages', () => {
|
|
|
80
111
|
const connection = {
|
|
81
112
|
eventSequences: { append: vi.fn().mockResolvedValue({ Response: {
|
|
82
113
|
SequenceNumber: 0n, Errors: [], ConstraintViolations: [{
|
|
83
|
-
|
|
84
|
-
|
|
114
|
+
EventTypeId: 'message-once', SequenceNumber: 0n, ConstraintType: ConstraintType.UniqueEventType,
|
|
115
|
+
ConstraintName: 'OneRegistration', Message: 'Kernel default', Details: {}
|
|
116
|
+
} satisfies ContractsConstraintViolation]
|
|
85
117
|
} }) }
|
|
86
118
|
} as unknown as ChronicleConnection;
|
|
87
119
|
const sequence = new EventSequence(EventSequenceId.eventLog, 'store', 'namespace', connection,
|
|
88
120
|
{} as IUnitOfWorkManager, discovered.resolveMessageFor.bind(discovered));
|
|
89
121
|
const result = await sequence.append('source', new OnlyOnce());
|
|
122
|
+
result.constraintViolations[0].constraintId.should.equal('OneRegistration');
|
|
90
123
|
result.constraintViolations[0].message.should.equal('Already registered');
|
|
91
124
|
});
|
|
92
125
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
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 'reflect-metadata';
|
|
5
|
+
import { chai, describe, it } from 'vitest';
|
|
6
|
+
import { field } from '@cratis/fundamentals';
|
|
7
|
+
import { eventType } from '../events/eventTypeDecorator.js';
|
|
8
|
+
import { unique } from '../events/constraints/unique.js';
|
|
9
|
+
import { TypeDiscoverer } from '../types/TypeDiscoverer.js';
|
|
10
|
+
import { EventScenario, UnsupportedEventSequenceOperation } from './index.js';
|
|
11
|
+
|
|
12
|
+
chai.should();
|
|
13
|
+
|
|
14
|
+
describe('selected constraint discovery', () => {
|
|
15
|
+
it('enforces selected decorators when global discovery has been cleared', async () => {
|
|
16
|
+
class SelectedClaim {
|
|
17
|
+
@field(String) @unique('ClearedRegistryKey') key: string;
|
|
18
|
+
constructor(key: string) { this.key = key; }
|
|
19
|
+
}
|
|
20
|
+
eventType('ClearedRegistryClaim')(SelectedClaim);
|
|
21
|
+
TypeDiscoverer.default.clear();
|
|
22
|
+
|
|
23
|
+
const subject = new EventScenario({ artifacts: { eventTypes: [SelectedClaim] } });
|
|
24
|
+
(await subject.append('A', new SelectedClaim('Alpha'))).isSuccess.should.be.true;
|
|
25
|
+
const result = await subject.append('B', new SelectedClaim('Alpha'));
|
|
26
|
+
result.isSuccess.should.be.false;
|
|
27
|
+
result.constraintViolations[0].constraintId.should.equal('ClearedRegistryKey');
|
|
28
|
+
subject.appendedEvents.length.should.equal(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('appends a constraint-free selected class shadowed by a later registration with the same ID', async () => {
|
|
32
|
+
class SelectedItemAdded {
|
|
33
|
+
@field(String) label: string;
|
|
34
|
+
constructor(label: string) { this.label = label; }
|
|
35
|
+
}
|
|
36
|
+
eventType('ShadowedPlainItemAdded')(SelectedItemAdded);
|
|
37
|
+
class ShadowItemAdded {
|
|
38
|
+
@field(String) name: string;
|
|
39
|
+
constructor(name: string) { this.name = name; }
|
|
40
|
+
}
|
|
41
|
+
eventType('ShadowedPlainItemAdded')(ShadowItemAdded);
|
|
42
|
+
|
|
43
|
+
const subject = new EventScenario({ artifacts: { eventTypes: [SelectedItemAdded] } });
|
|
44
|
+
(await subject.append('A', new SelectedItemAdded('Alpha'))).isSuccess.should.be.true;
|
|
45
|
+
subject.appendedEvents.length.should.equal(1);
|
|
46
|
+
subject.appendedEvents[0].content.should.deep.equal({ label: 'Alpha' });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('rejects a globally registered constructor shadowing the selected event type ID', () => {
|
|
50
|
+
class SelectedClaim {
|
|
51
|
+
@field(String) @unique('ShadowedRegistryKey') key = 'Alpha';
|
|
52
|
+
}
|
|
53
|
+
eventType('ShadowedRegistryClaim')(SelectedClaim);
|
|
54
|
+
class ShadowClaim {
|
|
55
|
+
@field(String) key = 'Alpha';
|
|
56
|
+
}
|
|
57
|
+
eventType('ShadowedRegistryClaim')(ShadowClaim);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
new EventScenario({ artifacts: { eventTypes: [SelectedClaim] } });
|
|
61
|
+
throw new Error('Expected conflicting constructors to be rejected');
|
|
62
|
+
} catch (error) {
|
|
63
|
+
(error instanceof UnsupportedEventSequenceOperation).should.be.true;
|
|
64
|
+
(error as Error).message.should.include('artifacts.eventTypes');
|
|
65
|
+
(error as Error).message.should.include('ShadowedRegistryClaim');
|
|
66
|
+
(error as Error).message.should.include('Conflicting constructors share an event type ID');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|