@cratis/chronicle 3.1.1 → 4.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 (194) hide show
  1. package/EventStore.ts +1 -1
  2. package/compliance/ComplianceContracts.ts +59 -0
  3. package/compliance/IPIIManager.ts +12 -0
  4. package/compliance/PIIManager.spec.ts +50 -0
  5. package/compliance/PIIManager.ts +9 -0
  6. package/compliance/PIINotSupportedOnEventSourceId.ts +21 -0
  7. package/compliance/index.ts +2 -0
  8. package/compliance/pii.spec.ts +37 -0
  9. package/compliance/pii.ts +16 -1
  10. package/compliance/subject.spec.ts +45 -0
  11. package/compliance/subject.ts +64 -0
  12. package/dist/EventStore.js +1 -1
  13. package/dist/EventStore.js.map +1 -1
  14. package/dist/compliance/ComplianceContracts.d.ts +31 -0
  15. package/dist/compliance/ComplianceContracts.d.ts.map +1 -1
  16. package/dist/compliance/ComplianceContracts.js +52 -0
  17. package/dist/compliance/ComplianceContracts.js.map +1 -1
  18. package/dist/compliance/IPIIManager.d.ts +11 -0
  19. package/dist/compliance/IPIIManager.d.ts.map +1 -1
  20. package/dist/compliance/PIIManager.d.ts +2 -0
  21. package/dist/compliance/PIIManager.d.ts.map +1 -1
  22. package/dist/compliance/PIIManager.js +8 -0
  23. package/dist/compliance/PIIManager.js.map +1 -1
  24. package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts +11 -0
  25. package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts.map +1 -0
  26. package/dist/compliance/PIINotSupportedOnEventSourceId.js +19 -0
  27. package/dist/compliance/PIINotSupportedOnEventSourceId.js.map +1 -0
  28. package/dist/compliance/index.d.ts +2 -0
  29. package/dist/compliance/index.d.ts.map +1 -1
  30. package/dist/compliance/index.js +2 -0
  31. package/dist/compliance/index.js.map +1 -1
  32. package/dist/compliance/pii.d.ts.map +1 -1
  33. package/dist/compliance/pii.js +13 -1
  34. package/dist/compliance/pii.js.map +1 -1
  35. package/dist/compliance/subject.d.ts +39 -0
  36. package/dist/compliance/subject.d.ts.map +1 -0
  37. package/dist/compliance/subject.js +58 -0
  38. package/dist/compliance/subject.js.map +1 -0
  39. package/dist/eventSequences/AppendOptions.d.ts +8 -0
  40. package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
  41. package/dist/eventSequences/EventForEventSourceId.d.ts +7 -0
  42. package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
  43. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  44. package/dist/eventSequences/EventSequence.js +17 -6
  45. package/dist/eventSequences/EventSequence.js.map +1 -1
  46. package/dist/events/EventContext.d.ts +3 -0
  47. package/dist/events/EventContext.d.ts.map +1 -1
  48. package/dist/events/Tag.d.ts +10 -0
  49. package/dist/events/Tag.d.ts.map +1 -0
  50. package/dist/events/Tag.js +16 -0
  51. package/dist/events/Tag.js.map +1 -0
  52. package/dist/events/filterEventsByTagDecorator.d.ts +35 -0
  53. package/dist/events/filterEventsByTagDecorator.d.ts.map +1 -0
  54. package/dist/events/filterEventsByTagDecorator.js +48 -0
  55. package/dist/events/filterEventsByTagDecorator.js.map +1 -0
  56. package/dist/events/index.d.ts +4 -0
  57. package/dist/events/index.d.ts.map +1 -1
  58. package/dist/events/index.js +4 -0
  59. package/dist/events/index.js.map +1 -1
  60. package/dist/events/mergeTags.d.ts +12 -0
  61. package/dist/events/mergeTags.d.ts.map +1 -0
  62. package/dist/events/mergeTags.js +26 -0
  63. package/dist/events/mergeTags.js.map +1 -0
  64. package/dist/events/tagDecorator.d.ts +38 -0
  65. package/dist/events/tagDecorator.d.ts.map +1 -0
  66. package/dist/events/tagDecorator.js +54 -0
  67. package/dist/events/tagDecorator.js.map +1 -0
  68. package/dist/observation/ObserverRunningState.d.ts +9 -15
  69. package/dist/observation/ObserverRunningState.d.ts.map +1 -1
  70. package/dist/observation/ObserverRunningState.js +8 -14
  71. package/dist/observation/ObserverRunningState.js.map +1 -1
  72. package/dist/observation/index.d.ts +1 -0
  73. package/dist/observation/index.d.ts.map +1 -1
  74. package/dist/observation/index.js +1 -0
  75. package/dist/observation/index.js.map +1 -1
  76. package/dist/observation/toObserverRunningState.d.ts +9 -0
  77. package/dist/observation/toObserverRunningState.d.ts.map +1 -0
  78. package/dist/observation/toObserverRunningState.js +26 -0
  79. package/dist/observation/toObserverRunningState.js.map +1 -0
  80. package/dist/projections/IProjections.d.ts +71 -1
  81. package/dist/projections/IProjections.d.ts.map +1 -1
  82. package/dist/projections/ProjectionQueryResult.d.ts +8 -0
  83. package/dist/projections/ProjectionQueryResult.d.ts.map +1 -0
  84. package/dist/projections/ProjectionQueryResult.js +4 -0
  85. package/dist/projections/ProjectionQueryResult.js.map +1 -0
  86. package/dist/projections/ProjectionState.d.ts +18 -0
  87. package/dist/projections/ProjectionState.d.ts.map +1 -0
  88. package/dist/projections/ProjectionState.js +4 -0
  89. package/dist/projections/ProjectionState.js.map +1 -0
  90. package/dist/projections/Projections.d.ts +46 -1
  91. package/dist/projections/Projections.d.ts.map +1 -1
  92. package/dist/projections/Projections.js +153 -6
  93. package/dist/projections/Projections.js.map +1 -1
  94. package/dist/projections/UnableToQueryProjection.d.ts +12 -0
  95. package/dist/projections/UnableToQueryProjection.d.ts.map +1 -0
  96. package/dist/projections/UnableToQueryProjection.js +17 -0
  97. package/dist/projections/UnableToQueryProjection.js.map +1 -0
  98. package/dist/projections/index.d.ts +3 -0
  99. package/dist/projections/index.d.ts.map +1 -1
  100. package/dist/projections/index.js +1 -0
  101. package/dist/projections/index.js.map +1 -1
  102. package/dist/projections/modelBound/eventSequence.d.ts +22 -0
  103. package/dist/projections/modelBound/eventSequence.d.ts.map +1 -0
  104. package/dist/projections/modelBound/eventSequence.js +34 -0
  105. package/dist/projections/modelBound/eventSequence.js.map +1 -0
  106. package/dist/projections/modelBound/fromAll.d.ts +26 -0
  107. package/dist/projections/modelBound/fromAll.d.ts.map +1 -0
  108. package/dist/projections/modelBound/fromAll.js +31 -0
  109. package/dist/projections/modelBound/fromAll.js.map +1 -0
  110. package/dist/projections/modelBound/index.d.ts +4 -0
  111. package/dist/projections/modelBound/index.d.ts.map +1 -1
  112. package/dist/projections/modelBound/index.js +3 -0
  113. package/dist/projections/modelBound/index.js.map +1 -1
  114. package/dist/projections/modelBound/noAutoMap.d.ts +26 -0
  115. package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -0
  116. package/dist/projections/modelBound/noAutoMap.js +44 -0
  117. package/dist/projections/modelBound/noAutoMap.js.map +1 -0
  118. package/dist/reactors/Reactors.d.ts.map +1 -1
  119. package/dist/reactors/Reactors.js +7 -3
  120. package/dist/reactors/Reactors.js.map +1 -1
  121. package/dist/readModels/MaterializedReadModels.d.ts +0 -1
  122. package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
  123. package/dist/readModels/MaterializedReadModels.js +2 -8
  124. package/dist/readModels/MaterializedReadModels.js.map +1 -1
  125. package/dist/readModels/ReadModelSubjectResolver.d.ts +22 -0
  126. package/dist/readModels/ReadModelSubjectResolver.d.ts.map +1 -0
  127. package/dist/readModels/ReadModelSubjectResolver.js +38 -0
  128. package/dist/readModels/ReadModelSubjectResolver.js.map +1 -0
  129. package/dist/readModels/ReadModels.d.ts.map +1 -1
  130. package/dist/readModels/ReadModels.js +7 -7
  131. package/dist/readModels/ReadModels.js.map +1 -1
  132. package/dist/readModels/index.d.ts +1 -0
  133. package/dist/readModels/index.d.ts.map +1 -1
  134. package/dist/readModels/index.js +1 -0
  135. package/dist/readModels/index.js.map +1 -1
  136. package/dist/reducers/Reducers.d.ts.map +1 -1
  137. package/dist/reducers/Reducers.js +21 -3
  138. package/dist/reducers/Reducers.js.map +1 -1
  139. package/dist/schemas/JsonSchema.d.ts +14 -0
  140. package/dist/schemas/JsonSchema.d.ts.map +1 -1
  141. package/dist/schemas/JsonSchemaGenerator.d.ts +61 -5
  142. package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
  143. package/dist/schemas/JsonSchemaGenerator.js +130 -26
  144. package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
  145. package/dist/schemas/index.d.ts +1 -1
  146. package/dist/schemas/index.d.ts.map +1 -1
  147. package/dist/seeding/EventSeeding.d.ts.map +1 -1
  148. package/dist/seeding/EventSeeding.js +3 -1
  149. package/dist/seeding/EventSeeding.js.map +1 -1
  150. package/dist/tsconfig.tsbuildinfo +1 -1
  151. package/eventSequences/AppendOptions.ts +9 -0
  152. package/eventSequences/EventForEventSourceId.ts +9 -0
  153. package/eventSequences/EventSequence.ts +20 -6
  154. package/events/EventContext.ts +4 -0
  155. package/events/Tag.ts +14 -0
  156. package/events/filterEventsByTagDecorator.ts +51 -0
  157. package/events/index.ts +4 -0
  158. package/events/mergeTags.spec.ts +50 -0
  159. package/events/mergeTags.ts +27 -0
  160. package/events/tagDecorator.spec.ts +115 -0
  161. package/events/tagDecorator.ts +59 -0
  162. package/observation/ObserverRunningState.ts +9 -18
  163. package/observation/index.ts +1 -0
  164. package/observation/toObserverRunningState.ts +27 -0
  165. package/package.json +8 -8
  166. package/projections/IProjections.ts +81 -1
  167. package/projections/ProjectionQueryResult.ts +10 -0
  168. package/projections/ProjectionState.ts +25 -0
  169. package/projections/Projections.childrenAndNested.spec.ts +1 -1
  170. package/projections/Projections.modelBoundCompleteness.spec.ts +224 -0
  171. package/projections/Projections.operationalSurface.spec.ts +257 -0
  172. package/projections/Projections.spec.ts +1 -1
  173. package/projections/Projections.ts +180 -5
  174. package/projections/UnableToQueryProjection.ts +17 -0
  175. package/projections/index.ts +3 -0
  176. package/projections/modelBound/eventSequence.ts +38 -0
  177. package/projections/modelBound/fromAll.ts +42 -0
  178. package/projections/modelBound/index.ts +4 -0
  179. package/projections/modelBound/noAutoMap.ts +47 -0
  180. package/reactors/Reactors.ts +7 -3
  181. package/readModels/MaterializedReadModels.spec.ts +98 -0
  182. package/readModels/MaterializedReadModels.ts +2 -8
  183. package/readModels/ReadModelSubjectResolver.spec.ts +84 -0
  184. package/readModels/ReadModelSubjectResolver.ts +44 -0
  185. package/readModels/ReadModels.spec.ts +118 -0
  186. package/readModels/ReadModels.ts +7 -7
  187. package/readModels/index.ts +1 -0
  188. package/reducers/Reducers.spec.ts +80 -0
  189. package/reducers/Reducers.ts +22 -3
  190. package/schemas/JsonSchema.ts +16 -0
  191. package/schemas/JsonSchemaGenerator.spec.ts +195 -0
  192. package/schemas/JsonSchemaGenerator.ts +145 -27
  193. package/schemas/index.ts +1 -1
  194. package/seeding/EventSeeding.ts +3 -1
@@ -12,6 +12,11 @@ import { ChronicleConnection } from '../connection';
12
12
  import { toContractsGuid } from '../connection/Guid';
13
13
  import { WellKnownSinks } from '../sinks';
14
14
  import { EventSequenceId } from '../eventSequences/EventSequenceId';
15
+ import { EventSequenceNumber } from '../eventSequences/EventSequenceNumber';
16
+ import { JobId } from '../jobs/JobId';
17
+ import { FailedPartition } from '../observation/FailedPartition';
18
+ import { FailedPartitions } from '../observation/FailedPartitions';
19
+ import { toObserverRunningState } from '../observation/toObserverRunningState';
15
20
  import { getReadModelMetadata } from '../readModels';
16
21
  import { TypeIntrospector } from '../types';
17
22
  import { IProjections } from './IProjections';
@@ -24,20 +29,28 @@ import {
24
29
  buildNestedEntry,
25
30
  ChildrenDefinitionLike,
26
31
  ContractEventType,
32
+ ensureFromEntry,
27
33
  FromRecord,
28
34
  getEventTypeMapKey,
29
35
  toContractEventType
30
36
  } from './modelBound/childrenAndNestedBuilder';
31
37
  import { getChildrenFromMetadata } from './modelBound/childrenFrom';
38
+ import { getClearWithPropertyMetadata } from './modelBound/clearWith';
39
+ import { getEventSequenceMetadata } from './modelBound/eventSequence';
40
+ import { getFromAllMetadata } from './modelBound/fromAll';
32
41
  import { getFromEveryMetadata } from './modelBound/fromEvery';
33
42
  import { getFromEventMetadata, hasFromEventMetadata } from './modelBound/fromEvent';
34
43
  import { getJoinMetadata } from './modelBound/join';
44
+ import { isNoAutoMap, isPropertyNoAutoMap } from './modelBound/noAutoMap';
35
45
  import { ProjectionId } from './ProjectionId';
46
+ import { ProjectionQueryResult } from './ProjectionQueryResult';
47
+ import { ProjectionState } from './ProjectionState';
36
48
  import { isNested } from './modelBound/nested';
37
49
  import { isNotRewindable } from './modelBound/notRewindable';
38
50
  import { isPassive } from './modelBound/passive';
39
51
  import { getRemovedWithClassMetadata, getRemovedWithPropertyMetadata } from './modelBound/removedWith';
40
52
  import { getRemovedWithJoinClassMetadata, getRemovedWithJoinPropertyMetadata } from './modelBound/removedWithJoin';
53
+ import { UnableToQueryProjection } from './UnableToQueryProjection';
41
54
 
42
55
  interface ResolvedModelBoundMetadata {
43
56
  id: ProjectionId;
@@ -52,19 +65,27 @@ interface ResolvedModelBoundMetadata {
52
65
  export class Projections implements IProjections {
53
66
  private readonly _declarative = new Map<string, Constructor>();
54
67
  private readonly _modelBound = new Map<string, Constructor>();
68
+ private readonly _failedPartitions: FailedPartitions;
55
69
 
56
70
  private readonly _logger = diag.createComponentLogger({ namespace: '@cratis/chronicle/projections' });
57
71
 
58
72
  /**
59
73
  * Creates a new {@link Projections} instance.
74
+ * @param _eventStore - The event store name.
75
+ * @param _namespace - The event store namespace.
76
+ * @param _connection - Chronicle connection.
60
77
  * @param _clientArtifacts - Provider for discovered client artifact types.
78
+ * @param _defaultSinkTypeId - The identifier of the default read model sink.
61
79
  */
62
80
  constructor(
63
81
  private readonly _eventStore: string,
82
+ private readonly _namespace: string,
64
83
  private readonly _connection: ChronicleConnection,
65
84
  private readonly _clientArtifacts: IClientArtifactsProvider,
66
85
  private readonly _defaultSinkTypeId: string
67
- ) {}
86
+ ) {
87
+ this._failedPartitions = new FailedPartitions(_eventStore, _namespace, _connection);
88
+ }
68
89
 
69
90
  /** @inheritdoc */
70
91
  async discover(): Promise<void> {
@@ -137,6 +158,148 @@ export class Projections implements IProjections {
137
158
  }
138
159
  }
139
160
 
161
+ /** @inheritdoc */
162
+ hasFor(projectionId: ProjectionId | string): boolean {
163
+ const id = this.toProjectionIdValue(projectionId);
164
+ return this._declarative.has(id) || this._modelBound.has(id);
165
+ }
166
+
167
+ /** @inheritdoc */
168
+ hasForModel(readModelType: Constructor): boolean {
169
+ try {
170
+ this.resolveProjectionIdForModel(readModelType);
171
+ return true;
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
176
+
177
+ /** @inheritdoc */
178
+ getProjectionIdFor(readModelType: Constructor): ProjectionId {
179
+ return this.resolveProjectionIdForModel(readModelType);
180
+ }
181
+
182
+ /** @inheritdoc */
183
+ async getStateFor(projectionId: ProjectionId | string): Promise<ProjectionState> {
184
+ const id = this.toProjectionIdValue(projectionId);
185
+ const eventSequenceId = this.resolveEventSequenceIdFor(id);
186
+
187
+ const response = await this._connection.observers.getObserverInformation({
188
+ EventStore: this._eventStore,
189
+ Namespace: this._namespace,
190
+ ObserverId: id,
191
+ EventSequenceId: eventSequenceId
192
+ });
193
+
194
+ return {
195
+ runningState: toObserverRunningState(response.RunningState),
196
+ isSubscribed: response.IsSubscribed,
197
+ nextEventSequenceNumber: new EventSequenceNumber(response.NextEventSequenceNumber),
198
+ lastHandledEventSequenceNumber: new EventSequenceNumber(response.LastHandledEventSequenceNumber),
199
+ tailEventSequenceNumber: new EventSequenceNumber(response.TailEventSequenceNumber)
200
+ };
201
+ }
202
+
203
+ /** @inheritdoc */
204
+ getStateForModel(readModelType: Constructor): Promise<ProjectionState> {
205
+ return this.getStateFor(this.resolveProjectionIdForModel(readModelType));
206
+ }
207
+
208
+ /** @inheritdoc */
209
+ getFailedPartitionsForModel(readModelType: Constructor): Promise<FailedPartition[]> {
210
+ const projectionId = this.resolveProjectionIdForModel(readModelType);
211
+ return this._failedPartitions.getFailedPartitionsFor(projectionId.value);
212
+ }
213
+
214
+ /** @inheritdoc */
215
+ async replay(projectionId: ProjectionId | string): Promise<JobId> {
216
+ const id = this.toProjectionIdValue(projectionId);
217
+
218
+ const response = await this._connection.observers.replay({
219
+ EventStore: this._eventStore,
220
+ Namespace: this._namespace,
221
+ ObserverId: id,
222
+
223
+ // The kernel resolves the observer's own event sequence from its identifier - it does not
224
+ // need to be told which one to replay.
225
+ EventSequenceId: ''
226
+ });
227
+
228
+ return response.JobId ? JobId.from(response.JobId) : JobId.from(Guid.empty);
229
+ }
230
+
231
+ /** @inheritdoc */
232
+ replayForModel(readModelType: Constructor): Promise<JobId> {
233
+ return this.replay(this.resolveProjectionIdForModel(readModelType));
234
+ }
235
+
236
+ /** @inheritdoc */
237
+ async query(declaration: string, eventSequenceId: string = EventSequenceId.eventLog.value): Promise<ProjectionQueryResult> {
238
+ const result = await this._connection.projections.preview({
239
+ EventStore: this._eventStore,
240
+ Namespace: this._namespace,
241
+ EventSequenceId: eventSequenceId,
242
+ Declaration: declaration
243
+ });
244
+
245
+ if (result.Value1) {
246
+ throw new UnableToQueryProjection(result.Value1.Errors.map(error => error.Message));
247
+ }
248
+
249
+ return {
250
+ readModelEntries: result.Value0?.ReadModelEntries ?? []
251
+ };
252
+ }
253
+
254
+ private toProjectionIdValue(projectionId: ProjectionId | string): string {
255
+ return typeof projectionId === 'string' ? projectionId : projectionId.value;
256
+ }
257
+
258
+ /**
259
+ * Resolves the {@link ProjectionId} of the projection that maintains a specific read model type,
260
+ * from what has already been discovered.
261
+ * @param readModelType - The read model type to resolve for.
262
+ * @returns The resolved {@link ProjectionId}.
263
+ */
264
+ private resolveProjectionIdForModel(readModelType: Constructor): ProjectionId {
265
+ for (const [id, type] of this._modelBound) {
266
+ if (type === readModelType) {
267
+ return new ProjectionId(id);
268
+ }
269
+ }
270
+
271
+ for (const [id, type] of this._declarative) {
272
+ const metadata = getProjectionMetadata(type);
273
+ if (metadata?.readModelType === readModelType) {
274
+ return new ProjectionId(id);
275
+ }
276
+ }
277
+
278
+ throw new Error(
279
+ `No projection found for read model '${readModelType.name}'. Make sure discover() has run and the ` +
280
+ 'read model is model-bound, or its declarative projection() decorator specifies an explicit readModelType.');
281
+ }
282
+
283
+ /**
284
+ * Resolves the event sequence identifier a discovered projection reads from, without requiring
285
+ * its full definition to have been built.
286
+ * @param projectionId - The projection identifier to resolve for.
287
+ * @returns The resolved event sequence identifier.
288
+ */
289
+ private resolveEventSequenceIdFor(projectionId: string): string {
290
+ const modelBoundType = this._modelBound.get(projectionId);
291
+ if (modelBoundType) {
292
+ return getEventSequenceMetadata(modelBoundType) ?? EventSequenceId.eventLog.value;
293
+ }
294
+
295
+ const declarativeType = this._declarative.get(projectionId);
296
+ if (declarativeType) {
297
+ return getProjectionMetadata(declarativeType)?.eventSequenceId ?? EventSequenceId.eventLog.value;
298
+ }
299
+
300
+ return EventSequenceId.eventLog.value;
301
+ }
302
+
140
303
  private async registerWithRetry(projection: unknown, identifier: string, maxAttempts = 5): Promise<void> {
141
304
  let delay = 2000;
142
305
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -344,14 +507,25 @@ export class Projections implements IProjections {
344
507
  childrenByProperty[property] = buildChildrenEntry(type, property, childrenFromList);
345
508
  }
346
509
 
347
- if (isNested(prototype, property)) {
510
+ const propertyIsNested = isNested(prototype, property);
511
+ if (propertyIsNested) {
348
512
  nestedByProperty[property] = buildNestedEntry(type, property);
349
513
  }
514
+
515
+ // A scalar (non-nested, non-children-collection) root property clears back to no value
516
+ // every time the given event is observed. A nested single-object property's clearWith is
517
+ // handled by buildNestedEntry instead, which clears the whole nested object.
518
+ if (childrenFromList.length === 0 && !propertyIsNested) {
519
+ for (const clearWith of getClearWithPropertyMetadata(prototype, property)) {
520
+ const entry = ensureFromEntry(fromByEventType, clearWith.eventType);
521
+ entry.Value.Properties[property] = '$null';
522
+ }
523
+ }
350
524
  }
351
525
 
352
526
  const allProperties: Record<string, string> = {};
353
527
  for (const property of properties) {
354
- const fromEvery = getFromEveryMetadata(prototype, property);
528
+ const fromEvery = getFromEveryMetadata(prototype, property) ?? getFromAllMetadata(prototype, property);
355
529
  if (fromEvery) {
356
530
  allProperties[property] = fromEvery.contextProperty
357
531
  ? fromEvery.contextProperty
@@ -379,7 +553,8 @@ export class Projections implements IProjections {
379
553
  RemovedWithJoin: Array.from(removedWithJoinByEventType.values()),
380
554
  LastUpdated: { Value: '' },
381
555
  Tags: [],
382
- AutoMap: AutoMap.Enabled,
556
+ AutoMap: isNoAutoMap(type) ? AutoMap.Disabled : AutoMap.Enabled,
557
+ NoAutoMapProperties: properties.filter(property => isPropertyNoAutoMap(prototype, property)),
383
558
  Nested: nestedByProperty
384
559
  };
385
560
  definition.LastUpdated = { Value: this.computeStableLastUpdated(definition) };
@@ -392,7 +567,7 @@ export class Projections implements IProjections {
392
567
  if (readModelMetadata && hasFromEventMetadata(type)) {
393
568
  return {
394
569
  id: new ProjectionId(readModelMetadata.id.value),
395
- eventSequenceId: undefined,
570
+ eventSequenceId: getEventSequenceMetadata(type),
396
571
  readModelIdentifier: readModelMetadata.id.value
397
572
  };
398
573
  }
@@ -0,0 +1,17 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /**
5
+ * Error thrown when a projection query fails because the Projection Declaration Language
6
+ * declaration contains errors.
7
+ */
8
+ export class UnableToQueryProjection extends Error {
9
+ /**
10
+ * Creates a new {@link UnableToQueryProjection}.
11
+ * @param errors - The collection of error messages describing why the query failed.
12
+ */
13
+ constructor(errors: ReadonlyArray<string>) {
14
+ super(`Unable to query projection. Errors:\n${errors.join('\n')}`);
15
+ this.name = 'UnableToQueryProjection';
16
+ }
17
+ }
@@ -4,5 +4,8 @@
4
4
  export { ProjectionId } from './ProjectionId';
5
5
  export type { IProjections } from './IProjections';
6
6
  export { Projections } from './Projections';
7
+ export type { ProjectionState } from './ProjectionState';
8
+ export type { ProjectionQueryResult } from './ProjectionQueryResult';
9
+ export { UnableToQueryProjection } from './UnableToQueryProjection';
7
10
  export * from './declarative';
8
11
  export * from './modelBound';
@@ -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 'reflect-metadata';
5
+ import { EventSequenceId } from '../../eventSequences/EventSequenceId';
6
+
7
+ const METADATA_KEY = 'chronicle:projection:eventSequence';
8
+
9
+ /**
10
+ * Class decorator that overrides the event sequence a model-bound projection reads from.
11
+ * When applied, auto-inbox routing is suppressed - the explicit value is always honored.
12
+ * @param sequence - The event sequence identifier to read from.
13
+ * @returns A class decorator.
14
+ */
15
+ export function eventSequence(sequence: string): ClassDecorator {
16
+ return (target: object) => {
17
+ Reflect.defineMetadata(METADATA_KEY, sequence, target);
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Convenience class decorator that pins a model-bound projection to the default event log
23
+ * sequence. Equivalent to `eventSequence(EventSequenceId.eventLog.value)`, but more expressive
24
+ * about the intent to read from the local event log rather than an inbox or another sequence.
25
+ * @param target - The class constructor.
26
+ */
27
+ export function eventLog(target: Function): void {
28
+ Reflect.defineMetadata(METADATA_KEY, EventSequenceId.eventLog.value, target);
29
+ }
30
+
31
+ /**
32
+ * Retrieves the explicit event sequence identifier stored on a class, if any.
33
+ * @param target - The class constructor.
34
+ * @returns The event sequence identifier, or undefined when the class has no explicit override.
35
+ */
36
+ export function getEventSequenceMetadata(target: Function): string | undefined {
37
+ return Reflect.getMetadata(METADATA_KEY, target);
38
+ }
@@ -0,0 +1,42 @@
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 { TypeIntrospector } from '../../types';
6
+
7
+ /** Metadata stored by the fromAll property decorator. */
8
+ export interface FromAllMetadata {
9
+ /** The event property name to read the value from. */
10
+ readonly property?: string;
11
+ /** The event context property name to read the value from. */
12
+ readonly contextProperty?: string;
13
+ }
14
+
15
+ const METADATA_KEY = 'chronicle:projection:fromAll';
16
+
17
+ /**
18
+ * Property decorator that sets the decorated read model property from a property present on every
19
+ * event type the projection is built from - the convention-based counterpart to declaring the same
20
+ * mapping on every individual `fromEvent`. Equivalent to {@link fromEvery} for model-bound
21
+ * projections; use whichever name reads better for the property being mapped.
22
+ * @param property - Optional event property name. If not specified, uses the model property name.
23
+ * @param contextProperty - Optional event context property name.
24
+ * @returns A property decorator.
25
+ */
26
+ export function fromAll(property?: string, contextProperty?: string): PropertyDecorator {
27
+ return (target: object, propertyKey: string | symbol) => {
28
+ TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, propertyKey.toString());
29
+ const metadata: FromAllMetadata = { property, contextProperty };
30
+ Reflect.defineMetadata(METADATA_KEY, metadata, target, propertyKey.toString());
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Retrieves fromAll metadata stored on the given property.
36
+ * @param target - The class prototype.
37
+ * @param propertyKey - The property name.
38
+ * @returns The fromAll metadata, or undefined if not decorated.
39
+ */
40
+ export function getFromAllMetadata(target: object, propertyKey: string): FromAllMetadata | undefined {
41
+ return Reflect.getMetadata(METADATA_KEY, target, propertyKey);
42
+ }
@@ -35,3 +35,7 @@ export { setValue, getSetValueMetadata } from './setValue';
35
35
  export type { SetValueMetadata } from './setValue';
36
36
  export { fromEvery, getFromEveryMetadata } from './fromEvery';
37
37
  export type { FromEveryMetadata } from './fromEvery';
38
+ export { fromAll, getFromAllMetadata } from './fromAll';
39
+ export type { FromAllMetadata } from './fromAll';
40
+ export { noAutoMap, isNoAutoMap, isPropertyNoAutoMap } from './noAutoMap';
41
+ export { eventSequence, eventLog, getEventSequenceMetadata } from './eventSequence';
@@ -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 'reflect-metadata';
5
+ import { TypeIntrospector } from '../../types';
6
+
7
+ const CLASS_METADATA_KEY = 'chronicle:projection:noAutoMap:class';
8
+ const PROPERTY_METADATA_KEY = 'chronicle:projection:noAutoMap:property';
9
+
10
+ /**
11
+ * Class or property decorator that disables AutoMap for a model-bound projection.
12
+ * Applied to a class, it prevents AutoMap from mapping any property automatically.
13
+ * Applied to a property, it excludes only that single property from AutoMap while every other
14
+ * property keeps mapping - use it to stop an unrelated event that carries an identically named
15
+ * property from silently overwriting a property whose value is set explicitly (for example via
16
+ * `setFrom`).
17
+ * @param target - The class constructor, or the class prototype when used on a property.
18
+ * @param propertyKey - The property name, when used as a property decorator.
19
+ */
20
+ export function noAutoMap(target: object, propertyKey?: string | symbol): void {
21
+ if (propertyKey !== undefined) {
22
+ const key = propertyKey.toString();
23
+ TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
24
+ Reflect.defineMetadata(PROPERTY_METADATA_KEY, true, target, key);
25
+ } else {
26
+ Reflect.defineMetadata(CLASS_METADATA_KEY, true, target as Function);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Checks whether the given class has AutoMap disabled entirely.
32
+ * @param target - The class constructor.
33
+ * @returns True if the class is marked with {@link noAutoMap}; false otherwise.
34
+ */
35
+ export function isNoAutoMap(target: Function): boolean {
36
+ return Reflect.hasMetadata(CLASS_METADATA_KEY, target);
37
+ }
38
+
39
+ /**
40
+ * Checks whether a specific property is excluded from AutoMap.
41
+ * @param target - The class prototype.
42
+ * @param propertyKey - The property name.
43
+ * @returns True if the property is marked with {@link noAutoMap}; false otherwise.
44
+ */
45
+ export function isPropertyNoAutoMap(target: object, propertyKey: string): boolean {
46
+ return Reflect.hasMetadata(PROPERTY_METADATA_KEY, target, propertyKey);
47
+ }
@@ -12,6 +12,9 @@ import { getEventTypeMetadata } from '../events/eventTypeDecorator';
12
12
  import { EventContext } from '../events/EventContext';
13
13
  import { EventTypeId } from '../events/EventTypeId';
14
14
  import { EventTypeGeneration } from '../events/EventTypeGeneration';
15
+ import { Tag } from '../events/Tag';
16
+ import { getTagsFor } from '../events/tagDecorator';
17
+ import { getFilterTagsFor } from '../events/filterEventsByTagDecorator';
15
18
  import { EventSequenceId } from '../eventSequences/EventSequenceId';
16
19
  import type { IEventLog } from '../eventSequences/IEventLog';
17
20
  import { notifyReplayLifecycle } from '../observation/notifyReplayLifecycle';
@@ -239,9 +242,9 @@ export class Reactors implements IReactors {
239
242
  Key: EVENT_SOURCE_ID_KEY
240
243
  })),
241
244
  IsReplayable: false,
242
- Tags: [],
245
+ Tags: getTagsFor(reactorType).map(t => t.value),
243
246
  Filters: {
244
- FilterTags: [],
247
+ FilterTags: getFilterTagsFor(reactorType).map(t => t.value),
245
248
  EventSourceType: '',
246
249
  EventStreamType: 'All'
247
250
  }
@@ -303,7 +306,8 @@ export class Reactors implements IReactors {
303
306
  },
304
307
  occurred: new Date(event.Context!.Occurred?.Value ?? ''),
305
308
  correlationId: event.Context?.CorrelationId ? `${event.Context.CorrelationId.lo}-${event.Context.CorrelationId.hi}` : '',
306
- causation: []
309
+ causation: [],
310
+ tags: (event.Context!.Tags ?? []).map(value => new Tag(value))
307
311
  };
308
312
 
309
313
  this._logger.info('Invoking reactor handler', {
@@ -0,0 +1,98 @@
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 { field } from '@cratis/fundamentals';
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import type { ChronicleConnection } from '../connection';
8
+ import { pii } from '../compliance/pii';
9
+ import { subject } from '../compliance/subject';
10
+ import { readModel } from './readModel';
11
+ import { MaterializedReadModels } from './MaterializedReadModels';
12
+
13
+ // Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
14
+ // fixtures don't depend on the test runner's decorator-syntax support.
15
+
16
+ function createMaterializedReadModels(instancesJson: string[], releaseResponse: Record<string, unknown> = { HasError: false, Payload: '{}' }) {
17
+ const release = vi.fn().mockResolvedValue(releaseResponse);
18
+ const getInstances = vi.fn().mockResolvedValue({ Instances: instancesJson });
19
+ const connection = {
20
+ compliance: { release },
21
+ materializedReadModels: { getInstances }
22
+ } as unknown as ChronicleConnection;
23
+
24
+ const readModels = new MaterializedReadModels('test-store', 'test-namespace', connection);
25
+ return { readModels, release };
26
+ }
27
+
28
+ describe('MaterializedReadModels', () => {
29
+ describe('when a read model has a property decorated with @subject()', () => {
30
+ class Employee {
31
+ id = '';
32
+ personId = '';
33
+ ssn = '';
34
+ }
35
+ field(String)(Employee.prototype, 'id');
36
+ field(String)(Employee.prototype, 'personId');
37
+ field(String)(Employee.prototype, 'ssn');
38
+ pii()(Employee.prototype, 'ssn');
39
+ subject()(Employee.prototype, 'personId');
40
+ readModel('EmployeeWithSubjectMaterialized')(Employee);
41
+
42
+ it('should release using the decorated property as the subject', async () => {
43
+ const json = JSON.stringify({ id: 'employee-1', personId: 'person-42', ssn: '123-45-6789' });
44
+ const { readModels, release } = createMaterializedReadModels([json]);
45
+
46
+ await readModels.getInstances(Employee);
47
+
48
+ expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'person-42' }));
49
+ });
50
+ });
51
+
52
+ describe('when a read model has no property decorated with @subject() but has an id property', () => {
53
+ class Customer {
54
+ id = '';
55
+ ssn = '';
56
+ }
57
+ field(String)(Customer.prototype, 'id');
58
+ field(String)(Customer.prototype, 'ssn');
59
+ pii()(Customer.prototype, 'ssn');
60
+ readModel('CustomerWithIdOnlyMaterialized')(Customer);
61
+
62
+ it('should fall back to the id property as the subject, unchanged from today', async () => {
63
+ const json = JSON.stringify({ id: 'customer-7', ssn: '987-65-4321' });
64
+ const { readModels, release } = createMaterializedReadModels([json]);
65
+
66
+ await readModels.getInstances(Customer);
67
+
68
+ expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'customer-7' }));
69
+ });
70
+ });
71
+
72
+ describe('when a read model has neither a decorated property nor an id property', () => {
73
+ class Anonymous {
74
+ ssn = '';
75
+ }
76
+ field(String)(Anonymous.prototype, 'ssn');
77
+ pii()(Anonymous.prototype, 'ssn');
78
+ readModel('AnonymousMaterialized')(Anonymous);
79
+
80
+ it('should not call release, unchanged from today', async () => {
81
+ const json = JSON.stringify({ ssn: '000-00-0000' });
82
+ const { readModels, release } = createMaterializedReadModels([json]);
83
+
84
+ await readModels.getInstances(Anonymous);
85
+
86
+ expect(release).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it('should return the instance unreleased', async () => {
90
+ const json = JSON.stringify({ ssn: '000-00-0000' });
91
+ const { readModels } = createMaterializedReadModels([json]);
92
+
93
+ const [instance] = await readModels.getInstances(Anonymous);
94
+
95
+ expect(instance.ssn).toBe('000-00-0000');
96
+ });
97
+ });
98
+ });
@@ -6,6 +6,7 @@ import { JsonSerializer } from '@cratis/fundamentals';
6
6
  import { ChronicleConnection } from '../connection';
7
7
  import { JsonSchemaGenerator } from '../schemas';
8
8
  import { getReadModelMetadata } from './readModel';
9
+ import { ReadModelSubjectResolver } from './ReadModelSubjectResolver';
9
10
  import type { IMaterializedReadModels } from './IMaterializedReadModels';
10
11
 
11
12
  const defaultTake = 50;
@@ -97,7 +98,7 @@ export class MaterializedReadModels implements IMaterializedReadModels {
97
98
  }
98
99
 
99
100
  private async releaseInstance<TReadModel>(readModelType: Constructor<TReadModel>, instance: TReadModel, schema: string): Promise<TReadModel> {
100
- const subject = this.extractSubject(instance);
101
+ const subject = ReadModelSubjectResolver.resolveFrom(readModelType, instance);
101
102
  if (!subject) {
102
103
  return instance;
103
104
  }
@@ -141,11 +142,4 @@ export class MaterializedReadModels implements IMaterializedReadModels {
141
142
  }
142
143
  }
143
144
 
144
- private extractSubject<TReadModel>(instance: TReadModel): string | undefined {
145
- const anyInstance = instance as Record<string, unknown>;
146
- if (anyInstance.id !== undefined && anyInstance.id !== null) {
147
- return String(anyInstance.id);
148
- }
149
- return undefined;
150
- }
151
145
  }
@@ -0,0 +1,84 @@
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 { describe, expect, it } from 'vitest';
6
+ import { subject } from '../compliance/subject';
7
+ import { ReadModelSubjectResolver } from './ReadModelSubjectResolver';
8
+
9
+ // Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
10
+ // fixtures don't depend on the test runner's decorator-syntax support.
11
+
12
+ describe('ReadModelSubjectResolver', () => {
13
+ describe('when the read model has a property decorated with @subject()', () => {
14
+ class Employee {
15
+ id = '';
16
+ personId = '';
17
+ }
18
+ subject()(Employee.prototype, 'personId');
19
+
20
+ it('should resolve to the decorated property value', () => {
21
+ const instance = new Employee();
22
+ instance.id = 'employee-1';
23
+ instance.personId = 'person-42';
24
+
25
+ expect(ReadModelSubjectResolver.resolveFrom(Employee, instance)).toBe('person-42');
26
+ });
27
+
28
+ it('should take precedence over the id property', () => {
29
+ const instance = new Employee();
30
+ instance.id = 'employee-1';
31
+ instance.personId = 'person-42';
32
+
33
+ expect(ReadModelSubjectResolver.resolveFrom(Employee, instance)).not.toBe('employee-1');
34
+ });
35
+
36
+ it('should fall back to id when the decorated property has no value', () => {
37
+ const instance = new Employee();
38
+ instance.id = 'employee-1';
39
+ instance.personId = '';
40
+
41
+ expect(ReadModelSubjectResolver.resolveFrom(Employee, instance)).toBe('employee-1');
42
+ });
43
+ });
44
+
45
+ describe('when the read model has no property decorated with @subject()', () => {
46
+ class Customer {
47
+ id = '';
48
+ name = '';
49
+ }
50
+
51
+ it('should fall back to the id property', () => {
52
+ const instance = new Customer();
53
+ instance.id = 'customer-7';
54
+
55
+ expect(ReadModelSubjectResolver.resolveFrom(Customer, instance)).toBe('customer-7');
56
+ });
57
+ });
58
+
59
+ describe('when the read model has neither a decorated property nor an id property', () => {
60
+ class Anonymous {
61
+ name = '';
62
+ }
63
+
64
+ it('should not resolve a subject', () => {
65
+ const instance = new Anonymous();
66
+
67
+ expect(ReadModelSubjectResolver.resolveFrom(Anonymous, instance)).toBeUndefined();
68
+ });
69
+ });
70
+
71
+ describe('when the instance does not exist', () => {
72
+ class Customer {
73
+ id = '';
74
+ }
75
+
76
+ it('should not resolve a subject for null', () => {
77
+ expect(ReadModelSubjectResolver.resolveFrom(Customer, null as unknown as Customer)).toBeUndefined();
78
+ });
79
+
80
+ it('should not resolve a subject for undefined', () => {
81
+ expect(ReadModelSubjectResolver.resolveFrom(Customer, undefined as unknown as Customer)).toBeUndefined();
82
+ });
83
+ });
84
+ });