@cratis/chronicle 6.50.0 → 6.51.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 (119) hide show
  1. package/dist/eventSequences/ITransactionalEventSequence.d.ts +2 -2
  2. package/dist/events/constraints/ConstraintBuilder.d.ts +7 -0
  3. package/dist/events/constraints/ConstraintBuilder.d.ts.map +1 -1
  4. package/dist/events/constraints/ConstraintBuilder.js +13 -0
  5. package/dist/events/constraints/ConstraintBuilder.js.map +1 -1
  6. package/dist/events/constraints/ConstraintEventSequences.d.ts +19 -0
  7. package/dist/events/constraints/ConstraintEventSequences.d.ts.map +1 -0
  8. package/dist/events/constraints/ConstraintEventSequences.js +39 -0
  9. package/dist/events/constraints/ConstraintEventSequences.js.map +1 -0
  10. package/dist/events/constraints/Constraints.d.ts.map +1 -1
  11. package/dist/events/constraints/Constraints.js +24 -3
  12. package/dist/events/constraints/Constraints.js.map +1 -1
  13. package/dist/events/constraints/IConstraintBuilder.d.ts +18 -0
  14. package/dist/events/constraints/IConstraintBuilder.d.ts.map +1 -1
  15. package/dist/events/constraints/UniqueOptions.d.ts +15 -0
  16. package/dist/events/constraints/UniqueOptions.d.ts.map +1 -0
  17. package/dist/events/constraints/UniqueOptions.js +4 -0
  18. package/dist/events/constraints/UniqueOptions.js.map +1 -0
  19. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts +9 -0
  20. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts.map +1 -0
  21. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js +22 -0
  22. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js.map +1 -0
  23. package/dist/events/constraints/index.d.ts +2 -0
  24. package/dist/events/constraints/index.d.ts.map +1 -1
  25. package/dist/events/constraints/index.js.map +1 -1
  26. package/dist/events/constraints/unique.d.ts +10 -0
  27. package/dist/events/constraints/unique.d.ts.map +1 -1
  28. package/dist/events/constraints/unique.js +11 -10
  29. package/dist/events/constraints/unique.js.map +1 -1
  30. package/dist/testing/EventScenario.d.ts.map +1 -1
  31. package/dist/testing/EventScenario.js +8 -3
  32. package/dist/testing/EventScenario.js.map +1 -1
  33. package/dist/testing/InProcessConstraints.d.ts +4 -1
  34. package/dist/testing/InProcessConstraints.d.ts.map +1 -1
  35. package/dist/testing/InProcessConstraints.js +5 -1
  36. package/dist/testing/InProcessConstraints.js.map +1 -1
  37. package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
  38. package/dist/testing/InProcessEventSequence.js +2 -1
  39. package/dist/testing/InProcessEventSequence.js.map +1 -1
  40. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts +14 -0
  41. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts.map +1 -0
  42. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js +20 -0
  43. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js.map +1 -0
  44. package/dist/transactions/IUnitOfWork.d.ts +2 -1
  45. package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
  46. package/dist/transactions/TransactionalAppendOptions.d.ts +23 -2
  47. package/dist/transactions/TransactionalAppendOptions.d.ts.map +1 -1
  48. package/dist/transactions/UnitOfWork.d.ts +1 -0
  49. package/dist/transactions/UnitOfWork.d.ts.map +1 -1
  50. package/dist/transactions/UnitOfWork.js +28 -13
  51. package/dist/transactions/UnitOfWork.js.map +1 -1
  52. package/dist/transactions/concurrencyScopesAreEqual.d.ts +9 -0
  53. package/dist/transactions/concurrencyScopesAreEqual.d.ts.map +1 -0
  54. package/dist/transactions/concurrencyScopesAreEqual.js +19 -0
  55. package/dist/transactions/concurrencyScopesAreEqual.js.map +1 -0
  56. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts +26 -0
  57. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts.map +1 -0
  58. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js +31 -0
  59. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js.map +1 -0
  60. package/dist/transactions/index.d.ts +1 -0
  61. package/dist/transactions/index.d.ts.map +1 -1
  62. package/dist/transactions/index.js +1 -0
  63. package/dist/transactions/index.js.map +1 -1
  64. package/dist/transactions/toEventForEventSourceId.d.ts +13 -0
  65. package/dist/transactions/toEventForEventSourceId.d.ts.map +1 -0
  66. package/dist/transactions/toEventForEventSourceId.js +29 -0
  67. package/dist/transactions/toEventForEventSourceId.js.map +1 -0
  68. package/dist/tsconfig.tsbuildinfo +1 -1
  69. package/eventSequences/ITransactionalEventSequence.ts +2 -2
  70. package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_options.spec.ts +29 -0
  71. package/events/constraints/ConstraintBuilder.ts +16 -0
  72. package/events/constraints/ConstraintEventSequences.ts +41 -0
  73. package/events/constraints/Constraints.ts +22 -3
  74. package/events/constraints/IConstraintBuilder.ts +20 -0
  75. package/events/constraints/UniqueOptions.ts +20 -0
  76. package/events/constraints/for_Constraints/against_a_kernel/when_appending_to_event_sequences_a_constraint_is_scoped_to.integration.spec.ts +118 -0
  77. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.ts +26 -0
  78. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_fluent_builder.spec.ts +40 -0
  79. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_legacy_decorators.legacy.spec.ts +47 -0
  80. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_merged_declarations.spec.ts +36 -0
  81. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_standard_decorators.standard.spec.ts +46 -0
  82. package/events/constraints/index.ts +2 -0
  83. package/events/constraints/unique.ts +22 -2
  84. package/package.json +2 -2
  85. package/testing/EventScenario.fixtures.spec.ts +1 -1
  86. package/testing/EventScenario.ts +10 -4
  87. package/testing/InProcessConstraints.ts +5 -1
  88. package/testing/InProcessEventSequence.ts +2 -1
  89. package/testing/for_EventScenario/when_appending_event_sequence_constraints/event_sequence_scenario_behaviors.fixture.ts +101 -0
  90. package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.legacy.spec.ts +28 -0
  91. package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.standard.spec.ts +27 -0
  92. package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key_behaviors.fixture.ts +40 -0
  93. package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_legacy_decorators.legacy.spec.ts +55 -0
  94. package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_standard_decorators.standard.spec.ts +54 -0
  95. package/transactions/ConflictingConcurrencyScopesInUnitOfWork.ts +19 -0
  96. package/transactions/IUnitOfWork.ts +2 -1
  97. package/transactions/TransactionalAppendOptions.ts +30 -2
  98. package/transactions/UnitOfWork.ts +29 -13
  99. package/transactions/concurrencyScopesAreEqual.ts +23 -0
  100. package/transactions/for_UnitOfWork/against_a_kernel/when_committing_with_append_metadata.integration.spec.ts +70 -0
  101. package/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.ts +54 -0
  102. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_are_equivalent.spec.ts +32 -0
  103. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_differ.spec.ts +40 -0
  104. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_are_equal_or_omitted.spec.ts +29 -0
  105. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_differ_for_one_event_source.spec.ts +29 -0
  106. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_a_later_event_sequence_fails.spec.ts +30 -0
  107. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_events_span_event_sequences.spec.ts +34 -0
  108. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_options_differ_per_event.spec.ts +45 -0
  109. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_the_append_is_rejected.spec.ts +26 -0
  110. package/transactions/for_UnitOfWork/when_rolling_back_events_with_append_metadata.spec.ts +24 -0
  111. package/transactions/index.ts +1 -0
  112. package/transactions/toEventForEventSourceId.ts +32 -0
  113. package/vitest.integration.config.ts +2 -0
  114. package/webhooks/for_Webhooks/against_a_kernel/given/ReceivedWebhookRequest.fixture.ts +21 -0
  115. package/webhooks/for_Webhooks/against_a_kernel/given/WebhookReceiver.fixture.ts +99 -0
  116. package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_failing_webhook.integration.spec.ts +101 -0
  117. package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_webhook.integration.spec.ts +130 -0
  118. package/webhooks/for_Webhooks/against_a_kernel/when_registering_a_webhook_that_is_not_replayable_and_not_active.integration.spec.ts +101 -0
  119. package/webhooks/for_Webhooks/when_registering_a_webhook_that_is_not_replayable_and_not_active.spec.ts +43 -0
@@ -0,0 +1,20 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import type { ConstraintEventSequence } from './ConstraintEventSequences.js';
5
+
6
+ /** Options for the {@link unique} decorator. Mirrors the .NET UniqueAttribute. */
7
+ export interface UniqueOptions {
8
+ /** Optional shared constraint name; defaults to the class or property name. */
9
+ name?: string;
10
+
11
+ /** Optional fixed violation message. */
12
+ message?: string;
13
+
14
+ /**
15
+ * The event sequences the constraint applies to. Empty or omitted means every event sequence.
16
+ * When several declarations share a constraint name, their event sequences are combined, and a
17
+ * declaration naming none keeps the constraint applying to every event sequence.
18
+ */
19
+ eventSequences?: ConstraintEventSequence[];
20
+ }
@@ -0,0 +1,118 @@
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 { randomUUID } from 'node:crypto';
5
+ import { afterAll, beforeAll, chai, describe, it } from 'vitest';
6
+ import { field } from '@cratis/fundamentals';
7
+ import { ChronicleClient } from '../../../../ChronicleClient.js';
8
+ import { ChronicleOptions } from '../../../../ChronicleOptions.js';
9
+ import type { IClientArtifactsProvider } from '../../../../artifacts/index.js';
10
+ import { EventSequenceId } from '../../../../eventSequences/EventSequenceId.js';
11
+ import type { IEventSequence } from '../../../../eventSequences/IEventSequence.js';
12
+ import type { IEventStore } from '../../../../IEventStore.js';
13
+ import { eventType } from '../../../eventTypeDecorator.js';
14
+ import { constraint } from '../../constraint.js';
15
+ import type { IConstraint } from '../../IConstraint.js';
16
+ import type { IConstraintBuilder } from '../../IConstraintBuilder.js';
17
+ import { unique } from '../../unique.js';
18
+
19
+ chai.should();
20
+
21
+ // Runs against a real Chronicle kernel; skipped locally unless the connection string is set (see vitest.integration.config.ts).
22
+ const connectionString = process.env.CHRONICLE_INTEGRATION_CONNECTION_STRING;
23
+
24
+ @eventType()
25
+ class HandleClaimed {
26
+ @field(String) handle = '';
27
+ }
28
+
29
+ @eventType()
30
+ class NicknameClaimed {
31
+ @field(String) nickname = '';
32
+ }
33
+
34
+ @eventType()
35
+ @unique({ name: 'OneProfilePerUser', eventSequences: [EventSequenceId.eventLog] })
36
+ class ProfileCreated {
37
+ @field(String) label = '';
38
+ }
39
+
40
+ @constraint('UniqueHandleOnEventLog')
41
+ class UniqueHandleOnEventLog implements IConstraint {
42
+ define(builder: IConstraintBuilder): void {
43
+ builder.forEventLog().unique(_ => _.on(HandleClaimed, e => e.handle));
44
+ }
45
+ }
46
+
47
+ @constraint('UniqueNicknameEverywhere')
48
+ class UniqueNicknameEverywhere implements IConstraint {
49
+ define(builder: IConstraintBuilder): void {
50
+ builder.unique(_ => _.on(NicknameClaimed, e => e.nickname));
51
+ }
52
+ }
53
+
54
+ const artifacts: IClientArtifactsProvider = {
55
+ eventTypes: [HandleClaimed, NicknameClaimed, ProfileCreated],
56
+ readModels: [],
57
+ reactors: [],
58
+ reducers: [],
59
+ seeders: [],
60
+ constraints: [UniqueHandleOnEventLog, UniqueNicknameEverywhere],
61
+ projections: [],
62
+ webhooks: [],
63
+ eventTypeMigrations: [],
64
+ globalForHandlers: []
65
+ };
66
+
67
+ const handle = (value: string) => Object.assign(new HandleClaimed(), { handle: value });
68
+ const nickname = (value: string) => Object.assign(new NicknameClaimed(), { nickname: value });
69
+
70
+ describe.skipIf(!connectionString && !process.env.CI)('when appending to event sequences a constraint is scoped to against a kernel', () => {
71
+ const storeName = `Sequenced${randomUUID().replaceAll('-', '').slice(0, 12)}`;
72
+ let client: ChronicleClient;
73
+ let store: IEventStore;
74
+ let outbox: IEventSequence;
75
+
76
+ beforeAll(async () => {
77
+ client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, {
78
+ discoveryPatterns: [],
79
+ clientArtifactsProvider: artifacts
80
+ }));
81
+ store = await client.getEventStore(storeName);
82
+ outbox = store.getEventSequence(new EventSequenceId('outbox'));
83
+ });
84
+
85
+ afterAll(() => client?.dispose());
86
+
87
+ it('validates an event-log constraint on the event log', async () => {
88
+ (await store.eventLog.append(randomUUID(), handle('ada'))).isSuccess.should.be.true;
89
+ const rejected = await store.eventLog.append(randomUUID(), handle('ada'));
90
+ rejected.isSuccess.should.be.false;
91
+ rejected.constraintViolations[0].constraintId.should.equal('UniqueHandleOnEventLog');
92
+ });
93
+
94
+ it('neither validates nor claims an event-log constraint in the outbox', async () => {
95
+ (await store.eventLog.append(randomUUID(), handle('grace'))).isSuccess.should.be.true;
96
+ (await outbox.append(randomUUID(), handle('grace'))).isSuccess.should.be.true;
97
+ (await outbox.append(randomUUID(), handle('grace'))).isSuccess.should.be.true;
98
+ });
99
+
100
+ it('keeps a forwarded value from claiming the event-log value', async () => {
101
+ (await outbox.append(randomUUID(), handle('linus'))).isSuccess.should.be.true;
102
+ (await store.eventLog.append(randomUUID(), handle('linus'))).isSuccess.should.be.true;
103
+ });
104
+
105
+ it('validates a decorated event-log constraint only on the event log', async () => {
106
+ const source = randomUUID();
107
+ (await store.eventLog.append(source, new ProfileCreated())).isSuccess.should.be.true;
108
+ (await store.eventLog.append(source, new ProfileCreated())).isSuccess.should.be.false;
109
+ (await outbox.append(source, new ProfileCreated())).isSuccess.should.be.true;
110
+ (await outbox.append(source, new ProfileCreated())).isSuccess.should.be.true;
111
+ });
112
+
113
+ it('validates an unscoped constraint in the outbox against its own index', async () => {
114
+ (await store.eventLog.append(randomUUID(), nickname('margaret'))).isSuccess.should.be.true;
115
+ (await outbox.append(randomUUID(), nickname('margaret'))).isSuccess.should.be.true;
116
+ (await outbox.append(randomUUID(), nickname('margaret'))).isSuccess.should.be.false;
117
+ });
118
+ });
@@ -0,0 +1,26 @@
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 { vi } from 'vitest';
5
+ import type { Constraint, RegisterConstraintsRequest } from '@cratis/chronicle.contracts';
6
+ import type { IClientArtifactsProvider } from '../../../../../artifacts/index.js';
7
+ import type { ChronicleConnection } from '../../../../../connection/index.js';
8
+ import { Constraints } from '../../../Constraints.js';
9
+
10
+ /**
11
+ * Registers the given constraint classes and decorated event types and returns the registered wire definitions by name.
12
+ * @param constraints - The constraint classes.
13
+ * @param eventTypes - The event types whose decorators contribute constraints.
14
+ * @returns The registered constraints keyed by name.
15
+ */
16
+ export async function registeredConstraints(constraints: Function[], eventTypes: Function[] = []): Promise<Map<string, Constraint>> {
17
+ const register = vi.fn().mockResolvedValue({});
18
+ const connection = { constraints: { register } } as unknown as ChronicleConnection;
19
+ const artifacts = {
20
+ eventTypes, readModels: [], reactors: [], reducers: [], seeders: [], constraints,
21
+ projections: [], webhooks: [], eventTypeMigrations: [], globalForHandlers: []
22
+ } as unknown as IClientArtifactsProvider;
23
+ await new Constraints('store', connection, artifacts).register();
24
+ const request = register.mock.calls[0][0] as RegisterConstraintsRequest;
25
+ return new Map(request.Constraints.map(registered => [registered.Name, registered]));
26
+ }
@@ -0,0 +1,40 @@
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 { beforeAll, chai, describe, it } from 'vitest';
5
+ import type { Constraint } from '@cratis/chronicle.contracts';
6
+ import { EventSequenceId } from '../../../../eventSequences/EventSequenceId.js';
7
+ import { eventType } from '../../../eventTypeDecorator.js';
8
+ import { constraint } from '../../constraint.js';
9
+ import type { IConstraintBuilder } from '../../IConstraintBuilder.js';
10
+ import { registeredConstraints } from './given/registered_constraints.js';
11
+
12
+ chai.should();
13
+
14
+ class NameRegistered { name = ''; }
15
+ class OnlyOnce {}
16
+ eventType('name-registered')(NameRegistered);
17
+ eventType('only-once')(OnlyOnce);
18
+
19
+ class UnscopedName { define(builder: IConstraintBuilder) { builder.unique(unique => unique.on(NameRegistered, event => event.name)); } }
20
+ constraint('unscoped-name')(UnscopedName);
21
+ class EventLogName { define(builder: IConstraintBuilder) { builder.unique(unique => unique.on(NameRegistered, event => event.name)).forEventLog(); } }
22
+ constraint('event-log-name')(EventLogName);
23
+ class SelectedOnce {
24
+ define(builder: IConstraintBuilder) {
25
+ builder.forEventSequences(new EventSequenceId('outbox'), 'inbox', ' ', 'outbox')
26
+ .uniqueFor(OnlyOnce, undefined, 'selected-once')
27
+ .forEventSequences(EventSequenceId.eventLog);
28
+ }
29
+ }
30
+ constraint('selected-once-constraint')(SelectedOnce);
31
+
32
+ describe('when scoping constraints to event sequences with the fluent builder', () => {
33
+ let registered: Map<string, Constraint>;
34
+ beforeAll(async () => { registered = await registeredConstraints([UnscopedName, EventLogName, SelectedOnce]); });
35
+
36
+ it('should register an unscoped constraint for every event sequence', () => registered.get('unscoped-name')!.EventSequences.should.deep.equal([]));
37
+ it('should register a constraint declared after the definition for the event log', () => registered.get('event-log-name')!.EventSequences.should.deep.equal(['event-log']));
38
+ it('should register distinct nonblank event sequences across every call in declaration order', () =>
39
+ registered.get('selected-once')!.EventSequences.should.deep.equal(['outbox', 'inbox', 'event-log']));
40
+ });
@@ -0,0 +1,47 @@
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 { field } from '@cratis/fundamentals';
6
+ import { beforeAll, chai, describe, it } from 'vitest';
7
+ import type { Constraint } from '@cratis/chronicle.contracts';
8
+ import { EventSequenceId } from '../../../../eventSequences/EventSequenceId.js';
9
+ import { eventType } from '../../../eventTypeDecorator.js';
10
+ import { unique } from '../../unique.js';
11
+ import { registeredConstraints } from './given/registered_constraints.js';
12
+
13
+ chai.should();
14
+
15
+ @eventType('legacy-account-opened')
16
+ @unique({ name: 'legacy-one-account', message: 'Already opened', eventSequences: [EventSequenceId.eventLog] })
17
+ class AccountOpened { @field(String) label = ''; }
18
+
19
+ @eventType('legacy-account-noted')
20
+ @unique('legacy-unscoped-account')
21
+ class AccountNoted { @field(String) label = ''; }
22
+
23
+ @eventType('legacy-email-registered')
24
+ class EmailRegistered {
25
+ @field(String) @unique({ name: 'legacy-email', eventSequences: ['event-log'] }) email = '';
26
+ @field(String) @unique({ name: 'legacy-alias' }) alias = '';
27
+ }
28
+
29
+ @eventType('legacy-email-forwarded')
30
+ class EmailForwarded {
31
+ @field(String) @unique({ name: 'legacy-email', eventSequences: ['outbox'] }) email = '';
32
+ @field(String) @unique({ name: 'legacy-alias', eventSequences: ['outbox'] }) alias = '';
33
+ }
34
+
35
+ describe('when scoping legacy unique decorators to event sequences', () => {
36
+ let registered: Map<string, Constraint>;
37
+ beforeAll(async () => { registered = await registeredConstraints([], [AccountOpened, AccountNoted, EmailRegistered, EmailForwarded]); });
38
+
39
+ it('should register a decorated event type for the declared event sequences', () =>
40
+ registered.get('legacy-one-account')!.EventSequences.should.deep.equal(['event-log']));
41
+ it('should register the positional form for every event sequence', () =>
42
+ registered.get('legacy-unscoped-account')!.EventSequences.should.deep.equal([]));
43
+ it('should combine the event sequences of properties sharing a constraint name', () =>
44
+ registered.get('legacy-email')!.EventSequences.should.deep.equal(['event-log', 'outbox']));
45
+ it('should keep a shared constraint applying everywhere when one property names no event sequence', () =>
46
+ registered.get('legacy-alias')!.EventSequences.should.deep.equal([]));
47
+ });
@@ -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 { beforeAll, chai, describe, it } from 'vitest';
5
+ import type { Constraint } from '@cratis/chronicle.contracts';
6
+ import { eventType } from '../../../eventTypeDecorator.js';
7
+ import { constraint } from '../../constraint.js';
8
+ import type { IConstraintBuilder } from '../../IConstraintBuilder.js';
9
+ import { registeredConstraints } from './given/registered_constraints.js';
10
+
11
+ chai.should();
12
+
13
+ class Opened {}
14
+ class Closed {}
15
+ class Archived {}
16
+ eventType('merge-opened')(Opened);
17
+ eventType('merge-closed')(Closed);
18
+ eventType('merge-archived')(Archived);
19
+
20
+ class OpenedOnLog { define(builder: IConstraintBuilder) { builder.forEventLog().uniqueFor(Opened, undefined, 'narrowed'); } }
21
+ constraint('opened-on-log')(OpenedOnLog);
22
+ class ClosedOnOutbox { define(builder: IConstraintBuilder) { builder.forEventSequences('outbox').uniqueFor(Closed, undefined, 'narrowed'); } }
23
+ constraint('closed-on-outbox')(ClosedOnOutbox);
24
+ class OpenedEverywhere { define(builder: IConstraintBuilder) { builder.uniqueFor(Opened, undefined, 'widened'); } }
25
+ constraint('opened-everywhere')(OpenedEverywhere);
26
+ class ArchivedOnLog { define(builder: IConstraintBuilder) { builder.forEventLog().uniqueFor(Archived, undefined, 'widened'); } }
27
+ constraint('archived-on-log')(ArchivedOnLog);
28
+
29
+ describe('when scoping merged unique event type declarations to event sequences', () => {
30
+ let registered: Map<string, Constraint>;
31
+ beforeAll(async () => { registered = await registeredConstraints([OpenedOnLog, ClosedOnOutbox, OpenedEverywhere, ArchivedOnLog]); });
32
+
33
+ it('should combine the event sequences of every declaration', () => registered.get('narrowed')!.EventSequences.should.deep.equal(['event-log', 'outbox']));
34
+ it('should keep a constraint applying everywhere when one declaration names no event sequence', () =>
35
+ registered.get('widened')!.EventSequences.should.deep.equal([]));
36
+ });
@@ -0,0 +1,46 @@
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 { field } from '@cratis/fundamentals';
5
+ import { beforeAll, chai, describe, it } from 'vitest';
6
+ import type { Constraint } from '@cratis/chronicle.contracts';
7
+ import { EventSequenceId } from '../../../../eventSequences/EventSequenceId.js';
8
+ import { eventType } from '../../../eventTypeDecorator.js';
9
+ import { unique } from '../../unique.js';
10
+ import { registeredConstraints } from './given/registered_constraints.js';
11
+
12
+ chai.should();
13
+
14
+ @eventType('standard-account-opened')
15
+ @unique({ name: 'standard-one-account', message: 'Already opened', eventSequences: [EventSequenceId.eventLog] })
16
+ class AccountOpened { @field(String) label = ''; }
17
+
18
+ @eventType('standard-account-noted')
19
+ @unique('standard-unscoped-account')
20
+ class AccountNoted { @field(String) label = ''; }
21
+
22
+ @eventType('standard-email-registered')
23
+ class EmailRegistered {
24
+ @field(String) @unique({ name: 'standard-email', eventSequences: ['event-log'] }) email = '';
25
+ @field(String) @unique({ name: 'standard-alias' }) alias = '';
26
+ }
27
+
28
+ @eventType('standard-email-forwarded')
29
+ class EmailForwarded {
30
+ @field(String) @unique({ name: 'standard-email', eventSequences: ['outbox'] }) email = '';
31
+ @field(String) @unique({ name: 'standard-alias', eventSequences: ['outbox'] }) alias = '';
32
+ }
33
+
34
+ describe('when scoping standard unique decorators to event sequences', () => {
35
+ let registered: Map<string, Constraint>;
36
+ beforeAll(async () => { registered = await registeredConstraints([], [AccountOpened, AccountNoted, EmailRegistered, EmailForwarded]); });
37
+
38
+ it('should register a decorated event type for the declared event sequences', () =>
39
+ registered.get('standard-one-account')!.EventSequences.should.deep.equal(['event-log']));
40
+ it('should register the positional form for every event sequence', () =>
41
+ registered.get('standard-unscoped-account')!.EventSequences.should.deep.equal([]));
42
+ it('should combine the event sequences of properties sharing a constraint name', () =>
43
+ registered.get('standard-email')!.EventSequences.should.deep.equal(['event-log', 'outbox']));
44
+ it('should keep a shared constraint applying everywhere when one property names no event sequence', () =>
45
+ registered.get('standard-alias')!.EventSequences.should.deep.equal([]));
46
+ });
@@ -7,6 +7,8 @@ export type { IConstraintBuilder } from './IConstraintBuilder.js';
7
7
  export type { IUniqueConstraintBuilder } from './IUniqueConstraintBuilder.js';
8
8
  export { constraint, getConstraintMetadata, isConstraint } from './constraint.js';
9
9
  export { unique } from './unique.js';
10
+ export type { UniqueOptions } from './UniqueOptions.js';
11
+ export type { ConstraintEventSequence } from './ConstraintEventSequences.js';
10
12
  export { removeConstraint } from './removeConstraint.js';
11
13
  export type { ConstraintMetadata } from './constraint.js';
12
14
  export type { IConstraints } from './IConstraints.js';
@@ -4,6 +4,8 @@
4
4
  import 'reflect-metadata';
5
5
  import { ChronicleClassOrPropertyDecorator, decorateClassOrProperty, getPropertyMetadata, getTypeOrFieldMetadata } from '../../types/propertyDecoratorMetadata.js';
6
6
  import { TypeIntrospector } from '../../types/TypeIntrospector.js';
7
+ import { normalizeConstraintEventSequences } from './ConstraintEventSequences.js';
8
+ import type { UniqueOptions } from './UniqueOptions.js';
7
9
 
8
10
  const CLASS_KEY = 'chronicle:constraint:unique:class';
9
11
  const PROPERTY_KEY = 'chronicle:constraint:unique:property';
@@ -11,6 +13,8 @@ const PROPERTY_KEY = 'chronicle:constraint:unique:property';
11
13
  interface UniqueMetadata {
12
14
  name?: string;
13
15
  message?: string;
16
+ /** Normalized event sequence identifiers; omitted means every event sequence. */
17
+ eventSequences?: string[];
14
18
  }
15
19
 
16
20
  /**
@@ -21,9 +25,25 @@ interface UniqueMetadata {
21
25
  * @param message - Optional fixed violation message.
22
26
  * @returns A class or public instance field decorator.
23
27
  */
24
- export function unique(name?: string, message?: string): ChronicleClassOrPropertyDecorator {
28
+ export function unique(name?: string, message?: string): ChronicleClassOrPropertyDecorator;
29
+ /**
30
+ * Marks an event class as unique per event source, or an event property as unique across event sources,
31
+ * optionally applying the constraint only to selected event sequences.
32
+ * @param options - The constraint name, message and event sequences.
33
+ * @returns A class or public instance field decorator.
34
+ */
35
+ export function unique(options: UniqueOptions): ChronicleClassOrPropertyDecorator;
36
+ export function unique(nameOrOptions?: string | UniqueOptions, message?: string): ChronicleClassOrPropertyDecorator {
37
+ const options: UniqueOptions = typeof nameOrOptions === 'object' && nameOrOptions !== null
38
+ ? nameOrOptions
39
+ : { name: nameOrOptions, message };
40
+ const eventSequences = normalizeConstraintEventSequences(options.eventSequences ?? []);
41
+ const metadata: UniqueMetadata = {
42
+ name: options.name,
43
+ message: options.message,
44
+ ...(eventSequences.length > 0 ? { eventSequences } : {})
45
+ };
25
46
  return decorateClassOrProperty((target, property) => {
26
- const metadata: UniqueMetadata = { name, message };
27
47
  if (property !== undefined) {
28
48
  const key = property.toString();
29
49
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/chronicle",
3
- "version": "6.50.0",
3
+ "version": "6.51.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",
@@ -173,7 +173,7 @@
173
173
  "clean": "rm -rf dist",
174
174
  "test": "vitest run",
175
175
  "test:integration": "vitest run --config vitest.integration.config.ts",
176
- "test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/declarative/addChildCompatibility.standard.spec.ts projections/declarative/for_CompositeKeyBuilder/when_typing_constant_parts.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts events/for_handles/when_decorating_a_method.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_joins/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_field_types/with_standard_decorators.standard.spec.ts eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_tags/with_standard_decorators.standard.spec.ts telemetry/for_ChronicleTelemetry/when_exposing_names/with_convention_names.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all_events.standard.spec.ts events/for_observerFilters/when_registering/with_standard_decorators.standard.spec.ts reactors/for_Reactors/against_a_kernel/when_filtering_source_and_stream_types.integration.spec.ts testing/for_observerScenarios/when_selecting_filters/with_source_and_stream_types.spec.ts readModels/for_ReadModelWatcher/watchCompatibility.standard.spec.ts for_EventStore/when_implementing_a_legacy_event_store.spec.ts",
176
+ "test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/declarative/addChildCompatibility.standard.spec.ts projections/declarative/for_CompositeKeyBuilder/when_typing_constant_parts.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_event_sequence_constraints/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.standard.spec.ts events/constraints/for_Constraints/when_scoping_to_event_sequences/with_standard_decorators.standard.spec.ts schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts events/for_handles/when_decorating_a_method.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_joins/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_field_types/with_standard_decorators.standard.spec.ts eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_tags/with_standard_decorators.standard.spec.ts telemetry/for_ChronicleTelemetry/when_exposing_names/with_convention_names.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all_events.standard.spec.ts events/for_observerFilters/when_registering/with_standard_decorators.standard.spec.ts reactors/for_Reactors/against_a_kernel/when_filtering_source_and_stream_types.integration.spec.ts testing/for_observerScenarios/when_selecting_filters/with_source_and_stream_types.spec.ts readModels/for_ReadModelWatcher/watchCompatibility.standard.spec.ts for_EventStore/when_implementing_a_legacy_event_store.spec.ts",
177
177
  "test:native": "node ./scripts/check-native-esm.mjs"
178
178
  },
179
179
  "dependencies": {
@@ -59,7 +59,7 @@ const fixtures = readdirSync(directory).filter(name => name.endsWith('.json') &&
59
59
  !['batches.json', 'batch-omitted-routes.json', 'batch-rollback.json', 'builders.json', 'constraints.json', 'constraints-isolation.json', 'constraints-key-domain.json',
60
60
  'constraints-property-lifecycle.json', 'constraints-property-covered-removal.json', 'constraints-event-type-cycles.json',
61
61
  'constraints-event-type-siblings.json', 'constraints-composite.json',
62
- 'constraints-ignore-casing.json', 'constraints-scopes.json', 'constraints-field-types.json'].includes(name)).map(name => ({
62
+ 'constraints-ignore-casing.json', 'constraints-scopes.json', 'constraints-field-types.json', 'constraints-event-sequences.json'].includes(name)).map(name => ({
63
63
  name, fixture: JSON.parse(readFileSync(new URL(name, directory), 'utf8')) as Fixture
64
64
  }));
65
65
  const artifacts = { eventTypes: [OracleEventRecorded, AlternateRecorded, BoundaryRecorded], constraints: [] };
@@ -85,9 +85,15 @@ export class EventScenario {
85
85
  }
86
86
  }
87
87
  }
88
- constraints = new InProcessConstraints(definitions, new Map(eventTypes.map(type =>
89
- [getEventTypeFor(type).id.value, getEventTypeMetadata(type)!.schema])));
90
- for (const [name, capture] of definitions) {
88
+ // The scenario appends to the event log only. A definition scoped to other event sequences is neither
89
+ // validated nor indexed there by the kernel (constraints-event-sequences.json), so it takes no part.
90
+ const eventLog = EventSequenceId.eventLog.value;
91
+ const applicable = new Map([...definitions].filter(([, capture]) =>
92
+ !capture.eventSequences?.length || capture.eventSequences.includes(eventLog)));
93
+ const inapplicable = new Set([...definitions.keys()].filter(name => !applicable.has(name)));
94
+ constraints = new InProcessConstraints(applicable, new Map(eventTypes.map(type =>
95
+ [getEventTypeFor(type).id.value, getEventTypeMetadata(type)!.schema])), inapplicable);
96
+ for (const [name, capture] of applicable) {
91
97
  const ids = capture.uniqueConstraint?.eventDefinitions.map(entry => entry.eventTypeId) ??
92
98
  capture.uniqueEventType?.eventTypeIds ?? [capture.uniqueEventType?.eventTypeId];
93
99
  const removedWith = [...(capture.uniqueConstraint?.removedWithEventTypeIds ?? []),
@@ -121,7 +127,7 @@ export class EventScenario {
121
127
  `Unique keys with schema ${schema?.type ?? 'missing'}/${schema?.format ?? 'unformatted'} are not fixture-backed; date, object and other numeric formats require a kernel-backed test.`);
122
128
  }
123
129
  const extendedKey = schemas.some(schema => schema?.type === 'number' || schema?.format === 'guid');
124
- if (extendedKey && (definitions.size !== 1 || capture.uniqueConstraint!.eventDefinitions.length !== 1 ||
130
+ if (extendedKey && (applicable.size !== 1 || capture.uniqueConstraint!.eventDefinitions.length !== 1 ||
125
131
  removedWith.some(id => id !== undefined))) {
126
132
  throw new UnsupportedEventSequenceOperation('artifacts.constraints', name,
127
133
  'Guid and numeric keys require one isolated definition covering one event type without removers; shared definitions and removal combinations are not fixture-backed.');
@@ -69,7 +69,8 @@ export class InProcessConstraints {
69
69
  private readonly _propertyRemovalTypes = new Set<string>();
70
70
 
71
71
  constructor(private readonly _definitions: ReadonlyMap<string, ConstraintCapture>,
72
- private readonly _schemas: ReadonlyMap<string, JsonSchema> = new Map()) {
72
+ private readonly _schemas: ReadonlyMap<string, JsonSchema> = new Map(),
73
+ private readonly _inapplicable: ReadonlySet<string> = new Set()) {
73
74
  const coveredTypes = new Map<string, string>();
74
75
  const removalOwners = new Map<string, string>();
75
76
  for (const [name, capture] of _definitions) {
@@ -161,6 +162,9 @@ export class InProcessConstraints {
161
162
 
162
163
  hasRemovalType(eventTypeId: string): boolean { return this._removalTypes.has(eventTypeId); }
163
164
 
165
+ /** Whether a named definition is scoped to event sequences other than the scenario's, so it takes no part. */
166
+ isInapplicable(name: string): boolean { return this._inapplicable.has(name); }
167
+
164
168
  isConstrainedProperty(eventTypeId: string, property: string): boolean {
165
169
  return this._constrainedProperties.get(eventTypeId)?.has(property) ?? false;
166
170
  }
@@ -110,7 +110,8 @@ export class InProcessEventSequence implements IEventSequence {
110
110
  if (eventType.generation.value !== 1 || eventType.tombstone) {
111
111
  throw this.unsupported('artifacts.eventTypes', type.name, 'Only generation 1, non-tombstone events are proven.');
112
112
  }
113
- if (options.constraints !== 'disabled' && getRemovedConstraintNames(type).length &&
113
+ if (options.constraints !== 'disabled' &&
114
+ getRemovedConstraintNames(type).some(name => !this._constraints?.isInapplicable(name)) &&
114
115
  !this._constraints?.hasRemovalType(eventType.id.value)) {
115
116
  throw this.unsupported('artifacts.eventTypes.constraints', type.name, 'Constraint removal is not fixture-backed.');
116
117
  }
@@ -0,0 +1,101 @@
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 { readFileSync } from 'node:fs';
5
+ import { chai, describe, it, vi, type Assertion } from 'vitest';
6
+ import type { Constructor } from '@cratis/fundamentals';
7
+ import { getEventTypeMetadata } from '../../../events/eventTypeDecorator.js';
8
+ import { compileConstraints } from '../../../events/constraints/Constraints.js';
9
+ import { EventScenario } from '../../index.js';
10
+ import { InProcessConstraints } from '../../InProcessConstraints.js';
11
+
12
+ chai.should();
13
+ function should(value: unknown): Assertion { return (value as { should: Assertion }).should; }
14
+
15
+ type Entry = { source: string; type: string; value: string };
16
+ type Definition = { kind: string; name: string; events?: Array<{ type: string; properties: string[] }>; eventTypes?: string[];
17
+ ignoreCasing?: boolean; removedWithEventTypeIds: string[]; message: string; eventSequences: string[] };
18
+ type History = { sequence: string; source: string; sourceType?: string; streamType?: string; streamId?: string;
19
+ type: string; content: Record<string, unknown>; hash?: string };
20
+ type Outcome = { success: boolean; sequences: string[]; errors: string[];
21
+ wireViolations: Array<{ EventTypeId: string; SequenceNumber: string; ConstraintType: number; ConstraintName: string; Message: string; Details: Record<string, string> }>;
22
+ violations: Array<{ id: string; message: string; details: Record<string, string> }>; history: History[]; next: string };
23
+ type SequenceCase = { name: string; kind: string; eventSchemas: Record<string, { eventTypeId: string; properties: Record<string, string> }>;
24
+ constraintDefinitions: Definition[]; isolatedConstraintOperations: Array<{ mode: string; events: Entry[] }> };
25
+ type Fixture = { eventSequenceCases: SequenceCase[]; expected: { cases: Array<{ name: string; result: { outcomes: Outcome[] } }> } };
26
+
27
+ /**
28
+ * Replays constraints-event-sequences.json: a definition applying to the event log behaves as unscoped, and one
29
+ * applying only to other event sequences is neither validated nor indexed on the scenario's event log.
30
+ * @param types - Event constructors by fixture alias.
31
+ * @param constraints - Selects the constraint classes for a case name.
32
+ */
33
+ export function eventSequenceScenarioBehaviors(
34
+ types: Record<string, new (value: string) => object>,
35
+ constraints: (caseName: string) => Constructor[]
36
+ ): void {
37
+ const fixture = JSON.parse(readFileSync(new URL('../../fixtures/constraints-event-sequences.json', import.meta.url), 'utf8')) as Fixture;
38
+ const historyOf = (scenario: EventScenario): History[] => scenario.appendedEvents.map(event => ({
39
+ sequence: event.context.sequenceNumber.toString(), source: event.context.eventSourceId,
40
+ sourceType: event.context.eventSourceType, streamType: event.context.eventStreamType, streamId: event.context.eventStreamId,
41
+ type: event.eventType.id.value, content: event.content, hash: event.context.hash
42
+ }));
43
+ const aliases = Object.fromEntries(Object.entries(types).map(([alias, type]) => [getEventTypeMetadata(type)!.eventType.id.value, alias]));
44
+
45
+ it('should retain every event sequence case', () => {
46
+ should(fixture.eventSequenceCases.map(test => test.name)).deep.equal([
47
+ 'property-event-log', 'property-event-log-and-outbox', 'property-outbox',
48
+ 'cycle-event-log', 'cycle-outbox', 'once-event-log', 'once-outbox'
49
+ ]);
50
+ should(fixture.expected.cases.map(test => test.name)).deep.equal(fixture.eventSequenceCases.map(test => test.name));
51
+ });
52
+
53
+ for (const [caseIndex, test] of fixture.eventSequenceCases.entries()) {
54
+ describe(`with ${test.name}`, () => {
55
+ it('should match the installed definitions and kernel violations, history and next sequence', async () => {
56
+ const selected = Object.keys(test.eventSchemas).map(alias => types[alias]);
57
+ const selectedConstraints = constraints(test.name);
58
+ const compiled = compileConstraints({ eventTypes: selected, constraints: selectedConstraints });
59
+ should([...compiled].map(([name, capture]) => ({
60
+ kind: capture.uniqueConstraint ? 'uniqueProperty' : 'uniqueEventType', name,
61
+ ...(capture.uniqueConstraint ? {
62
+ events: capture.uniqueConstraint.eventDefinitions.map(entry => ({ type: aliases[entry.eventTypeId], properties: entry.properties })),
63
+ ignoreCasing: capture.uniqueConstraint.ignoreCasing
64
+ } : { eventTypes: (capture.uniqueEventType!.eventTypeIds ?? [capture.uniqueEventType!.eventTypeId]).map(id => aliases[id]) }),
65
+ removedWithEventTypeIds: (capture.uniqueConstraint?.removedWithEventTypeIds ?? capture.uniqueEventType?.removedWithEventTypeIds ?? []).map(id => aliases[id]),
66
+ message: capture.uniqueConstraint?.message ?? capture.uniqueEventType?.message ?? '',
67
+ eventSequences: capture.eventSequences ?? []
68
+ }))).deep.equal(test.constraintDefinitions);
69
+ const scenario = new EventScenario({ artifacts: { eventTypes: selected, constraints: selectedConstraints } });
70
+ const validate = vi.spyOn(InProcessConstraints.prototype, 'validate');
71
+ const expected = fixture.expected.cases[caseIndex].result.outcomes;
72
+ should(expected.length).equal(test.isolatedConstraintOperations.length);
73
+ try {
74
+ for (const [index, operation] of test.isolatedConstraintOperations.entries()) {
75
+ const before = historyOf(scenario);
76
+ const entries = operation.events.map(entry => ({ eventSourceId: entry.source, event: new types[entry.type](entry.value) }));
77
+ const results = operation.mode === 'single'
78
+ ? [await scenario.append(entries[0].eventSourceId, entries[0].event)]
79
+ : await scenario.appendMany(entries);
80
+ const outcome = expected[index];
81
+ const raw = validate.mock.results.at(-1)!.value as ReturnType<InProcessConstraints['validate']>;
82
+ should(raw.map(({ SequenceNumber, ...violation }) => ({ ...violation, SequenceNumber: SequenceNumber.toString() })))
83
+ .deep.equal(outcome.wireViolations);
84
+ for (const [position, result] of results.entries()) {
85
+ should(result.isSuccess).equal(outcome.success);
86
+ should(result.sequenceNumber.value.toString()).equal(outcome.success ? outcome.sequences[position] : '0');
87
+ should(result.constraintViolations.map(violation => ({ id: violation.constraintId, message: violation.message, details: violation.details })))
88
+ .deep.equal(outcome.violations);
89
+ should(result.errors.map(error => error.message)).deep.equal(outcome.errors);
90
+ }
91
+ if (!outcome.success) should(historyOf(scenario)).deep.equal(before);
92
+ should(historyOf(scenario)).deep.equal(outcome.history);
93
+ should((await scenario.eventLog.getNextSequenceNumber()).value.toString()).equal(outcome.next);
94
+ }
95
+ } finally {
96
+ validate.mockRestore();
97
+ }
98
+ });
99
+ });
100
+ }
101
+ }
@@ -0,0 +1,28 @@
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 { field } from '@cratis/fundamentals';
6
+ import { describe } from 'vitest';
7
+ import { eventType } from '../../../events/eventTypeDecorator.js';
8
+ import { constraint } from '../../../events/constraints/constraint.js';
9
+ import type { IConstraintBuilder } from '../../../events/constraints/IConstraintBuilder.js';
10
+ import { otherSequenceExtendedKeyBehaviors } from './other_sequence_extended_key_behaviors.fixture.js';
11
+
12
+ @eventType('OracleOtherSequenceNumber')
13
+ class NumberClaimed { @field(Number) key = 1; }
14
+ @eventType('OracleOtherSequenceOutbox')
15
+ class OutboxClaimed { @field(String) label = 'x'; }
16
+
17
+ @constraint('OracleOtherSequenceNumberKey')
18
+ class NumberKey {
19
+ define(builder: IConstraintBuilder) { builder.forEventLog().unique(key => key.on(NumberClaimed, event => event.key)); }
20
+ }
21
+ @constraint('OracleOtherSequenceOutboxKey')
22
+ class OutboxKey {
23
+ define(builder: IConstraintBuilder) { builder.forEventSequences('outbox').unique(key => key.on(OutboxClaimed, event => event.label)); }
24
+ }
25
+
26
+ describe('when appending extended keys beside other event sequence constraints with decorators', () => {
27
+ otherSequenceExtendedKeyBehaviors(NumberClaimed, OutboxClaimed, [NumberKey, OutboxKey]);
28
+ });