@cratis/chronicle 3.1.1 → 5.0.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/ChronicleClient.ts +3 -3
- package/EventStore.ts +4 -4
- package/compliance/ComplianceContracts.ts +59 -0
- package/compliance/IPIIManager.ts +12 -0
- package/compliance/PIIManager.spec.ts +50 -0
- package/compliance/PIIManager.ts +9 -0
- package/compliance/PIINotSupportedOnEventSourceId.ts +21 -0
- package/compliance/index.ts +2 -0
- package/compliance/pii.spec.ts +37 -0
- package/compliance/pii.ts +16 -1
- package/compliance/subject.spec.ts +45 -0
- package/compliance/subject.ts +64 -0
- package/connection/ChronicleConnection.ts +6 -0
- package/connection/ChronicleServices.ts +2 -0
- package/connection/callResults.ts +23 -0
- package/dist/ChronicleClient.js +3 -3
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/EventStore.js +4 -4
- package/dist/EventStore.js.map +1 -1
- package/dist/compliance/ComplianceContracts.d.ts +31 -0
- package/dist/compliance/ComplianceContracts.d.ts.map +1 -1
- package/dist/compliance/ComplianceContracts.js +52 -0
- package/dist/compliance/ComplianceContracts.js.map +1 -1
- package/dist/compliance/IPIIManager.d.ts +11 -0
- package/dist/compliance/IPIIManager.d.ts.map +1 -1
- package/dist/compliance/PIIManager.d.ts +2 -0
- package/dist/compliance/PIIManager.d.ts.map +1 -1
- package/dist/compliance/PIIManager.js +8 -0
- package/dist/compliance/PIIManager.js.map +1 -1
- package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts +11 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts.map +1 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.js +19 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.js.map +1 -0
- package/dist/compliance/index.d.ts +2 -0
- package/dist/compliance/index.d.ts.map +1 -1
- package/dist/compliance/index.js +2 -0
- package/dist/compliance/index.js.map +1 -1
- package/dist/compliance/pii.d.ts.map +1 -1
- package/dist/compliance/pii.js +13 -1
- package/dist/compliance/pii.js.map +1 -1
- package/dist/compliance/subject.d.ts +39 -0
- package/dist/compliance/subject.d.ts.map +1 -0
- package/dist/compliance/subject.js +58 -0
- package/dist/compliance/subject.js.map +1 -0
- 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/connection/callResults.d.ts +13 -0
- package/dist/connection/callResults.d.ts.map +1 -1
- package/dist/connection/callResults.js +14 -0
- package/dist/connection/callResults.js.map +1 -1
- package/dist/eventSequences/AppendOptions.d.ts +8 -0
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +7 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts +2 -2
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +56 -63
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/events/EventContext.d.ts +3 -0
- package/dist/events/EventContext.d.ts.map +1 -1
- package/dist/events/EventTypes.d.ts.map +1 -1
- package/dist/events/EventTypes.js +3 -2
- package/dist/events/EventTypes.js.map +1 -1
- package/dist/events/Tag.d.ts +10 -0
- package/dist/events/Tag.d.ts.map +1 -0
- package/dist/events/Tag.js +16 -0
- package/dist/events/Tag.js.map +1 -0
- package/dist/events/filterEventsByTagDecorator.d.ts +35 -0
- package/dist/events/filterEventsByTagDecorator.d.ts.map +1 -0
- package/dist/events/filterEventsByTagDecorator.js +48 -0
- package/dist/events/filterEventsByTagDecorator.js.map +1 -0
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +4 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/mergeTags.d.ts +12 -0
- package/dist/events/mergeTags.d.ts.map +1 -0
- package/dist/events/mergeTags.js +26 -0
- package/dist/events/mergeTags.js.map +1 -0
- package/dist/events/tagDecorator.d.ts +38 -0
- package/dist/events/tagDecorator.d.ts.map +1 -0
- package/dist/events/tagDecorator.js +54 -0
- package/dist/events/tagDecorator.js.map +1 -0
- package/dist/jobs/Jobs.js +3 -3
- package/dist/jobs/Jobs.js.map +1 -1
- package/dist/observation/ObserverRunningState.d.ts +9 -15
- package/dist/observation/ObserverRunningState.d.ts.map +1 -1
- package/dist/observation/ObserverRunningState.js +8 -14
- package/dist/observation/ObserverRunningState.js.map +1 -1
- package/dist/observation/index.d.ts +1 -0
- package/dist/observation/index.d.ts.map +1 -1
- package/dist/observation/index.js +1 -0
- package/dist/observation/index.js.map +1 -1
- package/dist/observation/toObserverRunningState.d.ts +9 -0
- package/dist/observation/toObserverRunningState.d.ts.map +1 -0
- package/dist/observation/toObserverRunningState.js +26 -0
- package/dist/observation/toObserverRunningState.js.map +1 -0
- package/dist/projections/IProjections.d.ts +71 -1
- package/dist/projections/IProjections.d.ts.map +1 -1
- package/dist/projections/ProjectionQueryResult.d.ts +8 -0
- package/dist/projections/ProjectionQueryResult.d.ts.map +1 -0
- package/dist/projections/ProjectionQueryResult.js +4 -0
- package/dist/projections/ProjectionQueryResult.js.map +1 -0
- package/dist/projections/ProjectionState.d.ts +18 -0
- package/dist/projections/ProjectionState.d.ts.map +1 -0
- package/dist/projections/ProjectionState.js +4 -0
- package/dist/projections/ProjectionState.js.map +1 -0
- package/dist/projections/Projections.d.ts +46 -1
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +153 -6
- package/dist/projections/Projections.js.map +1 -1
- package/dist/projections/UnableToQueryProjection.d.ts +12 -0
- package/dist/projections/UnableToQueryProjection.d.ts.map +1 -0
- package/dist/projections/UnableToQueryProjection.js +17 -0
- package/dist/projections/UnableToQueryProjection.js.map +1 -0
- package/dist/projections/index.d.ts +3 -0
- package/dist/projections/index.d.ts.map +1 -1
- package/dist/projections/index.js +1 -0
- package/dist/projections/index.js.map +1 -1
- package/dist/projections/modelBound/eventSequence.d.ts +22 -0
- package/dist/projections/modelBound/eventSequence.d.ts.map +1 -0
- package/dist/projections/modelBound/eventSequence.js +34 -0
- package/dist/projections/modelBound/eventSequence.js.map +1 -0
- package/dist/projections/modelBound/fromAll.d.ts +26 -0
- package/dist/projections/modelBound/fromAll.d.ts.map +1 -0
- package/dist/projections/modelBound/fromAll.js +31 -0
- package/dist/projections/modelBound/fromAll.js.map +1 -0
- package/dist/projections/modelBound/index.d.ts +4 -0
- package/dist/projections/modelBound/index.d.ts.map +1 -1
- package/dist/projections/modelBound/index.js +3 -0
- package/dist/projections/modelBound/index.js.map +1 -1
- package/dist/projections/modelBound/noAutoMap.d.ts +26 -0
- package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -0
- package/dist/projections/modelBound/noAutoMap.js +44 -0
- package/dist/projections/modelBound/noAutoMap.js.map +1 -0
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +7 -3
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/readModels/MaterializedReadModels.d.ts +0 -1
- package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
- package/dist/readModels/MaterializedReadModels.js +2 -8
- package/dist/readModels/MaterializedReadModels.js.map +1 -1
- package/dist/readModels/ReadModelSubjectResolver.d.ts +22 -0
- package/dist/readModels/ReadModelSubjectResolver.d.ts.map +1 -0
- package/dist/readModels/ReadModelSubjectResolver.js +38 -0
- package/dist/readModels/ReadModelSubjectResolver.js.map +1 -0
- package/dist/readModels/ReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.js +15 -13
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/readModels/index.d.ts +1 -0
- package/dist/readModels/index.d.ts.map +1 -1
- package/dist/readModels/index.js +1 -0
- package/dist/readModels/index.js.map +1 -1
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +21 -3
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/schemas/JsonSchema.d.ts +14 -0
- package/dist/schemas/JsonSchema.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.d.ts +61 -5
- package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.js +130 -26
- package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.js +5 -2
- package/dist/seeding/EventSeeding.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/webhooks/Webhooks.d.ts.map +1 -1
- package/dist/webhooks/Webhooks.js +22 -10
- package/dist/webhooks/Webhooks.js.map +1 -1
- package/eventSequences/AppendOptions.ts +9 -0
- package/eventSequences/EventForEventSourceId.ts +9 -0
- package/eventSequences/EventSequence.spec.ts +83 -74
- package/eventSequences/EventSequence.ts +64 -69
- package/events/EventContext.ts +4 -0
- package/events/EventTypes.ts +3 -2
- package/events/Tag.ts +14 -0
- package/events/filterEventsByTagDecorator.ts +51 -0
- package/events/index.ts +4 -0
- package/events/mergeTags.spec.ts +50 -0
- package/events/mergeTags.ts +27 -0
- package/events/tagDecorator.spec.ts +115 -0
- package/events/tagDecorator.ts +59 -0
- package/jobs/Jobs.ts +3 -3
- package/observation/ObserverRunningState.ts +9 -18
- package/observation/index.ts +1 -0
- package/observation/toObserverRunningState.ts +27 -0
- package/package.json +9 -9
- package/projections/IProjections.ts +81 -1
- package/projections/ProjectionQueryResult.ts +10 -0
- package/projections/ProjectionState.ts +25 -0
- package/projections/Projections.childrenAndNested.spec.ts +1 -1
- package/projections/Projections.modelBoundCompleteness.spec.ts +224 -0
- package/projections/Projections.operationalSurface.spec.ts +257 -0
- package/projections/Projections.spec.ts +1 -1
- package/projections/Projections.ts +180 -5
- package/projections/UnableToQueryProjection.ts +17 -0
- package/projections/index.ts +3 -0
- package/projections/modelBound/eventSequence.ts +38 -0
- package/projections/modelBound/fromAll.ts +42 -0
- package/projections/modelBound/index.ts +4 -0
- package/projections/modelBound/noAutoMap.ts +47 -0
- package/reactors/Reactors.ts +7 -3
- package/readModels/MaterializedReadModels.spec.ts +98 -0
- package/readModels/MaterializedReadModels.ts +2 -8
- package/readModels/ReadModelSubjectResolver.spec.ts +84 -0
- package/readModels/ReadModelSubjectResolver.ts +44 -0
- package/readModels/ReadModels.spec.ts +118 -0
- package/readModels/ReadModels.ts +15 -13
- package/readModels/index.ts +1 -0
- package/reducers/Reducers.spec.ts +80 -0
- package/reducers/Reducers.ts +22 -3
- package/schemas/JsonSchema.ts +16 -0
- package/schemas/JsonSchemaGenerator.spec.ts +195 -0
- package/schemas/JsonSchemaGenerator.ts +145 -27
- package/schemas/index.ts +1 -1
- package/seeding/EventSeeding.ts +5 -2
- package/webhooks/Webhooks.ts +24 -10
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
import { ChronicleConnection } from '../connection';
|
|
5
5
|
import { SpanStatusCode } from '@opentelemetry/api';
|
|
6
|
-
import type {
|
|
6
|
+
import type { AppendedEventResponse as ContractsAppendedEvent } from '@cratis/chronicle.contracts';
|
|
7
7
|
import { Constructor, Guid, JsonSerializer } from '@cratis/fundamentals';
|
|
8
8
|
import { getEventTypeFor } from '../events/eventTypeDecorator';
|
|
9
9
|
import type { AppendedEvent } from '../events/AppendedEvent';
|
|
10
10
|
import { EventType } from '../events/EventType';
|
|
11
11
|
import { EventTypeId } from '../events/EventTypeId';
|
|
12
12
|
import { EventTypeGeneration } from '../events/EventTypeGeneration';
|
|
13
|
+
import { Tag } from '../events/Tag';
|
|
14
|
+
import { getTagsFor } from '../events/tagDecorator';
|
|
15
|
+
import { mergeTags } from '../events/mergeTags';
|
|
13
16
|
import { DecoratorType } from '../types/DecoratorType';
|
|
14
17
|
import { TypeDiscoverer } from '../types/TypeDiscoverer';
|
|
15
18
|
import { toClientFailedPartition } from '../observation/toClientFailedPartition';
|
|
@@ -37,6 +40,7 @@ import { identityProvider, Identity } from '../identity';
|
|
|
37
40
|
import { causationManager, CausationType } from '../auditing';
|
|
38
41
|
import { correlationIdManager } from '../correlation';
|
|
39
42
|
import { fromContractsGuid, toContractsGuid } from '../connection/Guid';
|
|
43
|
+
import { ensureCommandResponse, ensureCommandSuccess, ensureQuerySuccess } from '../connection/callResults';
|
|
40
44
|
import type { ConcurrencyScope } from './ConcurrencyScope';
|
|
41
45
|
import { IUnitOfWorkManager } from '../transactions/IUnitOfWorkManager';
|
|
42
46
|
|
|
@@ -64,7 +68,10 @@ export class EventSequence implements IEventSequence {
|
|
|
64
68
|
const correlationId = options?.correlationId === undefined
|
|
65
69
|
? Guid.as(correlationIdManager.current.value)
|
|
66
70
|
: Guid.as(options.correlationId);
|
|
67
|
-
const content = JsonSerializer.serialize(event);
|
|
71
|
+
const content = JSON.parse(JsonSerializer.serialize(event));
|
|
72
|
+
|
|
73
|
+
// Merge static tags declared on the event type with tags supplied at append time.
|
|
74
|
+
const tags = mergeTags(getTagsFor(event.constructor as Function), options?.tags);
|
|
68
75
|
|
|
69
76
|
causationManager.add(CausationType.appendEvent, { eventType: eventType.id.value });
|
|
70
77
|
const causationChain = causationManager.getCurrentChain();
|
|
@@ -108,17 +115,18 @@ export class EventSequence implements IEventSequence {
|
|
|
108
115
|
})),
|
|
109
116
|
CausedBy: toContractsCausedBy(identity),
|
|
110
117
|
ConcurrencyScope: this.toContractConcurrencyScope(options?.concurrencyScope),
|
|
111
|
-
Tags:
|
|
118
|
+
Tags: tags,
|
|
112
119
|
Occurred: undefined,
|
|
113
120
|
Subject: eventSourceId
|
|
114
121
|
});
|
|
115
122
|
|
|
123
|
+
const appendResponse = ensureCommandResponse('append event', response);
|
|
116
124
|
const duration = Date.now() - startTime;
|
|
117
125
|
const result = this.mapAppendResponse(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
appendResponse.SequenceNumber,
|
|
127
|
+
appendResponse.ConstraintViolations ?? [],
|
|
128
|
+
appendResponse.Errors ?? [],
|
|
129
|
+
appendResponse.ConcurrencyViolation
|
|
122
130
|
);
|
|
123
131
|
span.setAttribute('chronicle.sequence_number', result.sequenceNumber.value.toString());
|
|
124
132
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
@@ -149,7 +157,8 @@ export class EventSequence implements IEventSequence {
|
|
|
149
157
|
eventType,
|
|
150
158
|
occurred: new Date(),
|
|
151
159
|
correlationId: correlationId.toString(),
|
|
152
|
-
causation: causationChain.map(c => ({ type: c.type.name, properties: { ...c.properties } }))
|
|
160
|
+
causation: causationChain.map(c => ({ type: c.type.name, properties: { ...c.properties } })),
|
|
161
|
+
tags: tags.map(value => new Tag(value))
|
|
153
162
|
},
|
|
154
163
|
eventType,
|
|
155
164
|
content: event as Record<string, unknown>
|
|
@@ -223,8 +232,13 @@ export class EventSequence implements IEventSequence {
|
|
|
223
232
|
const resolveConcurrencyScope = (eventSourceId: string) =>
|
|
224
233
|
this.toContractConcurrencyScope(concurrencyScopesByEventSourceId?.[eventSourceId] ?? defaultConcurrencyScope);
|
|
225
234
|
|
|
226
|
-
const eventsToAppend = eventsForEventSourceIds.map(({ eventSourceId, event, eventStreamType, eventStreamId, eventSourceType, subject }) => {
|
|
235
|
+
const eventsToAppend = eventsForEventSourceIds.map(({ eventSourceId, event, eventStreamType, eventStreamId, eventSourceType, subject, tags: instanceTags }) => {
|
|
227
236
|
const eventType = getEventTypeFor(event.constructor as Function);
|
|
237
|
+
|
|
238
|
+
// Merge static tags declared on the event type, tags carried by this specific
|
|
239
|
+
// EventForEventSourceId entry, and tags supplied at call time for the whole batch.
|
|
240
|
+
const tags = mergeTags(getTagsFor(event.constructor as Function), instanceTags, appendOptions?.tags);
|
|
241
|
+
|
|
228
242
|
return {
|
|
229
243
|
EventSourceType: eventSourceType ?? 'Default',
|
|
230
244
|
EventSourceId: eventSourceId,
|
|
@@ -235,14 +249,8 @@ export class EventSequence implements IEventSequence {
|
|
|
235
249
|
Generation: eventType.generation.value,
|
|
236
250
|
Tombstone: eventType.tombstone
|
|
237
251
|
},
|
|
238
|
-
Content: JsonSerializer.serialize(event),
|
|
239
|
-
|
|
240
|
-
Occurred: { Value: c.occurred.toISOString() },
|
|
241
|
-
Type: c.type.name,
|
|
242
|
-
Properties: { ...c.properties }
|
|
243
|
-
})),
|
|
244
|
-
CausedBy: toContractsCausedBy(identity),
|
|
245
|
-
Tags: [],
|
|
252
|
+
Content: JSON.parse(JsonSerializer.serialize(event)),
|
|
253
|
+
Tags: tags,
|
|
246
254
|
Occurred: undefined,
|
|
247
255
|
Subject: subject ?? eventSourceId
|
|
248
256
|
};
|
|
@@ -267,7 +275,7 @@ export class EventSequence implements IEventSequence {
|
|
|
267
275
|
span.setAttribute('chronicle.events_count', eventsForEventSourceIds.length);
|
|
268
276
|
const startTime = Date.now();
|
|
269
277
|
try {
|
|
270
|
-
const response = await this._connection.eventSequences.
|
|
278
|
+
const response = await this._connection.eventSequences.appendManyForEventSources({
|
|
271
279
|
EventStore: this._eventStoreName,
|
|
272
280
|
Namespace: this._namespace,
|
|
273
281
|
EventSequenceId: this.id.value,
|
|
@@ -279,21 +287,23 @@ export class EventSequence implements IEventSequence {
|
|
|
279
287
|
Properties: { ...c.properties }
|
|
280
288
|
})),
|
|
281
289
|
CausedBy: toContractsCausedBy(identity),
|
|
282
|
-
ConcurrencyScopes: {
|
|
283
|
-
|
|
284
|
-
|
|
290
|
+
ConcurrencyScopes: distinctEventSourceIds.map(eventSourceId => ({
|
|
291
|
+
EventSourceId: eventSourceId,
|
|
292
|
+
Scope: resolveConcurrencyScope(eventSourceId)
|
|
293
|
+
}))
|
|
285
294
|
});
|
|
286
295
|
|
|
296
|
+
const appendManyResponse = ensureCommandResponse('append many events', response);
|
|
287
297
|
const duration = Date.now() - startTime;
|
|
288
298
|
// Mirrors the C# client: every per-event AppendResult in a batch carries all
|
|
289
299
|
// constraint violations and the first concurrency violation of the whole batch —
|
|
290
300
|
// the wire response doesn't correlate either back to a specific event index.
|
|
291
|
-
const firstConcurrencyViolation = (
|
|
292
|
-
const result = (
|
|
301
|
+
const firstConcurrencyViolation = (appendManyResponse.ConcurrencyViolations ?? [])[0];
|
|
302
|
+
const result = (appendManyResponse.SequenceNumbers ?? []).map((sequenceNumber: bigint, index: number) =>
|
|
293
303
|
this.mapAppendResponse(
|
|
294
304
|
sequenceNumber,
|
|
295
|
-
|
|
296
|
-
(
|
|
305
|
+
appendManyResponse.ConstraintViolations ?? [],
|
|
306
|
+
(appendManyResponse.Errors ?? []).filter((_: string, errorIndex: number) => errorIndex === index),
|
|
297
307
|
firstConcurrencyViolation
|
|
298
308
|
)
|
|
299
309
|
);
|
|
@@ -334,7 +344,8 @@ export class EventSequence implements IEventSequence {
|
|
|
334
344
|
eventType,
|
|
335
345
|
occurred: occurredAt,
|
|
336
346
|
correlationId: correlationId.toString(),
|
|
337
|
-
causation: causationEntries
|
|
347
|
+
causation: causationEntries,
|
|
348
|
+
tags: eventsToAppend[index].Tags.map(value => new Tag(value))
|
|
338
349
|
},
|
|
339
350
|
eventType,
|
|
340
351
|
content: event as Record<string, unknown>
|
|
@@ -385,18 +396,19 @@ export class EventSequence implements IEventSequence {
|
|
|
385
396
|
span.setAttribute('chronicle.event_source_id', eventSourceId);
|
|
386
397
|
}
|
|
387
398
|
try {
|
|
388
|
-
const response = await this._connection.eventSequences.
|
|
399
|
+
const response = await this._connection.eventSequences.tailSequenceNumber({
|
|
389
400
|
EventStore: this._eventStoreName,
|
|
390
401
|
Namespace: this._namespace,
|
|
391
402
|
EventSequenceId: this.id.value,
|
|
392
403
|
EventSourceId: eventSourceId ?? '',
|
|
393
|
-
|
|
404
|
+
EventTypeIds: this.joinEventTypeIds(filterEventTypes ?? []),
|
|
394
405
|
EventSourceType: eventSourceType ?? 'Default',
|
|
395
406
|
EventStreamId: eventStreamId ?? '',
|
|
396
407
|
EventStreamType: eventStreamType ?? 'Default'
|
|
397
408
|
});
|
|
398
409
|
|
|
399
|
-
const
|
|
410
|
+
const data = ensureQuerySuccess('get tail sequence number', response);
|
|
411
|
+
const result = new EventSequenceNumber(data?.SequenceNumber ?? 0n);
|
|
400
412
|
span.setAttribute('chronicle.sequence_number', result.value.toString());
|
|
401
413
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
402
414
|
return result;
|
|
@@ -431,7 +443,7 @@ export class EventSequence implements IEventSequence {
|
|
|
431
443
|
EventSourceId: eventSourceId
|
|
432
444
|
});
|
|
433
445
|
|
|
434
|
-
const result = response
|
|
446
|
+
const result = ensureQuerySuccess('has events for event source', response)?.HasEvents ?? false;
|
|
435
447
|
span.setAttribute('chronicle.has_events', result);
|
|
436
448
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
437
449
|
return result;
|
|
@@ -451,7 +463,8 @@ export class EventSequence implements IEventSequence {
|
|
|
451
463
|
eventTypes: Constructor[],
|
|
452
464
|
eventStreamType?: string,
|
|
453
465
|
eventStreamId?: string,
|
|
454
|
-
|
|
466
|
+
// Kept for IEventSequence signature compatibility; the wire no longer carries an event source type for this query.
|
|
467
|
+
_eventSourceType?: string
|
|
455
468
|
): Promise<AppendedEvent[]> {
|
|
456
469
|
return ChronicleTracer.startActiveSpan('chronicle.event_sequences.get_for_event_source_id_and_event_types', async span => {
|
|
457
470
|
span.setAttribute('chronicle.event_store', this._eventStoreName);
|
|
@@ -459,18 +472,17 @@ export class EventSequence implements IEventSequence {
|
|
|
459
472
|
span.setAttribute('chronicle.event_sequence_id', this.id.value);
|
|
460
473
|
span.setAttribute('chronicle.event_source_id', eventSourceId);
|
|
461
474
|
try {
|
|
462
|
-
const response = await this._connection.eventSequences.
|
|
475
|
+
const response = await this._connection.eventSequences.forEventSourceIdAndEventTypes({
|
|
463
476
|
EventStore: this._eventStoreName,
|
|
464
477
|
Namespace: this._namespace,
|
|
465
478
|
EventSequenceId: this.id.value,
|
|
466
|
-
EventSourceType: eventSourceType ?? 'Default',
|
|
467
479
|
EventSourceId: eventSourceId,
|
|
468
480
|
EventStreamType: eventStreamType ?? 'Default',
|
|
469
481
|
EventStreamId: eventStreamId ?? '',
|
|
470
|
-
|
|
482
|
+
EventTypeIds: this.joinEventTypeIds(eventTypes)
|
|
471
483
|
});
|
|
472
484
|
|
|
473
|
-
const result = (
|
|
485
|
+
const result = ensureQuerySuccess('get events for event source id and event types', response).map(event => this.toClientAppendedEvent(event));
|
|
474
486
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
475
487
|
return result;
|
|
476
488
|
} catch (error) {
|
|
@@ -495,17 +507,16 @@ export class EventSequence implements IEventSequence {
|
|
|
495
507
|
span.setAttribute('chronicle.event_sequence_id', this.id.value);
|
|
496
508
|
span.setAttribute('chronicle.sequence_number', sequenceNumber.value.toString());
|
|
497
509
|
try {
|
|
498
|
-
const response = await this._connection.eventSequences.
|
|
510
|
+
const response = await this._connection.eventSequences.fromSequenceNumber({
|
|
499
511
|
EventStore: this._eventStoreName,
|
|
500
512
|
Namespace: this._namespace,
|
|
501
513
|
EventSequenceId: this.id.value,
|
|
502
514
|
FromEventSequenceNumber: sequenceNumber.value,
|
|
503
|
-
ToEventSequenceNumber: 0n,
|
|
504
515
|
EventSourceId: eventSourceId ?? '',
|
|
505
|
-
|
|
516
|
+
EventTypeIds: this.joinEventTypeIds(filterEventTypes ?? [])
|
|
506
517
|
});
|
|
507
518
|
|
|
508
|
-
const result = (
|
|
519
|
+
const result = ensureQuerySuccess('get events from sequence number', response).map(event => this.toClientAppendedEvent(event));
|
|
509
520
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
510
521
|
return result;
|
|
511
522
|
} catch (error) {
|
|
@@ -523,7 +534,6 @@ export class EventSequence implements IEventSequence {
|
|
|
523
534
|
causationManager.add(CausationType.redact, { sequenceNumber: sequenceNumber.value.toString() });
|
|
524
535
|
const causationChain = causationManager.getCurrentChain();
|
|
525
536
|
const identity = identityProvider.getCurrent();
|
|
526
|
-
const correlationId = Guid.as(correlationIdManager.current.value);
|
|
527
537
|
|
|
528
538
|
return ChronicleTracer.startActiveSpan('chronicle.event_sequences.redact', async span => {
|
|
529
539
|
span.setAttribute('chronicle.event_store', this._eventStoreName);
|
|
@@ -531,20 +541,19 @@ export class EventSequence implements IEventSequence {
|
|
|
531
541
|
span.setAttribute('chronicle.event_sequence_id', this.id.value);
|
|
532
542
|
span.setAttribute('chronicle.sequence_number', sequenceNumber.value.toString());
|
|
533
543
|
try {
|
|
534
|
-
await this._connection.eventSequences.redact({
|
|
544
|
+
ensureCommandSuccess('redact event', await this._connection.eventSequences.redact({
|
|
535
545
|
EventStore: this._eventStoreName,
|
|
536
546
|
Namespace: this._namespace,
|
|
537
547
|
EventSequenceId: this.id.value,
|
|
538
548
|
SequenceNumber: sequenceNumber.value,
|
|
539
549
|
Reason: reason,
|
|
540
|
-
CorrelationId: toContractsGuid(correlationId),
|
|
541
550
|
Causation: causationChain.map(c => ({
|
|
542
551
|
Occurred: { Value: c.occurred.toISOString() },
|
|
543
552
|
Type: c.type.name,
|
|
544
553
|
Properties: { ...c.properties }
|
|
545
554
|
})),
|
|
546
555
|
CausedBy: toContractsCausedBy(identity)
|
|
547
|
-
});
|
|
556
|
+
}));
|
|
548
557
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
549
558
|
} catch (error) {
|
|
550
559
|
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
|
|
@@ -561,15 +570,7 @@ export class EventSequence implements IEventSequence {
|
|
|
561
570
|
causationManager.add(CausationType.redactForEventSource, { eventSourceId });
|
|
562
571
|
const causationChain = causationManager.getCurrentChain();
|
|
563
572
|
const identity = identityProvider.getCurrent();
|
|
564
|
-
const
|
|
565
|
-
const wireEventTypes = (eventTypes ?? []).map(constructor => {
|
|
566
|
-
const eventType = getEventTypeFor(constructor as unknown as Function);
|
|
567
|
-
return {
|
|
568
|
-
Id: eventType.id.value,
|
|
569
|
-
Generation: eventType.generation.value,
|
|
570
|
-
Tombstone: eventType.tombstone
|
|
571
|
-
};
|
|
572
|
-
});
|
|
573
|
+
const wireEventTypeIds = (eventTypes ?? []).map(constructor => getEventTypeFor(constructor as unknown as Function).id.value);
|
|
573
574
|
|
|
574
575
|
return ChronicleTracer.startActiveSpan('chronicle.event_sequences.redact_for_event_source', async span => {
|
|
575
576
|
span.setAttribute('chronicle.event_store', this._eventStoreName);
|
|
@@ -577,21 +578,20 @@ export class EventSequence implements IEventSequence {
|
|
|
577
578
|
span.setAttribute('chronicle.event_sequence_id', this.id.value);
|
|
578
579
|
span.setAttribute('chronicle.event_source_id', eventSourceId);
|
|
579
580
|
try {
|
|
580
|
-
await this._connection.eventSequences.redactForEventSource({
|
|
581
|
+
ensureCommandSuccess('redact event source', await this._connection.eventSequences.redactForEventSource({
|
|
581
582
|
EventStore: this._eventStoreName,
|
|
582
583
|
Namespace: this._namespace,
|
|
583
584
|
EventSequenceId: this.id.value,
|
|
584
585
|
EventSourceId: eventSourceId,
|
|
585
586
|
Reason: reason,
|
|
586
|
-
EventTypes:
|
|
587
|
-
CorrelationId: toContractsGuid(correlationId),
|
|
587
|
+
EventTypes: wireEventTypeIds,
|
|
588
588
|
Causation: causationChain.map(c => ({
|
|
589
589
|
Occurred: { Value: c.occurred.toISOString() },
|
|
590
590
|
Type: c.type.name,
|
|
591
591
|
Properties: { ...c.properties }
|
|
592
592
|
})),
|
|
593
593
|
CausedBy: toContractsCausedBy(identity)
|
|
594
|
-
});
|
|
594
|
+
}));
|
|
595
595
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
596
596
|
} catch (error) {
|
|
597
597
|
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
|
|
@@ -636,9 +636,10 @@ export class EventSequence implements IEventSequence {
|
|
|
636
636
|
EventStreamId: eventStreamId
|
|
637
637
|
});
|
|
638
638
|
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
639
|
+
const completeStreamResponse = ensureCommandResponse('complete stream', response);
|
|
640
|
+
const result: CompleteStreamResult = completeStreamResponse.IsSuccess
|
|
641
|
+
? { isSuccess: true, sequenceNumber: new EventSequenceNumber(completeStreamResponse.SequenceNumber ?? 0n) }
|
|
642
|
+
: { isSuccess: false, error: this.toClientCompleteStreamError(completeStreamResponse.Error) };
|
|
642
643
|
|
|
643
644
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
644
645
|
return result;
|
|
@@ -658,15 +659,8 @@ export class EventSequence implements IEventSequence {
|
|
|
658
659
|
return error === 1 ? CompleteStreamError.DefaultStreamCannotBeCompleted : CompleteStreamError.AlreadyCompleted;
|
|
659
660
|
}
|
|
660
661
|
|
|
661
|
-
private
|
|
662
|
-
return eventTypes.map(constructor =>
|
|
663
|
-
const eventType = getEventTypeFor(constructor as unknown as Function);
|
|
664
|
-
return {
|
|
665
|
-
Id: eventType.id.value,
|
|
666
|
-
Generation: eventType.generation.value,
|
|
667
|
-
Tombstone: eventType.tombstone
|
|
668
|
-
};
|
|
669
|
-
});
|
|
662
|
+
private joinEventTypeIds(eventTypes: Constructor[]): string {
|
|
663
|
+
return eventTypes.map(constructor => getEventTypeFor(constructor as unknown as Function).id.value).join(',');
|
|
670
664
|
}
|
|
671
665
|
|
|
672
666
|
private toClientAppendedEvent(wireEvent: ContractsAppendedEvent): AppendedEvent {
|
|
@@ -687,7 +681,8 @@ export class EventSequence implements IEventSequence {
|
|
|
687
681
|
causation: (context.Causation ?? []).map(c => ({
|
|
688
682
|
type: c.Type,
|
|
689
683
|
properties: { ...c.Properties }
|
|
690
|
-
}))
|
|
684
|
+
})),
|
|
685
|
+
tags: (context.Tags ?? []).map(value => new Tag(value))
|
|
691
686
|
},
|
|
692
687
|
eventType,
|
|
693
688
|
content: JSON.parse(wireEvent.Content) as Record<string, unknown>
|
package/events/EventContext.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { EventType } from './EventType';
|
|
5
5
|
import { CausationEntry } from './CausationEntry';
|
|
6
|
+
import { Tag } from './Tag';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Represents contextual information about an appended event.
|
|
@@ -25,4 +26,7 @@ export interface EventContext {
|
|
|
25
26
|
|
|
26
27
|
/** The causation chain for the event. */
|
|
27
28
|
readonly causation: ReadonlyArray<CausationEntry>;
|
|
29
|
+
|
|
30
|
+
/** The tags the event carries. */
|
|
31
|
+
readonly tags: ReadonlyArray<Tag>;
|
|
28
32
|
}
|
package/events/EventTypes.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { Constructor } from '@cratis/fundamentals';
|
|
5
5
|
import { IClientArtifactsProvider } from '../artifacts';
|
|
6
6
|
import { ChronicleConnection } from '../connection';
|
|
7
|
+
import { ensureCommandSuccess } from '../connection/callResults';
|
|
7
8
|
import { EventTypeId } from './EventTypeId';
|
|
8
9
|
import { IEventTypes } from './IEventTypes';
|
|
9
10
|
import { getEventTypeMetadata, getEventTypeJsonSchemaFor } from './eventTypeDecorator';
|
|
@@ -163,11 +164,11 @@ export class EventTypes implements IEventTypes {
|
|
|
163
164
|
return;
|
|
164
165
|
}
|
|
165
166
|
|
|
166
|
-
await this._connection.eventTypes.
|
|
167
|
+
ensureCommandSuccess('register event types', await this._connection.eventTypes.registerEventTypes({
|
|
167
168
|
EventStore: this._eventStore,
|
|
168
169
|
Types: registrations,
|
|
169
170
|
DisableValidation: false
|
|
170
|
-
});
|
|
171
|
+
}));
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
/** @inheritdoc */
|
package/events/Tag.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Represents a tag associated with an event, observer, or read model.
|
|
6
|
+
*/
|
|
7
|
+
export class Tag {
|
|
8
|
+
constructor(readonly value: string) {}
|
|
9
|
+
|
|
10
|
+
/** @inheritdoc */
|
|
11
|
+
toString(): string {
|
|
12
|
+
return this.value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import 'reflect-metadata';
|
|
5
|
+
import { Tag } from './Tag';
|
|
6
|
+
import { mergeTags } from './mergeTags';
|
|
7
|
+
|
|
8
|
+
/** Metadata key used to store the tags a reactor or reducer filters events by. */
|
|
9
|
+
const FILTER_TAGS_METADATA_KEY = 'chronicle:filterEventsByTag';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* TypeScript decorator that restricts a reactor or reducer so that it only handles events
|
|
13
|
+
* that carry a specific tag. This is the TypeScript equivalent of the C# `[FilterEventsByTag]`
|
|
14
|
+
* attribute.
|
|
15
|
+
*
|
|
16
|
+
* Apply this decorator to a `@reactor()` or `@reducer()`-decorated class to filter the observed
|
|
17
|
+
* event stream to events tagged with the given value. Use {@link tag}/{@link tags} when the
|
|
18
|
+
* intent is to *label* an observer for discoverability; use this decorator when the intent is
|
|
19
|
+
* to *filter* which events reach it. Applying the decorator more than once accumulates filter
|
|
20
|
+
* tags - a class handles events carrying any of the applied tags.
|
|
21
|
+
*
|
|
22
|
+
* @param value - The tag value that an event must carry in order to be dispatched to the observer.
|
|
23
|
+
* @returns A class decorator.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* @reactor()
|
|
28
|
+
* @filterEventsByTag('vip')
|
|
29
|
+
* class VipWelcomeReactor {
|
|
30
|
+
* async customerRegistered(event: CustomerRegistered): Promise<void> {
|
|
31
|
+
* console.log(`Welcome VIP customer ${event.emailAddress}`);
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function filterEventsByTag(value: string): ClassDecorator {
|
|
37
|
+
return (target: object) => {
|
|
38
|
+
const existing = (Reflect.getMetadata(FILTER_TAGS_METADATA_KEY, target) as Tag[] | undefined) ?? [];
|
|
39
|
+
const merged = mergeTags(existing, [value]).map(tagValue => new Tag(tagValue));
|
|
40
|
+
Reflect.defineMetadata(FILTER_TAGS_METADATA_KEY, merged, target);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Gets all filter tags applied to a class via {@link filterEventsByTag}.
|
|
46
|
+
* @param target - The class constructor to retrieve filter tags for.
|
|
47
|
+
* @returns The filter tags applied to the class, or an empty array if none are.
|
|
48
|
+
*/
|
|
49
|
+
export function getFilterTagsFor(target: Function): Tag[] {
|
|
50
|
+
return (Reflect.getMetadata(FILTER_TAGS_METADATA_KEY, target) as Tag[] | undefined) ?? [];
|
|
51
|
+
}
|
package/events/index.ts
CHANGED
|
@@ -11,5 +11,9 @@ export type { CausationEntry } from './CausationEntry';
|
|
|
11
11
|
export type { AppendedEvent } from './AppendedEvent';
|
|
12
12
|
export type { IEventTypes } from './IEventTypes';
|
|
13
13
|
export { EventTypes } from './EventTypes';
|
|
14
|
+
export { Tag } from './Tag';
|
|
15
|
+
export { tag, tags, getTagsFor } from './tagDecorator';
|
|
16
|
+
export { filterEventsByTag, getFilterTagsFor } from './filterEventsByTagDecorator';
|
|
17
|
+
export { mergeTags } from './mergeTags';
|
|
14
18
|
export * from './constraints';
|
|
15
19
|
export * from './migrations';
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
5
|
+
import { Tag } from './Tag';
|
|
6
|
+
import { mergeTags } from './mergeTags';
|
|
7
|
+
|
|
8
|
+
describe('mergeTags', () => {
|
|
9
|
+
describe('when merging a single source of strings', () => {
|
|
10
|
+
it('should return the values unchanged', () => {
|
|
11
|
+
expect(mergeTags(['a', 'b'])).toEqual(['a', 'b']);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('when merging several sources', () => {
|
|
16
|
+
it('should combine them in order', () => {
|
|
17
|
+
expect(mergeTags(['a'], ['b'], ['c'])).toEqual(['a', 'b', 'c']);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('when the same value appears in more than one source', () => {
|
|
22
|
+
it('should keep only the first occurrence', () => {
|
|
23
|
+
expect(mergeTags(['a', 'b'], ['b', 'c'])).toEqual(['a', 'b', 'c']);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('when a source holds Tag instances', () => {
|
|
28
|
+
it('should unwrap them to their values', () => {
|
|
29
|
+
expect(mergeTags([new Tag('a')], ['b'])).toEqual(['a', 'b']);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('when a source is undefined', () => {
|
|
34
|
+
it('should skip it', () => {
|
|
35
|
+
expect(mergeTags(['a'], undefined, ['b'])).toEqual(['a', 'b']);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('when a value is blank', () => {
|
|
40
|
+
it('should drop it, so an empty tag never reaches the wire', () => {
|
|
41
|
+
expect(mergeTags(['a', '', ' '])).toEqual(['a']);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('when there are no sources at all', () => {
|
|
46
|
+
it('should return an empty list', () => {
|
|
47
|
+
expect(mergeTags()).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { Tag } from './Tag';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Merges one or more sources of tags - static tags declared on a type, tags carried by an
|
|
8
|
+
* individual event instance, and tags supplied at call time - into a single, distinct list of
|
|
9
|
+
* tag values. Mirrors the .NET client, which merges static, instance, and dynamic tags the
|
|
10
|
+
* same way before sending an append request.
|
|
11
|
+
* @param sources - The tag sources to merge. Each may be a mix of strings and {@link Tag} instances,
|
|
12
|
+
* or undefined when that source contributed no tags.
|
|
13
|
+
* @returns The distinct, merged tag values.
|
|
14
|
+
*/
|
|
15
|
+
export function mergeTags(...sources: ReadonlyArray<Iterable<string | Tag> | undefined>): string[] {
|
|
16
|
+
const values = new Set<string>();
|
|
17
|
+
for (const source of sources) {
|
|
18
|
+
if (!source) continue;
|
|
19
|
+
for (const entry of source) {
|
|
20
|
+
const value = typeof entry === 'string' ? entry : entry.value;
|
|
21
|
+
if (value.trim().length > 0) {
|
|
22
|
+
values.add(value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return [...values];
|
|
27
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
5
|
+
import { getTagsFor, tag, tags } from './tagDecorator';
|
|
6
|
+
import { filterEventsByTag, getFilterTagsFor } from './filterEventsByTagDecorator';
|
|
7
|
+
|
|
8
|
+
describe('tag', () => {
|
|
9
|
+
describe('when a class carries no tags', () => {
|
|
10
|
+
class Untagged {}
|
|
11
|
+
|
|
12
|
+
it('should report no tags', () => {
|
|
13
|
+
expect(getTagsFor(Untagged)).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('when decorating a class with a single tag', () => {
|
|
18
|
+
class SingleTagged {}
|
|
19
|
+
tag('analytics')(SingleTagged);
|
|
20
|
+
|
|
21
|
+
it('should carry that tag', () => {
|
|
22
|
+
expect(getTagsFor(SingleTagged).map(_ => _.value)).toEqual(['analytics']);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('when decorating a class with several tags at once', () => {
|
|
27
|
+
class MultiTagged {}
|
|
28
|
+
tag('analytics', 'user-action')(MultiTagged);
|
|
29
|
+
|
|
30
|
+
it('should carry every tag', () => {
|
|
31
|
+
expect(getTagsFor(MultiTagged).map(_ => _.value)).toEqual(['analytics', 'user-action']);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('when applying the decorator more than once', () => {
|
|
36
|
+
class Accumulated {}
|
|
37
|
+
tag('first')(Accumulated);
|
|
38
|
+
tag('second')(Accumulated);
|
|
39
|
+
|
|
40
|
+
it('should accumulate rather than replace', () => {
|
|
41
|
+
expect(getTagsFor(Accumulated).map(_ => _.value)).toEqual(['first', 'second']);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('when the same tag is applied twice', () => {
|
|
46
|
+
class Duplicated {}
|
|
47
|
+
tag('same')(Duplicated);
|
|
48
|
+
tag('same')(Duplicated);
|
|
49
|
+
|
|
50
|
+
it('should keep it once', () => {
|
|
51
|
+
expect(getTagsFor(Duplicated).map(_ => _.value)).toEqual(['same']);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('when using the plural tags decorator', () => {
|
|
56
|
+
class PluralTagged {}
|
|
57
|
+
tags('a', 'b')(PluralTagged);
|
|
58
|
+
|
|
59
|
+
it('should behave identically to tag', () => {
|
|
60
|
+
expect(getTagsFor(PluralTagged).map(_ => _.value)).toEqual(['a', 'b']);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('when two classes are tagged separately', () => {
|
|
65
|
+
class FirstTagged {}
|
|
66
|
+
class SecondTagged {}
|
|
67
|
+
tag('one')(FirstTagged);
|
|
68
|
+
tag('two')(SecondTagged);
|
|
69
|
+
|
|
70
|
+
it('should not leak tags between them', () => {
|
|
71
|
+
expect(getTagsFor(FirstTagged).map(_ => _.value)).toEqual(['one']);
|
|
72
|
+
expect(getTagsFor(SecondTagged).map(_ => _.value)).toEqual(['two']);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('filterEventsByTag', () => {
|
|
78
|
+
describe('when a class carries no filter', () => {
|
|
79
|
+
class Unfiltered {}
|
|
80
|
+
|
|
81
|
+
it('should report no filter tags', () => {
|
|
82
|
+
expect(getFilterTagsFor(Unfiltered)).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('when decorating a class with a filter tag', () => {
|
|
87
|
+
class Filtered {}
|
|
88
|
+
filterEventsByTag('vip')(Filtered);
|
|
89
|
+
|
|
90
|
+
it('should carry that filter tag', () => {
|
|
91
|
+
expect(getFilterTagsFor(Filtered).map(_ => _.value)).toEqual(['vip']);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('when applying the decorator more than once', () => {
|
|
96
|
+
class MultiFiltered {}
|
|
97
|
+
filterEventsByTag('vip')(MultiFiltered);
|
|
98
|
+
filterEventsByTag('premium')(MultiFiltered);
|
|
99
|
+
|
|
100
|
+
it('should accumulate, so the observer handles events carrying any of them', () => {
|
|
101
|
+
expect(getFilterTagsFor(MultiFiltered).map(_ => _.value)).toEqual(['vip', 'premium']);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('when a class is both labeled and filtered', () => {
|
|
106
|
+
class LabeledAndFiltered {}
|
|
107
|
+
tag('reporting')(LabeledAndFiltered);
|
|
108
|
+
filterEventsByTag('vip')(LabeledAndFiltered);
|
|
109
|
+
|
|
110
|
+
it('should keep labeling and filtering separate', () => {
|
|
111
|
+
expect(getTagsFor(LabeledAndFiltered).map(_ => _.value)).toEqual(['reporting']);
|
|
112
|
+
expect(getFilterTagsFor(LabeledAndFiltered).map(_ => _.value)).toEqual(['vip']);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|