@cratis/chronicle 6.48.0 → 6.50.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 (261) hide show
  1. package/EventStore.ts +8 -2
  2. package/IEventStore.ts +8 -0
  3. package/artifacts/DefaultClientArtifactsProvider.ts +5 -0
  4. package/artifacts/IClientArtifactsProvider.ts +3 -0
  5. package/connection/ChronicleConnection.ts +6 -0
  6. package/connection/ChronicleServices.ts +2 -0
  7. package/dist/EventStore.d.ts +2 -0
  8. package/dist/EventStore.d.ts.map +1 -1
  9. package/dist/EventStore.js +7 -2
  10. package/dist/EventStore.js.map +1 -1
  11. package/dist/IEventStore.d.ts +7 -0
  12. package/dist/IEventStore.d.ts.map +1 -1
  13. package/dist/artifacts/DefaultClientArtifactsProvider.d.ts +2 -0
  14. package/dist/artifacts/DefaultClientArtifactsProvider.d.ts.map +1 -1
  15. package/dist/artifacts/DefaultClientArtifactsProvider.js +4 -0
  16. package/dist/artifacts/DefaultClientArtifactsProvider.js.map +1 -1
  17. package/dist/artifacts/IClientArtifactsProvider.d.ts +2 -0
  18. package/dist/artifacts/IClientArtifactsProvider.d.ts.map +1 -1
  19. package/dist/connection/ChronicleConnection.d.ts +1 -0
  20. package/dist/connection/ChronicleConnection.d.ts.map +1 -1
  21. package/dist/connection/ChronicleConnection.js +5 -1
  22. package/dist/connection/ChronicleConnection.js.map +1 -1
  23. package/dist/connection/ChronicleServices.d.ts +2 -1
  24. package/dist/connection/ChronicleServices.d.ts.map +1 -1
  25. package/dist/eventSequences/AppendOptions.d.ts +22 -0
  26. package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
  27. package/dist/eventSequences/EventForEventSourceId.d.ts +17 -0
  28. package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
  29. package/dist/eventSequences/EventLog.d.ts +2 -1
  30. package/dist/eventSequences/EventLog.d.ts.map +1 -1
  31. package/dist/eventSequences/EventLog.js +2 -2
  32. package/dist/eventSequences/EventLog.js.map +1 -1
  33. package/dist/eventSequences/EventSequence.d.ts +4 -1
  34. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  35. package/dist/eventSequences/EventSequence.js +59 -13
  36. package/dist/eventSequences/EventSequence.js.map +1 -1
  37. package/dist/eventSequences/ITransactionalEventSequence.d.ts +5 -2
  38. package/dist/eventSequences/ITransactionalEventSequence.d.ts.map +1 -1
  39. package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts +5 -0
  40. package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts.map +1 -0
  41. package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js +12 -0
  42. package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js.map +1 -0
  43. package/dist/eventSequences/TransactionalEventSequence.d.ts +3 -2
  44. package/dist/eventSequences/TransactionalEventSequence.d.ts.map +1 -1
  45. package/dist/eventSequences/TransactionalEventSequence.js +7 -4
  46. package/dist/eventSequences/TransactionalEventSequence.js.map +1 -1
  47. package/dist/eventSequences/createAppendNotification.d.ts +2 -1
  48. package/dist/eventSequences/createAppendNotification.d.ts.map +1 -1
  49. package/dist/eventSequences/createAppendNotification.js +3 -2
  50. package/dist/eventSequences/createAppendNotification.js.map +1 -1
  51. package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts +3 -0
  52. package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts.map +1 -0
  53. package/dist/eventSequences/ensureNamedTagBatchIsSupported.js +10 -0
  54. package/dist/eventSequences/ensureNamedTagBatchIsSupported.js.map +1 -0
  55. package/dist/eventSequences/index.d.ts +1 -0
  56. package/dist/eventSequences/index.d.ts.map +1 -1
  57. package/dist/eventSequences/index.js +1 -0
  58. package/dist/eventSequences/index.js.map +1 -1
  59. package/dist/eventSequences/prepareBatchAppend.d.ts +1 -0
  60. package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -1
  61. package/dist/eventSequences/prepareBatchAppend.js +4 -1
  62. package/dist/eventSequences/prepareBatchAppend.js.map +1 -1
  63. package/dist/eventSequences/prepareSingleAppend.d.ts +1 -0
  64. package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -1
  65. package/dist/eventSequences/prepareSingleAppend.js +3 -1
  66. package/dist/eventSequences/prepareSingleAppend.js.map +1 -1
  67. package/dist/eventSequences/resolveEventSourceRouting.d.ts +39 -0
  68. package/dist/eventSequences/resolveEventSourceRouting.d.ts.map +1 -0
  69. package/dist/eventSequences/resolveEventSourceRouting.js +92 -0
  70. package/dist/eventSequences/resolveEventSourceRouting.js.map +1 -0
  71. package/dist/eventSources/ConcurrencyDimensions.d.ts +19 -0
  72. package/dist/eventSources/ConcurrencyDimensions.d.ts.map +1 -0
  73. package/dist/eventSources/ConcurrencyDimensions.js +19 -0
  74. package/dist/eventSources/ConcurrencyDimensions.js.map +1 -0
  75. package/dist/eventSources/DuplicateEventSourceName.d.ts +6 -0
  76. package/dist/eventSources/DuplicateEventSourceName.d.ts.map +1 -0
  77. package/dist/eventSources/DuplicateEventSourceName.js +12 -0
  78. package/dist/eventSources/DuplicateEventSourceName.js.map +1 -0
  79. package/dist/eventSources/DuplicateEventStreamName.d.ts +7 -0
  80. package/dist/eventSources/DuplicateEventStreamName.d.ts.map +1 -0
  81. package/dist/eventSources/DuplicateEventStreamName.js +14 -0
  82. package/dist/eventSources/DuplicateEventStreamName.js.map +1 -0
  83. package/dist/eventSources/EventRoutingContradictsEventSource.d.ts +9 -0
  84. package/dist/eventSources/EventRoutingContradictsEventSource.d.ts.map +1 -0
  85. package/dist/eventSources/EventRoutingContradictsEventSource.js +18 -0
  86. package/dist/eventSources/EventRoutingContradictsEventSource.js.map +1 -0
  87. package/dist/eventSources/EventSourceDefinition.d.ts +20 -0
  88. package/dist/eventSources/EventSourceDefinition.d.ts.map +1 -0
  89. package/dist/eventSources/EventSourceDefinition.js +4 -0
  90. package/dist/eventSources/EventSourceDefinition.js.map +1 -0
  91. package/dist/eventSources/EventSources.d.ts +31 -0
  92. package/dist/eventSources/EventSources.d.ts.map +1 -0
  93. package/dist/eventSources/EventSources.js +90 -0
  94. package/dist/eventSources/EventSources.js.map +1 -0
  95. package/dist/eventSources/EventStreamDefinition.d.ts +13 -0
  96. package/dist/eventSources/EventStreamDefinition.d.ts.map +1 -0
  97. package/dist/eventSources/EventStreamDefinition.js +4 -0
  98. package/dist/eventSources/EventStreamDefinition.js.map +1 -0
  99. package/dist/eventSources/EventStreamDoesNotBelongToEventSource.d.ts +7 -0
  100. package/dist/eventSources/EventStreamDoesNotBelongToEventSource.d.ts.map +1 -0
  101. package/dist/eventSources/EventStreamDoesNotBelongToEventSource.js +14 -0
  102. package/dist/eventSources/EventStreamDoesNotBelongToEventSource.js.map +1 -0
  103. package/dist/eventSources/EventStreamRequiresEventSource.d.ts +6 -0
  104. package/dist/eventSources/EventStreamRequiresEventSource.d.ts.map +1 -0
  105. package/dist/eventSources/EventStreamRequiresEventSource.js +12 -0
  106. package/dist/eventSources/EventStreamRequiresEventSource.js.map +1 -0
  107. package/dist/eventSources/IEventSources.d.ts +27 -0
  108. package/dist/eventSources/IEventSources.d.ts.map +1 -0
  109. package/dist/eventSources/IEventSources.js +4 -0
  110. package/dist/eventSources/IEventSources.js.map +1 -0
  111. package/dist/eventSources/ResolvedEventRouting.d.ts +37 -0
  112. package/dist/eventSources/ResolvedEventRouting.d.ts.map +1 -0
  113. package/dist/eventSources/ResolvedEventRouting.js +60 -0
  114. package/dist/eventSources/ResolvedEventRouting.js.map +1 -0
  115. package/dist/eventSources/UnknownEventSource.d.ts +6 -0
  116. package/dist/eventSources/UnknownEventSource.d.ts.map +1 -0
  117. package/dist/eventSources/UnknownEventSource.js +12 -0
  118. package/dist/eventSources/UnknownEventSource.js.map +1 -0
  119. package/dist/eventSources/eventSource.d.ts +60 -0
  120. package/dist/eventSources/eventSource.d.ts.map +1 -0
  121. package/dist/eventSources/eventSource.js +72 -0
  122. package/dist/eventSources/eventSource.js.map +1 -0
  123. package/dist/eventSources/index.d.ts +17 -0
  124. package/dist/eventSources/index.d.ts.map +1 -0
  125. package/dist/eventSources/index.js +13 -0
  126. package/dist/eventSources/index.js.map +1 -0
  127. package/dist/events/EventContext.d.ts +12 -0
  128. package/dist/events/EventContext.d.ts.map +1 -1
  129. package/dist/events/InvalidNamedTag.d.ts +8 -0
  130. package/dist/events/InvalidNamedTag.d.ts.map +1 -0
  131. package/dist/events/InvalidNamedTag.js +13 -0
  132. package/dist/events/InvalidNamedTag.js.map +1 -0
  133. package/dist/events/NamedTag.d.ts +25 -0
  134. package/dist/events/NamedTag.d.ts.map +1 -0
  135. package/dist/events/NamedTag.js +39 -0
  136. package/dist/events/NamedTag.js.map +1 -0
  137. package/dist/events/WireEventContext.d.ts +2 -0
  138. package/dist/events/WireEventContext.d.ts.map +1 -1
  139. package/dist/events/index.d.ts +3 -0
  140. package/dist/events/index.d.ts.map +1 -1
  141. package/dist/events/index.js +3 -0
  142. package/dist/events/index.js.map +1 -1
  143. package/dist/events/mergeNamedTags.d.ts +13 -0
  144. package/dist/events/mergeNamedTags.d.ts.map +1 -0
  145. package/dist/events/mergeNamedTags.js +39 -0
  146. package/dist/events/mergeNamedTags.js.map +1 -0
  147. package/dist/events/toClientEventContext.d.ts.map +1 -1
  148. package/dist/events/toClientEventContext.js +4 -1
  149. package/dist/events/toClientEventContext.js.map +1 -1
  150. package/dist/index.d.ts +3 -1
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/index.js +3 -1
  153. package/dist/index.js.map +1 -1
  154. package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -1
  155. package/dist/projections/eventContextPropertyExpression.js +2 -0
  156. package/dist/projections/eventContextPropertyExpression.js.map +1 -1
  157. package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
  158. package/dist/testing/InProcessEventSequence.js +17 -7
  159. package/dist/testing/InProcessEventSequence.js.map +1 -1
  160. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  161. package/dist/testing/ReactorScenario.js +6 -0
  162. package/dist/testing/ReactorScenario.js.map +1 -1
  163. package/dist/testing/projections/ProjectionReadModelProcessor.d.ts.map +1 -1
  164. package/dist/testing/projections/ProjectionReadModelProcessor.js +2 -1
  165. package/dist/testing/projections/ProjectionReadModelProcessor.js.map +1 -1
  166. package/dist/testing/singleAppendRoute.d.ts +5 -0
  167. package/dist/testing/singleAppendRoute.d.ts.map +1 -1
  168. package/dist/testing/singleAppendRoute.js +12 -1
  169. package/dist/testing/singleAppendRoute.js.map +1 -1
  170. package/dist/transactions/IUnitOfWork.d.ts +4 -1
  171. package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
  172. package/dist/transactions/TransactionalAppendOptions.d.ts +14 -0
  173. package/dist/transactions/TransactionalAppendOptions.d.ts.map +1 -0
  174. package/dist/transactions/TransactionalAppendOptions.js +4 -0
  175. package/dist/transactions/TransactionalAppendOptions.js.map +1 -0
  176. package/dist/transactions/TransactionalEventRouting.d.ts +12 -0
  177. package/dist/transactions/TransactionalEventRouting.d.ts.map +1 -0
  178. package/dist/transactions/TransactionalEventRouting.js +4 -0
  179. package/dist/transactions/TransactionalEventRouting.js.map +1 -0
  180. package/dist/transactions/UnitOfWork.d.ts +2 -1
  181. package/dist/transactions/UnitOfWork.d.ts.map +1 -1
  182. package/dist/transactions/UnitOfWork.js +17 -2
  183. package/dist/transactions/UnitOfWork.js.map +1 -1
  184. package/dist/transactions/index.d.ts +2 -0
  185. package/dist/transactions/index.d.ts.map +1 -1
  186. package/dist/tsconfig.tsbuildinfo +1 -1
  187. package/dist/types/DecoratorType.d.ts +3 -1
  188. package/dist/types/DecoratorType.d.ts.map +1 -1
  189. package/dist/types/DecoratorType.js +2 -0
  190. package/dist/types/DecoratorType.js.map +1 -1
  191. package/eventSequences/AppendOptions.ts +25 -0
  192. package/eventSequences/EventForEventSourceId.ts +20 -0
  193. package/eventSequences/EventLog.ts +4 -2
  194. package/eventSequences/EventSequence.ts +62 -13
  195. package/eventSequences/ITransactionalEventSequence.ts +5 -2
  196. package/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.ts +12 -0
  197. package/eventSequences/TransactionalEventSequence.ts +8 -4
  198. package/eventSequences/createAppendNotification.ts +4 -2
  199. package/eventSequences/ensureNamedTagBatchIsSupported.ts +11 -0
  200. package/eventSequences/for_EventSequence/against_a_kernel/when_appending_named_tags.integration.spec.ts +107 -0
  201. package/eventSequences/for_EventSequence/when_appending_named_tags/given.fixture.ts +38 -0
  202. package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_batch.spec.ts +49 -0
  203. package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_single_event.spec.ts +47 -0
  204. package/eventSequences/for_EventSequence/when_appending_named_tags/with_an_invalid_tag.spec.ts +36 -0
  205. package/eventSequences/for_EventSequence/when_appending_named_tags/with_registered_sources.spec.ts +74 -0
  206. package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_named_tags.spec.ts +41 -0
  207. package/eventSequences/index.ts +1 -0
  208. package/eventSequences/prepareBatchAppend.ts +4 -1
  209. package/eventSequences/prepareSingleAppend.ts +3 -1
  210. package/eventSequences/resolveEventSourceRouting.ts +125 -0
  211. package/eventSources/ConcurrencyDimensions.ts +26 -0
  212. package/eventSources/DuplicateEventSourceName.ts +10 -0
  213. package/eventSources/DuplicateEventStreamName.ts +10 -0
  214. package/eventSources/EventRoutingContradictsEventSource.ts +10 -0
  215. package/eventSources/EventSourceDefinition.ts +27 -0
  216. package/eventSources/EventSources.ts +93 -0
  217. package/eventSources/EventStreamDefinition.ts +18 -0
  218. package/eventSources/EventStreamDoesNotBelongToEventSource.ts +10 -0
  219. package/eventSources/EventStreamRequiresEventSource.ts +10 -0
  220. package/eventSources/IEventSources.ts +33 -0
  221. package/eventSources/ResolvedEventRouting.ts +78 -0
  222. package/eventSources/UnknownEventSource.ts +10 -0
  223. package/eventSources/eventSource.ts +116 -0
  224. package/eventSources/for_EventSources/against_a_kernel/when_appending_through_registered_sources.integration.spec.ts +106 -0
  225. package/eventSources/for_EventSources/when_appending_through_a_definition.spec.ts +190 -0
  226. package/eventSources/for_EventSources/when_batch_requires_differing_guards.spec.ts +139 -0
  227. package/eventSources/for_EventSources/when_discovering_and_registering.spec.ts +94 -0
  228. package/eventSources/index.ts +19 -0
  229. package/events/EventContext.ts +14 -0
  230. package/events/InvalidNamedTag.ts +13 -0
  231. package/events/NamedTag.ts +44 -0
  232. package/events/WireEventContext.ts +2 -0
  233. package/events/for_EventContext/with_event_source.spec.ts +22 -0
  234. package/events/for_NamedTag/when_constructing/with_invalid_values.spec.ts +17 -0
  235. package/events/for_NamedTag/when_constructing/with_valid_values.spec.ts +19 -0
  236. package/events/for_mergeNamedTags/when_merging/with_event_and_call_tags.spec.ts +31 -0
  237. package/events/for_mergeNamedTags/when_merging/with_invalid_entries.spec.ts +17 -0
  238. package/events/for_toClientEventContext/when_converting_named_tags.spec.ts +31 -0
  239. package/events/index.ts +3 -0
  240. package/events/mergeNamedTags.ts +37 -0
  241. package/events/toClientEventContext.ts +4 -1
  242. package/for_EventStore/when_implementing_a_legacy_event_store.spec.ts +19 -0
  243. package/index.ts +3 -0
  244. package/package.json +6 -6
  245. package/projections/eventContextPropertyExpression.ts +3 -1
  246. package/testing/EventScenario.event-sources.spec.ts +34 -0
  247. package/testing/InProcessEventSequence.ts +17 -7
  248. package/testing/ReactorScenario.ts +6 -0
  249. package/testing/for_EventScenario/when_appending_named_tags/with_named_tags.spec.ts +48 -0
  250. package/testing/for_ReactorScenario/when_recording_side_effects/with_named_tags.spec.ts +119 -0
  251. package/testing/for_ReadModelScenario/when_seeding_with_options/with_named_tags.spec.ts +35 -0
  252. package/testing/projections/ProjectionReadModelProcessor.ts +2 -1
  253. package/testing/singleAppendRoute.ts +13 -1
  254. package/transactions/IUnitOfWork.ts +4 -1
  255. package/transactions/TransactionalAppendOptions.ts +17 -0
  256. package/transactions/TransactionalEventRouting.ts +16 -0
  257. package/transactions/UnitOfWork.ts +22 -2
  258. package/transactions/for_UnitOfWork/when_committing_events_with_event_source_routing.spec.ts +41 -0
  259. package/transactions/for_UnitOfWork/when_committing_events_with_named_tags.spec.ts +112 -0
  260. package/transactions/index.ts +2 -0
  261. package/types/DecoratorType.ts +4 -1
@@ -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';
@@ -215,7 +215,8 @@ export class InProcessEventSequence implements IEventSequence {
215
215
  subject: resolveStoredSubject(prepared.subject, eventSourceId), hash, causedBy: prepared.identity, observationState: EventObservationState.Initial,
216
216
  eventType: prepared.eventType, occurred, correlationId: prepared.correlationId.toString(),
217
217
  causation: prepared.causationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
218
- tags: prepared.tags.map(tag => new Tag(tag))
218
+ tags: prepared.tags.map(tag => new Tag(tag)),
219
+ namedTags: []
219
220
  }
220
221
  };
221
222
  const violations = this.validateConstraints([...this._history, ...(this._stagedSeeds ?? [])], [stored]);
@@ -254,10 +255,13 @@ export class InProcessEventSequence implements IEventSequence {
254
255
  for (const entry of entries) {
255
256
  if (typeof sourceOrEvents === 'string') break;
256
257
  if (!entry || typeof entry !== 'object' || Reflect.ownKeys(entry).some(key =>
257
- !['eventSourceId', 'event', 'eventSourceType', 'eventStreamType', 'eventStreamId', 'subject', 'occurred', 'tags'].includes(String(key)))) {
258
+ !['eventSourceId', 'event', 'eventSourceType', 'eventStreamType', 'eventStreamId', 'subject', 'occurred', 'tags', 'namedTags'].includes(String(key)))) {
258
259
  throw this.unsupported('appendMany.entry', this.id.value, 'Unrecognized per-entry metadata.');
259
260
  }
260
261
  const individual = entry as EventForEventSourceId;
262
+ if (hasNamedTags(individual.namedTags)) {
263
+ throw this.unsupported('appendMany.namedTags', this.id.value, 'Named tags are not fixture-backed.');
264
+ }
261
265
  if (individual.occurred !== undefined) this.checkedDate(individual.occurred, 'appendMany.occurred');
262
266
  if (individual.subject !== undefined && individual.subject !== null && typeof individual.subject !== 'string') {
263
267
  throw this.unsupported('appendMany.subject', this.id.value, 'Subject must be a string.');
@@ -271,7 +275,8 @@ export class InProcessEventSequence implements IEventSequence {
271
275
  try {
272
276
  const batchOptions: AppendOptions | undefined = shared?.correlationId === undefined && this._correlationId
273
277
  ? { correlationId: this._correlationId(), sourceType: shared?.sourceType, streamType: shared?.streamType,
274
- streamId: shared?.streamId, subject: shared?.subject, occurred: shared?.occurred, tags: shared?.tags }
278
+ streamId: shared?.streamId, subject: shared?.subject, occurred: shared?.occurred, tags: shared?.tags,
279
+ namedTags: shared?.namedTags }
275
280
  : shared;
276
281
  if (batchOptions?.correlationId !== undefined) this.validateGuid(batchOptions.correlationId, 'appendMany.correlationId');
277
282
  prepared = typeof sourceOrEvents === 'string'
@@ -318,7 +323,8 @@ export class InProcessEventSequence implements IEventSequence {
318
323
  eventStreamId: wire.EventStreamId || 'Default', subject: resolveStoredSubject(wire.Subject, eventSourceId), hash, causedBy: identity,
319
324
  observationState: EventObservationState.Initial, eventType, occurred, correlationId: correlationId.toString(),
320
325
  causation: batchCausationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
321
- tags: wire.Tags.map(tag => new Tag(tag))
326
+ tags: wire.Tags.map(tag => new Tag(tag)),
327
+ namedTags: []
322
328
  } };
323
329
  return { stored, result: this.success(sequenceNumber, event.constructor.name) };
324
330
  });
@@ -411,7 +417,10 @@ export class InProcessEventSequence implements IEventSequence {
411
417
  if (!options || typeof options !== 'object' || Array.isArray(options)) {
412
418
  throw this.unsupported('appendMany.options', this.id.value, 'Append options must be an object.');
413
419
  }
414
- const allowed = ['correlationId', 'sourceType', 'streamType', 'streamId', 'subject', 'occurred', 'tags'];
420
+ if (hasNamedTags(options.namedTags)) {
421
+ throw this.unsupported('appendMany.namedTags', this.id.value, 'Named tags are not fixture-backed.');
422
+ }
423
+ const allowed = ['correlationId', 'sourceType', 'streamType', 'streamId', 'subject', 'occurred', 'tags', 'namedTags'];
415
424
  if (Reflect.ownKeys(options).some(key => !allowed.includes(String(key))) ||
416
425
  options.concurrencyScope !== undefined || options.concurrencyScopes !== undefined || options.eventSourceId !== undefined) {
417
426
  throw this.unsupported('appendMany.options', this.id.value, 'Concurrency and unrecognized metadata are not fixture-backed.');
@@ -492,7 +501,8 @@ export class InProcessEventSequence implements IEventSequence {
492
501
  context: { ...event.context, eventType, occurred: new Date(event.context.occurred),
493
502
  causedBy: causedBy && new Identity(causedBy.subject, causedBy.name, causedBy.userName, causedBy.onBehalfOf),
494
503
  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)) }
504
+ tags: event.context.tags.map(item => new Tag(item.value)),
505
+ namedTags: [...(event.context.namedTags ?? [])] }
496
506
  };
497
507
  }
498
508
 
@@ -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,48 @@
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 { NamedTag } from '../../../events/NamedTag.js';
6
+ import { EventScenario, UnsupportedEventSequenceOperation } from '../../index.js';
7
+ import { artifacts, Claim, createEvents, rejection } from '../given/routed_scenario.fixture.js';
8
+
9
+ chai.should();
10
+ const namedTags = [new NamedTag('batch', 'b-1')];
11
+
12
+ describe('when appending a single event with named tags in an event scenario', () => {
13
+ let scenario: EventScenario;
14
+ let error: UnsupportedEventSequenceOperation;
15
+ beforeEach(async () => {
16
+ scenario = createEvents();
17
+ error = await rejection(() => scenario.append('source', new Claim('one'), { namedTags }));
18
+ });
19
+ it('should reject the named tags explicitly', () => error.message.should.contain('append.namedTags'));
20
+ it('should not append the event', () => scenario.appendedEvents.should.have.lengthOf(0));
21
+ });
22
+
23
+ describe('when appending a batch with shared named tags in an event scenario', () => {
24
+ let error: UnsupportedEventSequenceOperation;
25
+ beforeEach(async () => {
26
+ error = await rejection(() => createEvents().appendMany('source', [new Claim('one')], { namedTags }));
27
+ });
28
+ it('should reject the named tags explicitly', () => error.message.should.contain('appendMany.namedTags'));
29
+ });
30
+
31
+ describe('when appending a batch entry with named tags in an event scenario', () => {
32
+ let error: UnsupportedEventSequenceOperation;
33
+ beforeEach(async () => {
34
+ error = await rejection(() => createEvents().appendMany([{ eventSourceId: 'source', event: new Claim('one'), namedTags }]));
35
+ });
36
+ it('should reject the named tags explicitly', () => error.message.should.contain('appendMany.namedTags'));
37
+ });
38
+
39
+ describe('when appending with an empty named tag list in an event scenario', () => {
40
+ let scenario: EventScenario;
41
+ beforeEach(async () => {
42
+ scenario = new EventScenario({ artifacts });
43
+ await scenario.append('source', new Claim('one'), { namedTags: [] });
44
+ });
45
+ it('should append the event', () => scenario.appendedEvents.should.have.lengthOf(1));
46
+ it('should expose an empty named tag list on the stored context like a kernel delivery', () =>
47
+ scenario.appendedEvents[0].context.namedTags!.should.deep.equal([]));
48
+ });
@@ -0,0 +1,119 @@
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 { beforeEach, chai, describe, it } from 'vitest';
6
+ import { eventType } from '../../../events/eventTypeDecorator.js';
7
+ import { InvalidNamedTag } from '../../../events/InvalidNamedTag.js';
8
+ import { NamedTag } from '../../../events/NamedTag.js';
9
+ import { NamedTagsWithRegisteredEventSourceNotSupported } from '../../../eventSequences/index.js';
10
+ import type { EventContext } from '../../../events/EventContext.js';
11
+ import { reactor } from '../../../reactors/reactor.js';
12
+ import { ReactorScenario } from '../../ReactorScenario.js';
13
+
14
+ chai.should();
15
+ @eventType('scenario-named-tag-input')
16
+ class NamedTagInput { @field(String) name = 'input'; }
17
+ @eventType('scenario-named-tag-output')
18
+ class NamedTagOutput { @field(String) name = 'output'; }
19
+
20
+ const contexts: EventContext[] = [];
21
+ @reactor('scenario-named-tag-reactor')
22
+ class NamedTagReactor {
23
+ namedTagInput(_: NamedTagInput, context: EventContext) {
24
+ contexts.push(context);
25
+ return { eventSourceId: 'other', event: new NamedTagOutput(), namedTags: [new NamedTag('origin', context.eventSourceId)] };
26
+ }
27
+ }
28
+ @reactor('scenario-invalid-named-tag-reactor')
29
+ class InvalidNamedTagReactor {
30
+ namedTagInput() {
31
+ return { eventSourceId: 'other', event: new NamedTagOutput(), namedTags: [{ name: ' ', value: 'x' }] };
32
+ }
33
+ }
34
+ @reactor('scenario-named-tags-registered-source-same-entry')
35
+ class NamedTagRegisteredSourceReactor {
36
+ namedTagInput() {
37
+ return { eventSourceId: 'other', event: new NamedTagOutput(), eventSource: 'Account', namedTags: [new NamedTag('origin', 'trigger')] };
38
+ }
39
+ }
40
+ @reactor('scenario-named-tags-registered-source-cross-entry')
41
+ class NamedTagCrossEntryReactor {
42
+ namedTagInput() {
43
+ return [
44
+ { eventSourceId: 'tagged', event: new NamedTagOutput(), namedTags: [new NamedTag('origin', 'trigger')] },
45
+ { eventSourceId: 'registered', event: new NamedTagOutput(), eventSource: 'Account' }
46
+ ];
47
+ }
48
+ }
49
+ @reactor('scenario-named-tags-registered-source-cross-entry-reversed')
50
+ class NamedTagCrossEntryReversedReactor {
51
+ namedTagInput() {
52
+ return [
53
+ { eventSourceId: 'registered', event: new NamedTagOutput(), eventSource: 'Account' },
54
+ { eventSourceId: 'tagged', event: new NamedTagOutput(), namedTags: [new NamedTag('origin', 'trigger')] }
55
+ ];
56
+ }
57
+ }
58
+ @reactor('scenario-empty-named-tags-registered-source')
59
+ class EmptyNamedTagsRegisteredSourceReactor {
60
+ namedTagInput() {
61
+ return { eventSourceId: 'other', event: new NamedTagOutput(), eventSource: 'Account', namedTags: [] };
62
+ }
63
+ }
64
+ const artifacts = { eventTypes: [NamedTagInput, NamedTagOutput] };
65
+
66
+ describe('when recording a side effect with named tags', () => {
67
+ let scenario: ReactorScenario;
68
+ beforeEach(async () => {
69
+ contexts.length = 0;
70
+ scenario = new ReactorScenario(NamedTagReactor, { artifacts, constraints: 'disabled' });
71
+ await scenario.when.forEventSource('trigger').events(new NamedTagInput());
72
+ });
73
+ it('should record the named tags on the side-effect target', () =>
74
+ scenario.sideEffects[0].target.namedTags!.map(tag => `${tag.name}=${tag.value}`).should.deep.equal(['origin=trigger']));
75
+ it('should deliver an empty named tag list like a kernel delivery of an untagged event', () =>
76
+ contexts[0].namedTags!.should.deep.equal([]));
77
+ });
78
+
79
+ describe('when recording a side effect with an invalid named tag', () => {
80
+ let error: unknown;
81
+ beforeEach(async () => {
82
+ const scenario = new ReactorScenario(InvalidNamedTagReactor, { artifacts, constraints: 'disabled' });
83
+ error = await scenario.when.forEventSource('trigger').events(new NamedTagInput()).catch(caught => caught);
84
+ });
85
+ it('should fail the delivery with an invalid named tag error, as production append does', () =>
86
+ (error as Error).should.be.instanceOf(InvalidNamedTag));
87
+ });
88
+
89
+ for (const [description, reactorType] of [
90
+ ['the same entry', NamedTagRegisteredSourceReactor],
91
+ ['a tagged entry before a registered entry', NamedTagCrossEntryReactor],
92
+ ['a registered entry before a tagged entry', NamedTagCrossEntryReversedReactor]
93
+ ] as const) {
94
+ describe(`when recording named tags and a registered source in ${description}`, () => {
95
+ let scenario: ReactorScenario;
96
+ let error: unknown;
97
+ beforeEach(async () => {
98
+ scenario = new ReactorScenario(reactorType, { artifacts, constraints: 'disabled' });
99
+ error = await scenario.when.forEventSource('trigger').events(new NamedTagInput()).catch(caught => caught);
100
+ });
101
+ it('should reject with the production unsupported combination error', () =>
102
+ (error as Error).should.be.instanceOf(NamedTagsWithRegisteredEventSourceNotSupported));
103
+ it('should record an unsuccessful delivery', () => scenario.results[0].completed.should.be.false);
104
+ it('should retain the rejection on the delivery result', () => scenario.results[0].error!.should.equal(error));
105
+ it('should not record any returned side effect', () => scenario.sideEffects.should.have.lengthOf(0));
106
+ it('should not report any produced event', () => scenario.produced.should.have.lengthOf(0));
107
+ });
108
+ }
109
+
110
+ describe('when recording a registered source with empty named tags', () => {
111
+ let scenario: ReactorScenario;
112
+ beforeEach(async () => {
113
+ scenario = new ReactorScenario(EmptyNamedTagsRegisteredSourceReactor, { artifacts, constraints: 'disabled' });
114
+ await scenario.when.forEventSource('trigger').events(new NamedTagInput());
115
+ });
116
+ it('should complete the delivery', () => scenario.results[0].completed.should.be.true);
117
+ it('should retain the registered source on the recorded effect', () => scenario.sideEffects[0].target.eventSource!.should.equal('Account'));
118
+ it('should retain the empty named tag array', () => scenario.sideEffects[0].target.namedTags!.should.deep.equal([]));
119
+ });
@@ -0,0 +1,35 @@
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 { NamedTag } from '../../../events/index.js';
6
+ import { reducer } from '../../../reducers/reducer.js';
7
+ import { ReadModelScenario, UnsupportedEventSequenceOperation } from '../../index.js';
8
+ import { Claim, rejection } from '../../for_EventScenario/given/routed_scenario.fixture.js';
9
+
10
+ chai.should();
11
+ class Count { claimed = 0; }
12
+ class Counter { claim(_event: Claim, current: Count | undefined): Count { return { claimed: (current?.claimed ?? 0) + 1 }; } }
13
+ reducer('NamedTagSetupCounter', undefined, Count)(Counter);
14
+ const create = () => new ReadModelScenario(Count, { eventTypes: [Claim], reducers: [Counter], projections: [] });
15
+
16
+ describe('when read-model setup receives non-empty named tags', () => {
17
+ let scenario: ReadModelScenario<Count>;
18
+ let error: Error;
19
+ beforeEach(async () => {
20
+ scenario = create();
21
+ error = await rejection(() => scenario.given.forEventSource('A', { namedTags: [new NamedTag('batch', 'b-1')] }).events(new Claim('Rejected')));
22
+ });
23
+ it('should reject with the typed unsupported operation error', () => error.should.be.instanceOf(UnsupportedEventSequenceOperation));
24
+ it('should identify the append named tags operation', () => error.message.should.contain('UnsupportedEventSequenceOperation: append.namedTags (Claim)'));
25
+ it('should not collect the rejected event', async () => ((await scenario.instance) === null).should.be.true);
26
+ });
27
+
28
+ describe('when read-model setup receives an empty named tag array', () => {
29
+ let scenario: ReadModelScenario<Count>;
30
+ beforeEach(() => {
31
+ scenario = create();
32
+ scenario.given.forEventSource('A', { namedTags: [] }).events(new Claim('Accepted'));
33
+ });
34
+ it('should accept and fold the event', async () => (await scenario.instance)!.claimed.should.equal(1));
35
+ });
@@ -138,7 +138,8 @@ export class ProjectionReadModelProcessor<TReadModel extends object> implements
138
138
  if (!state) {
139
139
  // SetInitialState: an event only a child subscribes to creates an uninitialized parent.
140
140
  state = from ? this.initialState(key, event) : this.identityState(key, event, false);
141
- } else if (state.__initialized === false) {
141
+ } else if (state.__initialized === false && from) {
142
+ // Chronicle 19.30.0 no longer initializes a parent on events only a child subscribes to.
142
143
  for (const [name, value] of Object.entries(this.initialValues())) if (!(name in state)) state[name] = value;
143
144
  state.__initialized = true;
144
145
  }
@@ -4,6 +4,14 @@
4
4
  import type { AppendOptions } from '../eventSequences/AppendOptions.js';
5
5
  import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOperation.js';
6
6
 
7
+ /**
8
+ * Whether a named-tag option asks for anything. An explicitly empty array carries no tags, exactly like an
9
+ * omitted option; anything else is reported, including malformed values the production client would reject.
10
+ */
11
+ export function hasNamedTags(namedTags: unknown): boolean {
12
+ return namedTags !== undefined && !(Array.isArray(namedTags) && namedTags.length === 0);
13
+ }
14
+
7
15
  /** The fixture-proven single-append route boundary, shared with read-model setup. */
8
16
  export function singleAppendRoute(options: AppendOptions | undefined, artifact: string,
9
17
  unsupported = (operation: string, artifact: string, reason: string) => new UnsupportedEventSequenceOperation(operation, artifact, reason),
@@ -12,8 +20,12 @@ export function singleAppendRoute(options: AppendOptions | undefined, artifact:
12
20
  if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options))) {
13
21
  throw unsupported('append.options', artifact, 'Append options must be an object.');
14
22
  }
23
+ if (options && hasNamedTags(options.namedTags)) {
24
+ throw unsupported('append.namedTags', artifact, 'Named tags are not fixture-backed.');
25
+ }
15
26
  const allowed = ['sourceType', 'streamType', 'streamId', ...(allowSubject ? ['subject'] : [])];
16
- if (options && (Reflect.ownKeys(options).some(key => !allowed.includes(String(key))) ||
27
+ // An explicitly empty namedTags array is accepted (checked above) but not advertised as a supported option.
28
+ if (options && (Reflect.ownKeys(options).some(key => key !== 'namedTags' && !allowed.includes(String(key))) ||
17
29
  options.correlationId !== undefined || (!allowSubject && options.subject !== undefined) || options.occurred !== undefined ||
18
30
  options.eventSourceId !== undefined || options.concurrencyScope !== undefined || options.tags !== undefined ||
19
31
  options.concurrencyScopes !== undefined)) {
@@ -6,6 +6,7 @@ import { AppendError } from '../eventSequences/AppendError.js';
6
6
  import { ConcurrencyViolation } from '../eventSequences/ConcurrencyViolation.js';
7
7
  import { ConstraintViolation } from '../eventSequences/ConstraintViolation.js';
8
8
  import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
9
+ import type { TransactionalAppendOptions } from './TransactionalAppendOptions.js';
9
10
  import { AppendResult } from '../eventSequences/AppendResult.js';
10
11
 
11
12
  /**
@@ -26,8 +27,10 @@ export interface IUnitOfWork {
26
27
  * @param eventSequenceId - The identifier of the event sequence to append to.
27
28
  * @param eventSourceId - The event source identifier.
28
29
  * @param event - The event payload.
30
+ * @param options - Optional registered event source / stream routing and named tags for this event.
31
+ * @throws {@link InvalidNamedTag} when a named tag is invalid; the event is not added.
29
32
  */
30
- addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object): void;
33
+ addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object, options?: TransactionalAppendOptions): void;
31
34
 
32
35
  /**
33
36
  * Gets all events currently buffered in this unit of work.
@@ -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 type { NamedTag } from '../events/NamedTag.js';
5
+ import type { TransactionalEventRouting } from './TransactionalEventRouting.js';
6
+
7
+ /**
8
+ * Optional metadata for an event added to a unit of work: registered event source routing and
9
+ * structured named tags. Carried through to the append on commit.
10
+ */
11
+ export interface TransactionalAppendOptions extends TransactionalEventRouting {
12
+ /**
13
+ * Structured named tags for the event. Validated when the event is added, so an invalid tag fails
14
+ * before anything is committed; the first occurrence of each exact name and value pair is kept.
15
+ */
16
+ readonly namedTags?: ReadonlyArray<NamedTag>;
17
+ }
@@ -0,0 +1,16 @@
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 { Constructor } from '@cratis/fundamentals';
5
+
6
+ /**
7
+ * Optional registered event source routing for an event added to a unit of work.
8
+ * Carried through to the append on commit, where it is validated like any other append.
9
+ */
10
+ export interface TransactionalEventRouting {
11
+ /** Registered event source definition (class decorated with `@eventSource`, or its name). */
12
+ readonly eventSource?: Constructor | string;
13
+
14
+ /** Stream name declared by {@link eventSource}. */
15
+ readonly eventStream?: string;
16
+ }
@@ -8,7 +8,11 @@ import { ConcurrencyViolation } from '../eventSequences/ConcurrencyViolation.js'
8
8
  import { ConstraintViolation } from '../eventSequences/ConstraintViolation.js';
9
9
  import { EventForEventSourceId } from '../eventSequences/EventForEventSourceId.js';
10
10
  import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
11
+ import { planDerivedGuards, resolveBatchRouting } from '../eventSequences/resolveEventSourceRouting.js';
12
+ import { ensureNamedTagBatchIsSupported } from '../eventSequences/ensureNamedTagBatchIsSupported.js';
11
13
  import { IEventStore } from '../IEventStore.js';
14
+ import type { TransactionalAppendOptions } from './TransactionalAppendOptions.js';
15
+ import { mergeNamedTags } from '../events/mergeNamedTags.js';
12
16
  import { IUnitOfWork } from './IUnitOfWork.js';
13
17
 
14
18
  interface EventForEventSourceIdWithSequenceNumber {
@@ -46,14 +50,18 @@ export class UnitOfWork implements IUnitOfWork {
46
50
  }
47
51
 
48
52
  /** @inheritdoc */
49
- addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object): void {
53
+ addEvent(eventSequenceId: EventSequenceId, eventSourceId: string, event: object, options?: TransactionalAppendOptions): void {
50
54
  this.throwIfCompleted();
55
+ const namedTags = mergeNamedTags(options?.namedTags);
51
56
  this._events.push({
52
57
  sequenceNumber: this._events.length,
53
58
  eventSequenceId,
54
59
  eventForEventSourceId: {
55
60
  eventSourceId,
56
- event
61
+ event,
62
+ ...(options?.eventSource !== undefined ? { eventSource: options.eventSource } : {}),
63
+ ...(options?.eventStream !== undefined ? { eventStream: options.eventStream } : {}),
64
+ ...(namedTags.length > 0 ? { namedTags } : {})
57
65
  }
58
66
  });
59
67
  }
@@ -90,6 +98,11 @@ export class UnitOfWork implements IUnitOfWork {
90
98
  this.throwIfCompleted();
91
99
 
92
100
  if (this._events.length > 0) {
101
+ // Validate the entire commit before writing any sequence, including combinations in different groups.
102
+ ensureNamedTagBatchIsSupported(
103
+ this._events.some(entry => (entry.eventForEventSourceId.namedTags?.length ?? 0) > 0),
104
+ this._events.some(entry => entry.eventForEventSourceId.eventSource !== undefined)
105
+ );
93
106
  const resultsBySequenceNumber = new Map<number, AppendResult>();
94
107
  const eventsByEventSequence = new Map<string, EventForEventSourceIdWithSequenceNumber[]>();
95
108
 
@@ -100,6 +113,13 @@ export class UnitOfWork implements IUnitOfWork {
100
113
  eventsByEventSequence.set(key, events);
101
114
  }
102
115
 
116
+ // Resolve routing and required guards for every group first: a rejection must not follow a written group.
117
+ const eventSources = this._eventStore.eventSources;
118
+ for (const eventsForSequence of eventSources ? eventsByEventSequence.values() : []) {
119
+ const events = eventsForSequence.map(_ => _.eventForEventSourceId);
120
+ planDerivedGuards(events.map(_ => resolveBatchRouting(eventSources, _)), events, undefined);
121
+ }
122
+
103
123
  for (const [eventSequenceId, eventsForSequence] of eventsByEventSequence) {
104
124
  const sequence = this._eventStore.getEventSequence(new EventSequenceId(eventSequenceId));
105
125
  const appendResults = await sequence.appendMany(
@@ -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 { Guid } from '@cratis/fundamentals';
5
+ import { beforeEach, chai, describe, it, vi } from 'vitest';
6
+ import { EventSequenceId } from '../../eventSequences/index.js';
7
+ import type { IEventStore } from '../../IEventStore.js';
8
+ import { UnitOfWork } from '../UnitOfWork.js';
9
+
10
+ const should = chai.should();
11
+
12
+ describe('when committing events with event source routing in a unit of work', () => {
13
+ let batch: any[];
14
+ beforeEach(async () => {
15
+ const appendMany = vi.fn().mockResolvedValue([{ isSuccess: true, constraintViolations: [], errors: [] }, { isSuccess: true, constraintViolations: [], errors: [] }]);
16
+ const store = { getEventSequence: () => ({ appendMany }) } as unknown as IEventStore;
17
+ const unitOfWork = new UnitOfWork(Guid.create(), () => {}, store);
18
+ unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { first: 1 }, { eventSource: 'Account', eventStream: 'Transactions' });
19
+ unitOfWork.addEvent(EventSequenceId.eventLog, 'c-1', { second: 2 }, { eventSource: 'Customer' });
20
+ await unitOfWork.commit();
21
+ batch = appendMany.mock.calls[0][0];
22
+ });
23
+ it('should keep the source and stream of the first event', () => {
24
+ batch[0].eventSource.should.equal('Account');
25
+ batch[0].eventStream.should.equal('Transactions');
26
+ });
27
+ it('should keep a different source on the second event without a stream', () => {
28
+ batch[1].eventSource.should.equal('Customer');
29
+ should.not.exist(batch[1].eventStream);
30
+ });
31
+ });
32
+
33
+ describe('when adding an event to a unit of work without routing', () => {
34
+ it('should not add routing keys to the buffered event', async () => {
35
+ const appendMany = vi.fn().mockResolvedValue([{ isSuccess: true, constraintViolations: [], errors: [] }]);
36
+ const unitOfWork = new UnitOfWork(Guid.create(), () => {}, { getEventSequence: () => ({ appendMany }) } as unknown as IEventStore);
37
+ unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', {});
38
+ await unitOfWork.commit();
39
+ Object.keys(appendMany.mock.calls[0][0][0]).sort().should.deep.equal(['event', 'eventSourceId']);
40
+ });
41
+ });
@@ -0,0 +1,112 @@
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 { Guid } from '@cratis/fundamentals';
5
+ import { beforeEach, chai, describe, it, vi } from 'vitest';
6
+ import { InvalidNamedTag, NamedTag } from '../../events/index.js';
7
+ import { EventSequenceId, NamedTagsWithRegisteredEventSourceNotSupported, type EventForEventSourceId } from '../../eventSequences/index.js';
8
+ import { ConcurrencyDimensions, type IEventSources } from '../../eventSources/index.js';
9
+ import type { IEventStore } from '../../IEventStore.js';
10
+ import { UnitOfWork } from '../UnitOfWork.js';
11
+
12
+ const should = chai.should();
13
+ const success = { isSuccess: true, constraintViolations: [], errors: [] };
14
+
15
+ class Account {}
16
+ const eventSources = {
17
+ getFor: () => ({ type: Account, name: 'Account', description: '', concurrency: ConcurrencyDimensions.none, streams: [] })
18
+ } as unknown as IEventSources;
19
+
20
+ function a_unit_of_work() {
21
+ const appendMany = vi.fn().mockImplementation(async (events: EventForEventSourceId[]) => events.map(() => success));
22
+ const completed = vi.fn();
23
+ const unitOfWork = new UnitOfWork(Guid.create(), completed, { eventSources, getEventSequence: () => ({ appendMany }) } as unknown as IEventStore);
24
+ return { unitOfWork, appendMany, completed };
25
+ }
26
+
27
+ describe('when committing events with named tags in a unit of work', () => {
28
+ let batch: EventForEventSourceId[];
29
+ beforeEach(async () => {
30
+ const { unitOfWork, appendMany } = a_unit_of_work();
31
+ unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', { first: 1 },
32
+ { namedTags: [new NamedTag('batch', 'b-1'), new NamedTag('batch', 'b-1')] });
33
+ unitOfWork.addEvent(EventSequenceId.eventLog, 'a-2', { second: 2 }, { namedTags: [] });
34
+ await unitOfWork.commit();
35
+ batch = appendMany.mock.calls[0][0];
36
+ });
37
+ it('should carry the distinct named tags on the event they were added with', () =>
38
+ batch[0].namedTags!.map(tag => `${tag.name}=${tag.value}`).should.deep.equal(['batch=b-1']));
39
+ it('should not add named tags to an event added with none', () => should.not.exist(batch[1].namedTags));
40
+ });
41
+
42
+ describe('when adding an event with an invalid named tag to a unit of work', () => {
43
+ let error: unknown;
44
+ let unitOfWork: UnitOfWork;
45
+ beforeEach(() => {
46
+ ({ unitOfWork } = a_unit_of_work());
47
+ try {
48
+ unitOfWork.addEvent(EventSequenceId.eventLog, 'a-1', {}, { namedTags: [{ name: ' ', value: 'x' } as NamedTag] });
49
+ } catch (caught) {
50
+ error = caught;
51
+ }
52
+ });
53
+ it('should fail with an invalid named tag error', () => (error as Error).should.be.instanceOf(InvalidNamedTag));
54
+ it('should not buffer the event', () => unitOfWork.getEvents().should.have.lengthOf(0));
55
+ });
56
+
57
+ for (const separateSequences of [false, true]) {
58
+ describe(`when committing named tags and a registered source in ${separateSequences ? 'different event sequences' : 'one batch'}`, () => {
59
+ let calls: ReturnType<typeof a_unit_of_work>;
60
+ let error: unknown;
61
+ beforeEach(async () => {
62
+ calls = a_unit_of_work();
63
+ // An otherwise valid group must not be written before a later unsupported combination is found.
64
+ calls.unitOfWork.addEvent(EventSequenceId.eventLog, 'plain', { first: 1 });
65
+ const sequenceId = separateSequences ? new EventSequenceId('other') : EventSequenceId.eventLog;
66
+ calls.unitOfWork.addEvent(sequenceId, 'tagged', { second: 2 }, { namedTags: [new NamedTag('batch', 'b-1')] });
67
+ calls.unitOfWork.addEvent(sequenceId, 'registered', { third: 3 }, { eventSource: Account });
68
+ error = await calls.unitOfWork.commit().catch(caught => caught);
69
+ });
70
+ it('should reject with the typed unsupported combination error', () =>
71
+ (error as Error).should.be.instanceOf(NamedTagsWithRegisteredEventSourceNotSupported));
72
+ it('should not write any group', () => calls.appendMany.mock.calls.should.have.lengthOf(0));
73
+ it('should leave the unit of work uncompleted', () => calls.unitOfWork.isCompleted.should.be.false);
74
+ it('should not invoke completion callbacks', () => calls.completed.mock.calls.should.have.lengthOf(0));
75
+ it('should retain the buffered events for rollback', () => calls.unitOfWork.getEvents().should.have.lengthOf(3));
76
+ });
77
+ }
78
+
79
+ describe('when committing tags and registered sources on different event sequences', () => {
80
+ let calls: ReturnType<typeof a_unit_of_work>;
81
+ let error: unknown;
82
+ beforeEach(async () => {
83
+ calls = a_unit_of_work();
84
+ calls.unitOfWork.addEvent(EventSequenceId.eventLog, 'tagged', {}, { namedTags: [new NamedTag('batch', 'b-1')] });
85
+ calls.unitOfWork.addEvent(new EventSequenceId('other'), 'registered', {}, { eventSource: 'Account' });
86
+ error = await calls.unitOfWork.commit().catch(caught => caught);
87
+ });
88
+ it('should reject the entire commit', () => (error as Error).should.be.instanceOf(NamedTagsWithRegisteredEventSourceNotSupported));
89
+ it('should not append the first event sequence', () => calls.appendMany.mock.calls.should.have.lengthOf(0));
90
+ });
91
+
92
+ describe('when committing a registered source and named tags on one event', () => {
93
+ let error: unknown;
94
+ beforeEach(async () => {
95
+ const { unitOfWork } = a_unit_of_work();
96
+ unitOfWork.addEvent(EventSequenceId.eventLog, 'registered', {}, { eventSource: 'Account', namedTags: [new NamedTag('batch', 'b-1')] });
97
+ error = await unitOfWork.commit().catch(caught => caught);
98
+ });
99
+ it('should reject with the typed unsupported combination error', () =>
100
+ (error as Error).should.be.instanceOf(NamedTagsWithRegisteredEventSourceNotSupported));
101
+ });
102
+
103
+ describe('when committing a registered source with an empty named tag array', () => {
104
+ let calls: ReturnType<typeof a_unit_of_work>;
105
+ beforeEach(async () => {
106
+ calls = a_unit_of_work();
107
+ calls.unitOfWork.addEvent(EventSequenceId.eventLog, 'registered', {}, { eventSource: 'Account', namedTags: [] });
108
+ await calls.unitOfWork.commit();
109
+ });
110
+ it('should append the registered source', () => calls.appendMany.mock.calls[0][0][0].eventSource.should.equal('Account'));
111
+ it('should complete the unit of work', () => calls.unitOfWork.isCompleted.should.be.true);
112
+ });
@@ -6,3 +6,5 @@ export type { IUnitOfWorkManager } from './IUnitOfWorkManager.js';
6
6
  export { NoUnitOfWorkHasBeenStarted } from './NoUnitOfWorkHasBeenStarted.js';
7
7
  export { UnitOfWork } from './UnitOfWork.js';
8
8
  export { UnitOfWorkManager } from './UnitOfWorkManager.js';
9
+ export type { TransactionalEventRouting } from './TransactionalEventRouting.js';
10
+ export type { TransactionalAppendOptions } from './TransactionalAppendOptions.js';
@@ -33,5 +33,8 @@ export enum DecoratorType {
33
33
  EventTypeMigration = 'eventTypeMigration',
34
34
 
35
35
  /** Global read model variant handler artifacts discovered through the globalFor decorator. */
36
- GlobalForHandler = 'globalForHandler'
36
+ GlobalForHandler = 'globalForHandler',
37
+
38
+ /** Event source definition artifacts discovered through the eventSource decorator. */
39
+ EventSource = 'eventSource'
37
40
  }