@cratis/chronicle 6.49.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 (223) hide show
  1. package/dist/eventSequences/AppendOptions.d.ts +10 -0
  2. package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
  3. package/dist/eventSequences/EventForEventSourceId.d.ts +6 -0
  4. package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
  5. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  6. package/dist/eventSequences/EventSequence.js +28 -8
  7. package/dist/eventSequences/EventSequence.js.map +1 -1
  8. package/dist/eventSequences/ITransactionalEventSequence.d.ts +5 -5
  9. package/dist/eventSequences/ITransactionalEventSequence.d.ts.map +1 -1
  10. package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts +5 -0
  11. package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts.map +1 -0
  12. package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js +12 -0
  13. package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js.map +1 -0
  14. package/dist/eventSequences/TransactionalEventSequence.d.ts +3 -3
  15. package/dist/eventSequences/TransactionalEventSequence.d.ts.map +1 -1
  16. package/dist/eventSequences/TransactionalEventSequence.js +7 -4
  17. package/dist/eventSequences/TransactionalEventSequence.js.map +1 -1
  18. package/dist/eventSequences/createAppendNotification.d.ts +2 -1
  19. package/dist/eventSequences/createAppendNotification.d.ts.map +1 -1
  20. package/dist/eventSequences/createAppendNotification.js +3 -2
  21. package/dist/eventSequences/createAppendNotification.js.map +1 -1
  22. package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts +3 -0
  23. package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts.map +1 -0
  24. package/dist/eventSequences/ensureNamedTagBatchIsSupported.js +10 -0
  25. package/dist/eventSequences/ensureNamedTagBatchIsSupported.js.map +1 -0
  26. package/dist/eventSequences/index.d.ts +1 -0
  27. package/dist/eventSequences/index.d.ts.map +1 -1
  28. package/dist/eventSequences/index.js +1 -0
  29. package/dist/eventSequences/index.js.map +1 -1
  30. package/dist/eventSequences/prepareBatchAppend.d.ts +1 -0
  31. package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -1
  32. package/dist/eventSequences/prepareBatchAppend.js +4 -1
  33. package/dist/eventSequences/prepareBatchAppend.js.map +1 -1
  34. package/dist/eventSequences/prepareSingleAppend.d.ts +1 -0
  35. package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -1
  36. package/dist/eventSequences/prepareSingleAppend.js +3 -1
  37. package/dist/eventSequences/prepareSingleAppend.js.map +1 -1
  38. package/dist/events/EventContext.d.ts +7 -0
  39. package/dist/events/EventContext.d.ts.map +1 -1
  40. package/dist/events/InvalidNamedTag.d.ts +8 -0
  41. package/dist/events/InvalidNamedTag.d.ts.map +1 -0
  42. package/dist/events/InvalidNamedTag.js +13 -0
  43. package/dist/events/InvalidNamedTag.js.map +1 -0
  44. package/dist/events/NamedTag.d.ts +25 -0
  45. package/dist/events/NamedTag.d.ts.map +1 -0
  46. package/dist/events/NamedTag.js +39 -0
  47. package/dist/events/NamedTag.js.map +1 -0
  48. package/dist/events/constraints/ConstraintBuilder.d.ts +7 -0
  49. package/dist/events/constraints/ConstraintBuilder.d.ts.map +1 -1
  50. package/dist/events/constraints/ConstraintBuilder.js +13 -0
  51. package/dist/events/constraints/ConstraintBuilder.js.map +1 -1
  52. package/dist/events/constraints/ConstraintEventSequences.d.ts +19 -0
  53. package/dist/events/constraints/ConstraintEventSequences.d.ts.map +1 -0
  54. package/dist/events/constraints/ConstraintEventSequences.js +39 -0
  55. package/dist/events/constraints/ConstraintEventSequences.js.map +1 -0
  56. package/dist/events/constraints/Constraints.d.ts.map +1 -1
  57. package/dist/events/constraints/Constraints.js +24 -3
  58. package/dist/events/constraints/Constraints.js.map +1 -1
  59. package/dist/events/constraints/IConstraintBuilder.d.ts +18 -0
  60. package/dist/events/constraints/IConstraintBuilder.d.ts.map +1 -1
  61. package/dist/events/constraints/UniqueOptions.d.ts +15 -0
  62. package/dist/events/constraints/UniqueOptions.d.ts.map +1 -0
  63. package/dist/events/constraints/UniqueOptions.js +4 -0
  64. package/dist/events/constraints/UniqueOptions.js.map +1 -0
  65. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts +9 -0
  66. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.d.ts.map +1 -0
  67. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js +22 -0
  68. package/dist/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.js.map +1 -0
  69. package/dist/events/constraints/index.d.ts +2 -0
  70. package/dist/events/constraints/index.d.ts.map +1 -1
  71. package/dist/events/constraints/index.js.map +1 -1
  72. package/dist/events/constraints/unique.d.ts +10 -0
  73. package/dist/events/constraints/unique.d.ts.map +1 -1
  74. package/dist/events/constraints/unique.js +11 -10
  75. package/dist/events/constraints/unique.js.map +1 -1
  76. package/dist/events/index.d.ts +3 -0
  77. package/dist/events/index.d.ts.map +1 -1
  78. package/dist/events/index.js +3 -0
  79. package/dist/events/index.js.map +1 -1
  80. package/dist/events/mergeNamedTags.d.ts +13 -0
  81. package/dist/events/mergeNamedTags.d.ts.map +1 -0
  82. package/dist/events/mergeNamedTags.js +39 -0
  83. package/dist/events/mergeNamedTags.js.map +1 -0
  84. package/dist/events/toClientEventContext.d.ts.map +1 -1
  85. package/dist/events/toClientEventContext.js +3 -1
  86. package/dist/events/toClientEventContext.js.map +1 -1
  87. package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -1
  88. package/dist/projections/eventContextPropertyExpression.js +2 -1
  89. package/dist/projections/eventContextPropertyExpression.js.map +1 -1
  90. package/dist/testing/EventScenario.d.ts.map +1 -1
  91. package/dist/testing/EventScenario.js +8 -3
  92. package/dist/testing/EventScenario.js.map +1 -1
  93. package/dist/testing/InProcessConstraints.d.ts +4 -1
  94. package/dist/testing/InProcessConstraints.d.ts.map +1 -1
  95. package/dist/testing/InProcessConstraints.js +5 -1
  96. package/dist/testing/InProcessConstraints.js.map +1 -1
  97. package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
  98. package/dist/testing/InProcessEventSequence.js +19 -8
  99. package/dist/testing/InProcessEventSequence.js.map +1 -1
  100. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  101. package/dist/testing/ReactorScenario.js +6 -0
  102. package/dist/testing/ReactorScenario.js.map +1 -1
  103. package/dist/testing/singleAppendRoute.d.ts +5 -0
  104. package/dist/testing/singleAppendRoute.d.ts.map +1 -1
  105. package/dist/testing/singleAppendRoute.js +12 -1
  106. package/dist/testing/singleAppendRoute.js.map +1 -1
  107. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts +14 -0
  108. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.d.ts.map +1 -0
  109. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js +20 -0
  110. package/dist/transactions/ConflictingConcurrencyScopesInUnitOfWork.js.map +1 -0
  111. package/dist/transactions/IUnitOfWork.d.ts +5 -3
  112. package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
  113. package/dist/transactions/TransactionalAppendOptions.d.ts +35 -0
  114. package/dist/transactions/TransactionalAppendOptions.d.ts.map +1 -0
  115. package/dist/transactions/TransactionalAppendOptions.js +4 -0
  116. package/dist/transactions/TransactionalAppendOptions.js.map +1 -0
  117. package/dist/transactions/UnitOfWork.d.ts +3 -2
  118. package/dist/transactions/UnitOfWork.d.ts.map +1 -1
  119. package/dist/transactions/UnitOfWork.js +32 -11
  120. package/dist/transactions/UnitOfWork.js.map +1 -1
  121. package/dist/transactions/concurrencyScopesAreEqual.d.ts +9 -0
  122. package/dist/transactions/concurrencyScopesAreEqual.d.ts.map +1 -0
  123. package/dist/transactions/concurrencyScopesAreEqual.js +19 -0
  124. package/dist/transactions/concurrencyScopesAreEqual.js.map +1 -0
  125. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts +26 -0
  126. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.d.ts.map +1 -0
  127. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js +31 -0
  128. package/dist/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.js.map +1 -0
  129. package/dist/transactions/index.d.ts +2 -0
  130. package/dist/transactions/index.d.ts.map +1 -1
  131. package/dist/transactions/index.js +1 -0
  132. package/dist/transactions/index.js.map +1 -1
  133. package/dist/transactions/toEventForEventSourceId.d.ts +13 -0
  134. package/dist/transactions/toEventForEventSourceId.d.ts.map +1 -0
  135. package/dist/transactions/toEventForEventSourceId.js +29 -0
  136. package/dist/transactions/toEventForEventSourceId.js.map +1 -0
  137. package/dist/tsconfig.tsbuildinfo +1 -1
  138. package/eventSequences/AppendOptions.ts +11 -0
  139. package/eventSequences/EventForEventSourceId.ts +7 -0
  140. package/eventSequences/EventSequence.ts +30 -8
  141. package/eventSequences/ITransactionalEventSequence.ts +5 -5
  142. package/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.ts +12 -0
  143. package/eventSequences/TransactionalEventSequence.ts +8 -5
  144. package/eventSequences/createAppendNotification.ts +4 -2
  145. package/eventSequences/ensureNamedTagBatchIsSupported.ts +11 -0
  146. package/eventSequences/for_EventSequence/against_a_kernel/when_appending_named_tags.integration.spec.ts +107 -0
  147. package/eventSequences/for_EventSequence/when_appending_named_tags/given.fixture.ts +38 -0
  148. package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_batch.spec.ts +49 -0
  149. package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_single_event.spec.ts +47 -0
  150. package/eventSequences/for_EventSequence/when_appending_named_tags/with_an_invalid_tag.spec.ts +36 -0
  151. package/eventSequences/for_EventSequence/when_appending_named_tags/with_registered_sources.spec.ts +74 -0
  152. package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_named_tags.spec.ts +41 -0
  153. package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_options.spec.ts +29 -0
  154. package/eventSequences/index.ts +1 -0
  155. package/eventSequences/prepareBatchAppend.ts +4 -1
  156. package/eventSequences/prepareSingleAppend.ts +3 -1
  157. package/events/EventContext.ts +8 -0
  158. package/events/InvalidNamedTag.ts +13 -0
  159. package/events/NamedTag.ts +44 -0
  160. package/events/constraints/ConstraintBuilder.ts +16 -0
  161. package/events/constraints/ConstraintEventSequences.ts +41 -0
  162. package/events/constraints/Constraints.ts +22 -3
  163. package/events/constraints/IConstraintBuilder.ts +20 -0
  164. package/events/constraints/UniqueOptions.ts +20 -0
  165. package/events/constraints/for_Constraints/against_a_kernel/when_appending_to_event_sequences_a_constraint_is_scoped_to.integration.spec.ts +118 -0
  166. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/given/registered_constraints.ts +26 -0
  167. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_fluent_builder.spec.ts +40 -0
  168. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_legacy_decorators.legacy.spec.ts +47 -0
  169. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_merged_declarations.spec.ts +36 -0
  170. package/events/constraints/for_Constraints/when_scoping_to_event_sequences/with_standard_decorators.standard.spec.ts +46 -0
  171. package/events/constraints/index.ts +2 -0
  172. package/events/constraints/unique.ts +22 -2
  173. package/events/for_NamedTag/when_constructing/with_invalid_values.spec.ts +17 -0
  174. package/events/for_NamedTag/when_constructing/with_valid_values.spec.ts +19 -0
  175. package/events/for_mergeNamedTags/when_merging/with_event_and_call_tags.spec.ts +31 -0
  176. package/events/for_mergeNamedTags/when_merging/with_invalid_entries.spec.ts +17 -0
  177. package/events/for_toClientEventContext/when_converting_named_tags.spec.ts +31 -0
  178. package/events/index.ts +3 -0
  179. package/events/mergeNamedTags.ts +37 -0
  180. package/events/toClientEventContext.ts +3 -1
  181. package/package.json +6 -6
  182. package/projections/eventContextPropertyExpression.ts +3 -2
  183. package/testing/EventScenario.fixtures.spec.ts +1 -1
  184. package/testing/EventScenario.ts +10 -4
  185. package/testing/InProcessConstraints.ts +5 -1
  186. package/testing/InProcessEventSequence.ts +19 -8
  187. package/testing/ReactorScenario.ts +6 -0
  188. package/testing/for_EventScenario/when_appending_event_sequence_constraints/event_sequence_scenario_behaviors.fixture.ts +101 -0
  189. package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.legacy.spec.ts +28 -0
  190. package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key.standard.spec.ts +27 -0
  191. package/testing/for_EventScenario/when_appending_event_sequence_constraints/other_sequence_extended_key_behaviors.fixture.ts +40 -0
  192. package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_legacy_decorators.legacy.spec.ts +55 -0
  193. package/testing/for_EventScenario/when_appending_event_sequence_constraints/with_standard_decorators.standard.spec.ts +54 -0
  194. package/testing/for_EventScenario/when_appending_named_tags/with_named_tags.spec.ts +48 -0
  195. package/testing/for_ReactorScenario/when_recording_side_effects/with_named_tags.spec.ts +119 -0
  196. package/testing/for_ReadModelScenario/when_seeding_with_options/with_named_tags.spec.ts +35 -0
  197. package/testing/singleAppendRoute.ts +13 -1
  198. package/transactions/ConflictingConcurrencyScopesInUnitOfWork.ts +19 -0
  199. package/transactions/IUnitOfWork.ts +5 -3
  200. package/transactions/TransactionalAppendOptions.ts +45 -0
  201. package/transactions/UnitOfWork.ts +37 -12
  202. package/transactions/concurrencyScopesAreEqual.ts +23 -0
  203. package/transactions/for_UnitOfWork/against_a_kernel/when_committing_with_append_metadata.integration.spec.ts +70 -0
  204. package/transactions/for_UnitOfWork/given/a_unit_of_work_with_a_recording_store.ts +54 -0
  205. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_are_equivalent.spec.ts +32 -0
  206. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_event_type_filters_differ.spec.ts +40 -0
  207. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_are_equal_or_omitted.spec.ts +29 -0
  208. package/transactions/for_UnitOfWork/when_adding_events_with_concurrency_scopes/and_the_scopes_differ_for_one_event_source.spec.ts +29 -0
  209. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_a_later_event_sequence_fails.spec.ts +30 -0
  210. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_events_span_event_sequences.spec.ts +34 -0
  211. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_options_differ_per_event.spec.ts +45 -0
  212. package/transactions/for_UnitOfWork/when_committing_events_with_append_metadata/and_the_append_is_rejected.spec.ts +26 -0
  213. package/transactions/for_UnitOfWork/when_committing_events_with_named_tags.spec.ts +112 -0
  214. package/transactions/for_UnitOfWork/when_rolling_back_events_with_append_metadata.spec.ts +24 -0
  215. package/transactions/index.ts +2 -0
  216. package/transactions/toEventForEventSourceId.ts +32 -0
  217. package/vitest.integration.config.ts +2 -0
  218. package/webhooks/for_Webhooks/against_a_kernel/given/ReceivedWebhookRequest.fixture.ts +21 -0
  219. package/webhooks/for_Webhooks/against_a_kernel/given/WebhookReceiver.fixture.ts +99 -0
  220. package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_failing_webhook.integration.spec.ts +101 -0
  221. package/webhooks/for_Webhooks/against_a_kernel/when_delivering_events_to_a_webhook.integration.spec.ts +130 -0
  222. package/webhooks/for_Webhooks/against_a_kernel/when_registering_a_webhook_that_is_not_replayable_and_not_active.integration.spec.ts +101 -0
  223. package/webhooks/for_Webhooks/when_registering_a_webhook_that_is_not_replayable_and_not_active.spec.ts +43 -0
@@ -0,0 +1,17 @@
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 } from 'vitest';
5
+ import { InvalidNamedTag } from '../../InvalidNamedTag.js';
6
+ import { NamedTag } from '../../NamedTag.js';
7
+
8
+ chai.should();
9
+
10
+ describe('when constructing a named tag with invalid values', () => {
11
+ it('should reject an empty name', () => (() => new NamedTag('', 'value')).should.throw(InvalidNamedTag));
12
+ it('should reject a whitespace name', () => (() => new NamedTag(' \t', 'value')).should.throw(InvalidNamedTag));
13
+ it('should reject a name of only next-line characters like .NET', () => (() => new NamedTag('\u0085', 'value')).should.throw(InvalidNamedTag));
14
+ it('should reject a missing name', () => (() => new NamedTag(undefined as unknown as string, 'value')).should.throw(InvalidNamedTag));
15
+ it('should reject a null value', () => (() => new NamedTag('name', null as unknown as string)).should.throw(InvalidNamedTag));
16
+ it('should reject a non-string value', () => (() => new NamedTag('name', 42 as unknown as string)).should.throw(InvalidNamedTag));
17
+ });
@@ -0,0 +1,19 @@
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 } from 'vitest';
5
+ import { NamedTag } from '../../NamedTag.js';
6
+
7
+ chai.should();
8
+
9
+ describe('when constructing a named tag with a name and an empty value', () => {
10
+ const tag = new NamedTag('import-batch', '');
11
+ it('should keep the name exactly', () => tag.name.should.equal('import-batch'));
12
+ it('should keep the empty value', () => tag.value.should.equal(''));
13
+ it('should be immutable', () => Object.isFrozen(tag).should.be.true);
14
+ });
15
+
16
+ describe('when comparing named tags', () => {
17
+ it('should treat the same name and value as equal', () => new NamedTag('a', 'B').equals(new NamedTag('a', 'B')).should.be.true);
18
+ it('should compare values case-sensitively', () => new NamedTag('a', 'B').equals(new NamedTag('a', 'b')).should.be.false);
19
+ });
@@ -0,0 +1,31 @@
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 { beforeEach, chai, describe, it } from 'vitest';
5
+ import { mergeNamedTags } from '../../mergeNamedTags.js';
6
+ import { NamedTag } from '../../NamedTag.js';
7
+
8
+ chai.should();
9
+
10
+ describe('when merging event and call named tags', () => {
11
+ let result: NamedTag[];
12
+ beforeEach(() => {
13
+ result = mergeNamedTags(
14
+ [new NamedTag('side', 'debit'), new NamedTag('transfer', 't-1')],
15
+ [new NamedTag('transfer', 't-1'), new NamedTag('transfer', 'T-1'), new NamedTag('side', 'credit')]);
16
+ });
17
+ it('should keep the first occurrence of each exact pair in order, including several values per name', () =>
18
+ result.map(tag => [tag.name, tag.value]).should.deep.equal([
19
+ ['side', 'debit'], ['transfer', 't-1'], ['transfer', 'T-1'], ['side', 'credit']
20
+ ]));
21
+ });
22
+
23
+ describe('when merging plain named tag objects', () => {
24
+ let result: NamedTag[];
25
+ beforeEach(() => { result = mergeNamedTags([{ name: 'channel', value: 'web' } as NamedTag]); });
26
+ it('should return validated named tag instances', () => result[0].should.be.instanceOf(NamedTag));
27
+ });
28
+
29
+ describe('when merging no named tags', () => {
30
+ it('should return an empty list', () => mergeNamedTags(undefined, null, []).should.deep.equal([]));
31
+ });
@@ -0,0 +1,17 @@
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 } from 'vitest';
5
+ import { InvalidNamedTag } from '../../InvalidNamedTag.js';
6
+ import { mergeNamedTags } from '../../mergeNamedTags.js';
7
+ import type { NamedTag } from '../../NamedTag.js';
8
+
9
+ chai.should();
10
+
11
+ describe('when merging named tags with invalid entries', () => {
12
+ it('should reject a null element', () => (() => mergeNamedTags([null as unknown as NamedTag])).should.throw(InvalidNamedTag));
13
+ it('should reject a plain object with a blank name', () =>
14
+ (() => mergeNamedTags([{ name: ' ', value: 'x' } as NamedTag])).should.throw(InvalidNamedTag));
15
+ it('should reject a string instead of a collection', () =>
16
+ (() => mergeNamedTags('name' as unknown as NamedTag[])).should.throw(InvalidNamedTag));
17
+ });
@@ -0,0 +1,31 @@
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 { beforeEach, chai, describe, it } from 'vitest';
5
+ import type { EventContext } from '../EventContext.js';
6
+ import { NamedTag } from '../NamedTag.js';
7
+ import { toClientEventContext } from '../toClientEventContext.js';
8
+ import type { WireEventContext } from '../WireEventContext.js';
9
+
10
+ chai.should();
11
+
12
+ function wireContext(namedTags?: { Name: string; Value: string }[]): WireEventContext {
13
+ return {
14
+ EventSourceId: 'source', SequenceNumber: 1n, EventSourceType: 'Default', EventStreamType: 'All', EventStreamId: 'Default',
15
+ EventType: { Id: 'type', Generation: 1, Tombstone: false }, Occurred: { Value: '2025-01-01T00:00:00.000Z' },
16
+ CorrelationId: undefined, Causation: [], CausedBy: undefined, Tags: [], Hash: '', ObservationState: 1, Subject: 'source',
17
+ NamedTags: namedTags
18
+ } as unknown as WireEventContext;
19
+ }
20
+
21
+ describe('when converting a delivered context with named tags', () => {
22
+ let context: EventContext;
23
+ beforeEach(() => { context = toClientEventContext(wireContext([{ Name: 'batch', Value: 'b-1' }, { Name: 'batch', Value: '' }])); });
24
+ it('should expose every named tag in delivered order', () =>
25
+ context.namedTags!.map(tag => [tag.name, tag.value]).should.deep.equal([['batch', 'b-1'], ['batch', '']]));
26
+ it('should expose named tag instances', () => context.namedTags![0].should.be.instanceOf(NamedTag));
27
+ });
28
+
29
+ describe('when converting a delivered context without named tags', () => {
30
+ it('should expose an empty list', () => toClientEventContext(wireContext()).namedTags!.should.deep.equal([]));
31
+ });
package/events/index.ts CHANGED
@@ -21,5 +21,8 @@ export { filterEventsByTag, getFilterTagsFor } from './filterEventsByTagDecorato
21
21
  export { eventSourceType, getEventSourceTypeFor } from './eventSourceTypeDecorator.js';
22
22
  export { eventStreamType, getEventStreamTypeFor } from './eventStreamTypeDecorator.js';
23
23
  export { mergeTags } from './mergeTags.js';
24
+ export { NamedTag } from './NamedTag.js';
25
+ export { InvalidNamedTag } from './InvalidNamedTag.js';
26
+ export { mergeNamedTags } from './mergeNamedTags.js';
24
27
  export * from './constraints/index.js';
25
28
  export * from './migrations/index.js';
@@ -0,0 +1,37 @@
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 { InvalidNamedTag } from './InvalidNamedTag.js';
5
+ import { NamedTag } from './NamedTag.js';
6
+
7
+ /**
8
+ * Validates and merges structured named tags - for example the tags carried by an individual event and
9
+ * the tags supplied for a whole append call - into one distinct list. The first occurrence of each exact
10
+ * (ordinal, case-sensitive) name and value pair is kept, in order. Mirrors the .NET client's
11
+ * `NamedTagConverters.Merge`.
12
+ * @param sources - The named tag sources to merge, in precedence order. An undefined or null source contributes nothing.
13
+ * @returns The distinct, validated named tags.
14
+ * @throws {@link InvalidNamedTag} when a source is not iterable, contains an entry that is not an object,
15
+ * or contains a tag with a blank name or a non-string value.
16
+ */
17
+ export function mergeNamedTags(...sources: ReadonlyArray<Iterable<NamedTag> | undefined | null>): NamedTag[] {
18
+ const seen = new Set<string>();
19
+ const merged: NamedTag[] = [];
20
+ for (const source of sources) {
21
+ if (source === undefined || source === null) continue;
22
+ if (typeof source !== 'object' || typeof (source as Iterable<NamedTag>)[Symbol.iterator] !== 'function') {
23
+ throw new InvalidNamedTag();
24
+ }
25
+ for (const entry of source as Iterable<unknown>) {
26
+ if (entry === null || typeof entry !== 'object') throw new InvalidNamedTag();
27
+ // Reconstruct so plain { name, value } objects are validated exactly like NamedTag instances.
28
+ const tag = entry instanceof NamedTag ? entry
29
+ : new NamedTag(Reflect.get(entry, 'name') as string, Reflect.get(entry, 'value') as string);
30
+ const key = JSON.stringify([tag.name, tag.value]);
31
+ if (seen.has(key)) continue;
32
+ seen.add(key);
33
+ merged.push(tag);
34
+ }
35
+ }
36
+ return merged;
37
+ }
@@ -8,6 +8,7 @@ import { EventType } from './EventType.js';
8
8
  import { EventTypeGeneration } from './EventTypeGeneration.js';
9
9
  import { EventTypeId } from './EventTypeId.js';
10
10
  import { Tag } from './Tag.js';
11
+ import { NamedTag } from './NamedTag.js';
11
12
  import type { WireEventContext } from './WireEventContext.js';
12
13
 
13
14
  /** Converts kernel metadata identically for reads, reactors, and reducers. */
@@ -37,7 +38,8 @@ export function toClientEventContext(context: WireEventContext): EventContext {
37
38
  occurred: causation.Occurred ? new Date(causation.Occurred.Value) : undefined,
38
39
  properties: { ...causation.Properties }
39
40
  })),
40
- tags: (context.Tags ?? []).map(value => new Tag(value))
41
+ tags: (context.Tags ?? []).map(value => new Tag(value)),
42
+ namedTags: (context.NamedTags ?? []).map(tag => new NamedTag(tag.Name, tag.Value))
41
43
  };
42
44
  }
43
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/chronicle",
3
- "version": "6.49.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,12 +173,12 @@
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": {
180
180
  "@bufbuild/protobuf": "^2.16.0",
181
- "@cratis/chronicle.contracts": "19.30.0",
181
+ "@cratis/chronicle.contracts": "19.31.10",
182
182
  "@grpc/grpc-js": "^1.14.5",
183
183
  "@opentelemetry/api": "^1.9.1",
184
184
  "glob": "^13.0.6",
@@ -188,14 +188,14 @@
188
188
  "undici": "^8.11.2"
189
189
  },
190
190
  "devDependencies": {
191
- "@cratis/fundamentals": "7.20.0",
191
+ "@cratis/fundamentals": "7.22.4",
192
192
  "@opentelemetry/context-async-hooks": "2.11.0",
193
193
  "@opentelemetry/core": "2.11.0",
194
194
  "@opentelemetry/instrumentation-grpc": "0.222.0",
195
195
  "@opentelemetry/instrumentation-http": "0.222.0",
196
196
  "@opentelemetry/sdk-metrics": "2.11.0",
197
197
  "@opentelemetry/sdk-trace-base": "2.11.0",
198
- "@types/node": "^26.6.3",
198
+ "@types/node": "^26.6.4",
199
199
  "@typescript/native": "npm:typescript@^7.0.2",
200
200
  "esbuild": "^0.28.2",
201
201
  "typescript": "npm:@typescript/typescript6@^6.0.2",
@@ -203,6 +203,6 @@
203
203
  "vitest": "^5.0.3"
204
204
  },
205
205
  "peerDependencies": {
206
- "@cratis/fundamentals": "^7.20.0"
206
+ "@cratis/fundamentals": "^7.22.4"
207
207
  }
208
208
  }
@@ -23,8 +23,9 @@ const contextProperties = {
23
23
  correlationId: true,
24
24
  causation: true,
25
25
  tags: true
26
- // eventSource is deliberately not projectable: its kernel resolution is not part of the supported expression set.
27
- } satisfies Record<Exclude<keyof EventContext, 'eventSource'>, true>;
26
+ // eventSource and namedTags are deliberately not projectable: their kernel resolution is not part of the
27
+ // supported expression set.
28
+ } satisfies Record<Exclude<keyof EventContext, 'eventSource' | 'namedTags'>, true>;
28
29
 
29
30
  // Mirror the kernel's DerivedPropertyFunctions registry: Week() applies to dates only.
30
31
  const derivedFunctions: Record<string, string> = { week: 'Week' };
@@ -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
  }
@@ -17,7 +17,7 @@ import { EventTypeId } from '../events/EventTypeId.js';
17
17
  import { EventTypeGeneration } from '../events/EventTypeGeneration.js';
18
18
  import { Identity } from '../identity/Identity.js';
19
19
  import type { AppendOptions } from '../eventSequences/AppendOptions.js';
20
- import { singleAppendRoute } from './singleAppendRoute.js';
20
+ import { hasNamedTags, singleAppendRoute } from './singleAppendRoute.js';
21
21
  import type { AppendResult } from '../eventSequences/AppendResult.js';
22
22
  import type { AppendedEventWithResult } from '../eventSequences/AppendedEventWithResult.js';
23
23
  import type { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
@@ -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
  }
@@ -215,7 +216,8 @@ export class InProcessEventSequence implements IEventSequence {
215
216
  subject: resolveStoredSubject(prepared.subject, eventSourceId), hash, causedBy: prepared.identity, observationState: EventObservationState.Initial,
216
217
  eventType: prepared.eventType, occurred, correlationId: prepared.correlationId.toString(),
217
218
  causation: prepared.causationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
218
- tags: prepared.tags.map(tag => new Tag(tag))
219
+ tags: prepared.tags.map(tag => new Tag(tag)),
220
+ namedTags: []
219
221
  }
220
222
  };
221
223
  const violations = this.validateConstraints([...this._history, ...(this._stagedSeeds ?? [])], [stored]);
@@ -254,10 +256,13 @@ export class InProcessEventSequence implements IEventSequence {
254
256
  for (const entry of entries) {
255
257
  if (typeof sourceOrEvents === 'string') break;
256
258
  if (!entry || typeof entry !== 'object' || Reflect.ownKeys(entry).some(key =>
257
- !['eventSourceId', 'event', 'eventSourceType', 'eventStreamType', 'eventStreamId', 'subject', 'occurred', 'tags'].includes(String(key)))) {
259
+ !['eventSourceId', 'event', 'eventSourceType', 'eventStreamType', 'eventStreamId', 'subject', 'occurred', 'tags', 'namedTags'].includes(String(key)))) {
258
260
  throw this.unsupported('appendMany.entry', this.id.value, 'Unrecognized per-entry metadata.');
259
261
  }
260
262
  const individual = entry as EventForEventSourceId;
263
+ if (hasNamedTags(individual.namedTags)) {
264
+ throw this.unsupported('appendMany.namedTags', this.id.value, 'Named tags are not fixture-backed.');
265
+ }
261
266
  if (individual.occurred !== undefined) this.checkedDate(individual.occurred, 'appendMany.occurred');
262
267
  if (individual.subject !== undefined && individual.subject !== null && typeof individual.subject !== 'string') {
263
268
  throw this.unsupported('appendMany.subject', this.id.value, 'Subject must be a string.');
@@ -271,7 +276,8 @@ export class InProcessEventSequence implements IEventSequence {
271
276
  try {
272
277
  const batchOptions: AppendOptions | undefined = shared?.correlationId === undefined && this._correlationId
273
278
  ? { correlationId: this._correlationId(), sourceType: shared?.sourceType, streamType: shared?.streamType,
274
- streamId: shared?.streamId, subject: shared?.subject, occurred: shared?.occurred, tags: shared?.tags }
279
+ streamId: shared?.streamId, subject: shared?.subject, occurred: shared?.occurred, tags: shared?.tags,
280
+ namedTags: shared?.namedTags }
275
281
  : shared;
276
282
  if (batchOptions?.correlationId !== undefined) this.validateGuid(batchOptions.correlationId, 'appendMany.correlationId');
277
283
  prepared = typeof sourceOrEvents === 'string'
@@ -318,7 +324,8 @@ export class InProcessEventSequence implements IEventSequence {
318
324
  eventStreamId: wire.EventStreamId || 'Default', subject: resolveStoredSubject(wire.Subject, eventSourceId), hash, causedBy: identity,
319
325
  observationState: EventObservationState.Initial, eventType, occurred, correlationId: correlationId.toString(),
320
326
  causation: batchCausationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
321
- tags: wire.Tags.map(tag => new Tag(tag))
327
+ tags: wire.Tags.map(tag => new Tag(tag)),
328
+ namedTags: []
322
329
  } };
323
330
  return { stored, result: this.success(sequenceNumber, event.constructor.name) };
324
331
  });
@@ -411,7 +418,10 @@ export class InProcessEventSequence implements IEventSequence {
411
418
  if (!options || typeof options !== 'object' || Array.isArray(options)) {
412
419
  throw this.unsupported('appendMany.options', this.id.value, 'Append options must be an object.');
413
420
  }
414
- const allowed = ['correlationId', 'sourceType', 'streamType', 'streamId', 'subject', 'occurred', 'tags'];
421
+ if (hasNamedTags(options.namedTags)) {
422
+ throw this.unsupported('appendMany.namedTags', this.id.value, 'Named tags are not fixture-backed.');
423
+ }
424
+ const allowed = ['correlationId', 'sourceType', 'streamType', 'streamId', 'subject', 'occurred', 'tags', 'namedTags'];
415
425
  if (Reflect.ownKeys(options).some(key => !allowed.includes(String(key))) ||
416
426
  options.concurrencyScope !== undefined || options.concurrencyScopes !== undefined || options.eventSourceId !== undefined) {
417
427
  throw this.unsupported('appendMany.options', this.id.value, 'Concurrency and unrecognized metadata are not fixture-backed.');
@@ -492,7 +502,8 @@ export class InProcessEventSequence implements IEventSequence {
492
502
  context: { ...event.context, eventType, occurred: new Date(event.context.occurred),
493
503
  causedBy: causedBy && new Identity(causedBy.subject, causedBy.name, causedBy.userName, causedBy.onBehalfOf),
494
504
  causation: event.context.causation.map(item => ({ ...item, occurred: item.occurred && new Date(item.occurred), properties: { ...item.properties } })),
495
- tags: event.context.tags.map(item => new Tag(item.value)) }
505
+ tags: event.context.tags.map(item => new Tag(item.value)),
506
+ namedTags: [...(event.context.namedTags ?? [])] }
496
507
  };
497
508
  }
498
509
 
@@ -13,11 +13,13 @@ import type { AppendedEvent } from '../events/AppendedEvent.js';
13
13
  import type { EventContext } from '../events/EventContext.js';
14
14
  import { getEventTypeMetadata } from '../events/eventTypeDecorator.js';
15
15
  import { getFilterTagsFor } from '../events/filterEventsByTagDecorator.js';
16
+ import { mergeNamedTags } from '../events/mergeNamedTags.js';
16
17
  import { getEventSourceTypeFor } from '../events/eventSourceTypeDecorator.js';
17
18
  import { getDeclaredEventStreamTypeFor, getEventStreamTypeFor } from '../events/eventStreamTypeDecorator.js';
18
19
  import type { IEventLog } from '../eventSequences/IEventLog.js';
19
20
  import type { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
20
21
  import type { AppendOptions } from '../eventSequences/AppendOptions.js';
22
+ import { ensureNamedTagBatchIsSupported } from '../eventSequences/ensureNamedTagBatchIsSupported.js';
21
23
  import { EventStoreName } from '../EventStoreName.js';
22
24
  import { EventStoreNamespaceName } from '../EventStoreNamespaceName.js';
23
25
  import type { IEventStore } from '../IEventStore.js';
@@ -297,6 +299,10 @@ export class ReactorScenario {
297
299
  throw new UnsupportedReactorOperation('handler.return', this._reactor.name,
298
300
  'Only registered events and production EventForEventSourceId wrappers may be recorded; commands and unknown values are unsupported.');
299
301
  }
302
+ // Production validates named tags before appending, failing the delivery on an invalid tag; so does recording.
303
+ for (const target of events) mergeNamedTags(target.namedTags);
304
+ ensureNamedTagBatchIsSupported(events.some(event => mergeNamedTags(event.namedTags).length > 0),
305
+ events.some(event => event.eventSource !== undefined));
300
306
  for (const target of events) {
301
307
  this._effects.push(Object.freeze({ kind: 'event', value: target.event, target, triggeringContext: context,
302
308
  handler, deliveryIndex }));
@@ -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
+ });
@@ -0,0 +1,27 @@
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 { describe } from 'vitest';
6
+ import { eventType } from '../../../events/eventTypeDecorator.js';
7
+ import { constraint } from '../../../events/constraints/constraint.js';
8
+ import type { IConstraintBuilder } from '../../../events/constraints/IConstraintBuilder.js';
9
+ import { otherSequenceExtendedKeyBehaviors } from './other_sequence_extended_key_behaviors.fixture.js';
10
+
11
+ @eventType('OracleOtherSequenceNumber')
12
+ class NumberClaimed { @field(Number) key = 1; }
13
+ @eventType('OracleOtherSequenceOutbox')
14
+ class OutboxClaimed { @field(String) label = 'x'; }
15
+
16
+ @constraint('OracleOtherSequenceNumberKey')
17
+ class NumberKey {
18
+ define(builder: IConstraintBuilder) { builder.forEventLog().unique(key => key.on(NumberClaimed, event => event.key)); }
19
+ }
20
+ @constraint('OracleOtherSequenceOutboxKey')
21
+ class OutboxKey {
22
+ define(builder: IConstraintBuilder) { builder.forEventSequences('outbox').unique(key => key.on(OutboxClaimed, event => event.label)); }
23
+ }
24
+
25
+ describe('when appending extended keys beside other event sequence constraints with decorators', () => {
26
+ otherSequenceExtendedKeyBehaviors(NumberClaimed, OutboxClaimed, [NumberKey, OutboxKey]);
27
+ });