@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.
Files changed (40) hide show
  1. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  2. package/dist/eventSequences/EventSequence.js +2 -27
  3. package/dist/eventSequences/EventSequence.js.map +1 -1
  4. package/dist/eventSequences/createAppendResult.d.ts +13 -0
  5. package/dist/eventSequences/createAppendResult.d.ts.map +1 -0
  6. package/dist/eventSequences/createAppendResult.js +31 -0
  7. package/dist/eventSequences/createAppendResult.js.map +1 -0
  8. package/dist/events/constraints/Constraints.d.ts +8 -3
  9. package/dist/events/constraints/Constraints.d.ts.map +1 -1
  10. package/dist/events/constraints/Constraints.js +135 -125
  11. package/dist/events/constraints/Constraints.js.map +1 -1
  12. package/dist/testing/EventScenario.d.ts.map +1 -1
  13. package/dist/testing/EventScenario.js +70 -10
  14. package/dist/testing/EventScenario.js.map +1 -1
  15. package/dist/testing/InProcessConstraints.d.ts +15 -0
  16. package/dist/testing/InProcessConstraints.d.ts.map +1 -0
  17. package/dist/testing/InProcessConstraints.js +124 -0
  18. package/dist/testing/InProcessConstraints.js.map +1 -0
  19. package/dist/testing/InProcessEventSequence.d.ts +5 -1
  20. package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
  21. package/dist/testing/InProcessEventSequence.js +34 -15
  22. package/dist/testing/InProcessEventSequence.js.map +1 -1
  23. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  24. package/dist/testing/ReactorScenario.js +3 -2
  25. package/dist/testing/ReactorScenario.js.map +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/eventSequences/EventSequence.spec.ts +37 -2
  28. package/eventSequences/EventSequence.ts +19 -43
  29. package/eventSequences/createAppendResult.ts +44 -0
  30. package/events/constraints/Constraints.ts +57 -47
  31. package/events/constraints/for_Constraints/when_resolving_violation_messages.spec.ts +37 -4
  32. package/package.json +1 -1
  33. package/testing/EventScenario.constraint-discovery.spec.ts +69 -0
  34. package/testing/EventScenario.constraints.spec.ts +414 -0
  35. package/testing/EventScenario.fixtures.spec.ts +5 -3
  36. package/testing/EventScenario.rejections.spec.ts +10 -3
  37. package/testing/EventScenario.ts +72 -11
  38. package/testing/InProcessConstraints.ts +127 -0
  39. package/testing/InProcessEventSequence.ts +32 -14
  40. 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
- /** Manages discovery and registration of constraints with the Chronicle Kernel. */
39
- export class Constraints implements IConstraints {
40
- private readonly _captures = new Map<string, ConstraintCapture>();
41
-
42
- /**
43
- * Creates a new {@link Constraints} instance.
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
- if (fluentIds.has(metadata.id.value)) throw new Error(`Duplicate constraint id '${metadata.id.value}'.`);
62
- fluentIds.add(metadata.id.value);
63
-
64
- const builder = new ConstraintBuilder(metadata.id.value);
65
- const instance = new (type as new () => IConstraint)();
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 = this._captures.get(name);
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
- this._captures.set(name, capture);
69
+ captures.set(name, capture);
86
70
  }
87
71
  }
88
72
 
89
73
  const removalEvents = new Map<string, Function[]>();
90
- for (const eventType of this._clientArtifacts.eventTypes) {
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 this._clientArtifacts.eventTypes) {
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 = this._captures.get(name);
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
- this._captures.set(name, capture);
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 = this._captures.get(name);
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
- this._captures.set(name, capture);
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 = this._captures.get(name);
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
- const capture = this._captures.get(violation.constraintId);
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
- ConstraintId: 'SharedAddress', Message: 'Kernel default', Details: { email: 'a$b' }
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
- ConstraintId: 'OneRegistration', Message: 'Kernel default', Details: {}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/chronicle",
3
- "version": "6.22.0",
3
+ "version": "6.23.0",
4
4
  "description": "Event sourcing for TypeScript and Node.js — the idiomatic client for Cratis Chronicle, the open-source event-sourcing database",
5
5
  "author": "Cratis",
6
6
  "license": "MIT",
@@ -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
+ });