@cratis/chronicle 6.49.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/dist/eventSequences/AppendOptions.d.ts +10 -0
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +6 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +28 -8
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/ITransactionalEventSequence.d.ts +5 -5
- 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 -3
- 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/events/EventContext.d.ts +7 -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/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 +3 -1
- package/dist/events/toClientEventContext.js.map +1 -1
- package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -1
- package/dist/projections/eventContextPropertyExpression.js +2 -1
- 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/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 -3
- 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/UnitOfWork.d.ts +2 -2
- package/dist/transactions/UnitOfWork.d.ts.map +1 -1
- package/dist/transactions/UnitOfWork.js +9 -3
- package/dist/transactions/UnitOfWork.js.map +1 -1
- package/dist/transactions/index.d.ts +1 -0
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/AppendOptions.ts +11 -0
- package/eventSequences/EventForEventSourceId.ts +7 -0
- package/eventSequences/EventSequence.ts +30 -8
- package/eventSequences/ITransactionalEventSequence.ts +5 -5
- package/eventSequences/NamedTagsWithRegisteredEventSourceNotSupported.ts +12 -0
- package/eventSequences/TransactionalEventSequence.ts +8 -5
- 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/events/EventContext.ts +8 -0
- package/events/InvalidNamedTag.ts +13 -0
- package/events/NamedTag.ts +44 -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 +3 -1
- package/package.json +5 -5
- package/projections/eventContextPropertyExpression.ts +3 -2
- 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/singleAppendRoute.ts +13 -1
- package/transactions/IUnitOfWork.ts +4 -3
- package/transactions/TransactionalAppendOptions.ts +17 -0
- package/transactions/UnitOfWork.ts +13 -4
- package/transactions/for_UnitOfWork/when_committing_events_with_named_tags.spec.ts +112 -0
- package/transactions/index.ts +1 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type { Constructor } from '@cratis/fundamentals';
|
|
5
5
|
import type { Tag } from '../events/Tag.js';
|
|
6
|
+
import type { NamedTag } from '../events/NamedTag.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Represents an event paired with the event source identifier it belongs to.
|
|
@@ -36,6 +37,12 @@ export interface EventForEventSourceId {
|
|
|
36
37
|
*/
|
|
37
38
|
readonly tags?: ReadonlyArray<string | Tag>;
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Optional structured named tags for this event. They are merged, in order, with the shared
|
|
42
|
+
* `AppendOptions.namedTags`; the first occurrence of each exact name and value pair is kept.
|
|
43
|
+
*/
|
|
44
|
+
readonly namedTags?: ReadonlyArray<NamedTag>;
|
|
45
|
+
|
|
39
46
|
/**
|
|
40
47
|
* Optional registered event source definition (class decorated with `@eventSource`, or its name) for this event.
|
|
41
48
|
* Overrides `AppendOptions.eventSource`, so one batch can mix several event sources.
|
|
@@ -14,6 +14,7 @@ import { prepareBatchAppend } from './prepareBatchAppend.js';
|
|
|
14
14
|
import { createAppendNotification, mapAppendNotificationCausation } from './createAppendNotification.js';
|
|
15
15
|
import { getEventTypeFor } from '../events/eventTypeDecorator.js';
|
|
16
16
|
import type { AppendedEvent } from '../events/AppendedEvent.js';
|
|
17
|
+
import type { NamedTag } from '../events/NamedTag.js';
|
|
17
18
|
import { toClientEventContext } from '../events/toClientEventContext.js';
|
|
18
19
|
import { DecoratorType } from '../types/DecoratorType.js';
|
|
19
20
|
import { TypeDiscoverer } from '../types/TypeDiscoverer.js';
|
|
@@ -50,6 +51,7 @@ import type { ConcurrencyScope } from './ConcurrencyScope.js';
|
|
|
50
51
|
import type { IEventSources } from '../eventSources/IEventSources.js';
|
|
51
52
|
import { deriveConcurrencyScope, planDerivedGuards, resolveBatchRouting, resolveSingleRouting } from './resolveEventSourceRouting.js';
|
|
52
53
|
import { IUnitOfWorkManager } from '../transactions/IUnitOfWorkManager.js';
|
|
54
|
+
import { ensureNamedTagBatchIsSupported } from './ensureNamedTagBatchIsSupported.js';
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
57
|
* Implements {@link IEventSequence} by communicating with the Chronicle Kernel
|
|
@@ -75,7 +77,7 @@ export class EventSequence implements IEventSequence {
|
|
|
75
77
|
/** @inheritdoc */
|
|
76
78
|
async append(eventSourceId: string, event: object, options?: AppendOptions): Promise<AppendResult> {
|
|
77
79
|
const routing = resolveSingleRouting(this._eventSources, options);
|
|
78
|
-
const { eventType, correlationId, content, tags, causationChain, identity, subject } = prepareSingleAppend(event, options);
|
|
80
|
+
const { eventType, correlationId, content, tags, namedTags, causationChain, identity, subject } = prepareSingleAppend(event, options);
|
|
79
81
|
|
|
80
82
|
const sequenceMetricAttributes = {
|
|
81
83
|
[names.attributes.eventStore]: this._eventStoreName,
|
|
@@ -95,7 +97,7 @@ export class EventSequence implements IEventSequence {
|
|
|
95
97
|
const concurrencyScope = options?.concurrencyScope ?? (routing
|
|
96
98
|
? await deriveConcurrencyScope(routing, eventSourceId, options?.streamId, this.tailReader)
|
|
97
99
|
: undefined);
|
|
98
|
-
const
|
|
100
|
+
const request = {
|
|
99
101
|
EventStore: this._eventStoreName,
|
|
100
102
|
Namespace: this._namespace,
|
|
101
103
|
EventSequenceId: this.id.value,
|
|
@@ -121,7 +123,11 @@ export class EventSequence implements IEventSequence {
|
|
|
121
123
|
Tags: tags,
|
|
122
124
|
Occurred: options?.occurred === undefined ? undefined : { Value: options.occurred.toISOString() },
|
|
123
125
|
Subject: subject ?? eventSourceId
|
|
124
|
-
}
|
|
126
|
+
};
|
|
127
|
+
// Named tags travel on the dedicated call so a kernel without support rejects them instead of dropping them.
|
|
128
|
+
const response = namedTags.length === 0
|
|
129
|
+
? await this._connection.eventSequences.append(request)
|
|
130
|
+
: await this._connection.eventSequences.appendWithNamedTags({ ...request, NamedTags: toContractsNamedTags(namedTags) });
|
|
125
131
|
|
|
126
132
|
const appendResponse = ensureCommandResponse('append event', response);
|
|
127
133
|
const durationInSeconds = (performance.now() - startTime) / 1000;
|
|
@@ -145,7 +151,7 @@ export class EventSequence implements IEventSequence {
|
|
|
145
151
|
|
|
146
152
|
if (this.appendOperations.hasSubscribers) {
|
|
147
153
|
this.appendOperations.publish([createAppendNotification(eventSourceId, event, result,
|
|
148
|
-
correlationId.toString(), mapAppendNotificationCausation(causationChain), tags)]);
|
|
154
|
+
correlationId.toString(), mapAppendNotificationCausation(causationChain), tags, undefined, namedTags)]);
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
return result;
|
|
@@ -167,11 +173,12 @@ export class EventSequence implements IEventSequence {
|
|
|
167
173
|
eventsOrOptions?: object[] | AppendOptions,
|
|
168
174
|
options?: AppendOptions
|
|
169
175
|
): Promise<AppendResult[]> {
|
|
170
|
-
const { eventsForEventSourceIds, appendOptions, correlationId, batchCausationChain, identity, concurrencyScopes, eventsToAppend } =
|
|
176
|
+
const { eventsForEventSourceIds, appendOptions, correlationId, batchCausationChain, identity, concurrencyScopes, eventsToAppend, namedTags } =
|
|
171
177
|
prepareBatchAppend(eventSourceIdOrEvents, eventsOrOptions, options);
|
|
172
178
|
|
|
173
179
|
const wireEvents: Array<(typeof eventsToAppend)[number] & { EventSource?: string }> = eventsToAppend;
|
|
174
180
|
const routings = eventsForEventSourceIds.map(event => resolveBatchRouting(this._eventSources, event, appendOptions));
|
|
181
|
+
ensureNamedTagBatchIsSupported(namedTags.some(tagsForEvent => tagsForEvent.length > 0), routings.some(routing => routing !== undefined));
|
|
175
182
|
const explicitIds = new Set([...concurrencyScopes].filter(([, scope]) => scope !== undefined).map(([id]) => id));
|
|
176
183
|
const derivedGuards = planDerivedGuards(routings, eventsForEventSourceIds, appendOptions?.streamId, explicitIds);
|
|
177
184
|
const distinctEventSourceIds = [...new Set(eventsForEventSourceIds.map(_ => _.eventSourceId))];
|
|
@@ -202,7 +209,7 @@ export class EventSequence implements IEventSequence {
|
|
|
202
209
|
for (const [id, { routing, streamId }] of derivedGuards) {
|
|
203
210
|
concurrencyScopes.set(id, await deriveConcurrencyScope(routing, id, streamId, this.tailReader));
|
|
204
211
|
}
|
|
205
|
-
const
|
|
212
|
+
const request = {
|
|
206
213
|
EventStore: this._eventStoreName,
|
|
207
214
|
Namespace: this._namespace,
|
|
208
215
|
EventSequenceId: this.id.value,
|
|
@@ -218,7 +225,13 @@ export class EventSequence implements IEventSequence {
|
|
|
218
225
|
EventSourceId: eventSourceId,
|
|
219
226
|
Scope: this.toContractConcurrencyScope(scope)
|
|
220
227
|
}))
|
|
221
|
-
}
|
|
228
|
+
};
|
|
229
|
+
const response = namedTags.every(tagsForEvent => tagsForEvent.length === 0)
|
|
230
|
+
? await this._connection.eventSequences.appendManyForEventSources(request)
|
|
231
|
+
: await this._connection.eventSequences.appendManyForEventSourcesWithNamedTags({
|
|
232
|
+
...request,
|
|
233
|
+
Events: wireEvents.map((wireEvent, index) => ({ ...wireEvent, NamedTags: toContractsNamedTags(namedTags[index]) }))
|
|
234
|
+
});
|
|
222
235
|
|
|
223
236
|
const appendManyResponse = ensureCommandResponse('append many events', response);
|
|
224
237
|
const durationInSeconds = (performance.now() - startTime) / 1000;
|
|
@@ -266,7 +279,7 @@ export class EventSequence implements IEventSequence {
|
|
|
266
279
|
this.appendOperations.publish(result.map((appendResult: AppendResult, index: number) => {
|
|
267
280
|
const { eventSourceId, event } = eventsForEventSourceIds[index];
|
|
268
281
|
return createAppendNotification(eventSourceId, event, appendResult, correlationId.toString(),
|
|
269
|
-
causationEntries, eventsToAppend[index].Tags, occurredAt);
|
|
282
|
+
causationEntries, eventsToAppend[index].Tags, occurredAt, namedTags[index]);
|
|
270
283
|
}));
|
|
271
284
|
}
|
|
272
285
|
|
|
@@ -644,6 +657,15 @@ export class EventSequence implements IEventSequence {
|
|
|
644
657
|
}
|
|
645
658
|
}
|
|
646
659
|
|
|
660
|
+
/**
|
|
661
|
+
* Converts structured named tags into the sequence contract shape.
|
|
662
|
+
* @param namedTags - The validated named tags.
|
|
663
|
+
* @returns The wire named tags.
|
|
664
|
+
*/
|
|
665
|
+
function toContractsNamedTags(namedTags: readonly NamedTag[]): { Name: string; Value: string }[] {
|
|
666
|
+
return namedTags.map(tag => ({ Name: tag.name, Value: tag.value }));
|
|
667
|
+
}
|
|
668
|
+
|
|
647
669
|
/**
|
|
648
670
|
* Converts an {@link Identity} into the CausedBy shape used by Chronicle contracts.
|
|
649
671
|
* @param identity - The identity to convert.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import type { TransactionalAppendOptions } from '../transactions/TransactionalAppendOptions.js';
|
|
5
5
|
import { IUnitOfWork } from '../transactions/IUnitOfWork.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -15,15 +15,15 @@ export interface ITransactionalEventSequence {
|
|
|
15
15
|
* Adds a single event to the current unit of work.
|
|
16
16
|
* @param eventSourceId - The identifier of the event source.
|
|
17
17
|
* @param event - The event to append.
|
|
18
|
-
* @param
|
|
18
|
+
* @param options - Optional registered event source / stream routing and named tags.
|
|
19
19
|
*/
|
|
20
|
-
append(eventSourceId: string, event: object,
|
|
20
|
+
append(eventSourceId: string, event: object, options?: TransactionalAppendOptions): Promise<void>;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Adds multiple events to the current unit of work.
|
|
24
24
|
* @param eventSourceId - The identifier of the event source.
|
|
25
25
|
* @param events - The events to append.
|
|
26
|
-
* @param
|
|
26
|
+
* @param options - Optional registered event source / stream routing and named tags applied to every event.
|
|
27
27
|
*/
|
|
28
|
-
appendMany(eventSourceId: string, events: object[],
|
|
28
|
+
appendMany(eventSourceId: string, events: object[], options?: TransactionalAppendOptions): Promise<void>;
|
|
29
29
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
/** Named-tag batches cannot preserve registered event source routing on the current kernel. */
|
|
5
|
+
export class NamedTagsWithRegisteredEventSourceNotSupported extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super('Batches and unit-of-work commits cannot combine non-empty named tags with registered event sources: ' +
|
|
8
|
+
'the kernel named-tag batch operation drops registered event source routing. ' +
|
|
9
|
+
'Use single append calls instead. See https://github.com/Cratis/Chronicle/issues/4603.');
|
|
10
|
+
this.name = 'NamedTagsWithRegisteredEventSourceNotSupported';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import type { TransactionalAppendOptions } from '../transactions/TransactionalAppendOptions.js';
|
|
5
|
+
import { mergeNamedTags } from '../events/mergeNamedTags.js';
|
|
5
6
|
import { IUnitOfWork } from '../transactions/IUnitOfWork.js';
|
|
6
7
|
import { IUnitOfWorkManager } from '../transactions/IUnitOfWorkManager.js';
|
|
7
8
|
import { IEventSequence } from './IEventSequence.js';
|
|
@@ -22,14 +23,16 @@ export class TransactionalEventSequence implements ITransactionalEventSequence {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/** @inheritdoc */
|
|
25
|
-
async append(eventSourceId: string, event: object,
|
|
26
|
-
this.unitOfWork.addEvent(this._eventSequence.id, eventSourceId, event,
|
|
26
|
+
async append(eventSourceId: string, event: object, options?: TransactionalAppendOptions): Promise<void> {
|
|
27
|
+
this.unitOfWork.addEvent(this._eventSequence.id, eventSourceId, event, options);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/** @inheritdoc */
|
|
30
|
-
async appendMany(eventSourceId: string, events: object[],
|
|
31
|
+
async appendMany(eventSourceId: string, events: object[], options?: TransactionalAppendOptions): Promise<void> {
|
|
32
|
+
// Validate once up front so an invalid tag adds none of the events.
|
|
33
|
+
const shared = options?.namedTags === undefined ? options : { ...options, namedTags: mergeNamedTags(options.namedTags) };
|
|
31
34
|
for (const event of events) {
|
|
32
|
-
this.unitOfWork.addEvent(this._eventSequence.id, eventSourceId, event,
|
|
35
|
+
this.unitOfWork.addEvent(this._eventSequence.id, eventSourceId, event, shared);
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { getEventTypeFor } from '../events/eventTypeDecorator.js';
|
|
5
5
|
import { Tag } from '../events/Tag.js';
|
|
6
|
+
import type { NamedTag } from '../events/NamedTag.js';
|
|
6
7
|
import type { CausationEntry } from '../events/CausationEntry.js';
|
|
7
8
|
import type { AppendResult } from './AppendResult.js';
|
|
8
9
|
import type { AppendedEventWithResult } from './AppendedEventWithResult.js';
|
|
@@ -17,7 +18,7 @@ export function mapAppendNotificationCausation(chain: readonly Causation[]): Cau
|
|
|
17
18
|
/** Maps a successful append into the client-side appendOperations notification shape. */
|
|
18
19
|
export function createAppendNotification(
|
|
19
20
|
eventSourceId: string, event: object, result: AppendResult, correlationId: string,
|
|
20
|
-
causation: CausationEntry[], tags: readonly string[], occurredAt = new Date()
|
|
21
|
+
causation: CausationEntry[], tags: readonly string[], occurredAt = new Date(), namedTags: readonly NamedTag[] = []
|
|
21
22
|
): AppendedEventWithResult {
|
|
22
23
|
const eventType = getEventTypeFor(event.constructor);
|
|
23
24
|
return {
|
|
@@ -25,7 +26,8 @@ export function createAppendNotification(
|
|
|
25
26
|
context: {
|
|
26
27
|
sequenceNumber: result.sequenceNumber.value, eventSourceId, eventType, occurred: occurredAt, correlationId,
|
|
27
28
|
causation,
|
|
28
|
-
tags: tags.map(value => new Tag(value))
|
|
29
|
+
tags: tags.map(value => new Tag(value)),
|
|
30
|
+
namedTags: [...namedTags]
|
|
29
31
|
},
|
|
30
32
|
eventType,
|
|
31
33
|
content: event as Record<string, unknown>
|
|
@@ -0,0 +1,11 @@
|
|
|
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 { NamedTagsWithRegisteredEventSourceNotSupported } from './NamedTagsWithRegisteredEventSourceNotSupported.js';
|
|
5
|
+
|
|
6
|
+
/** Rejects the combination before any RPC, even when tags and registered routing belong to different entries. */
|
|
7
|
+
export function ensureNamedTagBatchIsSupported(hasNamedTags: boolean, hasRegisteredEventSource: boolean): void {
|
|
8
|
+
if (hasNamedTags && hasRegisteredEventSource) {
|
|
9
|
+
throw new NamedTagsWithRegisteredEventSourceNotSupported();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { afterAll, beforeAll, chai, describe, it } from 'vitest';
|
|
6
|
+
import { field } from '@cratis/fundamentals';
|
|
7
|
+
import { ObserverRunningState } from '@cratis/chronicle.contracts';
|
|
8
|
+
import { ChronicleClient, ChronicleOptions, NamedTag, eventType, reactor, type AppendedEvent, type EventContext,
|
|
9
|
+
type IClientArtifactsProvider, type IEventStore } from '../../../index.js';
|
|
10
|
+
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
11
|
+
|
|
12
|
+
chai.should();
|
|
13
|
+
function should(value: unknown): ReturnType<typeof chai.expect> {
|
|
14
|
+
return (value as { should: ReturnType<typeof chai.expect> }).should;
|
|
15
|
+
}
|
|
16
|
+
const connectionString = process.env.CHRONICLE_INTEGRATION_CONNECTION_STRING;
|
|
17
|
+
|
|
18
|
+
@eventType()
|
|
19
|
+
class NamedTagsItemImported {
|
|
20
|
+
@field(String) sku = '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const delivered = new Map<string, EventContext>();
|
|
24
|
+
@reactor()
|
|
25
|
+
class NamedTagsImportTracker {
|
|
26
|
+
namedTagsItemImported(event: NamedTagsItemImported, context: EventContext) {
|
|
27
|
+
delivered.set(event.sku, context);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const artifacts: IClientArtifactsProvider = {
|
|
32
|
+
eventTypes: [NamedTagsItemImported], reactors: [NamedTagsImportTracker], reducers: [], readModels: [], seeders: [],
|
|
33
|
+
constraints: [], projections: [], webhooks: [], eventTypeMigrations: [], globalForHandlers: []
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
async function eventually(accept: () => boolean | Promise<boolean>, timeoutMs = 30_000): Promise<void> {
|
|
37
|
+
const deadline = Date.now() + timeoutMs;
|
|
38
|
+
while (!await accept()) {
|
|
39
|
+
if (Date.now() > deadline) throw new Error('Timed out waiting for the kernel');
|
|
40
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const pairs = (context: { namedTags?: ReadonlyArray<NamedTag> }) => (context.namedTags ?? []).map(tag => `${tag.name}=${tag.value}`);
|
|
45
|
+
const item = (sku: string) => Object.assign(new NamedTagsItemImported(), { sku });
|
|
46
|
+
|
|
47
|
+
describe.skipIf(!connectionString && !process.env.CI)('when appending named tags against a kernel', () => {
|
|
48
|
+
const storeName = `NamedTags${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
49
|
+
const source = randomUUID();
|
|
50
|
+
let client: ChronicleClient;
|
|
51
|
+
let store: IEventStore;
|
|
52
|
+
let stored: Map<string, AppendedEvent>;
|
|
53
|
+
|
|
54
|
+
beforeAll(async () => {
|
|
55
|
+
client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, {
|
|
56
|
+
discoveryPatterns: [], clientArtifactsProvider: artifacts
|
|
57
|
+
}));
|
|
58
|
+
store = await client.getEventStore(storeName);
|
|
59
|
+
const connection = (store as unknown as { _connection: ChronicleConnection })._connection;
|
|
60
|
+
await eventually(async () => {
|
|
61
|
+
const observer = await connection.observers.getObserverInformation({ EventStore: storeName, Namespace: 'Default',
|
|
62
|
+
ObserverId: NamedTagsImportTracker.name, EventSequenceId: 'event-log' });
|
|
63
|
+
return observer.IsSubscribed && observer.RunningState === ObserverRunningState.Active;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const single = await store.eventLog.append(source, item('single'), {
|
|
67
|
+
tags: ['import'],
|
|
68
|
+
namedTags: [new NamedTag('import-batch', 'b-1'), new NamedTag('import-batch', 'B-1'), new NamedTag('empty', ''),
|
|
69
|
+
new NamedTag('import-batch', 'b-1')]
|
|
70
|
+
});
|
|
71
|
+
should(single.isSuccess).be.true;
|
|
72
|
+
const batch = await store.eventLog.appendMany([
|
|
73
|
+
{ eventSourceId: source, event: item('debit'), namedTags: [new NamedTag('side', 'debit')] },
|
|
74
|
+
{ eventSourceId: source, event: item('credit'), namedTags: [new NamedTag('side', 'credit'), new NamedTag('transfer', 't-1')] }
|
|
75
|
+
], { namedTags: [new NamedTag('transfer', 't-1')] });
|
|
76
|
+
should(batch.every(result => result.isSuccess)).be.true;
|
|
77
|
+
const untagged = await store.eventLog.append(source, item('untagged'));
|
|
78
|
+
should(untagged.isSuccess).be.true;
|
|
79
|
+
|
|
80
|
+
const unitOfWork = store.unitOfWorkManager.begin();
|
|
81
|
+
await store.eventLog.transactional.append(source, item('transactional'), { namedTags: [new NamedTag('unit', 'u-1')] });
|
|
82
|
+
await unitOfWork.commit();
|
|
83
|
+
should(unitOfWork.isSuccess).be.true;
|
|
84
|
+
|
|
85
|
+
const events = await store.eventLog.getForEventSourceIdAndEventTypes(source, [NamedTagsItemImported]);
|
|
86
|
+
stored = new Map(events.map(event => [(event.content as { sku: string }).sku, event]));
|
|
87
|
+
await eventually(() => delivered.has('transactional'));
|
|
88
|
+
});
|
|
89
|
+
afterAll(() => client?.dispose());
|
|
90
|
+
|
|
91
|
+
it('should round-trip distinct single-append named tags in order through a read', () =>
|
|
92
|
+
should(pairs(stored.get('single')!.context)).deep.equal(['import-batch=b-1', 'import-batch=B-1', 'empty=']));
|
|
93
|
+
it('should keep plain tags beside named tags', () =>
|
|
94
|
+
should(stored.get('single')!.context.tags.map(tag => tag.value)).deep.equal(['import']));
|
|
95
|
+
it('should store each batch event with its own tags followed by the shared tags', () => {
|
|
96
|
+
should(pairs(stored.get('debit')!.context)).deep.equal(['side=debit', 'transfer=t-1']);
|
|
97
|
+
should(pairs(stored.get('credit')!.context)).deep.equal(['side=credit', 'transfer=t-1']);
|
|
98
|
+
});
|
|
99
|
+
it('should store an untagged event with no named tags', () => should(pairs(stored.get('untagged')!.context)).deep.equal([]));
|
|
100
|
+
it('should store named tags added through a unit of work', () =>
|
|
101
|
+
should(pairs(stored.get('transactional')!.context)).deep.equal(['unit=u-1']));
|
|
102
|
+
it('should deliver the named tags to reactors in the event context', () => {
|
|
103
|
+
should(pairs(delivered.get('single')!)).deep.equal(['import-batch=b-1', 'import-batch=B-1', 'empty=']);
|
|
104
|
+
should(pairs(delivered.get('credit')!)).deep.equal(['side=credit', 'transfer=t-1']);
|
|
105
|
+
should(pairs(delivered.get('untagged')!)).deep.equal([]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { vi } from 'vitest';
|
|
5
|
+
import { EventSequence, EventSequenceId } from '../../index.js';
|
|
6
|
+
import { eventType } from '../../../events/index.js';
|
|
7
|
+
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
8
|
+
import type { IUnitOfWorkManager } from '../../../transactions/index.js';
|
|
9
|
+
import { ConcurrencyDimensions, type IEventSources } from '../../../eventSources/index.js';
|
|
10
|
+
|
|
11
|
+
export class Account {}
|
|
12
|
+
|
|
13
|
+
/** A registered source, optionally requiring a derived tail read. */
|
|
14
|
+
export function registered_sources(concurrency = ConcurrencyDimensions.none): IEventSources {
|
|
15
|
+
return {
|
|
16
|
+
getFor: () => ({ type: Account, name: 'Account', description: '', concurrency, streams: [] })
|
|
17
|
+
} as unknown as IEventSources;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class NamedTagRecorded { constructor(readonly value = 'payload') {} }
|
|
21
|
+
eventType('NamedTagRecorded')(NamedTagRecorded);
|
|
22
|
+
|
|
23
|
+
/** An event sequence whose four append calls are recorded. */
|
|
24
|
+
export function a_sequence(eventSources?: IEventSources) {
|
|
25
|
+
const single = { Response: { SequenceNumber: 0n, ConstraintViolations: [], Errors: [] } };
|
|
26
|
+
const batch = async (request: { Events: unknown[] }) =>
|
|
27
|
+
({ Response: { SequenceNumbers: request.Events.map((_, index) => BigInt(index)), ConstraintViolations: [], Errors: [] } });
|
|
28
|
+
const append = vi.fn().mockResolvedValue(single);
|
|
29
|
+
const appendWithNamedTags = vi.fn().mockResolvedValue(single);
|
|
30
|
+
const appendManyForEventSources = vi.fn().mockImplementation(batch);
|
|
31
|
+
const appendManyForEventSourcesWithNamedTags = vi.fn().mockImplementation(batch);
|
|
32
|
+
const tailSequenceNumber = vi.fn().mockResolvedValue({ Data: { SequenceNumber: 0n } });
|
|
33
|
+
const connection = { eventSequences: { append, appendWithNamedTags, appendManyForEventSources, appendManyForEventSourcesWithNamedTags, tailSequenceNumber } } as unknown as ChronicleConnection;
|
|
34
|
+
return {
|
|
35
|
+
sequence: new EventSequence(EventSequenceId.eventLog, 'store', 'tenant', connection, {} as IUnitOfWorkManager, undefined, undefined, eventSources),
|
|
36
|
+
append, appendWithNamedTags, appendManyForEventSources, appendManyForEventSourcesWithNamedTags, tailSequenceNumber
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import type { AppendManyForEventSourcesWithNamedTagsRequest } from '@cratis/chronicle.contracts';
|
|
6
|
+
import { NamedTag } from '../../../events/index.js';
|
|
7
|
+
import { a_sequence, NamedTagRecorded } from './given.fixture.js';
|
|
8
|
+
|
|
9
|
+
chai.should();
|
|
10
|
+
|
|
11
|
+
describe('when appending a batch with per-event and shared named tags', () => {
|
|
12
|
+
let calls: ReturnType<typeof a_sequence>;
|
|
13
|
+
let request: AppendManyForEventSourcesWithNamedTagsRequest;
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
calls = a_sequence();
|
|
16
|
+
await calls.sequence.appendMany([
|
|
17
|
+
{ eventSourceId: 'from', event: new NamedTagRecorded(), namedTags: [new NamedTag('side', 'debit'), new NamedTag('transfer', 't-1')] },
|
|
18
|
+
{ eventSourceId: 'to', event: new NamedTagRecorded(), namedTags: [new NamedTag('side', 'credit')] },
|
|
19
|
+
{ eventSourceId: 'audit', event: new NamedTagRecorded() }
|
|
20
|
+
], { namedTags: [new NamedTag('transfer', 't-1')] });
|
|
21
|
+
request = calls.appendManyForEventSourcesWithNamedTags.mock.calls[0][0];
|
|
22
|
+
});
|
|
23
|
+
it('should use the dedicated named-tag batch call', () => calls.appendManyForEventSources.mock.calls.should.have.lengthOf(0));
|
|
24
|
+
it('should send each event its own tags before the shared tags without duplicates', () =>
|
|
25
|
+
request.Events.map(event => event.NamedTags.map(tag => `${tag.Name}=${tag.Value}`)).should.deep.equal([
|
|
26
|
+
['side=debit', 'transfer=t-1'], ['side=credit', 'transfer=t-1'], ['transfer=t-1']
|
|
27
|
+
]));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('when appending many events for one source with shared named tags', () => {
|
|
31
|
+
let request: AppendManyForEventSourcesWithNamedTagsRequest;
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
const calls = a_sequence();
|
|
34
|
+
await calls.sequence.appendMany('source', [new NamedTagRecorded(), new NamedTagRecorded()], { namedTags: [new NamedTag('import', 'i-1')] });
|
|
35
|
+
request = calls.appendManyForEventSourcesWithNamedTags.mock.calls[0][0];
|
|
36
|
+
});
|
|
37
|
+
it('should give every event the shared named tags', () =>
|
|
38
|
+
request.Events.map(event => event.NamedTags).should.deep.equal([[{ Name: 'import', Value: 'i-1' }], [{ Name: 'import', Value: 'i-1' }]]));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('when appending a batch without named tags', () => {
|
|
42
|
+
let calls: ReturnType<typeof a_sequence>;
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
calls = a_sequence();
|
|
45
|
+
await calls.sequence.appendMany([{ eventSourceId: 'one', event: new NamedTagRecorded(), namedTags: [] }]);
|
|
46
|
+
});
|
|
47
|
+
it('should keep using the plain batch call', () => calls.appendManyForEventSources.mock.calls.should.have.lengthOf(1));
|
|
48
|
+
it('should not use the named-tag batch call', () => calls.appendManyForEventSourcesWithNamedTags.mock.calls.should.have.lengthOf(0));
|
|
49
|
+
});
|
package/eventSequences/for_EventSequence/when_appending_named_tags/with_a_single_event.spec.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AppendWithNamedTagsRequest } from '@cratis/chronicle.contracts';
|
|
6
|
+
import { NamedTag } from '../../../events/index.js';
|
|
7
|
+
import type { AppendedEventWithResult } from '../../AppendedEventWithResult.js';
|
|
8
|
+
import { a_sequence, NamedTagRecorded } from './given.fixture.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
|
|
12
|
+
describe('when appending a single event with named tags', () => {
|
|
13
|
+
let calls: ReturnType<typeof a_sequence>;
|
|
14
|
+
let request: AppendWithNamedTagsRequest;
|
|
15
|
+
let notification: AppendedEventWithResult;
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
calls = a_sequence();
|
|
18
|
+
const iterator = calls.sequence.appendOperations[Symbol.asyncIterator]();
|
|
19
|
+
const next = iterator.next();
|
|
20
|
+
await calls.sequence.append('source', new NamedTagRecorded(), {
|
|
21
|
+
tags: ['checkout'],
|
|
22
|
+
namedTags: [new NamedTag('session', 's-1'), new NamedTag('session', 's-1'), new NamedTag('channel', 'web')]
|
|
23
|
+
});
|
|
24
|
+
notification = (await next).value[0];
|
|
25
|
+
await iterator.return?.();
|
|
26
|
+
request = calls.appendWithNamedTags.mock.calls[0][0];
|
|
27
|
+
});
|
|
28
|
+
it('should use the dedicated named-tag call instead of the plain append', () => calls.append.mock.calls.should.have.lengthOf(0));
|
|
29
|
+
it('should send the distinct named tags in order', () =>
|
|
30
|
+
request.NamedTags.should.deep.equal([{ Name: 'session', Value: 's-1' }, { Name: 'channel', Value: 'web' }]));
|
|
31
|
+
it('should keep the plain tags beside the named tags', () => request.Tags.should.deep.equal(['checkout']));
|
|
32
|
+
it('should encode through the published contract', () =>
|
|
33
|
+
AppendWithNamedTagsRequest.decode(AppendWithNamedTagsRequest.encode(AppendWithNamedTagsRequest.fromPartial(request)).finish())
|
|
34
|
+
.NamedTags.should.have.lengthOf(2));
|
|
35
|
+
it('should carry the named tags on the append notification context', () =>
|
|
36
|
+
notification.event.context.namedTags!.map(tag => tag.name).should.deep.equal(['session', 'channel']));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('when appending a single event with an empty named tag list', () => {
|
|
40
|
+
let calls: ReturnType<typeof a_sequence>;
|
|
41
|
+
beforeEach(async () => {
|
|
42
|
+
calls = a_sequence();
|
|
43
|
+
await calls.sequence.append('source', new NamedTagRecorded(), { namedTags: [] });
|
|
44
|
+
});
|
|
45
|
+
it('should use the plain append call', () => calls.append.mock.calls.should.have.lengthOf(1));
|
|
46
|
+
it('should not use the named-tag call', () => calls.appendWithNamedTags.mock.calls.should.have.lengthOf(0));
|
|
47
|
+
});
|
package/eventSequences/for_EventSequence/when_appending_named_tags/with_an_invalid_tag.spec.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { InvalidNamedTag, type NamedTag } from '../../../events/index.js';
|
|
6
|
+
import { a_sequence, NamedTagRecorded } from './given.fixture.js';
|
|
7
|
+
|
|
8
|
+
chai.should();
|
|
9
|
+
const blank = { name: '', value: 'x' } as NamedTag;
|
|
10
|
+
|
|
11
|
+
describe('when appending a single event with an invalid named tag', () => {
|
|
12
|
+
let calls: ReturnType<typeof a_sequence>;
|
|
13
|
+
let error: unknown;
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
calls = a_sequence();
|
|
16
|
+
error = await calls.sequence.append('source', new NamedTagRecorded(), { namedTags: [blank] }).catch(caught => caught);
|
|
17
|
+
});
|
|
18
|
+
it('should fail with an invalid named tag error', () => (error as Error).should.be.instanceOf(InvalidNamedTag));
|
|
19
|
+
it('should not call the kernel', () =>
|
|
20
|
+
(calls.append.mock.calls.length + calls.appendWithNamedTags.mock.calls.length).should.equal(0));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('when appending a batch where one entry has an invalid named tag', () => {
|
|
24
|
+
let calls: ReturnType<typeof a_sequence>;
|
|
25
|
+
let error: unknown;
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
calls = a_sequence();
|
|
28
|
+
error = await calls.sequence.appendMany([
|
|
29
|
+
{ eventSourceId: 'one', event: new NamedTagRecorded() },
|
|
30
|
+
{ eventSourceId: 'two', event: new NamedTagRecorded(), namedTags: [null as unknown as NamedTag] }
|
|
31
|
+
]).catch(caught => caught);
|
|
32
|
+
});
|
|
33
|
+
it('should fail with an invalid named tag error', () => (error as Error).should.be.instanceOf(InvalidNamedTag));
|
|
34
|
+
it('should not append any of the events', () =>
|
|
35
|
+
(calls.appendManyForEventSources.mock.calls.length + calls.appendManyForEventSourcesWithNamedTags.mock.calls.length).should.equal(0));
|
|
36
|
+
});
|
package/eventSequences/for_EventSequence/when_appending_named_tags/with_registered_sources.spec.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
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 { ConcurrencyDimensions } from '../../../eventSources/index.js';
|
|
6
|
+
import { NamedTag } from '../../../events/index.js';
|
|
7
|
+
import { NamedTagsWithRegisteredEventSourceNotSupported, type AppendOptions, type EventForEventSourceId } from '../../index.js';
|
|
8
|
+
import { Account, a_sequence, NamedTagRecorded, registered_sources } from './given.fixture.js';
|
|
9
|
+
|
|
10
|
+
chai.should();
|
|
11
|
+
const namedTags = [new NamedTag('batch', 'b-1')];
|
|
12
|
+
const entry = (options: Partial<EventForEventSourceId> = {}): EventForEventSourceId =>
|
|
13
|
+
({ eventSourceId: 'source', event: new NamedTagRecorded(), ...options });
|
|
14
|
+
|
|
15
|
+
const batches: Array<{ description: string; events: EventForEventSourceId[]; options?: AppendOptions }> = [
|
|
16
|
+
{ description: 'tags and a registered source on the same entry', events: [entry({ eventSource: Account, namedTags })] },
|
|
17
|
+
{ description: 'a tagged entry before an untagged registered entry', events: [entry({ namedTags }), entry({ eventSource: 'Account' })] },
|
|
18
|
+
{ description: 'an untagged registered entry before a tagged entry', events: [entry({ eventSource: 'Account' }), entry({ namedTags })] },
|
|
19
|
+
{ description: 'shared tags and a registered entry', events: [entry({ eventSource: 'Account' })], options: { namedTags } },
|
|
20
|
+
{ description: 'per-entry tags and a shared registered source', events: [entry({ namedTags })], options: { eventSource: Account } }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
for (const batch of batches) {
|
|
24
|
+
describe(`when appending a batch with ${batch.description}`, () => {
|
|
25
|
+
let calls: ReturnType<typeof a_sequence>;
|
|
26
|
+
let error: unknown;
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
calls = a_sequence(registered_sources(ConcurrencyDimensions.eventSourceId));
|
|
29
|
+
error = await calls.sequence.appendMany(batch.events, batch.options).catch(caught => caught);
|
|
30
|
+
});
|
|
31
|
+
it('should reject with the typed unsupported combination error', () =>
|
|
32
|
+
(error as Error).should.be.instanceOf(NamedTagsWithRegisteredEventSourceNotSupported));
|
|
33
|
+
it('should explain the kernel limitation', () => (error as Error).message.should.contain('drops registered event source routing'));
|
|
34
|
+
it('should link to the kernel issue', () => (error as Error).message.should.contain('https://github.com/Cratis/Chronicle/issues/4603'));
|
|
35
|
+
it('should not send a plain batch', () => calls.appendManyForEventSources.mock.calls.should.have.lengthOf(0));
|
|
36
|
+
it('should not send a named-tag batch', () => calls.appendManyForEventSourcesWithNamedTags.mock.calls.should.have.lengthOf(0));
|
|
37
|
+
it('should not read a tail to derive a guard', () => calls.tailSequenceNumber.mock.calls.should.have.lengthOf(0));
|
|
38
|
+
it('should not fall back to single appends', () =>
|
|
39
|
+
[...calls.append.mock.calls, ...calls.appendWithNamedTags.mock.calls].should.have.lengthOf(0));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('when appending a same-source batch with shared named tags and registered routing', () => {
|
|
44
|
+
let error: unknown;
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
const calls = a_sequence(registered_sources());
|
|
47
|
+
error = await calls.sequence.appendMany('source', [new NamedTagRecorded()], { eventSource: Account, namedTags }).catch(caught => caught);
|
|
48
|
+
});
|
|
49
|
+
it('should reject with the typed unsupported combination error', () =>
|
|
50
|
+
(error as Error).should.be.instanceOf(NamedTagsWithRegisteredEventSourceNotSupported));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('when appending a registered-source batch with empty named tags', () => {
|
|
54
|
+
let calls: ReturnType<typeof a_sequence>;
|
|
55
|
+
beforeEach(async () => {
|
|
56
|
+
calls = a_sequence(registered_sources());
|
|
57
|
+
await calls.sequence.appendMany([entry({ eventSource: Account, namedTags: [] })], { namedTags: [] });
|
|
58
|
+
});
|
|
59
|
+
it('should retain registered routing on the plain batch call', () =>
|
|
60
|
+
calls.appendManyForEventSources.mock.calls[0][0].Events[0].EventSource.should.equal('Account'));
|
|
61
|
+
it('should not send a named-tag batch', () => calls.appendManyForEventSourcesWithNamedTags.mock.calls.should.have.lengthOf(0));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('when appending a single event with named tags and a registered source', () => {
|
|
65
|
+
let calls: ReturnType<typeof a_sequence>;
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
calls = a_sequence(registered_sources());
|
|
68
|
+
await calls.sequence.append('source', new NamedTagRecorded(), { eventSource: Account, namedTags });
|
|
69
|
+
});
|
|
70
|
+
it('should retain registered routing on the dedicated single append call', () =>
|
|
71
|
+
calls.appendWithNamedTags.mock.calls[0][0].EventSource.should.equal('Account'));
|
|
72
|
+
it('should retain the named tags on the single append', () =>
|
|
73
|
+
calls.appendWithNamedTags.mock.calls[0][0].NamedTags.should.deep.equal([{ Name: 'batch', Value: 'b-1' }]));
|
|
74
|
+
});
|