@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,44 @@
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
+
6
+ // Matches .NET string.IsNullOrWhiteSpace, which also treats U+0085 and other Unicode white space as blank.
7
+ const blank = /^\p{White_Space}*$/u;
8
+
9
+ /**
10
+ * Associates a name with an exact, opaque event tag value. Names and values are compared exactly
11
+ * (ordinal and case-sensitive); one name may carry several values on the same event.
12
+ * Mirrors the .NET client's `NamedTag`.
13
+ */
14
+ export class NamedTag {
15
+ /** The name of the tag. Never blank. */
16
+ readonly name: string;
17
+
18
+ /** The exact, opaque value of the tag. May be empty. */
19
+ readonly value: string;
20
+
21
+ /**
22
+ * Initializes a new instance of {@link NamedTag}.
23
+ * @param name - The name. Must be a nonblank string.
24
+ * @param value - The opaque value. Must be a string; the empty string is allowed.
25
+ * @throws {@link InvalidNamedTag} when the name is blank or not a string, or the value is not a string.
26
+ */
27
+ constructor(name: string, value: string) {
28
+ if (typeof name !== 'string' || blank.test(name) || typeof value !== 'string') {
29
+ throw new InvalidNamedTag();
30
+ }
31
+ this.name = name;
32
+ this.value = value;
33
+ Object.freeze(this);
34
+ }
35
+
36
+ /**
37
+ * Determines whether this tag has the same name and value as another, compared exactly.
38
+ * @param other - The tag to compare with.
39
+ * @returns True when both name and value are equal.
40
+ */
41
+ equals(other: NamedTag): boolean {
42
+ return other instanceof NamedTag && other.name === this.name && other.value === this.value;
43
+ }
44
+ }
@@ -2,6 +2,8 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import { getEventTypeFor } from '../eventTypeDecorator.js';
5
+ import { EventSequenceId } from '../../eventSequences/EventSequenceId.js';
6
+ import { ConstraintEventSequence, normalizeConstraintEventSequences } from './ConstraintEventSequences.js';
5
7
  import { IConstraintBuilder } from './IConstraintBuilder.js';
6
8
  import { IUniqueConstraintBuilder } from './IUniqueConstraintBuilder.js';
7
9
  import { UniqueConstraintBuilder, UniqueConstraintCapture } from './UniqueConstraintBuilder.js';
@@ -28,6 +30,8 @@ export interface ConstraintCapture {
28
30
  scope: ConstraintScopeCapture;
29
31
  uniqueConstraint?: UniqueConstraintCapture;
30
32
  uniqueEventType?: UniqueEventTypeCapture;
33
+ /** Identifiers of the event sequences the constraint applies to; empty or omitted means every event sequence. */
34
+ eventSequences?: string[];
31
35
  }
32
36
 
33
37
  /**
@@ -63,6 +67,18 @@ export class ConstraintBuilder implements IConstraintBuilder {
63
67
  return this;
64
68
  }
65
69
 
70
+ /** @inheritdoc */
71
+ forEventSequences(...eventSequenceIds: ConstraintEventSequence[]): IConstraintBuilder {
72
+ const eventSequences = normalizeConstraintEventSequences([...(this.capture.eventSequences ?? []), ...eventSequenceIds]);
73
+ if (eventSequences.length > 0) this.capture.eventSequences = eventSequences;
74
+ return this;
75
+ }
76
+
77
+ /** @inheritdoc */
78
+ forEventLog(): IConstraintBuilder {
79
+ return this.forEventSequences(EventSequenceId.eventLog);
80
+ }
81
+
66
82
  /** @inheritdoc */
67
83
  unique(callback: (builder: IUniqueConstraintBuilder) => void): IConstraintBuilder {
68
84
  const uniqueCapture: UniqueConstraintCapture = {
@@ -0,0 +1,41 @@
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 { EventSequenceId } from '../../eventSequences/EventSequenceId.js';
5
+
6
+ /** An event sequence a constraint can be scoped to, by identifier or by its string value. */
7
+ export type ConstraintEventSequence = EventSequenceId | string;
8
+
9
+ /**
10
+ * Normalizes declared event sequences to distinct, nonblank identifier values in declaration order.
11
+ * @param eventSequences - The declared event sequences.
12
+ * @returns The normalized identifier values.
13
+ */
14
+ export function normalizeConstraintEventSequences(eventSequences: readonly ConstraintEventSequence[]): string[] {
15
+ const values: string[] = [];
16
+ for (const eventSequence of eventSequences) {
17
+ const value = eventSequence instanceof EventSequenceId ? eventSequence.value : eventSequence;
18
+ if (typeof value !== 'string' || value.trim().length === 0 || values.includes(value)) continue;
19
+ values.push(value);
20
+ }
21
+ return values;
22
+ }
23
+
24
+ /**
25
+ * Combines the event sequences declared for one constraint in more than one place.
26
+ * An empty declaration means every event sequence and wins: combining never narrows a constraint that
27
+ * one declaration applies everywhere. Otherwise the result is the distinct union in declaration order.
28
+ * Mirrors ConstraintEventSequences.Combine in the .NET client.
29
+ * @param declarations - The declarations to combine; undefined or empty means every event sequence.
30
+ * @returns The combined declaration, empty for every event sequence.
31
+ */
32
+ export function combineConstraintEventSequences(declarations: ReadonlyArray<readonly string[] | undefined>): string[] {
33
+ if (declarations.length === 0 || declarations.some(declaration => !declaration || declaration.length === 0)) return [];
34
+ const combined: string[] = [];
35
+ for (const declaration of declarations) {
36
+ for (const value of declaration!) {
37
+ if (!combined.includes(value)) combined.push(value);
38
+ }
39
+ }
40
+ return combined;
41
+ }
@@ -15,12 +15,20 @@ import { getUniqueEventMetadata, getUniquePropertyMetadata } from './unique.js';
15
15
  import { getRemovedConstraintNames } from './removeConstraint.js';
16
16
  import { TypeIntrospector } from '../../types/TypeIntrospector.js';
17
17
  import { getEventTypeFor } from '../eventTypeDecorator.js';
18
+ import { combineConstraintEventSequences } from './ConstraintEventSequences.js';
18
19
 
19
20
  /** Resolves the name registered with the Chronicle Kernel. */
20
21
  function wireNameOf(capture: ConstraintCapture): string {
21
22
  return capture.uniqueEventType?.name ?? capture.name;
22
23
  }
23
24
 
25
+ /** Combine another declaration's event sequences into a capture; an empty result means every event sequence. */
26
+ function combineEventSequencesInto(capture: ConstraintCapture, declared: readonly string[] | undefined): void {
27
+ const combined = combineConstraintEventSequences([capture.eventSequences, declared]);
28
+ if (combined.length > 0) capture.eventSequences = combined;
29
+ else delete capture.eventSequences;
30
+ }
31
+
24
32
  const decoratorScope: ConstraintScopeCapture = {
25
33
  perEventSourceType: false,
26
34
  perEventStreamType: false,
@@ -63,6 +71,7 @@ export function compileConstraints(provider: Pick<IClientArtifactsProvider, 'eve
63
71
  for (const id of capture.uniqueEventType.removedWithEventTypeIds ?? []) {
64
72
  if (!removedWith.includes(id)) removedWith.push(id);
65
73
  }
74
+ combineEventSequencesInto(existing, capture.eventSequences);
66
75
  } else if (existing) {
67
76
  throw new Error(`Duplicate constraint name '${name}'.`);
68
77
  } else {
@@ -87,12 +96,14 @@ export function compileConstraints(provider: Pick<IClientArtifactsProvider, 'eve
87
96
  if (!capture) {
88
97
  const builder = new ConstraintBuilder(name);
89
98
  builder.uniqueFor(eventType, eventMetadata.message, name);
99
+ builder.forEventSequences(...(eventMetadata.eventSequences ?? []));
90
100
  capture = builder.capture;
91
101
  captures.set(name, capture);
92
102
  } else if (!capture.uniqueEventType) {
93
103
  throw new Error(`Constraint '${name}' is not a unique event type constraint.`);
94
104
  } else {
95
105
  assertMatchingScope(name, capture.scope, decoratorScope);
106
+ combineEventSequencesInto(capture, eventMetadata.eventSequences);
96
107
  if (eventMetadata.message && !capture.uniqueEventType.message) capture.uniqueEventType.message = eventMetadata.message;
97
108
  capture.uniqueEventType.eventTypeIds ??= [capture.uniqueEventType.eventTypeId];
98
109
  const id = getEventTypeFor(eventType).id.value;
@@ -108,8 +119,11 @@ export function compileConstraints(provider: Pick<IClientArtifactsProvider, 'eve
108
119
  if (!capture) {
109
120
  const builder = new ConstraintBuilder(name);
110
121
  builder.unique(() => {});
122
+ builder.forEventSequences(...(metadata.eventSequences ?? []));
111
123
  capture = builder.capture;
112
124
  captures.set(name, capture);
125
+ } else {
126
+ combineEventSequencesInto(capture, metadata.eventSequences);
113
127
  }
114
128
  if (!capture.uniqueConstraint) throw new Error(`Constraint '${name}' is not a unique property constraint.`);
115
129
  assertMatchingScope(name, capture.scope, decoratorScope);
@@ -188,6 +202,8 @@ export class Constraints implements IConstraints {
188
202
  EventStreamType: capture.scope.perEventStreamType ? '*' : '',
189
203
  EventStreamId: capture.scope.perEventStreamId ? '*' : ''
190
204
  };
205
+ // Empty means every event sequence, which is what the Kernel assumes when the field is omitted.
206
+ const eventSequences = [...new Set(capture.eventSequences ?? [])];
191
207
 
192
208
  if (capture.uniqueConstraint) {
193
209
  const uc = capture.uniqueConstraint;
@@ -210,7 +226,8 @@ export class Constraints implements IConstraints {
210
226
  },
211
227
  Value1: undefined
212
228
  },
213
- Scope: scope
229
+ Scope: scope,
230
+ EventSequences: eventSequences
214
231
  };
215
232
  }
216
233
 
@@ -226,7 +243,8 @@ export class Constraints implements IConstraints {
226
243
  EventTypeIds: uet.eventTypeIds ?? [uet.eventTypeId]
227
244
  }
228
245
  },
229
- Scope: scope
246
+ Scope: scope,
247
+ EventSequences: eventSequences
230
248
  };
231
249
  }
232
250
 
@@ -235,7 +253,8 @@ export class Constraints implements IConstraints {
235
253
  Type: ConstraintType.Unknown,
236
254
  RemovedWith: [],
237
255
  Definition: undefined,
238
- Scope: scope
256
+ Scope: scope,
257
+ EventSequences: eventSequences
239
258
  };
240
259
  });
241
260
 
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ import type { ConstraintEventSequence } from './ConstraintEventSequences.js';
4
5
  import { IUniqueConstraintBuilder } from './IUniqueConstraintBuilder.js';
5
6
 
6
7
  /**
@@ -26,6 +27,25 @@ export interface IConstraintBuilder {
26
27
  */
27
28
  perEventStreamId(): IConstraintBuilder;
28
29
 
30
+ /**
31
+ * Applies the constraints defined on this builder only to specific event sequences.
32
+ * By default a constraint applies to every event sequence its event types are appended to, and each
33
+ * sequence keeps its own index. The Kernel neither validates nor indexes a constraint for a sequence it does
34
+ * not apply to, so a fact forwarded to another sequence (such as the outbox) does not claim a value there.
35
+ * It applies to every constraint on this builder wherever in the chain it is called; calling it again adds
36
+ * to the event sequences already declared.
37
+ * @param eventSequenceIds - The event sequences the constraints apply to.
38
+ * @returns This builder for fluent chaining.
39
+ */
40
+ forEventSequences(...eventSequenceIds: ConstraintEventSequence[]): IConstraintBuilder;
41
+
42
+ /**
43
+ * Applies the constraints defined on this builder only to the event log.
44
+ * Shorthand for {@link IConstraintBuilder.forEventSequences} with `EventSequenceId.eventLog`.
45
+ * @returns This builder for fluent chaining.
46
+ */
47
+ forEventLog(): IConstraintBuilder;
48
+
29
49
  /**
30
50
  * Starts building a unique constraint using a fluent builder callback.
31
51
  * @param callback - Callback that configures the unique constraint via {@link IUniqueConstraintBuilder}.
@@ -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);