@cratis/chronicle 0.0.1 → 0.1.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 (157) hide show
  1. package/Auditing/Causation.ts +29 -0
  2. package/Auditing/CausationManager.ts +51 -0
  3. package/Auditing/CausationType.ts +38 -0
  4. package/Auditing/ICausationManager.ts +29 -0
  5. package/Auditing/index.ts +15 -0
  6. package/Correlation/CorrelationId.ts +33 -0
  7. package/Correlation/CorrelationIdManager.ts +30 -0
  8. package/Correlation/ICorrelationIdAccessor.ts +15 -0
  9. package/Correlation/ICorrelationIdSetter.ts +21 -0
  10. package/Correlation/index.ts +15 -0
  11. package/EventSequences/EventSequence.ts +48 -50
  12. package/Identity/IIdentityProvider.ts +26 -0
  13. package/Identity/Identity.ts +61 -0
  14. package/Identity/IdentityProvider.ts +28 -0
  15. package/Identity/index.ts +14 -0
  16. package/Projections/Projections.ts +75 -20
  17. package/Projections/declarative/ProjectionBuilderFor.ts +19 -2
  18. package/Projections/declarative/projection.ts +6 -2
  19. package/Projections/modelBound/index.ts +0 -2
  20. package/Projections/modelBound/setFromContext.ts +20 -2
  21. package/README.md +1 -1
  22. package/Reactors/Reactors.ts +1 -0
  23. package/Reducers/Reducers.ts +66 -2
  24. package/Schemas/JsonSchemaGenerator.ts +20 -3
  25. package/artifacts/DefaultClientArtifactsProvider.ts +0 -5
  26. package/artifacts/IClientArtifactsProvider.ts +0 -3
  27. package/connection/Guid.ts +29 -1
  28. package/dist/Auditing/Causation.d.ts +22 -0
  29. package/dist/Auditing/Causation.d.ts.map +1 -0
  30. package/dist/Auditing/Causation.js +30 -0
  31. package/dist/Auditing/Causation.js.map +1 -0
  32. package/dist/Auditing/CausationManager.d.ts +23 -0
  33. package/dist/Auditing/CausationManager.d.ts.map +1 -0
  34. package/dist/Auditing/CausationManager.js +45 -0
  35. package/dist/Auditing/CausationManager.js.map +1 -0
  36. package/dist/Auditing/CausationType.d.ts +30 -0
  37. package/dist/Auditing/CausationType.d.ts.map +1 -0
  38. package/dist/Auditing/CausationType.js +36 -0
  39. package/dist/Auditing/CausationType.js.map +1 -0
  40. package/dist/Auditing/ICausationManager.d.ts +24 -0
  41. package/dist/Auditing/ICausationManager.d.ts.map +1 -0
  42. package/dist/Auditing/ICausationManager.js +4 -0
  43. package/dist/Auditing/ICausationManager.js.map +1 -0
  44. package/dist/Auditing/index.d.ts +11 -0
  45. package/dist/Auditing/index.d.ts.map +1 -0
  46. package/dist/Auditing/index.js +12 -0
  47. package/dist/Auditing/index.js.map +1 -0
  48. package/dist/Correlation/CorrelationId.d.ts +23 -0
  49. package/dist/Correlation/CorrelationId.d.ts.map +1 -0
  50. package/dist/Correlation/CorrelationId.js +32 -0
  51. package/dist/Correlation/CorrelationId.js.map +1 -0
  52. package/dist/Correlation/CorrelationIdManager.d.ts +17 -0
  53. package/dist/Correlation/CorrelationIdManager.d.ts.map +1 -0
  54. package/dist/Correlation/CorrelationIdManager.js +24 -0
  55. package/dist/Correlation/CorrelationIdManager.js.map +1 -0
  56. package/dist/Correlation/ICorrelationIdAccessor.d.ts +12 -0
  57. package/dist/Correlation/ICorrelationIdAccessor.d.ts.map +1 -0
  58. package/dist/Correlation/ICorrelationIdAccessor.js +4 -0
  59. package/dist/Correlation/ICorrelationIdAccessor.js.map +1 -0
  60. package/dist/Correlation/ICorrelationIdSetter.d.ts +17 -0
  61. package/dist/Correlation/ICorrelationIdSetter.d.ts.map +1 -0
  62. package/dist/Correlation/ICorrelationIdSetter.js +4 -0
  63. package/dist/Correlation/ICorrelationIdSetter.js.map +1 -0
  64. package/dist/Correlation/index.d.ts +11 -0
  65. package/dist/Correlation/index.d.ts.map +1 -0
  66. package/dist/Correlation/index.js +11 -0
  67. package/dist/Correlation/index.js.map +1 -0
  68. package/dist/EventSequences/EventSequence.d.ts.map +1 -1
  69. package/dist/EventSequences/EventSequence.js +45 -46
  70. package/dist/EventSequences/EventSequence.js.map +1 -1
  71. package/dist/Identity/IIdentityProvider.d.ts +21 -0
  72. package/dist/Identity/IIdentityProvider.d.ts.map +1 -0
  73. package/dist/Identity/IIdentityProvider.js +4 -0
  74. package/dist/Identity/IIdentityProvider.js.map +1 -0
  75. package/dist/Identity/Identity.d.ts +37 -0
  76. package/dist/Identity/Identity.d.ts.map +1 -0
  77. package/dist/Identity/Identity.js +60 -0
  78. package/dist/Identity/Identity.js.map +1 -0
  79. package/dist/Identity/IdentityProvider.d.ts +15 -0
  80. package/dist/Identity/IdentityProvider.d.ts.map +1 -0
  81. package/dist/Identity/IdentityProvider.js +23 -0
  82. package/dist/Identity/IdentityProvider.js.map +1 -0
  83. package/dist/Identity/index.d.ts +10 -0
  84. package/dist/Identity/index.d.ts.map +1 -0
  85. package/dist/Identity/index.js +11 -0
  86. package/dist/Identity/index.js.map +1 -0
  87. package/dist/Projections/Projections.d.ts +9 -1
  88. package/dist/Projections/Projections.d.ts.map +1 -1
  89. package/dist/Projections/Projections.js +70 -19
  90. package/dist/Projections/Projections.js.map +1 -1
  91. package/dist/Projections/declarative/ProjectionBuilderFor.d.ts +6 -0
  92. package/dist/Projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
  93. package/dist/Projections/declarative/ProjectionBuilderFor.js +18 -2
  94. package/dist/Projections/declarative/ProjectionBuilderFor.js.map +1 -1
  95. package/dist/Projections/declarative/projection.d.ts +5 -1
  96. package/dist/Projections/declarative/projection.d.ts.map +1 -1
  97. package/dist/Projections/declarative/projection.js +3 -2
  98. package/dist/Projections/declarative/projection.js.map +1 -1
  99. package/dist/Projections/modelBound/index.d.ts +0 -2
  100. package/dist/Projections/modelBound/index.d.ts.map +1 -1
  101. package/dist/Projections/modelBound/index.js +0 -1
  102. package/dist/Projections/modelBound/index.js.map +1 -1
  103. package/dist/Projections/modelBound/setFromContext.d.ts +4 -1
  104. package/dist/Projections/modelBound/setFromContext.d.ts.map +1 -1
  105. package/dist/Projections/modelBound/setFromContext.js +12 -7
  106. package/dist/Projections/modelBound/setFromContext.js.map +1 -1
  107. package/dist/Reactors/Reactors.d.ts.map +1 -1
  108. package/dist/Reactors/Reactors.js +1 -0
  109. package/dist/Reactors/Reactors.js.map +1 -1
  110. package/dist/Reducers/Reducers.d.ts +2 -0
  111. package/dist/Reducers/Reducers.d.ts.map +1 -1
  112. package/dist/Reducers/Reducers.js +60 -2
  113. package/dist/Reducers/Reducers.js.map +1 -1
  114. package/dist/Schemas/JsonSchemaGenerator.d.ts.map +1 -1
  115. package/dist/Schemas/JsonSchemaGenerator.js +17 -3
  116. package/dist/Schemas/JsonSchemaGenerator.js.map +1 -1
  117. package/dist/artifacts/DefaultClientArtifactsProvider.d.ts +0 -2
  118. package/dist/artifacts/DefaultClientArtifactsProvider.d.ts.map +1 -1
  119. package/dist/artifacts/DefaultClientArtifactsProvider.js +0 -4
  120. package/dist/artifacts/DefaultClientArtifactsProvider.js.map +1 -1
  121. package/dist/artifacts/IClientArtifactsProvider.d.ts +0 -2
  122. package/dist/artifacts/IClientArtifactsProvider.d.ts.map +1 -1
  123. package/dist/connection/Guid.d.ts +8 -1
  124. package/dist/connection/Guid.d.ts.map +1 -1
  125. package/dist/connection/Guid.js +24 -1
  126. package/dist/connection/Guid.js.map +1 -1
  127. package/dist/index.d.ts +4 -0
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +4 -0
  130. package/dist/index.js.map +1 -1
  131. package/dist/sinks/WellKnownSinks.d.ts +15 -0
  132. package/dist/sinks/WellKnownSinks.d.ts.map +1 -0
  133. package/dist/sinks/WellKnownSinks.js +17 -0
  134. package/dist/sinks/WellKnownSinks.js.map +1 -0
  135. package/dist/sinks/index.d.ts +2 -0
  136. package/dist/sinks/index.d.ts.map +1 -0
  137. package/dist/sinks/index.js +4 -0
  138. package/dist/sinks/index.js.map +1 -0
  139. package/dist/tsconfig.tsbuildinfo +1 -1
  140. package/dist/types/DecoratorType.d.ts +1 -3
  141. package/dist/types/DecoratorType.d.ts.map +1 -1
  142. package/dist/types/DecoratorType.js +0 -2
  143. package/dist/types/DecoratorType.js.map +1 -1
  144. package/index.ts +4 -0
  145. package/package.json +2 -2
  146. package/sinks/WellKnownSinks.ts +21 -0
  147. package/sinks/index.ts +4 -0
  148. package/types/DecoratorType.ts +1 -4
  149. package/Projections/modelBound/modelBound.ts +0 -59
  150. package/dist/Grpc.d.ts +0 -19
  151. package/dist/Grpc.d.ts.map +0 -1
  152. package/dist/Grpc.js +0 -33
  153. package/dist/Grpc.js.map +0 -1
  154. package/dist/Projections/modelBound/modelBound.d.ts +0 -31
  155. package/dist/Projections/modelBound/modelBound.d.ts.map +0 -1
  156. package/dist/Projections/modelBound/modelBound.js +0 -39
  157. package/dist/Projections/modelBound/modelBound.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ import { diag } from '@opentelemetry/api';
4
5
  import {
5
6
  AutoMap,
6
7
  ProjectionOwner
@@ -8,9 +9,11 @@ import {
8
9
  import { Constructor } from '@cratis/fundamentals';
9
10
  import { IClientArtifactsProvider } from '../artifacts';
10
11
  import { ChronicleConnection } from '../connection';
12
+ import { toContractsGuid } from '../connection/Guid';
11
13
  import { EventSequenceId } from '../EventSequences/EventSequenceId';
12
14
  import { getEventTypeFor } from '../Events/eventTypeDecorator';
13
15
  import { getReadModelMetadata } from '../ReadModels';
16
+ import { WellKnownSinks } from '../sinks';
14
17
  import { TypeIntrospector } from '../types';
15
18
  import { IProjections } from './IProjections';
16
19
  import { getProjectionMetadata } from './declarative/projection';
@@ -51,6 +54,8 @@ export class Projections implements IProjections {
51
54
  private readonly _declarative = new Map<string, Constructor>();
52
55
  private readonly _modelBound = new Map<string, Constructor>();
53
56
 
57
+ private readonly _logger = diag.createComponentLogger({ namespace: '@cratis/chronicle/Projections' });
58
+
54
59
  /**
55
60
  * Creates a new {@link Projections} instance.
56
61
  * @param _clientArtifacts - Provider for discovered client artifact types.
@@ -66,27 +71,36 @@ export class Projections implements IProjections {
66
71
  this._declarative.clear();
67
72
  this._modelBound.clear();
68
73
 
69
- for (const type of this._clientArtifacts.projections) {
74
+ const declarativeTypes = this._clientArtifacts.projections;
75
+ const readModelTypes = this._clientArtifacts.readModels;
76
+ this._logger.debug('Discovering projections', { declarativeCount: declarativeTypes.length, readModelCount: readModelTypes.length });
77
+
78
+ for (const type of declarativeTypes) {
70
79
  const metadata = getProjectionMetadata(type);
71
80
  if (metadata) {
81
+ this._logger.debug('Discovered declarative projection', { projectionId: metadata.id.value, type: type.name });
72
82
  this._declarative.set(metadata.id.value, type);
73
83
  }
74
84
  }
75
85
 
76
- for (const type of this._clientArtifacts.readModels) {
86
+ for (const type of readModelTypes) {
77
87
  if (!hasFromEventMetadata(type)) {
78
88
  continue;
79
89
  }
80
90
 
81
91
  const metadata = this.resolveModelBoundMetadata(type);
82
92
  if (!metadata) {
93
+ this._logger.debug('Read model has @fromEvent but no model-bound metadata resolved', { type: type.name });
83
94
  continue;
84
95
  }
85
96
 
97
+ this._logger.debug('Discovered model-bound projection', { projectionId: metadata.id.value, type: type.name });
86
98
  if (!this._modelBound.has(metadata.id.value)) {
87
99
  this._modelBound.set(metadata.id.value, type);
88
100
  }
89
101
  }
102
+
103
+ this._logger.debug('Projection discovery complete', { declarativeCount: this._declarative.size, modelBoundCount: this._modelBound.size });
90
104
  }
91
105
 
92
106
  /** @inheritdoc */
@@ -95,12 +109,15 @@ export class Projections implements IProjections {
95
109
  await this.discover();
96
110
  }
97
111
 
112
+ this._logger.info('Registering projections', { declarativeCount: this._declarative.size, modelBoundCount: this._modelBound.size });
113
+
98
114
  const projections = [
99
115
  ...Array.from(this._declarative.values()).map(type => this.buildDeclarativeDefinition(type)),
100
116
  ...Array.from(this._modelBound.values()).map(type => this.buildModelBoundDefinition(type))
101
117
  ];
102
118
 
103
119
  if (projections.length === 0) {
120
+ this._logger.info('No projections to register');
104
121
  return;
105
122
  }
106
123
 
@@ -115,15 +132,37 @@ export class Projections implements IProjections {
115
132
  }
116
133
 
117
134
  for (const projection of projections) {
135
+ const identifier = String((projection as { Identifier?: unknown }).Identifier ?? '<unknown>');
136
+ await this.registerWithRetry(projection, identifier);
137
+ }
138
+ }
139
+
140
+ private async registerWithRetry(projection: unknown, identifier: string, maxAttempts = 5): Promise<void> {
141
+ let delay = 2000;
142
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
118
143
  try {
144
+ this._logger.info('Registering projection', { identifier, readModel: (projection as any).ReadModel, attempt });
119
145
  await this._connection.projections.register({
120
146
  EventStore: this._eventStore,
121
147
  Owner: ProjectionOwner.PROJECTION_OWNER_Client,
122
- Projections: [projection]
148
+ Projections: [projection as any]
123
149
  });
150
+ this._logger.info('Projection registered successfully', { identifier });
151
+ return;
124
152
  } catch (error) {
125
- const identifier = String((projection as { Identifier?: unknown }).Identifier ?? '<unknown>');
126
- throw new Error(`Failed to register projection '${identifier}': ${String(error)}`);
153
+ const msg = String(error);
154
+ const isTransient = msg.includes('UNKNOWN') || msg.includes('UNAVAILABLE') || msg.includes('INTERNAL');
155
+ if (!isTransient || attempt === maxAttempts) {
156
+ throw new Error(`Failed to register projection '${identifier}' after ${attempt} attempt(s): ${msg}`);
157
+ }
158
+ this._logger.warn('Projection registration failed with transient error, retrying', {
159
+ identifier,
160
+ attempt,
161
+ nextAttemptInMs: delay,
162
+ error: msg
163
+ });
164
+ await new Promise(resolve => setTimeout(resolve, delay));
165
+ delay = Math.min(delay * 2, 15000);
127
166
  }
128
167
  }
129
168
  }
@@ -145,8 +184,8 @@ export class Projections implements IProjections {
145
184
  ContainerName: readModelIdentifier,
146
185
  DisplayName: readModelIdentifier,
147
186
  Sink: {
148
- ConfigurationId: this.toContractsGuid('00000000-0000-0000-0000-000000000000'),
149
- TypeId: this.toContractsGuid('22202c41-2be1-4547-9c00-f0b1f797fd75')
187
+ ConfigurationId: toContractsGuid(WellKnownSinks.Null),
188
+ TypeId: toContractsGuid(WellKnownSinks.MongoDB)
150
189
  },
151
190
  Schema: this.getReadModelSchema(readModelIdentifier),
152
191
  Indexes: [],
@@ -188,12 +227,19 @@ export class Projections implements IProjections {
188
227
 
189
228
  const explicitReadModelIdentifier = definition.ReadModel as string;
190
229
  if (explicitReadModelIdentifier === type.name) {
191
- const inferredReadModelIdentifier = this.inferReadModelIdentifier(builder.getMappedReadModelProperties());
192
- if (inferredReadModelIdentifier) {
193
- definition.ReadModel = inferredReadModelIdentifier;
230
+ // Use explicit readModelType from decorator if provided
231
+ if (metadata.readModelType) {
232
+ const rm = getReadModelMetadata(metadata.readModelType);
233
+ definition.ReadModel = rm?.id.value ?? (metadata.readModelType as Function).name;
234
+ } else {
235
+ const inferredReadModelIdentifier = this.inferReadModelIdentifier(builder.getMappedReadModelProperties());
236
+ if (inferredReadModelIdentifier) {
237
+ definition.ReadModel = inferredReadModelIdentifier;
238
+ }
194
239
  }
195
240
  }
196
241
 
242
+ definition.LastUpdated = { Value: this.computeStableLastUpdated(definition) };
197
243
  return definition;
198
244
  }
199
245
 
@@ -324,7 +370,7 @@ export class Projections implements IProjections {
324
370
  }
325
371
  }
326
372
 
327
- return {
373
+ const definition: Record<string, unknown> = {
328
374
  EventSequenceId: metadata.eventSequenceId ?? EventSequenceId.eventLog.value,
329
375
  Identifier: metadata.id.value,
330
376
  ReadModel: metadata.readModelIdentifier,
@@ -340,14 +386,15 @@ export class Projections implements IProjections {
340
386
  IncludeChildren: false,
341
387
  AutoMap: AutoMap.Inherit
342
388
  },
343
- FromEventProperty: undefined,
344
389
  RemovedWith: Array.from(removedWithByEventType.values()),
345
390
  RemovedWithJoin: Array.from(removedWithJoinByEventType.values()),
346
- LastUpdated: { Value: new Date().toISOString() },
391
+ LastUpdated: { Value: '' },
347
392
  Tags: [],
348
- AutoMap: AutoMap.Disabled,
393
+ AutoMap: AutoMap.Enabled,
349
394
  Nested: {}
350
395
  };
396
+ definition.LastUpdated = { Value: this.computeStableLastUpdated(definition) };
397
+ return definition;
351
398
  }
352
399
 
353
400
  private resolveModelBoundMetadata(type: Constructor): ResolvedModelBoundMetadata | undefined {
@@ -453,11 +500,19 @@ export class Projections implements IProjections {
453
500
  return `${eventType.Id}:${eventType.Generation}:${eventType.Tombstone ? '1' : '0'}`;
454
501
  }
455
502
 
456
- private toContractsGuid(value: string): { lo: number; hi: number } {
457
- const hex = value.replace(/-/g, '');
458
- return {
459
- hi: Number(BigInt(`0x${hex.substring(0, 16)}`)),
460
- lo: Number(BigInt(`0x${hex.substring(16, 32)}`))
461
- };
503
+ /**
504
+ * Computes a stable, deterministic ISO timestamp from the projection definition content,
505
+ * excluding the LastUpdated field itself. This ensures the server does not interpret
506
+ * a repeated registration of an unchanged definition as a definition change, which
507
+ * would otherwise trigger an unnecessary auto-replay.
508
+ */
509
+ private computeStableLastUpdated(definition: Record<string, unknown>): string {
510
+ const { LastUpdated: _omit, ...rest } = definition;
511
+ const content = JSON.stringify(rest, Object.keys(rest).sort());
512
+ let hash = 5381;
513
+ for (let i = 0; i < content.length; i++) {
514
+ hash = ((hash << 5) + hash + content.charCodeAt(i)) >>> 0;
515
+ }
516
+ return new Date(hash * 1000).toISOString();
462
517
  }
463
518
  }
@@ -215,7 +215,7 @@ export class ProjectionBuilderFor<TReadModel> implements IProjectionBuilderFor<T
215
215
  * @returns The projection definition object ready to send to the kernel.
216
216
  */
217
217
  build(identifier: string, readModelName: string): Record<string, unknown> {
218
- return {
218
+ const definition: Record<string, unknown> = {
219
219
  EventSequenceId: this._eventSequenceId,
220
220
  Identifier: identifier,
221
221
  ReadModel: this._containerName ?? readModelName,
@@ -230,11 +230,13 @@ export class ProjectionBuilderFor<TReadModel> implements IProjectionBuilderFor<T
230
230
  FromEventProperty: undefined,
231
231
  RemovedWith: this._removedWith,
232
232
  RemovedWithJoin: this._removedWithJoin,
233
- LastUpdated: { Value: new Date().toISOString() },
233
+ LastUpdated: { Value: '' },
234
234
  Tags: [],
235
235
  AutoMap: this._autoMap,
236
236
  Nested: {}
237
237
  };
238
+ definition.LastUpdated = { Value: this.computeStableLastUpdated(definition) };
239
+ return definition;
238
240
  }
239
241
 
240
242
  /**
@@ -274,4 +276,19 @@ export class ProjectionBuilderFor<TReadModel> implements IProjectionBuilderFor<T
274
276
  Tombstone: false
275
277
  };
276
278
  }
279
+
280
+ /**
281
+ * Computes a stable, deterministic ISO timestamp from the projection definition content,
282
+ * excluding the LastUpdated field itself. This ensures the server does not interpret
283
+ * a repeated registration of an unchanged definition as a definition change.
284
+ */
285
+ private computeStableLastUpdated(definition: Record<string, unknown>): string {
286
+ const { LastUpdated: _omit, ...rest } = definition;
287
+ const content = JSON.stringify(rest, Object.keys(rest).sort());
288
+ let hash = 5381;
289
+ for (let i = 0; i < content.length; i++) {
290
+ hash = ((hash << 5) + hash + content.charCodeAt(i)) >>> 0;
291
+ }
292
+ return new Date(hash * 1000).toISOString();
293
+ }
277
294
  }
@@ -18,19 +18,23 @@ export interface ProjectionMetadata {
18
18
 
19
19
  /** The optional explicit event sequence identifier. */
20
20
  readonly eventSequenceId: string | undefined;
21
+
22
+ /** The optional explicit read model constructor. */
23
+ readonly readModelType: Constructor | undefined;
21
24
  }
22
25
 
23
26
  /**
24
27
  * TypeScript decorator that marks a class as a declarative projection.
25
28
  * @param id - The unique identifier for the projection. Defaults to the class name if omitted.
29
+ * @param readModelType - Optional explicit read model constructor (needed when TypeScript generic inference is insufficient).
26
30
  * @param eventSequenceId - Optional explicit event sequence identifier.
27
31
  * @returns A class decorator.
28
32
  */
29
- export function projection(id: string = '', eventSequenceId?: string): ClassDecorator {
33
+ export function projection(id: string = '', readModelType?: Constructor, eventSequenceId?: string): ClassDecorator {
30
34
  return (target: object) => {
31
35
  const constructor = target as Function;
32
36
  const projectionId = new ProjectionId(id || constructor.name);
33
- const metadata: ProjectionMetadata = { id: projectionId, eventSequenceId };
37
+ const metadata: ProjectionMetadata = { id: projectionId, eventSequenceId, readModelType };
34
38
  Reflect.defineMetadata(PROJECTION_METADATA_KEY, metadata, target);
35
39
  TypeDiscoverer.default.register(
36
40
  DecoratorType.Projection,
@@ -1,8 +1,6 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- export { modelBound, getModelBoundMetadata, isModelBound } from './modelBound';
5
- export type { ModelBoundMetadata } from './modelBound';
6
4
  export { fromEvent, getFromEventMetadata, hasFromEventMetadata } from './fromEvent';
7
5
  export type { FromEventOptions } from './FromEventOptions';
8
6
  export type { FromEventMetadata } from './FromEventMetadata';
@@ -2,6 +2,8 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import 'reflect-metadata';
5
+ import { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals';
6
+ import type { EventContext } from '../../Events';
5
7
  import { TypeIntrospector } from '../../types';
6
8
 
7
9
  /** Metadata stored by the setFromContext property decorator. */
@@ -17,14 +19,30 @@ const METADATA_KEY = 'chronicle:projection:setFromContext';
17
19
  /**
18
20
  * Property decorator that maps a value from an event context property onto the decorated read model property.
19
21
  * @param eventType - The event constructor.
20
- * @param contextPropertyName - Optional event context property name. Defaults to the property name.
22
+ * @param contextPropertyOrAccessor - Optional event context property name or property accessor. Defaults to the property name.
21
23
  * @returns A property decorator.
22
24
  */
23
- export function setFromContext(eventType: Function, contextPropertyName?: string): PropertyDecorator {
25
+ export function setFromContext(eventType: Function, contextPropertyName?: string): PropertyDecorator;
26
+ export function setFromContext(eventType: Function, contextPropertyAccessor?: PropertyAccessor<EventContext>): PropertyDecorator;
27
+ export function setFromContext(
28
+ eventType: Function,
29
+ contextPropertyOrAccessor?: string | PropertyAccessor<EventContext>
30
+ ): PropertyDecorator {
24
31
  return (target: object, propertyKey: string | symbol) => {
25
32
  const key = propertyKey.toString();
26
33
  TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
27
34
  const existing: SetFromContextMetadata[] = Reflect.getMetadata(METADATA_KEY, target, key) ?? [];
35
+
36
+ let contextPropertyName: string | undefined;
37
+ if (typeof contextPropertyOrAccessor === 'string') {
38
+ contextPropertyName = contextPropertyOrAccessor;
39
+ } else if (typeof contextPropertyOrAccessor === 'function') {
40
+ const handler = new PropertyPathResolverProxyHandler();
41
+ const proxy = new Proxy({}, handler);
42
+ contextPropertyOrAccessor(proxy as EventContext);
43
+ contextPropertyName = handler.property;
44
+ }
45
+
28
46
  const metadata: SetFromContextMetadata = { eventType, contextPropertyName };
29
47
  Reflect.defineMetadata(METADATA_KEY, [...existing, metadata], target, key);
30
48
  };
package/README.md CHANGED
@@ -6,7 +6,7 @@ A TypeScript-idiomatic client for [Cratis Chronicle](https://github.com/Cratis/C
6
6
 
7
7
  `@cratis/chronicle` provides a clean, type-safe TypeScript API for interacting with the Chronicle Kernel. It builds on top of [`@cratis/chronicle.contracts`](https://www.npmjs.com/package/@cratis/chronicle.contracts) (the gRPC contracts package) and exposes idiomatic TypeScript constructs including:
8
8
 
9
- - **Decorators** — `@eventType`, `@readModel`, `@reactor`, `@reducer`, `@constraint`, `@projection`, `@modelBoundProjection`
9
+ - **Decorators** — `@eventType`, `@readModel`, `@reactor`, `@reducer`, `@constraint`, `@projection`, and model-bound decorators such as `@fromEvent`
10
10
  - **Value objects** — `EventSequenceNumber`, `EventTypeId`, `EventStoreName`, etc.
11
11
  - **Fluent client** — `ChronicleClient` → `EventStore` → `EventLog` → `append()`
12
12
 
@@ -233,6 +233,7 @@ export class Reactors implements IReactors {
233
233
  }
234
234
 
235
235
  const content = JSON.parse(event.Content) as Record<string, unknown>;
236
+ this._logger.debug('Event content', { reactorId: id, eventTypeId, contentKeys: Object.keys(content), rawContent: event.Content.substring(0, 200) });
236
237
  const context: EventContext = {
237
238
  sequenceNumber: event.Context!.SequenceNumber,
238
239
  eventSourceId: event.Context!.EventSourceId,
@@ -4,14 +4,18 @@
4
4
  import 'reflect-metadata';
5
5
  import { diag } from '@opentelemetry/api';
6
6
  import { Constructor } from '@cratis/fundamentals';
7
- import { ObservationState, ReducerMessage } from '@cratis/chronicle.contracts';
7
+ import { ObservationState, ReadModelObserverType, ReducerMessage } from '@cratis/chronicle.contracts';
8
8
  import { IClientArtifactsProvider } from '../artifacts';
9
9
  import { ChronicleConnection } from '../connection';
10
+ import { toContractsGuid } from '../connection/Guid';
10
11
  import { ConnectionLifecycle } from '../connection/ConnectionLifecycle';
11
12
  import { getEventTypeMetadata } from '../Events/eventTypeDecorator';
12
13
  import { EventSequenceId } from '../EventSequences/EventSequenceId';
14
+ import { WellKnownSinks } from '../sinks';
13
15
  import { IReducers } from './IReducers';
14
16
  import { getReducerMetadata } from './reducer';
17
+ import { getReadModelMetadata } from '../ReadModels';
18
+ import { JsonSchemaGenerator } from '../Schemas';
15
19
 
16
20
  /** Expression used to partition reducer observations by event source ID. */
17
21
  const EVENT_SOURCE_ID_KEY = '$eventSourceId';
@@ -139,6 +143,8 @@ export class Reducers implements IReducers {
139
143
  await this.discover();
140
144
  }
141
145
 
146
+ await this.registerReadModels();
147
+
142
148
  this._logger.info('Registering reducers', { count: this._reducers.size });
143
149
  for (const [id, reducerType] of this._reducers) {
144
150
  this.startObservation(id, reducerType);
@@ -147,6 +153,61 @@ export class Reducers implements IReducers {
147
153
  this._registered = true;
148
154
  }
149
155
 
156
+ private async registerReadModels(): Promise<void> {
157
+ if (this._reducers.size === 0) {
158
+ return;
159
+ }
160
+
161
+ const readModels = Array.from(this._reducers.entries()).map(([id, reducerType]) => {
162
+ const readModelName = (reducerType as Function).name;
163
+ return {
164
+ Type: {
165
+ Identifier: readModelName,
166
+ Generation: 1
167
+ },
168
+ ContainerName: readModelName,
169
+ DisplayName: readModelName,
170
+ Sink: {
171
+ ConfigurationId: toContractsGuid(WellKnownSinks.Null),
172
+ TypeId: toContractsGuid(WellKnownSinks.MongoDB)
173
+ },
174
+ Schema: this.getReducerSchema(reducerType, readModelName),
175
+ Indexes: [],
176
+ ObserverType: ReadModelObserverType.Reducer,
177
+ ObserverIdentifier: id,
178
+ Owner: 1,
179
+ Source: 1
180
+ };
181
+ });
182
+
183
+ this._logger.info('Registering read models for reducers', { count: readModels.length });
184
+ await this._connection.readModels.registerMany({
185
+ EventStore: this._eventStoreName,
186
+ Owner: 1,
187
+ ReadModels: readModels,
188
+ Source: 1
189
+ });
190
+ }
191
+
192
+ private getReducerSchema(reducerType: Constructor, readModelName: string): string {
193
+ const metadata = getReducerMetadata(reducerType);
194
+ if (metadata?.readModel) {
195
+ const readModelMeta = getReadModelMetadata(metadata.readModel);
196
+ if (readModelMeta?.schema) {
197
+ return JSON.stringify(readModelMeta.schema);
198
+ }
199
+ // No @readModel() decorator — generate from instance
200
+ return JSON.stringify(JsonSchemaGenerator.generate(metadata.readModel));
201
+ }
202
+
203
+ // No read model type declared — generate from a minimal schema
204
+ const minimalSchema = {
205
+ ...JsonSchemaGenerator.createEmptySchema(readModelName),
206
+ additionalProperties: true
207
+ };
208
+ return JSON.stringify(minimalSchema);
209
+ }
210
+
150
211
  private startObservation(id: string, reducerType: Constructor): void {
151
212
  const metadata = getReducerMetadata(reducerType)!;
152
213
  const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
@@ -191,7 +252,10 @@ export class Reducers implements IReducers {
191
252
  })),
192
253
  ReadModel: readModelName,
193
254
  IsActive: true,
194
- Sink: { TypeId: { lo: 0, hi: 0 }, ConfigurationId: undefined },
255
+ Sink: {
256
+ TypeId: toContractsGuid(WellKnownSinks.MongoDB),
257
+ ConfigurationId: toContractsGuid(WellKnownSinks.Null)
258
+ },
195
259
  Tags: [],
196
260
  Filters: {
197
261
  FilterTags: [],
@@ -44,14 +44,27 @@ export class JsonSchemaGenerator {
44
44
  static generate(target: Function, members?: ReadonlyMap<string, Function | undefined>): JsonSchema {
45
45
  const membersToUse = members ?? TypeIntrospector.getMembers(target);
46
46
  const schemaProperties: Record<string, JsonSchema> = {};
47
+
47
48
  for (const [memberName, memberType] of membersToUse.entries()) {
48
- schemaProperties[memberName] = this.mapRuntimeTypeToSchema(memberType);
49
+ const propertySchema = this.mapRuntimeTypeToSchema(memberType);
50
+ // Only include properties whose type was resolved. An empty schema ({}) means
51
+ // the runtime type was unavailable (e.g. esbuild/tsx omits design:paramtypes).
52
+ if (Object.keys(propertySchema).length > 0) {
53
+ schemaProperties[memberName] = propertySchema;
54
+ }
55
+ }
56
+
57
+ // When no property types could be resolved, return a minimal schema with empty
58
+ // properties so the server uses its fallback path that preserves all event
59
+ // content as-is via ConvertUnknownSchemaTypeToClrType.
60
+ if (Object.keys(schemaProperties).length === 0) {
61
+ return this.createEmptySchema(target.name);
49
62
  }
50
63
 
51
64
  return {
52
65
  ...this.createEmptySchema(target.name),
53
66
  properties: schemaProperties,
54
- required: Array.from(membersToUse.keys()),
67
+ required: Object.keys(schemaProperties),
55
68
  };
56
69
  }
57
70
 
@@ -77,7 +90,11 @@ export class JsonSchemaGenerator {
77
90
  return { type: 'array', items: { type: 'object' } };
78
91
  }
79
92
 
80
- if (runtimeType && runtimeType !== Object) {
93
+ if (!runtimeType) {
94
+ return {};
95
+ }
96
+
97
+ if (runtimeType !== Object) {
81
98
  return this.generate(runtimeType);
82
99
  }
83
100
 
@@ -48,9 +48,4 @@ export class DefaultClientArtifactsProvider implements IClientArtifactsProvider
48
48
  get projections(): Constructor[] {
49
49
  return this.discoverer.getTypesByDecoratorType(DecoratorType.Projection);
50
50
  }
51
-
52
- /** @inheritdoc */
53
- get modelBoundProjections(): Constructor[] {
54
- return this.discoverer.getTypesByDecoratorType(DecoratorType.ModelBoundProjection);
55
- }
56
51
  }
@@ -24,7 +24,4 @@ export interface IClientArtifactsProvider {
24
24
 
25
25
  /** Gets discovered declarative projection constructors. */
26
26
  readonly projections: Constructor[];
27
-
28
- /** Gets discovered model-bound projection constructors. */
29
- readonly modelBoundProjections: Constructor[];
30
27
  }
@@ -1,4 +1,32 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- export { Guid } from '@cratis/chronicle.contracts';
4
+ import type { Guid as ContractsGuid } from '@cratis/chronicle.contracts';
5
+ import { Guid as FundamentalGuid } from '@cratis/fundamentals';
6
+
7
+ export { Guid } from '@cratis/fundamentals';
8
+
9
+ /**
10
+ * Converts a Chronicle Guid into the protobuf-net bcl.Guid shape used by the
11
+ * contracts package.
12
+ */
13
+ export function toContractsGuid(guid: FundamentalGuid): ContractsGuid {
14
+ const bytes = guid.bytes ?? FundamentalGuid.empty.bytes ?? [];
15
+ if (bytes.length !== 16) {
16
+ throw new Error(`Invalid Guid '${guid.toString()}'. Expected 16 bytes.`);
17
+ }
18
+
19
+ let lo = 0n;
20
+ let hi = 0n;
21
+ for (let index = 0; index < 8; index++) {
22
+ lo |= BigInt(bytes[index]) << BigInt(index * 8);
23
+ hi |= BigInt(bytes[index + 8]) << BigInt(index * 8);
24
+ }
25
+
26
+ // The runtime accepts strings for fixed64 values, which preserves the exact
27
+ // 64-bit payload instead of truncating through Number.
28
+ return {
29
+ lo: lo.toString() as unknown as number,
30
+ hi: hi.toString() as unknown as number
31
+ };
32
+ }
@@ -0,0 +1,22 @@
1
+ import { CausationType } from './CausationType';
2
+ /**
3
+ * Represents a causation instance.
4
+ */
5
+ export declare class Causation {
6
+ readonly occurred: Date;
7
+ readonly type: CausationType;
8
+ readonly properties: Readonly<Record<string, string>>;
9
+ /**
10
+ * Creates an unknown causation instance.
11
+ * @returns A new {@link Causation} with the current time, type set to {@link CausationType.unknown}, and empty properties.
12
+ */
13
+ static unknown(): Causation;
14
+ /**
15
+ * Initializes a new instance of the {@link Causation} class.
16
+ * @param occurred - When it occurred.
17
+ * @param type - Type of causation.
18
+ * @param properties - Any properties associated with the causation.
19
+ */
20
+ constructor(occurred: Date, type: CausationType, properties: Readonly<Record<string, string>>);
21
+ }
22
+ //# sourceMappingURL=Causation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Causation.d.ts","sourceRoot":"","sources":["../../Auditing/Causation.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD;;GAEG;AACH,qBAAa,SAAS;IAgBd,QAAQ,CAAC,QAAQ,EAAE,IAAI;IACvB,QAAQ,CAAC,IAAI,EAAE,aAAa;IAC5B,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAjBzD;;;OAGG;IACH,MAAM,CAAC,OAAO,IAAI,SAAS;IAI3B;;;;;OAKG;gBAEU,QAAQ,EAAE,IAAI,EACd,IAAI,EAAE,aAAa,EACnB,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAE5D"}
@@ -0,0 +1,30 @@
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
+ import { CausationType } from './CausationType';
4
+ /**
5
+ * Represents a causation instance.
6
+ */
7
+ export class Causation {
8
+ occurred;
9
+ type;
10
+ properties;
11
+ /**
12
+ * Creates an unknown causation instance.
13
+ * @returns A new {@link Causation} with the current time, type set to {@link CausationType.unknown}, and empty properties.
14
+ */
15
+ static unknown() {
16
+ return new Causation(new Date(), CausationType.unknown, {});
17
+ }
18
+ /**
19
+ * Initializes a new instance of the {@link Causation} class.
20
+ * @param occurred - When it occurred.
21
+ * @param type - Type of causation.
22
+ * @param properties - Any properties associated with the causation.
23
+ */
24
+ constructor(occurred, type, properties) {
25
+ this.occurred = occurred;
26
+ this.type = type;
27
+ this.properties = properties;
28
+ }
29
+ }
30
+ //# sourceMappingURL=Causation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Causation.js","sourceRoot":"","sources":["../../Auditing/Causation.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,qGAAqG;AAErG,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD;;GAEG;AACH,MAAM,OAAO,SAAS;IAgBL;IACA;IACA;IAjBb;;;OAGG;IACH,MAAM,CAAC,OAAO;QACV,OAAO,IAAI,SAAS,CAAC,IAAI,IAAI,EAAE,EAAE,aAAa,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAChE,CAAC;IAED;;;;;OAKG;IACH,YACa,QAAc,EACd,IAAmB,EACnB,UAA4C;QAF5C,aAAQ,GAAR,QAAQ,CAAM;QACd,SAAI,GAAJ,IAAI,CAAe;QACnB,eAAU,GAAV,UAAU,CAAkC;IACtD,CAAC;CACP"}
@@ -0,0 +1,23 @@
1
+ import { Causation } from './Causation';
2
+ import { CausationType } from './CausationType';
3
+ import { ICausationManager } from './ICausationManager';
4
+ /**
5
+ * Implements {@link ICausationManager} using {@link AsyncLocalStorage} to scope the causation chain to the active async call context.
6
+ */
7
+ export declare class CausationManager implements ICausationManager {
8
+ private readonly _storage;
9
+ private _root;
10
+ /** @inheritdoc */
11
+ get root(): Causation;
12
+ /** @inheritdoc */
13
+ getCurrentChain(): ReadonlyArray<Causation>;
14
+ /** @inheritdoc */
15
+ add(type: CausationType, properties: Record<string, string>): void;
16
+ /**
17
+ * Defines the root causation for the current process.
18
+ * @param properties - Properties associated with the root causation.
19
+ */
20
+ defineRoot(properties: Record<string, string>): void;
21
+ private _getOrInitChain;
22
+ }
23
+ //# sourceMappingURL=CausationManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CausationManager.d.ts","sourceRoot":"","sources":["../../Auditing/CausationManager.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD;;GAEG;AACH,qBAAa,gBAAiB,YAAW,iBAAiB;IACtD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IACjE,OAAO,CAAC,KAAK,CAAgE;IAE7E,kBAAkB;IAClB,IAAI,IAAI,IAAI,SAAS,CAEpB;IAED,kBAAkB;IAClB,eAAe,IAAI,aAAa,CAAC,SAAS,CAAC;IAK3C,kBAAkB;IAClB,GAAG,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAKlE;;;OAGG;IACH,UAAU,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAIpD,OAAO,CAAC,eAAe;CAU1B"}