@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.
Files changed (223) hide show
  1. package/ChronicleClient.ts +3 -3
  2. package/EventStore.ts +4 -4
  3. package/compliance/ComplianceContracts.ts +59 -0
  4. package/compliance/IPIIManager.ts +12 -0
  5. package/compliance/PIIManager.spec.ts +50 -0
  6. package/compliance/PIIManager.ts +9 -0
  7. package/compliance/PIINotSupportedOnEventSourceId.ts +21 -0
  8. package/compliance/index.ts +2 -0
  9. package/compliance/pii.spec.ts +37 -0
  10. package/compliance/pii.ts +16 -1
  11. package/compliance/subject.spec.ts +45 -0
  12. package/compliance/subject.ts +64 -0
  13. package/connection/ChronicleConnection.ts +6 -0
  14. package/connection/ChronicleServices.ts +2 -0
  15. package/connection/callResults.ts +23 -0
  16. package/dist/ChronicleClient.js +3 -3
  17. package/dist/ChronicleClient.js.map +1 -1
  18. package/dist/EventStore.js +4 -4
  19. package/dist/EventStore.js.map +1 -1
  20. package/dist/compliance/ComplianceContracts.d.ts +31 -0
  21. package/dist/compliance/ComplianceContracts.d.ts.map +1 -1
  22. package/dist/compliance/ComplianceContracts.js +52 -0
  23. package/dist/compliance/ComplianceContracts.js.map +1 -1
  24. package/dist/compliance/IPIIManager.d.ts +11 -0
  25. package/dist/compliance/IPIIManager.d.ts.map +1 -1
  26. package/dist/compliance/PIIManager.d.ts +2 -0
  27. package/dist/compliance/PIIManager.d.ts.map +1 -1
  28. package/dist/compliance/PIIManager.js +8 -0
  29. package/dist/compliance/PIIManager.js.map +1 -1
  30. package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts +11 -0
  31. package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts.map +1 -0
  32. package/dist/compliance/PIINotSupportedOnEventSourceId.js +19 -0
  33. package/dist/compliance/PIINotSupportedOnEventSourceId.js.map +1 -0
  34. package/dist/compliance/index.d.ts +2 -0
  35. package/dist/compliance/index.d.ts.map +1 -1
  36. package/dist/compliance/index.js +2 -0
  37. package/dist/compliance/index.js.map +1 -1
  38. package/dist/compliance/pii.d.ts.map +1 -1
  39. package/dist/compliance/pii.js +13 -1
  40. package/dist/compliance/pii.js.map +1 -1
  41. package/dist/compliance/subject.d.ts +39 -0
  42. package/dist/compliance/subject.d.ts.map +1 -0
  43. package/dist/compliance/subject.js +58 -0
  44. package/dist/compliance/subject.js.map +1 -0
  45. package/dist/connection/ChronicleConnection.d.ts +1 -0
  46. package/dist/connection/ChronicleConnection.d.ts.map +1 -1
  47. package/dist/connection/ChronicleConnection.js +5 -1
  48. package/dist/connection/ChronicleConnection.js.map +1 -1
  49. package/dist/connection/ChronicleServices.d.ts +2 -1
  50. package/dist/connection/ChronicleServices.d.ts.map +1 -1
  51. package/dist/connection/callResults.d.ts +13 -0
  52. package/dist/connection/callResults.d.ts.map +1 -1
  53. package/dist/connection/callResults.js +14 -0
  54. package/dist/connection/callResults.js.map +1 -1
  55. package/dist/eventSequences/AppendOptions.d.ts +8 -0
  56. package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
  57. package/dist/eventSequences/EventForEventSourceId.d.ts +7 -0
  58. package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
  59. package/dist/eventSequences/EventSequence.d.ts +2 -2
  60. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  61. package/dist/eventSequences/EventSequence.js +56 -63
  62. package/dist/eventSequences/EventSequence.js.map +1 -1
  63. package/dist/events/EventContext.d.ts +3 -0
  64. package/dist/events/EventContext.d.ts.map +1 -1
  65. package/dist/events/EventTypes.d.ts.map +1 -1
  66. package/dist/events/EventTypes.js +3 -2
  67. package/dist/events/EventTypes.js.map +1 -1
  68. package/dist/events/Tag.d.ts +10 -0
  69. package/dist/events/Tag.d.ts.map +1 -0
  70. package/dist/events/Tag.js +16 -0
  71. package/dist/events/Tag.js.map +1 -0
  72. package/dist/events/filterEventsByTagDecorator.d.ts +35 -0
  73. package/dist/events/filterEventsByTagDecorator.d.ts.map +1 -0
  74. package/dist/events/filterEventsByTagDecorator.js +48 -0
  75. package/dist/events/filterEventsByTagDecorator.js.map +1 -0
  76. package/dist/events/index.d.ts +4 -0
  77. package/dist/events/index.d.ts.map +1 -1
  78. package/dist/events/index.js +4 -0
  79. package/dist/events/index.js.map +1 -1
  80. package/dist/events/mergeTags.d.ts +12 -0
  81. package/dist/events/mergeTags.d.ts.map +1 -0
  82. package/dist/events/mergeTags.js +26 -0
  83. package/dist/events/mergeTags.js.map +1 -0
  84. package/dist/events/tagDecorator.d.ts +38 -0
  85. package/dist/events/tagDecorator.d.ts.map +1 -0
  86. package/dist/events/tagDecorator.js +54 -0
  87. package/dist/events/tagDecorator.js.map +1 -0
  88. package/dist/jobs/Jobs.js +3 -3
  89. package/dist/jobs/Jobs.js.map +1 -1
  90. package/dist/observation/ObserverRunningState.d.ts +9 -15
  91. package/dist/observation/ObserverRunningState.d.ts.map +1 -1
  92. package/dist/observation/ObserverRunningState.js +8 -14
  93. package/dist/observation/ObserverRunningState.js.map +1 -1
  94. package/dist/observation/index.d.ts +1 -0
  95. package/dist/observation/index.d.ts.map +1 -1
  96. package/dist/observation/index.js +1 -0
  97. package/dist/observation/index.js.map +1 -1
  98. package/dist/observation/toObserverRunningState.d.ts +9 -0
  99. package/dist/observation/toObserverRunningState.d.ts.map +1 -0
  100. package/dist/observation/toObserverRunningState.js +26 -0
  101. package/dist/observation/toObserverRunningState.js.map +1 -0
  102. package/dist/projections/IProjections.d.ts +71 -1
  103. package/dist/projections/IProjections.d.ts.map +1 -1
  104. package/dist/projections/ProjectionQueryResult.d.ts +8 -0
  105. package/dist/projections/ProjectionQueryResult.d.ts.map +1 -0
  106. package/dist/projections/ProjectionQueryResult.js +4 -0
  107. package/dist/projections/ProjectionQueryResult.js.map +1 -0
  108. package/dist/projections/ProjectionState.d.ts +18 -0
  109. package/dist/projections/ProjectionState.d.ts.map +1 -0
  110. package/dist/projections/ProjectionState.js +4 -0
  111. package/dist/projections/ProjectionState.js.map +1 -0
  112. package/dist/projections/Projections.d.ts +46 -1
  113. package/dist/projections/Projections.d.ts.map +1 -1
  114. package/dist/projections/Projections.js +153 -6
  115. package/dist/projections/Projections.js.map +1 -1
  116. package/dist/projections/UnableToQueryProjection.d.ts +12 -0
  117. package/dist/projections/UnableToQueryProjection.d.ts.map +1 -0
  118. package/dist/projections/UnableToQueryProjection.js +17 -0
  119. package/dist/projections/UnableToQueryProjection.js.map +1 -0
  120. package/dist/projections/index.d.ts +3 -0
  121. package/dist/projections/index.d.ts.map +1 -1
  122. package/dist/projections/index.js +1 -0
  123. package/dist/projections/index.js.map +1 -1
  124. package/dist/projections/modelBound/eventSequence.d.ts +22 -0
  125. package/dist/projections/modelBound/eventSequence.d.ts.map +1 -0
  126. package/dist/projections/modelBound/eventSequence.js +34 -0
  127. package/dist/projections/modelBound/eventSequence.js.map +1 -0
  128. package/dist/projections/modelBound/fromAll.d.ts +26 -0
  129. package/dist/projections/modelBound/fromAll.d.ts.map +1 -0
  130. package/dist/projections/modelBound/fromAll.js +31 -0
  131. package/dist/projections/modelBound/fromAll.js.map +1 -0
  132. package/dist/projections/modelBound/index.d.ts +4 -0
  133. package/dist/projections/modelBound/index.d.ts.map +1 -1
  134. package/dist/projections/modelBound/index.js +3 -0
  135. package/dist/projections/modelBound/index.js.map +1 -1
  136. package/dist/projections/modelBound/noAutoMap.d.ts +26 -0
  137. package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -0
  138. package/dist/projections/modelBound/noAutoMap.js +44 -0
  139. package/dist/projections/modelBound/noAutoMap.js.map +1 -0
  140. package/dist/reactors/Reactors.d.ts.map +1 -1
  141. package/dist/reactors/Reactors.js +7 -3
  142. package/dist/reactors/Reactors.js.map +1 -1
  143. package/dist/readModels/MaterializedReadModels.d.ts +0 -1
  144. package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
  145. package/dist/readModels/MaterializedReadModels.js +2 -8
  146. package/dist/readModels/MaterializedReadModels.js.map +1 -1
  147. package/dist/readModels/ReadModelSubjectResolver.d.ts +22 -0
  148. package/dist/readModels/ReadModelSubjectResolver.d.ts.map +1 -0
  149. package/dist/readModels/ReadModelSubjectResolver.js +38 -0
  150. package/dist/readModels/ReadModelSubjectResolver.js.map +1 -0
  151. package/dist/readModels/ReadModels.d.ts.map +1 -1
  152. package/dist/readModels/ReadModels.js +15 -13
  153. package/dist/readModels/ReadModels.js.map +1 -1
  154. package/dist/readModels/index.d.ts +1 -0
  155. package/dist/readModels/index.d.ts.map +1 -1
  156. package/dist/readModels/index.js +1 -0
  157. package/dist/readModels/index.js.map +1 -1
  158. package/dist/reducers/Reducers.d.ts.map +1 -1
  159. package/dist/reducers/Reducers.js +21 -3
  160. package/dist/reducers/Reducers.js.map +1 -1
  161. package/dist/schemas/JsonSchema.d.ts +14 -0
  162. package/dist/schemas/JsonSchema.d.ts.map +1 -1
  163. package/dist/schemas/JsonSchemaGenerator.d.ts +61 -5
  164. package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
  165. package/dist/schemas/JsonSchemaGenerator.js +130 -26
  166. package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
  167. package/dist/schemas/index.d.ts +1 -1
  168. package/dist/schemas/index.d.ts.map +1 -1
  169. package/dist/seeding/EventSeeding.d.ts.map +1 -1
  170. package/dist/seeding/EventSeeding.js +5 -2
  171. package/dist/seeding/EventSeeding.js.map +1 -1
  172. package/dist/tsconfig.tsbuildinfo +1 -1
  173. package/dist/webhooks/Webhooks.d.ts.map +1 -1
  174. package/dist/webhooks/Webhooks.js +22 -10
  175. package/dist/webhooks/Webhooks.js.map +1 -1
  176. package/eventSequences/AppendOptions.ts +9 -0
  177. package/eventSequences/EventForEventSourceId.ts +9 -0
  178. package/eventSequences/EventSequence.spec.ts +83 -74
  179. package/eventSequences/EventSequence.ts +64 -69
  180. package/events/EventContext.ts +4 -0
  181. package/events/EventTypes.ts +3 -2
  182. package/events/Tag.ts +14 -0
  183. package/events/filterEventsByTagDecorator.ts +51 -0
  184. package/events/index.ts +4 -0
  185. package/events/mergeTags.spec.ts +50 -0
  186. package/events/mergeTags.ts +27 -0
  187. package/events/tagDecorator.spec.ts +115 -0
  188. package/events/tagDecorator.ts +59 -0
  189. package/jobs/Jobs.ts +3 -3
  190. package/observation/ObserverRunningState.ts +9 -18
  191. package/observation/index.ts +1 -0
  192. package/observation/toObserverRunningState.ts +27 -0
  193. package/package.json +9 -9
  194. package/projections/IProjections.ts +81 -1
  195. package/projections/ProjectionQueryResult.ts +10 -0
  196. package/projections/ProjectionState.ts +25 -0
  197. package/projections/Projections.childrenAndNested.spec.ts +1 -1
  198. package/projections/Projections.modelBoundCompleteness.spec.ts +224 -0
  199. package/projections/Projections.operationalSurface.spec.ts +257 -0
  200. package/projections/Projections.spec.ts +1 -1
  201. package/projections/Projections.ts +180 -5
  202. package/projections/UnableToQueryProjection.ts +17 -0
  203. package/projections/index.ts +3 -0
  204. package/projections/modelBound/eventSequence.ts +38 -0
  205. package/projections/modelBound/fromAll.ts +42 -0
  206. package/projections/modelBound/index.ts +4 -0
  207. package/projections/modelBound/noAutoMap.ts +47 -0
  208. package/reactors/Reactors.ts +7 -3
  209. package/readModels/MaterializedReadModels.spec.ts +98 -0
  210. package/readModels/MaterializedReadModels.ts +2 -8
  211. package/readModels/ReadModelSubjectResolver.spec.ts +84 -0
  212. package/readModels/ReadModelSubjectResolver.ts +44 -0
  213. package/readModels/ReadModels.spec.ts +118 -0
  214. package/readModels/ReadModels.ts +15 -13
  215. package/readModels/index.ts +1 -0
  216. package/reducers/Reducers.spec.ts +80 -0
  217. package/reducers/Reducers.ts +22 -3
  218. package/schemas/JsonSchema.ts +16 -0
  219. package/schemas/JsonSchemaGenerator.spec.ts +195 -0
  220. package/schemas/JsonSchemaGenerator.ts +145 -27
  221. package/schemas/index.ts +1 -1
  222. package/seeding/EventSeeding.ts +5 -2
  223. 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 { AppendedEvent as ContractsAppendedEvent } from '@cratis/chronicle.contracts';
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
- response.SequenceNumber,
119
- response.ConstraintViolations ?? [],
120
- response.Errors ?? [],
121
- response.ConcurrencyViolation
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
- Causation: batchCausationChain.map(c => ({
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.appendMany({
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
- ...Object.fromEntries(distinctEventSourceIds.map(eventSourceId => [eventSourceId, resolveConcurrencyScope(eventSourceId)]))
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 = (response.ConcurrencyViolations ?? [])[0];
292
- const result = (response.SequenceNumbers ?? []).map((sequenceNumber: bigint, index: number) =>
301
+ const firstConcurrencyViolation = (appendManyResponse.ConcurrencyViolations ?? [])[0];
302
+ const result = (appendManyResponse.SequenceNumbers ?? []).map((sequenceNumber: bigint, index: number) =>
293
303
  this.mapAppendResponse(
294
304
  sequenceNumber,
295
- response.ConstraintViolations ?? [],
296
- (response.Errors ?? []).filter((_: string, errorIndex: number) => errorIndex === index),
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.getTailSequenceNumber({
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
- EventTypes: this.toContractEventTypes(filterEventTypes ?? []),
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 result = new EventSequenceNumber(response.SequenceNumber ?? 0n);
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.HasEvents ?? false;
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
- eventSourceType?: string
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.getForEventSourceIdAndEventTypes({
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
- EventTypes: this.toContractEventTypes(eventTypes)
482
+ EventTypeIds: this.joinEventTypeIds(eventTypes)
471
483
  });
472
484
 
473
- const result = (response.Events ?? []).map(event => this.toClientAppendedEvent(event));
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.getEventsFromEventSequenceNumber({
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
- EventTypes: this.toContractEventTypes(filterEventTypes ?? [])
516
+ EventTypeIds: this.joinEventTypeIds(filterEventTypes ?? [])
506
517
  });
507
518
 
508
- const result = (response.Events ?? []).map(event => this.toClientAppendedEvent(event));
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 correlationId = Guid.as(correlationIdManager.current.value);
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: wireEventTypes,
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 result: CompleteStreamResult = response.IsSuccess
640
- ? { isSuccess: true, sequenceNumber: new EventSequenceNumber(response.SequenceNumber ?? 0n) }
641
- : { isSuccess: false, error: this.toClientCompleteStreamError(response.Error) };
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 toContractEventTypes(eventTypes: Constructor[]) {
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>
@@ -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
  }
@@ -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.register({
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
+ });