@cratis/chronicle 6.7.1 → 6.8.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 (49) hide show
  1. package/EventStore.ts +10 -4
  2. package/README.md +14 -5
  3. package/dist/EventStore.d.ts +1 -0
  4. package/dist/EventStore.d.ts.map +1 -1
  5. package/dist/EventStore.js +7 -4
  6. package/dist/EventStore.js.map +1 -1
  7. package/dist/eventSequences/EventLog.d.ts +2 -1
  8. package/dist/eventSequences/EventLog.d.ts.map +1 -1
  9. package/dist/eventSequences/EventLog.js +2 -2
  10. package/dist/eventSequences/EventLog.js.map +1 -1
  11. package/dist/eventSequences/EventSequence.d.ts +3 -1
  12. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  13. package/dist/eventSequences/EventSequence.js +11 -6
  14. package/dist/eventSequences/EventSequence.js.map +1 -1
  15. package/dist/events/constraints/ConstraintBuilder.d.ts +2 -0
  16. package/dist/events/constraints/ConstraintBuilder.d.ts.map +1 -1
  17. package/dist/events/constraints/ConstraintBuilder.js.map +1 -1
  18. package/dist/events/constraints/Constraints.d.ts +8 -3
  19. package/dist/events/constraints/Constraints.d.ts.map +1 -1
  20. package/dist/events/constraints/Constraints.js +147 -6
  21. package/dist/events/constraints/Constraints.js.map +1 -1
  22. package/dist/events/constraints/index.d.ts +2 -0
  23. package/dist/events/constraints/index.d.ts.map +1 -1
  24. package/dist/events/constraints/index.js +2 -0
  25. package/dist/events/constraints/index.js.map +1 -1
  26. package/dist/events/constraints/removeConstraint.d.ts +11 -0
  27. package/dist/events/constraints/removeConstraint.d.ts.map +1 -0
  28. package/dist/events/constraints/removeConstraint.js +24 -0
  29. package/dist/events/constraints/removeConstraint.js.map +1 -0
  30. package/dist/events/constraints/unique.d.ts +21 -0
  31. package/dist/events/constraints/unique.d.ts.map +1 -0
  32. package/dist/events/constraints/unique.js +37 -0
  33. package/dist/events/constraints/unique.js.map +1 -0
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/eventSequences/EventLog.ts +4 -2
  36. package/eventSequences/EventSequence.ts +10 -6
  37. package/events/classDecorators.standard.spec.ts +45 -1
  38. package/events/constraints/ConstraintBuilder.ts +2 -0
  39. package/events/constraints/Constraints.ts +140 -7
  40. package/events/constraints/for_Constraints/when_combining_decorators_with_fluent_constraints.spec.ts +142 -0
  41. package/events/constraints/for_Constraints/when_inheriting_removal_decorators.legacy.spec.ts +36 -0
  42. package/events/constraints/for_Constraints/when_registering_decorated_constraints.spec.ts +63 -0
  43. package/events/constraints/for_Constraints/when_registering_decorated_event_types.spec.ts +90 -0
  44. package/events/constraints/for_Constraints/when_rejecting_incompatible_unique_merges.spec.ts +108 -0
  45. package/events/constraints/for_Constraints/when_resolving_violation_messages.spec.ts +92 -0
  46. package/events/constraints/index.ts +2 -0
  47. package/events/constraints/removeConstraint.ts +28 -0
  48. package/events/constraints/unique.ts +45 -0
  49. package/package.json +1 -1
@@ -1,8 +1,14 @@
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 { describe, expect, it } from 'vitest';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { DefaultClientArtifactsProvider } from '../artifacts/DefaultClientArtifactsProvider.js';
6
+ import type { IClientArtifactsProvider } from '../artifacts/IClientArtifactsProvider.js';
7
+ import type { ChronicleConnection } from '../connection/ChronicleConnection.js';
8
+ import { Constraints } from './constraints/Constraints.js';
5
9
  import { constraint, getConstraintMetadata } from './constraints/constraint.js';
10
+ import { unique, getUniqueEventMetadata, getUniquePropertyMetadata } from './constraints/unique.js';
11
+ import { removeConstraint, getRemovedConstraintNames } from './constraints/removeConstraint.js';
6
12
  import { eventType, getEventTypeMetadata } from './eventTypeDecorator.js';
7
13
  import { filterEventsByTag, getFilterTagsFor } from './filterEventsByTagDecorator.js';
8
14
  import { eventTypeMigration, getEventTypeMigrationMetadata } from './migrations/eventTypeMigration.js';
@@ -29,6 +35,20 @@ class Migration {}
29
35
  @constraint('standard-constraint')
30
36
  class Constraint {}
31
37
 
38
+ @eventType('standard-unique-property')
39
+ class UniquePropertyEvent {
40
+ @unique('standard-unique-name', 'Already taken') name = '';
41
+ }
42
+
43
+ @eventType('standard-unique-class')
44
+ @unique('standard-unique-type', 'Already registered')
45
+ class UniqueEvent {}
46
+
47
+ @eventType('standard-unique-release')
48
+ @removeConstraint('standard-unique-name')
49
+ @removeConstraint('standard-unique-type')
50
+ class UniqueRelease {}
51
+
32
52
  @tag('first')
33
53
  @tags('second')
34
54
  @filterEventsByTag('required')
@@ -61,6 +81,9 @@ describe('standard class decorators', () => {
61
81
  expect(getEventTypeMetadata(Previous)?.schema.properties).toEqual({});
62
82
  expect(getEventTypeMigrationMetadata(Migration)?.eventType.generation.value).toBe(2);
63
83
  expect(getConstraintMetadata(Constraint)?.id.value).toBe('standard-constraint');
84
+ expect(getUniquePropertyMetadata(UniquePropertyEvent, 'name')).toEqual({ name: 'standard-unique-name', message: 'Already taken' });
85
+ expect(getUniqueEventMetadata(UniqueEvent)).toEqual({ name: 'standard-unique-type', message: 'Already registered' });
86
+ expect(getRemovedConstraintNames(UniqueRelease)).toEqual(['standard-unique-type', 'standard-unique-name']);
64
87
  expect(getTagsFor(Tagged).map(item => item.value)).toEqual(['second', 'first']);
65
88
  expect(getFilterTagsFor(Tagged).map(item => item.value)).toEqual(['required']);
66
89
  expect(getFromEventMetadata(ModelBound)).toHaveLength(1);
@@ -74,4 +97,25 @@ describe('standard class decorators', () => {
74
97
  expect(getSeederMetadata(Seeder)?.isSeeder).toBe(true);
75
98
  expect(getWebhookMetadata(Webhook)?.targetUrl.value).toBe('https://example.com/hooks');
76
99
  });
100
+
101
+ it('registers standard-decorated constraints through normal artifact discovery', async () => {
102
+ const register = vi.fn().mockResolvedValue({});
103
+ const connection = { constraints: { register } } as unknown as ChronicleConnection;
104
+ const artifacts = {
105
+ eventTypes: DefaultClientArtifactsProvider.default.eventTypes.filter(type =>
106
+ [UniquePropertyEvent, UniqueEvent, UniqueRelease].includes(type)),
107
+ constraints: []
108
+ } as unknown as IClientArtifactsProvider;
109
+ const constraints = new Constraints('store', connection, artifacts);
110
+ await constraints.register();
111
+ const definitions = register.mock.calls[0][0].Constraints;
112
+ const property = definitions.find((definition: { Name: string }) => definition.Name === 'standard-unique-name');
113
+ const eventType = definitions.find((definition: { Name: string }) => definition.Name === 'standard-unique-type');
114
+ expect(property.Definition.Value0.EventDefinitions).toEqual([
115
+ { EventTypeId: 'standard-unique-property', Properties: ['name'] }
116
+ ]);
117
+ expect(property.RemovedWith).toEqual(['standard-unique-release']);
118
+ expect(eventType.Definition.Value1.EventTypeIds).toEqual(['standard-unique-class']);
119
+ expect(eventType.RemovedWith).toEqual(['standard-unique-release']);
120
+ });
77
121
  });
@@ -16,6 +16,8 @@ export interface ConstraintScopeCapture {
16
16
  /** Represents the captured definition of a unique event type constraint. */
17
17
  export interface UniqueEventTypeCapture {
18
18
  eventTypeId: string;
19
+ eventTypeIds?: string[];
20
+ removedWithEventTypeIds?: string[];
19
21
  message?: string;
20
22
  name?: string;
21
23
  }
@@ -2,17 +2,40 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import { ConstraintType } from '@cratis/chronicle.contracts';
5
+ import type { ConstraintViolation } from '../../eventSequences/ConstraintViolation.js';
5
6
  import { IClientArtifactsProvider } from '../../artifacts/index.js';
6
7
  import { ChronicleConnection } from '../../connection/index.js';
7
8
  import { ConstraintId } from './ConstraintId.js';
8
9
  import { IConstraint } from './IConstraint.js';
9
10
  import { IConstraints } from './IConstraints.js';
10
- import { ConstraintBuilder, ConstraintCapture } from './ConstraintBuilder.js';
11
+ import { ConstraintBuilder, ConstraintCapture, ConstraintScopeCapture } from './ConstraintBuilder.js';
12
+ import { UniqueConstraintBuilder } from './UniqueConstraintBuilder.js';
11
13
  import { getConstraintMetadata } from './constraint.js';
14
+ import { getUniqueEventMetadata, getUniquePropertyMetadata } from './unique.js';
15
+ import { getRemovedConstraintNames } from './removeConstraint.js';
16
+ import { TypeIntrospector } from '../../types/TypeIntrospector.js';
17
+ import { getEventTypeFor } from '../eventTypeDecorator.js';
12
18
 
13
- /**
14
- * Manages discovery and registration of constraints with the Chronicle Kernel.
15
- */
19
+ /** Resolves the name registered with the Chronicle Kernel. */
20
+ function wireNameOf(capture: ConstraintCapture): string {
21
+ return capture.uniqueEventType?.name ?? capture.name;
22
+ }
23
+
24
+ const decoratorScope: ConstraintScopeCapture = {
25
+ perEventSourceType: false,
26
+ perEventStreamType: false,
27
+ perEventStreamId: false
28
+ };
29
+
30
+ function assertMatchingScope(name: string, left: ConstraintScopeCapture, right: ConstraintScopeCapture): void {
31
+ if (left.perEventSourceType !== right.perEventSourceType ||
32
+ left.perEventStreamType !== right.perEventStreamType ||
33
+ left.perEventStreamId !== right.perEventStreamId) {
34
+ throw new Error(`Conflicting scopes for constraint '${name}'.`);
35
+ }
36
+ }
37
+
38
+ /** Manages discovery and registration of constraints with the Chronicle Kernel. */
16
39
  export class Constraints implements IConstraints {
17
40
  private readonly _captures = new Map<string, ConstraintCapture>();
18
41
 
@@ -31,14 +54,107 @@ export class Constraints implements IConstraints {
31
54
  /** @inheritdoc */
32
55
  async discover(): Promise<void> {
33
56
  this._captures.clear();
57
+ const fluentIds = new Set<string>();
34
58
  for (const type of this._clientArtifacts.constraints) {
35
59
  const metadata = getConstraintMetadata(type);
36
60
  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);
37
63
 
38
64
  const builder = new ConstraintBuilder(metadata.id.value);
39
65
  const instance = new (type as new () => IConstraint)();
40
66
  instance.define(builder);
41
- this._captures.set(metadata.id.value, builder.capture);
67
+ const capture = builder.capture;
68
+ const name = wireNameOf(capture);
69
+ const existing = this._captures.get(name);
70
+ if (existing?.uniqueEventType && capture.uniqueEventType) {
71
+ assertMatchingScope(name, existing.scope, capture.scope);
72
+ const merged = existing.uniqueEventType;
73
+ if (capture.uniqueEventType.message && !merged.message) merged.message = capture.uniqueEventType.message;
74
+ const ids = merged.eventTypeIds ??= [merged.eventTypeId];
75
+ for (const id of capture.uniqueEventType.eventTypeIds ?? [capture.uniqueEventType.eventTypeId]) {
76
+ if (!ids.includes(id)) ids.push(id);
77
+ }
78
+ const removedWith = merged.removedWithEventTypeIds ??= [];
79
+ for (const id of capture.uniqueEventType.removedWithEventTypeIds ?? []) {
80
+ if (!removedWith.includes(id)) removedWith.push(id);
81
+ }
82
+ } else if (existing) {
83
+ throw new Error(`Duplicate constraint name '${name}'.`);
84
+ } else {
85
+ this._captures.set(name, capture);
86
+ }
87
+ }
88
+
89
+ const removalEvents = new Map<string, Function[]>();
90
+ for (const eventType of this._clientArtifacts.eventTypes) {
91
+ for (const name of getRemovedConstraintNames(eventType)) {
92
+ const events = removalEvents.get(name) ?? [];
93
+ events.push(eventType);
94
+ removalEvents.set(name, events);
95
+ }
96
+ }
97
+
98
+ for (const eventType of this._clientArtifacts.eventTypes) {
99
+ const eventMetadata = getUniqueEventMetadata(eventType);
100
+ if (eventMetadata) {
101
+ const name = eventMetadata.name ?? eventType.name;
102
+ let capture = this._captures.get(name);
103
+ if (!capture) {
104
+ const builder = new ConstraintBuilder(name);
105
+ builder.uniqueFor(eventType, eventMetadata.message, name);
106
+ capture = builder.capture;
107
+ this._captures.set(name, capture);
108
+ } else if (!capture.uniqueEventType) {
109
+ throw new Error(`Constraint '${name}' is not a unique event type constraint.`);
110
+ } else {
111
+ assertMatchingScope(name, capture.scope, decoratorScope);
112
+ if (eventMetadata.message && !capture.uniqueEventType.message) capture.uniqueEventType.message = eventMetadata.message;
113
+ capture.uniqueEventType.eventTypeIds ??= [capture.uniqueEventType.eventTypeId];
114
+ const id = getEventTypeFor(eventType).id.value;
115
+ if (!capture.uniqueEventType.eventTypeIds.includes(id)) capture.uniqueEventType.eventTypeIds.push(id);
116
+ }
117
+ }
118
+
119
+ for (const property of TypeIntrospector.getTrackedProperties(eventType)) {
120
+ const metadata = getUniquePropertyMetadata(eventType, property);
121
+ if (!metadata) continue;
122
+ const name = metadata.name ?? property;
123
+ let capture = this._captures.get(name);
124
+ if (!capture) {
125
+ const builder = new ConstraintBuilder(name);
126
+ builder.unique(() => {});
127
+ capture = builder.capture;
128
+ this._captures.set(name, capture);
129
+ }
130
+ if (!capture.uniqueConstraint) throw new Error(`Constraint '${name}' is not a unique property constraint.`);
131
+ assertMatchingScope(name, capture.scope, decoratorScope);
132
+ if (capture.uniqueConstraint.ignoreCasing) throw new Error(`Conflicting ignoreCasing for unique property constraint '${name}'.`);
133
+ const unique = new UniqueConstraintBuilder(capture.uniqueConstraint);
134
+ const id = getEventTypeFor(eventType).id.value;
135
+ const existing = capture.uniqueConstraint.eventDefinitions.find(definition => definition.eventTypeId === id);
136
+ if (existing && !existing.properties.includes(property)) {
137
+ throw new Error(`Event type '${id}' already added to unique constraint '${name}' with properties '${existing.properties.join(', ')}'.`);
138
+ }
139
+ if (!existing) unique.on(eventType, event => (event as Record<string, unknown>)[property]);
140
+ if (metadata.message && !capture.uniqueConstraint.message) unique.withMessage(metadata.message);
141
+ }
142
+ }
143
+
144
+ for (const [name, eventTypes] of removalEvents) {
145
+ const capture = this._captures.get(name);
146
+ if (capture) {
147
+ if (capture.uniqueConstraint) {
148
+ const unique = new UniqueConstraintBuilder(capture.uniqueConstraint);
149
+ eventTypes.forEach(eventType => unique.removedWith(eventType));
150
+ } else if (capture.uniqueEventType) {
151
+ const removedWith = capture.uniqueEventType.removedWithEventTypeIds ??= [];
152
+ for (const eventType of eventTypes) {
153
+ const id = getEventTypeFor(eventType).id.value;
154
+ if (!removedWith.includes(id)) removedWith.push(id);
155
+ }
156
+ }
157
+ }
42
158
  }
43
159
  }
44
160
 
@@ -85,11 +201,11 @@ export class Constraints implements IConstraints {
85
201
  return {
86
202
  Name: uet.name ?? capture.name,
87
203
  Type: ConstraintType.UniqueEventType,
88
- RemovedWith: [],
204
+ RemovedWith: uet.removedWithEventTypeIds ?? [],
89
205
  Definition: {
90
206
  Value0: undefined,
91
207
  Value1: {
92
- EventTypeIds: [uet.eventTypeId]
208
+ EventTypeIds: uet.eventTypeIds ?? [uet.eventTypeId]
93
209
  }
94
210
  },
95
211
  Scope: scope
@@ -119,4 +235,21 @@ export class Constraints implements IConstraints {
119
235
  hasFor(id: ConstraintId): boolean {
120
236
  return this._captures.has(id.value);
121
237
  }
238
+
239
+ /**
240
+ * Resolves a configured violation message, preserving the Kernel message when none was supplied.
241
+ * @param violation - Violation returned by the Kernel.
242
+ * @returns The violation with its configured message and substituted details, if available.
243
+ */
244
+ 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 };
254
+ }
122
255
  }
@@ -0,0 +1,142 @@
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 { chai, describe, it, vi } from 'vitest';
5
+ import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
6
+ import type { ChronicleConnection } from '../../../connection/index.js';
7
+ import { eventType } from '../../eventTypeDecorator.js';
8
+ import { constraint } from '../constraint.js';
9
+ import { Constraints } from '../Constraints.js';
10
+ import type { IConstraint } from '../IConstraint.js';
11
+ import type { IConstraintBuilder } from '../IConstraintBuilder.js';
12
+ import { unique } from '../unique.js';
13
+ import { removeConstraint } from '../removeConstraint.js';
14
+
15
+ const should = chai.should();
16
+
17
+ class Once {}
18
+ class Again {}
19
+ class Released {}
20
+ class ValueClaimed { address = ''; second = ''; }
21
+ for (const [type, id] of [[Once, 'once'], [Again, 'again'], [Released, 'released'], [ValueClaimed, 'value-claimed']] as const) eventType(id)(type);
22
+ unique('OneRegistration')(Once);
23
+ unique('OneRegistration')(Again);
24
+ unique('Shared')(ValueClaimed.prototype, 'address');
25
+ removeConstraint('OneRegistration')(Released);
26
+
27
+ class FluentOnce implements IConstraint {
28
+ define(builder: IConstraintBuilder): void {
29
+ builder.uniqueFor(Once, 'Registered {email}', 'OneRegistration');
30
+ }
31
+ }
32
+ constraint('FluentOnce')(FluentOnce);
33
+
34
+ class FluentWithoutMessage implements IConstraint {
35
+ define(builder: IConstraintBuilder): void {
36
+ builder.uniqueFor(Once, undefined, 'OneRegistration');
37
+ }
38
+ }
39
+ constraint('FluentWithoutMessage')(FluentWithoutMessage);
40
+
41
+ class FluentValue implements IConstraint {
42
+ define(builder: IConstraintBuilder): void {
43
+ builder.unique(uniqueBuilder => uniqueBuilder.on(ValueClaimed, event => event.address));
44
+ }
45
+ }
46
+ constraint('Shared')(FluentValue);
47
+
48
+ async function discover(types: Function[], constraintTypes: (new () => IConstraint)[]): Promise<{ definitions: { Name: string; Definition: { Value0?: { EventDefinitions: { Properties: string[] }[] }; Value1?: { EventTypeIds: string[] } }; RemovedWith: string[] }[]; constraints: Constraints }> {
49
+ const register = vi.fn().mockResolvedValue({});
50
+ const artifacts = { eventTypes: types, constraints: constraintTypes } as IClientArtifactsProvider;
51
+ const constraints = new Constraints('store', { constraints: { register } } as unknown as ChronicleConnection, artifacts);
52
+ await constraints.register();
53
+ return { definitions: register.mock.calls[0][0].Constraints, constraints };
54
+ }
55
+
56
+ describe('when combining fluent and decorated unique event type constraints', () => {
57
+ it('should register one definition per wire name and merge event types and removals', async () => {
58
+ const { definitions, constraints } = await discover([Once, Again, Released], [FluentOnce]);
59
+ definitions.length.should.equal(1);
60
+ definitions[0].Name.should.equal('OneRegistration');
61
+ definitions[0].Definition.Value1!.EventTypeIds.should.deep.equal(['once', 'again']);
62
+ definitions[0].RemovedWith.should.deep.equal(['released']);
63
+ constraints.resolveMessageFor({ constraintId: 'OneRegistration', message: 'Kernel', details: { email: 'a$b' } }).message.should.equal('Registered a$b');
64
+ });
65
+ });
66
+
67
+ describe('when merging fluent unique event types with the same name', () => {
68
+ it('should use the first supplied message', async () => {
69
+ class Additional {}
70
+ eventType('additional')(Additional);
71
+ class FluentWithMessage implements IConstraint {
72
+ define(builder: IConstraintBuilder): void {
73
+ builder.uniqueFor(Additional, 'Fluent message', 'OneRegistration');
74
+ }
75
+ }
76
+ constraint('FluentWithMessage')(FluentWithMessage);
77
+ class Another {}
78
+ eventType('another')(Another);
79
+ class FluentWithAnotherMessage implements IConstraint {
80
+ define(builder: IConstraintBuilder): void {
81
+ builder.uniqueFor(Another, 'Another message', 'OneRegistration');
82
+ }
83
+ }
84
+ constraint('FluentWithAnotherMessage')(FluentWithAnotherMessage);
85
+ const { definitions, constraints } = await discover([], [FluentWithoutMessage, FluentWithMessage, FluentWithAnotherMessage]);
86
+ definitions.length.should.equal(1);
87
+ definitions[0].Definition.Value1!.EventTypeIds.should.deep.equal(['once', 'additional', 'another']);
88
+ constraints.resolveMessageFor({ constraintId: 'OneRegistration', message: 'Kernel', details: {} }).message
89
+ .should.equal('Fluent message');
90
+ });
91
+ });
92
+
93
+ describe('when merging a fluent unique event type without a message and a decorated event type with a message', () => {
94
+ it('should use the decorated message', async () => {
95
+ class DecoratedOnce {}
96
+ eventType('decorated-once')(DecoratedOnce);
97
+ unique('OneRegistration', 'Decorated message')(DecoratedOnce);
98
+ const { constraints } = await discover([DecoratedOnce], [FluentWithoutMessage]);
99
+ constraints.resolveMessageFor({ constraintId: 'OneRegistration', message: 'Kernel', details: {} }).message
100
+ .should.equal('Decorated message');
101
+ });
102
+ });
103
+
104
+ describe('when combining fluent and decorated unique properties', () => {
105
+ it('should not register the same property twice', async () => {
106
+ const { definitions } = await discover([ValueClaimed], [FluentValue]);
107
+ definitions.length.should.equal(1);
108
+ definitions[0].Definition.Value0!.EventDefinitions[0].Properties.should.deep.equal(['address']);
109
+ });
110
+ });
111
+
112
+ describe('when a fluent property constraint and decorated event type share a name', () => {
113
+ it('should reject conflicting constraint definitions', async () => {
114
+ class Conflicting implements IConstraint {
115
+ define(builder: IConstraintBuilder): void {
116
+ builder.unique(uniqueBuilder => uniqueBuilder.on(ValueClaimed, event => event.address));
117
+ }
118
+ }
119
+ constraint('OneRegistration')(Conflicting);
120
+ const artifacts = { eventTypes: [Once], constraints: [Conflicting] } as unknown as IClientArtifactsProvider;
121
+ const constraints = new Constraints('store', {} as ChronicleConnection, artifacts);
122
+ await constraints.discover().then(
123
+ () => should.fail('Expected conflicting constraint types'),
124
+ (error: Error) => error.message.should.match(/not a unique event type constraint/)
125
+ );
126
+ });
127
+ });
128
+
129
+ describe('when decorating two properties on the same event with one unique name', () => {
130
+ it('should reject the second property instead of silently making a compound key', async () => {
131
+ class PairedValues { address = ''; second = ''; }
132
+ eventType('paired-values')(PairedValues);
133
+ unique('Pair')(PairedValues.prototype, 'address');
134
+ unique('Pair')(PairedValues.prototype, 'second');
135
+ const artifacts = { eventTypes: [PairedValues], constraints: [] } as unknown as IClientArtifactsProvider;
136
+ const constraints = new Constraints('store', {} as ChronicleConnection, artifacts);
137
+ await constraints.discover().then(
138
+ () => should.fail('Expected a duplicate event type error'),
139
+ (error: Error) => error.message.should.match(/already added to unique constraint 'Pair'/)
140
+ );
141
+ });
142
+ });
@@ -0,0 +1,36 @@
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 { chai, describe, it, vi } from 'vitest';
5
+ import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
6
+ import type { ChronicleConnection } from '../../../connection/index.js';
7
+ import { eventType } from '../../eventTypeDecorator.js';
8
+ import { Constraints } from '../Constraints.js';
9
+ import { removeConstraint } from '../removeConstraint.js';
10
+ import { unique } from '../unique.js';
11
+
12
+ const should = chai.should();
13
+
14
+ @eventType('legacy-reserved')
15
+ class Reserved {
16
+ @unique('LegacyValue', 'Already reserved') value = '';
17
+ }
18
+
19
+ @removeConstraint('LegacyValue')
20
+ class BaseRelease {}
21
+
22
+ @eventType('legacy-released')
23
+ class Released extends BaseRelease {}
24
+
25
+ describe('when registering legacy decorators with an inherited removal', () => {
26
+ it('should register the inherited removal and the decorated property', async () => {
27
+ const register = vi.fn().mockResolvedValue({});
28
+ const artifacts = { eventTypes: [Reserved, Released], constraints: [] } as unknown as IClientArtifactsProvider;
29
+ const constraints = new Constraints('store', { constraints: { register } } as unknown as ChronicleConnection, artifacts);
30
+ await constraints.register();
31
+ const definition = register.mock.calls[0][0].Constraints[0];
32
+ definition.Name.should.equal('LegacyValue');
33
+ definition.Definition.Value0.EventDefinitions.should.deep.equal([{ EventTypeId: 'legacy-reserved', Properties: ['value'] }]);
34
+ definition.RemovedWith.should.deep.equal(['legacy-released']);
35
+ });
36
+ });
@@ -0,0 +1,63 @@
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 { chai, describe, it, vi } from 'vitest';
5
+ import { ConstraintType } from '@cratis/chronicle.contracts';
6
+ import type { ChronicleConnection } from '../../../connection/ChronicleConnection.js';
7
+ import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifactsProvider.js';
8
+ import { eventType } from '../../eventTypeDecorator.js';
9
+ import { constraint } from '../constraint.js';
10
+ import { Constraints } from '../Constraints.js';
11
+ import type { IConstraint } from '../IConstraint.js';
12
+ import type { IConstraintBuilder } from '../IConstraintBuilder.js';
13
+ import { unique, getUniquePropertyMetadata } from '../unique.js';
14
+ import { removeConstraint } from '../removeConstraint.js';
15
+
16
+ const should = chai.should();
17
+
18
+ class Registered { address = ''; }
19
+ class AddressChanged { contact = ''; }
20
+ class Abandoned {}
21
+ class Deleted {}
22
+ class Reset {}
23
+ for (const [type, id] of [
24
+ [Registered, 'decorator-registered'], [AddressChanged, 'decorator-changed'],
25
+ [Abandoned, 'decorator-abandoned'], [Deleted, 'decorator-deleted'], [Reset, 'decorator-reset']
26
+ ] as const) eventType(id)(type);
27
+ unique('SharedAddress', 'Address {email} already registered')(Registered.prototype, 'address');
28
+ unique('SharedAddress')(AddressChanged.prototype, 'contact');
29
+ removeConstraint('SharedAddress')(Abandoned);
30
+ removeConstraint('SharedAddress')(Deleted);
31
+ removeConstraint('SharedAddress')(Reset);
32
+
33
+ class FluentAddress implements IConstraint {
34
+ define(builder: IConstraintBuilder): void {
35
+ builder.unique(uniqueBuilder => {
36
+ uniqueBuilder.on(Registered, event => event.address);
37
+ uniqueBuilder.on(AddressChanged, event => event.contact);
38
+ uniqueBuilder.withMessage('Address {email} already registered');
39
+ uniqueBuilder.removedWith(Abandoned).removedWith(Deleted).removedWith(Reset);
40
+ });
41
+ }
42
+ }
43
+ constraint('SharedAddress')(FluentAddress);
44
+
45
+ async function register(types: Function[], constraintTypes: (new () => IConstraint)[] = []) {
46
+ const send = vi.fn().mockResolvedValue({});
47
+ const connection = { constraints: { register: send } } as unknown as ChronicleConnection;
48
+ const artifacts = { eventTypes: types, constraints: constraintTypes } as IClientArtifactsProvider;
49
+ await new Constraints('store', connection, artifacts).register();
50
+ return send.mock.calls[0][0].Constraints;
51
+ }
52
+
53
+ describe('when registering decorated unique properties', () => {
54
+ it('should group properties by name with all releasing events and match the fluent definition', async () => {
55
+ const decorated = (await register([Registered, AddressChanged, Abandoned, Deleted, Reset]))[0];
56
+ const fluent = (await register([], [FluentAddress]))[0];
57
+ decorated.should.deep.equal(fluent);
58
+ decorated.Type.should.equal(ConstraintType.Unique);
59
+ decorated.RemovedWith.should.deep.equal(['decorator-abandoned', 'decorator-deleted', 'decorator-reset']);
60
+ should.equal(decorated.Definition.Value0.IgnoreCasing, false);
61
+ getUniquePropertyMetadata(Registered, 'address')!.message!.should.equal('Address {email} already registered');
62
+ });
63
+ });
@@ -0,0 +1,90 @@
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 { chai, describe, it, vi } from 'vitest';
5
+ import { ConstraintType } from '@cratis/chronicle.contracts';
6
+ import type { ChronicleConnection } from '../../../connection/ChronicleConnection.js';
7
+ import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifactsProvider.js';
8
+ import { eventType } from '../../eventTypeDecorator.js';
9
+ import { constraint } from '../constraint.js';
10
+ import { Constraints } from '../Constraints.js';
11
+ import type { IConstraint } from '../IConstraint.js';
12
+ import type { IConstraintBuilder } from '../IConstraintBuilder.js';
13
+ import { unique } from '../unique.js';
14
+ import { removeConstraint } from '../removeConstraint.js';
15
+
16
+ const should = chai.should();
17
+ class OnlyOnce {}
18
+ class Alternate {}
19
+ class Reset {}
20
+ class DefaultName { value = ''; }
21
+ class DefaultEventType {}
22
+ for (const [type, id] of [
23
+ [OnlyOnce, 'decorator-once'], [Alternate, 'decorator-alternate'], [Reset, 'decorator-reset'],
24
+ [DefaultName, 'decorator-default'], [DefaultEventType, 'decorator-default-event']
25
+ ] as const) eventType(id)(type);
26
+ unique()(DefaultName.prototype, 'value');
27
+ unique()(DefaultEventType);
28
+ unique('OneRegistration', 'Already registered')(OnlyOnce);
29
+ unique('OneRegistration')(Alternate);
30
+ removeConstraint('OneRegistration')(Reset);
31
+
32
+ class FluentRegistration implements IConstraint {
33
+ define(builder: IConstraintBuilder): void {
34
+ builder.uniqueFor(OnlyOnce, 'Already registered', 'OneRegistration');
35
+ }
36
+ }
37
+ constraint('OneRegistration')(FluentRegistration);
38
+
39
+ async function register(types: Function[], constraintTypes: (new () => IConstraint)[] = []) {
40
+ const send = vi.fn().mockResolvedValue({});
41
+ const connection = { constraints: { register: send } } as unknown as ChronicleConnection;
42
+ const artifacts = { eventTypes: types, constraints: constraintTypes } as IClientArtifactsProvider;
43
+ await new Constraints('store', connection, artifacts).register();
44
+ return send.mock.calls[0][0].Constraints;
45
+ }
46
+
47
+ describe('when registering decorated event types', () => {
48
+ it('should use the property name as the default property constraint name', async () => {
49
+ const definitions = await register([DefaultName]);
50
+ definitions[0].Name.should.equal('value');
51
+ definitions[0].Definition.Value0.EventDefinitions.should.deep.equal([{ EventTypeId: 'decorator-default', Properties: ['value'] }]);
52
+ });
53
+
54
+ it('should use the class name rather than the event type id as the default event constraint name', async () => {
55
+ const definitions = await register([DefaultEventType]);
56
+ definitions[0].Name.should.equal('DefaultEventType');
57
+ definitions[0].Definition.Value1.EventTypeIds.should.deep.equal(['decorator-default-event']);
58
+ });
59
+
60
+ it('should match the fluent unique event type definition', async () => {
61
+ const decorated = (await register([OnlyOnce]))[0];
62
+ const fluent = (await register([], [FluentRegistration]))[0];
63
+ decorated.should.deep.equal(fluent);
64
+ });
65
+
66
+ it('should group class-level unique declarations sharing a name', async () => {
67
+ const definitions = await register([OnlyOnce, Alternate, Reset]);
68
+ definitions.length.should.equal(1);
69
+ definitions[0].Type.should.equal(ConstraintType.UniqueEventType);
70
+ definitions[0].Definition.Value1.EventTypeIds.should.deep.equal(['decorator-once', 'decorator-alternate']);
71
+ definitions[0].RemovedWith.should.deep.equal(['decorator-reset']);
72
+ });
73
+
74
+ it('should retain the first supplied message when the first declaration has none', async () => {
75
+ class First {}
76
+ class Second {}
77
+ class Third {}
78
+ eventType('first-registration')(First);
79
+ eventType('second-registration')(Second);
80
+ eventType('third-registration')(Third);
81
+ unique('SharedRegistration')(First);
82
+ unique('SharedRegistration', 'Second message')(Second);
83
+ unique('SharedRegistration', 'Third message')(Third);
84
+ const artifacts = { eventTypes: [First, Second, Third], constraints: [] } as IClientArtifactsProvider;
85
+ const constraints = new Constraints('store', {} as ChronicleConnection, artifacts);
86
+ await constraints.discover();
87
+ constraints.resolveMessageFor({ constraintId: 'SharedRegistration', message: 'Kernel', details: {} }).message
88
+ .should.equal('Second message');
89
+ });
90
+ });