@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.
- package/EventStore.ts +8 -2
- package/IEventStore.ts +8 -0
- package/artifacts/DefaultClientArtifactsProvider.ts +5 -0
- package/artifacts/IClientArtifactsProvider.ts +3 -0
- package/connection/ChronicleConnection.ts +6 -0
- package/connection/ChronicleServices.ts +2 -0
- package/dist/EventStore.d.ts +2 -0
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +7 -2
- package/dist/EventStore.js.map +1 -1
- package/dist/IEventStore.d.ts +7 -0
- package/dist/IEventStore.d.ts.map +1 -1
- package/dist/artifacts/DefaultClientArtifactsProvider.d.ts +2 -0
- package/dist/artifacts/DefaultClientArtifactsProvider.d.ts.map +1 -1
- package/dist/artifacts/DefaultClientArtifactsProvider.js +4 -0
- package/dist/artifacts/DefaultClientArtifactsProvider.js.map +1 -1
- package/dist/artifacts/IClientArtifactsProvider.d.ts +2 -0
- package/dist/artifacts/IClientArtifactsProvider.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.d.ts +1 -0
- package/dist/connection/ChronicleConnection.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.js +5 -1
- package/dist/connection/ChronicleConnection.js.map +1 -1
- package/dist/connection/ChronicleServices.d.ts +2 -1
- package/dist/connection/ChronicleServices.d.ts.map +1 -1
- package/dist/eventSequences/AppendOptions.d.ts +22 -0
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +17 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventLog.d.ts +2 -1
- package/dist/eventSequences/EventLog.d.ts.map +1 -1
- package/dist/eventSequences/EventLog.js +2 -2
- package/dist/eventSequences/EventLog.js.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts +4 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +59 -13
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/ITransactionalEventSequence.d.ts +5 -2
- package/dist/eventSequences/ITransactionalEventSequence.d.ts.map +1 -1
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts +5 -0
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.d.ts.map +1 -0
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js +12 -0
- package/dist/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.js.map +1 -0
- package/dist/eventSequences/TransactionalEventSequence.d.ts +3 -2
- package/dist/eventSequences/TransactionalEventSequence.d.ts.map +1 -1
- package/dist/eventSequences/TransactionalEventSequence.js +7 -4
- package/dist/eventSequences/TransactionalEventSequence.js.map +1 -1
- package/dist/eventSequences/createAppendNotification.d.ts +2 -1
- package/dist/eventSequences/createAppendNotification.d.ts.map +1 -1
- package/dist/eventSequences/createAppendNotification.js +3 -2
- package/dist/eventSequences/createAppendNotification.js.map +1 -1
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts +3 -0
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.d.ts.map +1 -0
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.js +10 -0
- package/dist/eventSequences/ensureNamedTagBatchIsSupported.js.map +1 -0
- package/dist/eventSequences/index.d.ts +1 -0
- package/dist/eventSequences/index.d.ts.map +1 -1
- package/dist/eventSequences/index.js +1 -0
- package/dist/eventSequences/index.js.map +1 -1
- package/dist/eventSequences/prepareBatchAppend.d.ts +1 -0
- package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -1
- package/dist/eventSequences/prepareBatchAppend.js +4 -1
- package/dist/eventSequences/prepareBatchAppend.js.map +1 -1
- package/dist/eventSequences/prepareSingleAppend.d.ts +1 -0
- package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -1
- package/dist/eventSequences/prepareSingleAppend.js +3 -1
- package/dist/eventSequences/prepareSingleAppend.js.map +1 -1
- package/dist/eventSequences/resolveEventSourceRouting.d.ts +39 -0
- package/dist/eventSequences/resolveEventSourceRouting.d.ts.map +1 -0
- package/dist/eventSequences/resolveEventSourceRouting.js +92 -0
- package/dist/eventSequences/resolveEventSourceRouting.js.map +1 -0
- package/dist/eventSources/ConcurrencyDimensions.d.ts +19 -0
- package/dist/eventSources/ConcurrencyDimensions.d.ts.map +1 -0
- package/dist/eventSources/ConcurrencyDimensions.js +19 -0
- package/dist/eventSources/ConcurrencyDimensions.js.map +1 -0
- package/dist/eventSources/DuplicateEventSourceName.d.ts +6 -0
- package/dist/eventSources/DuplicateEventSourceName.d.ts.map +1 -0
- package/dist/eventSources/DuplicateEventSourceName.js +12 -0
- package/dist/eventSources/DuplicateEventSourceName.js.map +1 -0
- package/dist/eventSources/DuplicateEventStreamName.d.ts +7 -0
- package/dist/eventSources/DuplicateEventStreamName.d.ts.map +1 -0
- package/dist/eventSources/DuplicateEventStreamName.js +14 -0
- package/dist/eventSources/DuplicateEventStreamName.js.map +1 -0
- package/dist/eventSources/EventRoutingContradictsEventSource.d.ts +9 -0
- package/dist/eventSources/EventRoutingContradictsEventSource.d.ts.map +1 -0
- package/dist/eventSources/EventRoutingContradictsEventSource.js +18 -0
- package/dist/eventSources/EventRoutingContradictsEventSource.js.map +1 -0
- package/dist/eventSources/EventSourceDefinition.d.ts +20 -0
- package/dist/eventSources/EventSourceDefinition.d.ts.map +1 -0
- package/dist/eventSources/EventSourceDefinition.js +4 -0
- package/dist/eventSources/EventSourceDefinition.js.map +1 -0
- package/dist/eventSources/EventSources.d.ts +31 -0
- package/dist/eventSources/EventSources.d.ts.map +1 -0
- package/dist/eventSources/EventSources.js +90 -0
- package/dist/eventSources/EventSources.js.map +1 -0
- package/dist/eventSources/EventStreamDefinition.d.ts +13 -0
- package/dist/eventSources/EventStreamDefinition.d.ts.map +1 -0
- package/dist/eventSources/EventStreamDefinition.js +4 -0
- package/dist/eventSources/EventStreamDefinition.js.map +1 -0
- package/dist/eventSources/EventStreamDoesNotBelongToEventSource.d.ts +7 -0
- package/dist/eventSources/EventStreamDoesNotBelongToEventSource.d.ts.map +1 -0
- package/dist/eventSources/EventStreamDoesNotBelongToEventSource.js +14 -0
- package/dist/eventSources/EventStreamDoesNotBelongToEventSource.js.map +1 -0
- package/dist/eventSources/EventStreamRequiresEventSource.d.ts +6 -0
- package/dist/eventSources/EventStreamRequiresEventSource.d.ts.map +1 -0
- package/dist/eventSources/EventStreamRequiresEventSource.js +12 -0
- package/dist/eventSources/EventStreamRequiresEventSource.js.map +1 -0
- package/dist/eventSources/IEventSources.d.ts +27 -0
- package/dist/eventSources/IEventSources.d.ts.map +1 -0
- package/dist/eventSources/IEventSources.js +4 -0
- package/dist/eventSources/IEventSources.js.map +1 -0
- package/dist/eventSources/ResolvedEventRouting.d.ts +37 -0
- package/dist/eventSources/ResolvedEventRouting.d.ts.map +1 -0
- package/dist/eventSources/ResolvedEventRouting.js +60 -0
- package/dist/eventSources/ResolvedEventRouting.js.map +1 -0
- package/dist/eventSources/UnknownEventSource.d.ts +6 -0
- package/dist/eventSources/UnknownEventSource.d.ts.map +1 -0
- package/dist/eventSources/UnknownEventSource.js +12 -0
- package/dist/eventSources/UnknownEventSource.js.map +1 -0
- package/dist/eventSources/eventSource.d.ts +60 -0
- package/dist/eventSources/eventSource.d.ts.map +1 -0
- package/dist/eventSources/eventSource.js +72 -0
- package/dist/eventSources/eventSource.js.map +1 -0
- package/dist/eventSources/index.d.ts +17 -0
- package/dist/eventSources/index.d.ts.map +1 -0
- package/dist/eventSources/index.js +13 -0
- package/dist/eventSources/index.js.map +1 -0
- package/dist/events/EventContext.d.ts +12 -0
- package/dist/events/EventContext.d.ts.map +1 -1
- package/dist/events/InvalidNamedTag.d.ts +8 -0
- package/dist/events/InvalidNamedTag.d.ts.map +1 -0
- package/dist/events/InvalidNamedTag.js +13 -0
- package/dist/events/InvalidNamedTag.js.map +1 -0
- package/dist/events/NamedTag.d.ts +25 -0
- package/dist/events/NamedTag.d.ts.map +1 -0
- package/dist/events/NamedTag.js +39 -0
- package/dist/events/NamedTag.js.map +1 -0
- package/dist/events/WireEventContext.d.ts +2 -0
- package/dist/events/WireEventContext.d.ts.map +1 -1
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +3 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/mergeNamedTags.d.ts +13 -0
- package/dist/events/mergeNamedTags.d.ts.map +1 -0
- package/dist/events/mergeNamedTags.js +39 -0
- package/dist/events/mergeNamedTags.js.map +1 -0
- package/dist/events/toClientEventContext.d.ts.map +1 -1
- package/dist/events/toClientEventContext.js +4 -1
- package/dist/events/toClientEventContext.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -1
- package/dist/projections/eventContextPropertyExpression.js +2 -0
- package/dist/projections/eventContextPropertyExpression.js.map +1 -1
- package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
- package/dist/testing/InProcessEventSequence.js +17 -7
- package/dist/testing/InProcessEventSequence.js.map +1 -1
- package/dist/testing/ReactorScenario.d.ts.map +1 -1
- package/dist/testing/ReactorScenario.js +6 -0
- package/dist/testing/ReactorScenario.js.map +1 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.d.ts.map +1 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.js +2 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.js.map +1 -1
- package/dist/testing/singleAppendRoute.d.ts +5 -0
- package/dist/testing/singleAppendRoute.d.ts.map +1 -1
- package/dist/testing/singleAppendRoute.js +12 -1
- package/dist/testing/singleAppendRoute.js.map +1 -1
- package/dist/transactions/IUnitOfWork.d.ts +4 -1
- package/dist/transactions/IUnitOfWork.d.ts.map +1 -1
- package/dist/transactions/TransactionalAppendOptions.d.ts +14 -0
- package/dist/transactions/TransactionalAppendOptions.d.ts.map +1 -0
- package/dist/transactions/TransactionalAppendOptions.js +4 -0
- package/dist/transactions/TransactionalAppendOptions.js.map +1 -0
- package/dist/transactions/TransactionalEventRouting.d.ts +12 -0
- package/dist/transactions/TransactionalEventRouting.d.ts.map +1 -0
- package/dist/transactions/TransactionalEventRouting.js +4 -0
- package/dist/transactions/TransactionalEventRouting.js.map +1 -0
- package/dist/transactions/UnitOfWork.d.ts +2 -1
- package/dist/transactions/UnitOfWork.d.ts.map +1 -1
- package/dist/transactions/UnitOfWork.js +17 -2
- package/dist/transactions/UnitOfWork.js.map +1 -1
- package/dist/transactions/index.d.ts +2 -0
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/DecoratorType.d.ts +3 -1
- package/dist/types/DecoratorType.d.ts.map +1 -1
- package/dist/types/DecoratorType.js +2 -0
- package/dist/types/DecoratorType.js.map +1 -1
- package/eventSequences/AppendOptions.ts +25 -0
- package/eventSequences/EventForEventSourceId.ts +20 -0
- package/eventSequences/EventLog.ts +4 -2
- package/eventSequences/EventSequence.ts +62 -13
- package/eventSequences/ITransactionalEventSequence.ts +5 -2
- package/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.ts +12 -0
- package/eventSequences/TransactionalEventSequence.ts +8 -4
- package/eventSequences/createAppendNotification.ts +4 -2
- package/eventSequences/ensureNamedTagBatchIsSupported.ts +11 -0
- package/eventSequences/for_EventSequence/against_a_kernel/when_appending_named_tags.integration.spec.ts +107 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/given.fixture.ts +38 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_batch.spec.ts +49 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_single_event.spec.ts +47 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_an_invalid_tag.spec.ts +36 -0
- package/eventSequences/for_EventSequence/when_appending_named_tags/with_registered_sources.spec.ts +74 -0
- package/eventSequences/for_TransactionalEventSequence/when_appending_many_with_named_tags.spec.ts +41 -0
- package/eventSequences/index.ts +1 -0
- package/eventSequences/prepareBatchAppend.ts +4 -1
- package/eventSequences/prepareSingleAppend.ts +3 -1
- package/eventSequences/resolveEventSourceRouting.ts +125 -0
- package/eventSources/ConcurrencyDimensions.ts +26 -0
- package/eventSources/DuplicateEventSourceName.ts +10 -0
- package/eventSources/DuplicateEventStreamName.ts +10 -0
- package/eventSources/EventRoutingContradictsEventSource.ts +10 -0
- package/eventSources/EventSourceDefinition.ts +27 -0
- package/eventSources/EventSources.ts +93 -0
- package/eventSources/EventStreamDefinition.ts +18 -0
- package/eventSources/EventStreamDoesNotBelongToEventSource.ts +10 -0
- package/eventSources/EventStreamRequiresEventSource.ts +10 -0
- package/eventSources/IEventSources.ts +33 -0
- package/eventSources/ResolvedEventRouting.ts +78 -0
- package/eventSources/UnknownEventSource.ts +10 -0
- package/eventSources/eventSource.ts +116 -0
- package/eventSources/for_EventSources/against_a_kernel/when_appending_through_registered_sources.integration.spec.ts +106 -0
- package/eventSources/for_EventSources/when_appending_through_a_definition.spec.ts +190 -0
- package/eventSources/for_EventSources/when_batch_requires_differing_guards.spec.ts +139 -0
- package/eventSources/for_EventSources/when_discovering_and_registering.spec.ts +94 -0
- package/eventSources/index.ts +19 -0
- package/events/EventContext.ts +14 -0
- package/events/InvalidNamedTag.ts +13 -0
- package/events/NamedTag.ts +44 -0
- package/events/WireEventContext.ts +2 -0
- package/events/for_EventContext/with_event_source.spec.ts +22 -0
- package/events/for_NamedTag/when_constructing/with_invalid_values.spec.ts +17 -0
- package/events/for_NamedTag/when_constructing/with_valid_values.spec.ts +19 -0
- package/events/for_mergeNamedTags/when_merging/with_event_and_call_tags.spec.ts +31 -0
- package/events/for_mergeNamedTags/when_merging/with_invalid_entries.spec.ts +17 -0
- package/events/for_toClientEventContext/when_converting_named_tags.spec.ts +31 -0
- package/events/index.ts +3 -0
- package/events/mergeNamedTags.ts +37 -0
- package/events/toClientEventContext.ts +4 -1
- package/for_EventStore/when_implementing_a_legacy_event_store.spec.ts +19 -0
- package/index.ts +3 -0
- package/package.json +6 -6
- package/projections/eventContextPropertyExpression.ts +3 -1
- package/testing/EventScenario.event-sources.spec.ts +34 -0
- package/testing/InProcessEventSequence.ts +17 -7
- package/testing/ReactorScenario.ts +6 -0
- package/testing/for_EventScenario/when_appending_named_tags/with_named_tags.spec.ts +48 -0
- package/testing/for_ReactorScenario/when_recording_side_effects/with_named_tags.spec.ts +119 -0
- package/testing/for_ReadModelScenario/when_seeding_with_options/with_named_tags.spec.ts +35 -0
- package/testing/projections/ProjectionReadModelProcessor.ts +2 -1
- package/testing/singleAppendRoute.ts +13 -1
- package/transactions/IUnitOfWork.ts +4 -1
- package/transactions/TransactionalAppendOptions.ts +17 -0
- package/transactions/TransactionalEventRouting.ts +16 -0
- package/transactions/UnitOfWork.ts +22 -2
- package/transactions/for_UnitOfWork/when_committing_events_with_event_source_routing.spec.ts +41 -0
- package/transactions/for_UnitOfWork/when_committing_events_with_named_tags.spec.ts +112 -0
- package/transactions/index.ts +2 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
+
});
|
package/transactions/index.ts
CHANGED
|
@@ -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';
|
package/types/DecoratorType.ts
CHANGED
|
@@ -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
|
}
|