@cratis/chronicle 6.16.1 → 6.18.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 (141) hide show
  1. package/artifacts/ActivatedArtifact.ts +5 -1
  2. package/artifacts/ArtifactCompletionFailed.ts +20 -0
  3. package/artifacts/ArtifactInvocationContext.ts +21 -0
  4. package/artifacts/for_withActivatedArtifact/when_completing/after_processing_failure.spec.ts +34 -0
  5. package/artifacts/index.ts +2 -0
  6. package/artifacts/withActivatedArtifact.ts +20 -3
  7. package/dist/artifacts/ActivatedArtifact.d.ts +4 -1
  8. package/dist/artifacts/ActivatedArtifact.d.ts.map +1 -1
  9. package/dist/artifacts/ArtifactCompletionFailed.d.ts +9 -0
  10. package/dist/artifacts/ArtifactCompletionFailed.d.ts.map +1 -0
  11. package/dist/artifacts/ArtifactCompletionFailed.js +19 -0
  12. package/dist/artifacts/ArtifactCompletionFailed.js.map +1 -0
  13. package/dist/artifacts/ArtifactInvocationContext.d.ts +18 -0
  14. package/dist/artifacts/ArtifactInvocationContext.d.ts.map +1 -0
  15. package/dist/artifacts/ArtifactInvocationContext.js +4 -0
  16. package/dist/artifacts/ArtifactInvocationContext.js.map +1 -0
  17. package/dist/artifacts/index.d.ts +2 -0
  18. package/dist/artifacts/index.d.ts.map +1 -1
  19. package/dist/artifacts/index.js +1 -0
  20. package/dist/artifacts/index.js.map +1 -1
  21. package/dist/artifacts/withActivatedArtifact.d.ts +2 -1
  22. package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -1
  23. package/dist/artifacts/withActivatedArtifact.js +22 -3
  24. package/dist/artifacts/withActivatedArtifact.js.map +1 -1
  25. package/dist/projections/CompiledProjectionDefinitions.d.ts +6 -0
  26. package/dist/projections/CompiledProjectionDefinitions.d.ts.map +1 -1
  27. package/dist/projections/ProjectionCapabilityProvenance.d.ts +8 -0
  28. package/dist/projections/ProjectionCapabilityProvenance.d.ts.map +1 -0
  29. package/dist/projections/ProjectionCapabilityProvenance.js +4 -0
  30. package/dist/projections/ProjectionCapabilityProvenance.js.map +1 -0
  31. package/dist/projections/ProjectionDefinitionCompiler.d.ts +1 -0
  32. package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -1
  33. package/dist/projections/ProjectionDefinitionCompiler.js +130 -4
  34. package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -1
  35. package/dist/projections/ProjectionEventSchema.d.ts +10 -0
  36. package/dist/projections/ProjectionEventSchema.d.ts.map +1 -0
  37. package/dist/projections/ProjectionEventSchema.js +4 -0
  38. package/dist/projections/ProjectionEventSchema.js.map +1 -0
  39. package/dist/projections/VariantReclassifier.d.ts +2 -0
  40. package/dist/projections/VariantReclassifier.d.ts.map +1 -1
  41. package/dist/projections/VariantReclassifier.js.map +1 -1
  42. package/dist/projections/captureProjectionProvenance.d.ts +4 -0
  43. package/dist/projections/captureProjectionProvenance.d.ts.map +1 -0
  44. package/dist/projections/captureProjectionProvenance.js +56 -0
  45. package/dist/projections/captureProjectionProvenance.js.map +1 -0
  46. package/dist/projections/declarative/AddChildBuilder.d.ts +13 -2
  47. package/dist/projections/declarative/AddChildBuilder.d.ts.map +1 -1
  48. package/dist/projections/declarative/AddChildBuilder.js +27 -1
  49. package/dist/projections/declarative/AddChildBuilder.js.map +1 -1
  50. package/dist/projections/declarative/FromBuilder.d.ts +4 -0
  51. package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
  52. package/dist/projections/declarative/FromBuilder.js +10 -2
  53. package/dist/projections/declarative/FromBuilder.js.map +1 -1
  54. package/dist/projections/declarative/IAddChildBuilder.d.ts +12 -0
  55. package/dist/projections/declarative/IAddChildBuilder.d.ts.map +1 -1
  56. package/dist/projections/declarative/IReadModelPropertiesBuilder.d.ts +16 -1
  57. package/dist/projections/declarative/IReadModelPropertiesBuilder.d.ts.map +1 -1
  58. package/dist/projections/declarative/JoinBuilder.d.ts +4 -0
  59. package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
  60. package/dist/projections/declarative/JoinBuilder.js +1 -2
  61. package/dist/projections/declarative/JoinBuilder.js.map +1 -1
  62. package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -1
  63. package/dist/projections/declarative/ProjectionBuilderCore.js +9 -1
  64. package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
  65. package/dist/projections/declarative/projectionBuilderProvenance.d.ts +16 -0
  66. package/dist/projections/declarative/projectionBuilderProvenance.d.ts.map +1 -0
  67. package/dist/projections/declarative/projectionBuilderProvenance.js +28 -0
  68. package/dist/projections/declarative/projectionBuilderProvenance.js.map +1 -0
  69. package/dist/projections/eventContractPath.d.ts +4 -0
  70. package/dist/projections/eventContractPath.d.ts.map +1 -0
  71. package/dist/projections/eventContractPath.js +7 -0
  72. package/dist/projections/eventContractPath.js.map +1 -0
  73. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
  74. package/dist/projections/modelBound/childrenAndNestedBuilder.js +41 -10
  75. package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
  76. package/dist/reactors/Reactors.d.ts.map +1 -1
  77. package/dist/reactors/Reactors.js +12 -3
  78. package/dist/reactors/Reactors.js.map +1 -1
  79. package/dist/reducers/Reducers.d.ts.map +1 -1
  80. package/dist/reducers/Reducers.js +12 -3
  81. package/dist/reducers/Reducers.js.map +1 -1
  82. package/dist/testing/projections/ProjectionCapabilities.d.ts +17 -0
  83. package/dist/testing/projections/ProjectionCapabilities.d.ts.map +1 -0
  84. package/dist/testing/projections/ProjectionCapabilities.js +233 -0
  85. package/dist/testing/projections/ProjectionCapabilities.js.map +1 -0
  86. package/dist/testing/projections/UnsupportedProjectionOperation.d.ts +11 -0
  87. package/dist/testing/projections/UnsupportedProjectionOperation.d.ts.map +1 -0
  88. package/dist/testing/projections/UnsupportedProjectionOperation.js +16 -0
  89. package/dist/testing/projections/UnsupportedProjectionOperation.js.map +1 -0
  90. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.d.ts +7 -0
  91. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.d.ts.map +1 -0
  92. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.js +16 -0
  93. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.js.map +1 -0
  94. package/dist/testing/projections/for_ProjectionCapabilities/given/Model.d.ts +10 -0
  95. package/dist/testing/projections/for_ProjectionCapabilities/given/Model.d.ts.map +1 -0
  96. package/dist/testing/projections/for_ProjectionCapabilities/given/Model.js +20 -0
  97. package/dist/testing/projections/for_ProjectionCapabilities/given/Model.js.map +1 -0
  98. package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.d.ts +3 -0
  99. package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.d.ts.map +1 -0
  100. package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.js +7 -0
  101. package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.js.map +1 -0
  102. package/dist/testing/projections/for_ProjectionCapabilities/given/compile.d.ts +12 -0
  103. package/dist/testing/projections/for_ProjectionCapabilities/given/compile.d.ts.map +1 -0
  104. package/dist/testing/projections/for_ProjectionCapabilities/given/compile.js +34 -0
  105. package/dist/testing/projections/for_ProjectionCapabilities/given/compile.js.map +1 -0
  106. package/dist/tsconfig.tsbuildinfo +1 -1
  107. package/package.json +2 -2
  108. package/projections/CompiledProjectionDefinitions.ts +6 -0
  109. package/projections/ProjectionCapabilityProvenance.ts +10 -0
  110. package/projections/ProjectionDefinitionCompiler.registration.spec.ts +4 -2
  111. package/projections/ProjectionDefinitionCompiler.ts +127 -4
  112. package/projections/ProjectionEventSchema.ts +13 -0
  113. package/projections/Projections.childrenAndNested.spec.ts +250 -1
  114. package/projections/VariantReclassifier.ts +2 -0
  115. package/projections/captureProjectionProvenance.ts +50 -0
  116. package/projections/declarative/AddChildBuilder.ts +33 -2
  117. package/projections/declarative/FromBuilder.ts +18 -1
  118. package/projections/declarative/IAddChildBuilder.ts +14 -0
  119. package/projections/declarative/IReadModelPropertiesBuilder.ts +18 -1
  120. package/projections/declarative/JoinBuilder.ts +9 -1
  121. package/projections/declarative/ProjectionBuilderCore.ts +8 -1
  122. package/projections/declarative/addChildCompatibility.standard.spec.ts +80 -0
  123. package/projections/declarative/projectionBuilderProvenance.ts +40 -0
  124. package/projections/eventContextExpressions.spec.ts +20 -0
  125. package/projections/eventContractPath.ts +9 -0
  126. package/projections/modelBound/childrenAndNestedBuilder.ts +45 -11
  127. package/reactors/Reactors.ts +13 -3
  128. package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +93 -2
  129. package/reducers/Reducers.ts +14 -3
  130. package/reducers/for_Reducers/when_delivering/with_activation.spec.ts +67 -1
  131. package/testing/projections/ProjectionCapabilities.ts +213 -0
  132. package/testing/projections/UnsupportedProjectionOperation.ts +16 -0
  133. package/testing/projections/for_ProjectionCapabilities/given/Changed.ts +12 -0
  134. package/testing/projections/for_ProjectionCapabilities/given/Model.ts +13 -0
  135. package/testing/projections/for_ProjectionCapabilities/given/Removed.ts +7 -0
  136. package/testing/projections/for_ProjectionCapabilities/given/compile.ts +39 -0
  137. package/testing/projections/for_ProjectionCapabilities/when_compiling_capability_evidence.spec.ts +68 -0
  138. package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +230 -0
  139. package/testing/projections/for_ProjectionCapabilities/when_validating_numeric_schemas.spec.ts +73 -0
  140. package/testing/projections/for_ProjectionCapabilities/when_validating_schema_boundaries.spec.ts +118 -0
  141. package/testing/projections/for_ProjectionCapabilities/when_validating_supported_operations.spec.ts +103 -0
@@ -11,6 +11,7 @@ import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifac
11
11
  import { ArtifactDelivery } from '../../../artifacts/ArtifactDelivery.js';
12
12
  import { ArtifactKind } from '../../../artifacts/ArtifactKind.js';
13
13
  import type { ArtifactActivationContext } from '../../../artifacts/ArtifactActivationContext.js';
14
+ import type { ArtifactInvocationContext } from '../../../artifacts/ArtifactInvocationContext.js';
14
15
  import type { ClientArtifactsActivator } from '../../../artifacts/ClientArtifactsActivator.js';
15
16
  import type { IEventStore } from '../../../IEventStore.js';
16
17
  import type { IEventLog } from '../../../eventSequences/IEventLog.js';
@@ -159,10 +160,11 @@ describe('when delivering reactor batches', () => {
159
160
  observed.length = 0;
160
161
  serviceReferences.length = 0;
161
162
  const contexts: ArtifactActivationContext[] = [];
163
+ const invocations: ArtifactInvocationContext[] = [];
162
164
  const boundaries: string[] = [];
163
165
  const activator: ClientArtifactsActivator = (type, context) => {
164
166
  contexts.push(context);
165
- return { instance: new type(), run: async callback => { boundaries.push('enter');
167
+ return { instance: new type(), run: async (callback, invocation) => { invocations.push(invocation!); boundaries.push('enter');
166
168
  try { return await callback(); } finally { boundaries.push('exit'); } },
167
169
  dispose: () => { boundaries.push('dispose'); } };
168
170
  };
@@ -177,6 +179,11 @@ describe('when delivering reactor batches', () => {
177
179
  contexts[0].readModels.should.equal(result.store.readModels);
178
180
  if (contexts[0].delivery === ArtifactDelivery.Events) contexts[0].eventContext.sequenceNumber.should.equal(1n);
179
181
  boundaries.should.deep.equal(['enter', 'exit', 'enter', 'exit', 'dispose', 'enter', 'exit', 'dispose']);
182
+ invocations.map(invocation => invocation.delivery).should.deep.equal([ArtifactDelivery.Events, ArtifactDelivery.Events, ArtifactDelivery.Events]);
183
+ invocations.map(invocation => invocation.delivery === ArtifactDelivery.Events ? invocation.eventContext.sequenceNumber : 0n)
184
+ .should.deep.equal([1n, 2n, 3n]);
185
+ invocations.map(invocation => invocation.delivery === ArtifactDelivery.Events ? invocation.methodName : '')
186
+ .should.deep.equal(['activationEvent', 'activationEvent', 'activationEvent']);
180
187
  observed.should.deep.equal(['one:book-1', 'two:book-1', 'three:book-1']);
181
188
  serviceReferences.every(services => services.eventStore === result.store && services.readModels === result.store.readModels).should.be.true;
182
189
  });
@@ -241,9 +248,16 @@ describe('when delivering reactor batches', () => {
241
248
  it('should notify separately before processing events in the same message', async () => {
242
249
  observed.length = 0;
243
250
  const contexts: ArtifactActivationContext[] = [];
244
- const activator: ClientArtifactsActivator = (type, context) => { contexts.push(context); return { instance: new type() }; };
251
+ const invocations: ArtifactInvocationContext[] = [];
252
+ const activator: ClientArtifactsActivator = (type, context) => { contexts.push(context); return { instance: new type(),
253
+ run: async (callback, invocation) => { invocations.push(invocation!); return callback(); } }; };
245
254
  const result = await observe(ActivationReactor, [delivery([event('live', 1n)], ReplayState.BeginReplay)], 'tenant-a', activator);
246
255
  observed.should.deep.equal(['notification', 'live:book-1']);
256
+ invocations.should.deep.equal([
257
+ { delivery: ArtifactDelivery.ReplayNotification, replayState: ReplayState.BeginReplay },
258
+ { delivery: ArtifactDelivery.Events, eventContext: contexts[1].delivery === ArtifactDelivery.Events ? contexts[1].eventContext : undefined,
259
+ methodName: 'activationEvent' }
260
+ ]);
247
261
  contexts.map(context => context.delivery).should.deep.equal([ArtifactDelivery.ReplayNotification, ArtifactDelivery.Events]);
248
262
  result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
249
263
  });
@@ -370,6 +384,83 @@ describe('when delivering reactor batches', () => {
370
384
  reactors.dispose();
371
385
  });
372
386
 
387
+ it('should fail a replay notification when completion fails and suppress event handling', async () => {
388
+ observed.length = 0;
389
+ const steps: string[] = [];
390
+ const activator: ClientArtifactsActivator = (type, context) => ({ instance: new type(),
391
+ complete: () => { steps.push('complete'); if (context.delivery === ArtifactDelivery.ReplayNotification) throw new Error('replay cleanup'); },
392
+ dispose: () => { steps.push('dispose'); } });
393
+ const result = await observe(ActivationReactor, [delivery([event('one', 1n)], ReplayState.BeginReplay)], 'tenant-a', activator);
394
+ observed.should.deep.equal(['notification']);
395
+ steps.should.deep.equal(['complete', 'dispose']);
396
+ result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
397
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['ArtifactCompletionFailed: Artifact completion failed: Error: replay cleanup']);
398
+ result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(4294967295n);
399
+ });
400
+
401
+ it('should preserve both replay notification and completion errors', async () => {
402
+ const steps: string[] = [];
403
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
404
+ complete: () => { steps.push('complete'); throw new Error('replay cleanup'); },
405
+ dispose: () => { steps.push('dispose'); } });
406
+ const result = await observe(FailingNotificationReactor, [delivery([event('one', 1n)], ReplayState.BeginReplay)], 'tenant-a', activator);
407
+ steps.should.deep.equal(['complete', 'dispose']);
408
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal([
409
+ 'Error: notification failed', 'ArtifactCompletionFailed: Artifact completion failed: Error: replay cleanup'
410
+ ]);
411
+ });
412
+
413
+ it('should complete each successful lease before acknowledgement and then dispose it', async () => {
414
+ const steps: string[] = [];
415
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
416
+ complete: () => { steps.push('complete'); }, dispose: () => { steps.push('dispose'); } });
417
+ const result = await observe(ActivationReactor, [delivery([event('one', 1n), event('two', 2n)])], 'tenant-a', activator);
418
+ steps.should.deep.equal(['complete', 'dispose']);
419
+ result.acknowledgements[0]?.State.should.equal(ObservationState.Success);
420
+ });
421
+
422
+ it('should complete after processing failure before acknowledgement and preserve the processing error', async () => {
423
+ const steps: string[] = [];
424
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
425
+ complete: () => { steps.push('complete'); }, dispose: () => { steps.push('dispose'); } });
426
+ const result = await observe(AppendReactor, [delivery([event('one', 1n)])], 'tenant-a', activator, false);
427
+ steps.should.deep.equal(['complete', 'dispose']);
428
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['Error: append rejected']);
429
+ });
430
+
431
+ it('should fail completion distinctly without advancing a successful checkpoint and still dispose', async () => {
432
+ const steps: string[] = [];
433
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
434
+ complete: async () => { steps.push('complete'); throw new Error('scope cleanup'); },
435
+ dispose: () => { steps.push('dispose'); } });
436
+ const result = await observe(ActivationReactor, [delivery([event('one', 1n), event('two', 2n)])], 'tenant-a', activator);
437
+ steps.should.deep.equal(['complete', 'dispose']);
438
+ result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
439
+ result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(4294967295n);
440
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['ArtifactCompletionFailed: Artifact completion failed: Error: scope cleanup']);
441
+ });
442
+
443
+ it('should report both processing and completion errors without acknowledging earlier successful events', async () => {
444
+ const steps: string[] = [];
445
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
446
+ complete: () => { steps.push('complete'); throw new Error('scope cleanup'); },
447
+ dispose: () => { steps.push('dispose'); } });
448
+ const result = await observe(AppendReactor, [delivery([event('one', 1n), event('two', 2n)])], 'tenant-a', activator, false);
449
+ steps.should.deep.equal(['complete', 'dispose']);
450
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal([
451
+ 'Error: append rejected', 'ArtifactCompletionFailed: Artifact completion failed: Error: scope cleanup'
452
+ ]);
453
+ result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(4294967295n);
454
+ });
455
+
456
+ it('should leave a processing failure unchanged when legacy disposal also fails', async () => {
457
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
458
+ dispose: () => { throw new Error('legacy cleanup'); } });
459
+ const result = await observe(AppendReactor, [delivery([event('one', 1n)])], 'tenant-a', activator, false);
460
+ result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
461
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['Error: append rejected']);
462
+ });
463
+
373
464
  it('should log disposal failure without changing successful acknowledgement', async () => {
374
465
  const errors: string[] = [];
375
466
  diag.setLogger({ error: (...values: unknown[]) => { errors.push(values.map(String).join(' ')); } } as DiagLogger, DiagLogLevel.ERROR);
@@ -27,6 +27,7 @@ import type { ActivatedArtifact } from '../artifacts/ActivatedArtifact.js';
27
27
  import { ArtifactKind } from '../artifacts/ArtifactKind.js';
28
28
  import { ArtifactDelivery } from '../artifacts/ArtifactDelivery.js';
29
29
  import { withActivatedArtifact, runActivated } from '../artifacts/withActivatedArtifact.js';
30
+ import { ArtifactCompletionFailed } from '../artifacts/ArtifactCompletionFailed.js';
30
31
 
31
32
  /** Expression used to partition reducer observations by event source ID. */
32
33
  const EVENT_SOURCE_ID_KEY = '$eventSourceId';
@@ -367,12 +368,16 @@ export class Reducers implements IReducers {
367
368
  signal: controller.signal, delivery: ArtifactDelivery.ReplayNotification,
368
369
  replayState: operation.ReplayState
369
370
  }, this._artifactActivator, artifact => runActivated(artifact, () =>
370
- notifyReplayLifecycle(artifact.instance, operation.ReplayState, operation.Partition)));
371
+ notifyReplayLifecycle(artifact.instance, operation.ReplayState, operation.Partition),
372
+ { delivery: ArtifactDelivery.ReplayNotification, replayState: operation.ReplayState }));
371
373
  } else if (reducerInstance) {
372
374
  await notifyReplayLifecycle(reducerInstance, operation.ReplayState, operation.Partition);
373
375
  }
374
376
  } catch (err) {
375
377
  this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: id, error: String(err) });
378
+ if (err instanceof ArtifactCompletionFailed && err.processingError !== undefined) {
379
+ exceptionMessages.push(String(err.processingError));
380
+ }
376
381
  exceptionMessages.push(String(err));
377
382
  exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
378
383
  state = ObservationState.Failed;
@@ -409,7 +414,8 @@ export class Reducers implements IReducers {
409
414
  hasState: currentState !== undefined
410
415
  });
411
416
 
412
- currentState = await runActivated(artifact, () => dispatcher.invoke(artifact.instance, entry, content, currentState, context));
417
+ currentState = await runActivated(artifact, () => dispatcher.invoke(artifact.instance, entry, content, currentState, context),
418
+ { delivery: ArtifactDelivery.Events, eventContext: context, methodName: entry.methodName });
413
419
  lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
414
420
  } catch (err) {
415
421
  this._logger.error('Error handling event in reducer', { reducerId: id, error: String(err) });
@@ -437,8 +443,13 @@ export class Reducers implements IReducers {
437
443
  } catch (err) {
438
444
  this._logger.error('Error activating reducer', { reducerId: id, error: String(err) });
439
445
  exceptionMessages.push(String(err));
440
- exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
446
+ const completionStack = err instanceof Error ? (err.stack ?? '') : '';
447
+ exceptionStackTrace = err instanceof ArtifactCompletionFailed && exceptionStackTrace
448
+ ? `${exceptionStackTrace}\n${completionStack}` : completionStack;
441
449
  state = ObservationState.Failed;
450
+ if (err instanceof ArtifactCompletionFailed) {
451
+ lastSuccessfullyObservedEvent = SEQUENCE_NUMBER_UNAVAILABLE;
452
+ }
442
453
  }
443
454
  } else {
444
455
  await processEvents({ instance: reducerInstance ?? {} });
@@ -9,6 +9,7 @@ import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifac
9
9
  import { ArtifactDelivery } from '../../../artifacts/ArtifactDelivery.js';
10
10
  import { ArtifactKind } from '../../../artifacts/ArtifactKind.js';
11
11
  import type { ArtifactActivationContext } from '../../../artifacts/ArtifactActivationContext.js';
12
+ import type { ArtifactInvocationContext } from '../../../artifacts/ArtifactInvocationContext.js';
12
13
  import type { ClientArtifactsActivator } from '../../../artifacts/ClientArtifactsActivator.js';
13
14
  import type { IEventStore } from '../../../IEventStore.js';
14
15
  import { eventType } from '../../../events/eventTypeDecorator.js';
@@ -92,10 +93,11 @@ describe('when delivering reducer batches', () => {
92
93
  instances.length = 0;
93
94
  notifications.length = 0;
94
95
  const contexts: ArtifactActivationContext[] = [];
96
+ const invocations: ArtifactInvocationContext[] = [];
95
97
  const steps: string[] = [];
96
98
  const activator: ClientArtifactsActivator = (type, context) => {
97
99
  contexts.push(context);
98
- return { instance: new type(), run: async callback => { steps.push('enter');
100
+ return { instance: new type(), run: async (callback, invocation) => { invocations.push(invocation!); steps.push('enter');
99
101
  try { return await callback(); } finally { steps.push('exit'); } },
100
102
  dispose: () => { steps.push('dispose'); } };
101
103
  };
@@ -108,6 +110,11 @@ describe('when delivering reducer batches', () => {
108
110
  if (contexts[1].delivery === ArtifactDelivery.Events) contexts[1].eventContext.sequenceNumber.should.equal(1n);
109
111
  notifications.should.deep.equal(['begin']);
110
112
  steps.should.deep.equal(['enter', 'exit', 'dispose', 'enter', 'exit', 'enter', 'exit', 'dispose', 'enter', 'exit', 'dispose']);
113
+ invocations[0].should.deep.equal({ delivery: ArtifactDelivery.ReplayNotification, replayState: ReplayState.BeginReplay });
114
+ invocations.slice(1).map(invocation => invocation.delivery === ArtifactDelivery.Events ? invocation.eventContext.sequenceNumber : 0n)
115
+ .should.deep.equal([1n, 2n, 3n]);
116
+ invocations.slice(1).map(invocation => invocation.delivery === ArtifactDelivery.Events ? invocation.methodName : '')
117
+ .should.deep.equal(['reductionEvent', 'reductionEvent', 'reductionEvent']);
111
118
  result.acknowledgements[0]?.ReadModelState.should.equal('{"count":3}');
112
119
  });
113
120
 
@@ -166,6 +173,65 @@ describe('when delivering reducer batches', () => {
166
173
  }
167
174
  });
168
175
 
176
+ it('should fail a replay notification on completion failure without processing events or publishing state', async () => {
177
+ notifications.length = 0;
178
+ const steps: string[] = [];
179
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
180
+ complete: () => { steps.push('complete'); throw new Error('replay cleanup'); },
181
+ dispose: () => { steps.push('dispose'); } });
182
+ const result = await observe(CountingReducer, [batch([event(1, 1n)], ReplayState.BeginReplay)], activator);
183
+ notifications.should.deep.equal(['begin']);
184
+ steps.should.deep.equal(['complete', 'dispose']);
185
+ result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
186
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['ArtifactCompletionFailed: Artifact completion failed: Error: replay cleanup']);
187
+ result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(4294967295n);
188
+ result.acknowledgements[0]?.ReadModelState.should.equal('');
189
+ });
190
+
191
+ it('should complete once per successful lease before publishing reduced state and then dispose', async () => {
192
+ const steps: string[] = [];
193
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
194
+ complete: () => { steps.push('complete'); }, dispose: () => { steps.push('dispose'); } });
195
+ const result = await observe(CountingReducer, [batch([event(1, 1n), event(2, 2n)])], activator);
196
+ steps.should.deep.equal(['complete', 'dispose']);
197
+ result.acknowledgements[0]?.ReadModelState.should.equal('{"count":3}');
198
+ });
199
+
200
+ it('should complete after handler failure and preserve the processing error', async () => {
201
+ const steps: string[] = [];
202
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
203
+ complete: () => { steps.push('complete'); }, dispose: () => { steps.push('dispose'); } });
204
+ const result = await observe(FailingReducer, [batch([event(1, 1n)])], activator);
205
+ steps.should.deep.equal(['complete', 'dispose']);
206
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['Error: reduction failed']);
207
+ });
208
+
209
+ it('should fail completion distinctly, discard the tentative state and checkpoint, then dispose', async () => {
210
+ const steps: string[] = [];
211
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
212
+ complete: async () => { steps.push('complete'); throw new Error('scope cleanup'); },
213
+ dispose: () => { steps.push('dispose'); } });
214
+ const result = await observe(CountingReducer, [batch([event(1, 1n), event(2, 2n)])], activator);
215
+ steps.should.deep.equal(['complete', 'dispose']);
216
+ result.acknowledgements[0]?.State.should.equal(ObservationState.Failed);
217
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal(['ArtifactCompletionFailed: Artifact completion failed: Error: scope cleanup']);
218
+ result.acknowledgements[0]?.LastSuccessfulObservation.should.equal(4294967295n);
219
+ result.acknowledgements[0]?.ReadModelState.should.equal('');
220
+ });
221
+
222
+ it('should preserve both failures when completion also fails after processing', async () => {
223
+ const steps: string[] = [];
224
+ const activator: ClientArtifactsActivator = type => ({ instance: new type(),
225
+ complete: () => { steps.push('complete'); throw new Error('scope cleanup'); },
226
+ dispose: () => { steps.push('dispose'); } });
227
+ const result = await observe(FailingReducer, [batch([event(1, 1n)])], activator);
228
+ steps.should.deep.equal(['complete', 'dispose']);
229
+ result.acknowledgements[0]?.ExceptionMessages.should.deep.equal([
230
+ 'Error: reduction failed', 'ArtifactCompletionFailed: Artifact completion failed: Error: scope cleanup'
231
+ ]);
232
+ result.acknowledgements[0]?.ReadModelState.should.equal('');
233
+ });
234
+
169
235
  it('should dispose after handler failure without acknowledging the failed event', async () => {
170
236
  let disposed = 0;
171
237
  const activator: ClientArtifactsActivator = type => ({ instance: new type(), dispose: () => { disposed++; } });
@@ -0,0 +1,213 @@
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 { AutoMap, type ProjectionDefinition } from '@cratis/chronicle.contracts';
5
+ import { EventSequenceId } from '../../eventSequences/EventSequenceId.js';
6
+ import type { CompiledProjectionDefinitions } from '../../projections/CompiledProjectionDefinitions.js';
7
+ import { eventContractPath } from '../../projections/eventContractPath.js';
8
+ import { eventContextPropertyExpression } from '../../projections/eventContextPropertyExpression.js';
9
+ import { InvalidEventContextPropertyError } from '../../projections/InvalidEventContextPropertyError.js';
10
+ import type { ContractEventType, FromRecord, RemovedWithRecord } from '../../projections/declarative/ProjectionBuilderCore.js';
11
+ import { getEventTypeMapKey } from '../../projections/modelBound/childrenAndNestedBuilder.js';
12
+ import type { JsonSchema } from '../../schemas/JsonSchema.js';
13
+ import { UnsupportedProjectionOperation } from './UnsupportedProjectionOperation.js';
14
+
15
+ /** Validates *all* subscribed operations against the bounded scenario subset, before replay. */
16
+ export class ProjectionCapabilities {
17
+ /**
18
+ * Throws with the original declaration and contract path for the first unsupported operation.
19
+ * @param compiled - Definitions and their schema/provenance evidence.
20
+ * @param definition - The compiled contract to validate.
21
+ */
22
+ static validate(compiled: CompiledProjectionDefinitions, definition: ProjectionDefinition): void {
23
+ const model = String(definition.ReadModel ?? definition.Identifier ?? '<unknown>');
24
+ const wire = definition as unknown as Record<string, unknown>;
25
+ const reject = (path: string, reason: string, fallback?: string) : never => {
26
+ const source = compiled.provenance.get(definition)?.find(entry => entry.contractPath.startsWith('From['))?.declaration ?? '.from';
27
+ const declaration = compiled.provenance.get(definition)?.find(entry => entry.contractPath === path)?.declaration
28
+ ?? fallback ?? (path.includes('.AutoMap.') ? `${source} (AutoMap)`
29
+ : path.startsWith('InitialModelState') ? '.withInitialValues'
30
+ : path.startsWith('ReadModel') ? source : 'contract');
31
+ throw new UnsupportedProjectionOperation(model, path, declaration, reason);
32
+ };
33
+ if (!compiled.definitions.includes(definition)) reject('Definition', 'definition is not part of this compile');
34
+ const provenance = compiled.provenance.get(definition) ?? reject('Definition', 'capability provenance is unavailable');
35
+ const catalog = compiled.eventSchemas.get(definition) ?? reject('Definition', 'event-schema catalog is unavailable');
36
+ const variant = provenance.find(entry => entry.contractPath === 'Variant');
37
+ if (variant) reject('Variant', 'variants require a kernel-backed test');
38
+ if (wire.IsActive === false) reject('IsActive', 'passive projections require a kernel-backed test');
39
+ if (wire.EventSequenceId !== EventSequenceId.eventLog.value) reject('EventSequenceId', 'non-default event sequences require a kernel-backed test');
40
+ for (const section of ['Join', 'Children', 'Nested', 'RemovedWithJoin'] as const) {
41
+ const value = wire[section];
42
+ const paths = Array.isArray(value) ? value.map((entry: { Key: ContractEventType }) => eventContractPath(section, entry.Key))
43
+ : Object.keys(value as Record<string, unknown> ?? {}).map(property => `${section}.${property}`);
44
+ if (paths.length) reject(paths[0], `${section === 'Children' ? 'children projections' : section === 'Nested' ? 'nested projections' : section === 'Join' ? 'joins' : 'removedWithJoin'} require a kernel-backed test`);
45
+ }
46
+ if ((wire.FromEvery as unknown[] | undefined)?.length) reject('FromEvery', 'derivative projections require a kernel-backed test');
47
+ if (wire.FromEventProperty) reject('FromEventProperty', 'event-property subscriptions require a kernel-backed test');
48
+ const all = wire.All as { Properties?: Record<string, string>; IncludeChildren?: boolean; AutoMap?: AutoMap } | undefined;
49
+ if (provenance.some(entry => entry.contractPath === 'All') || all?.IncludeChildren ||
50
+ Object.keys(all?.Properties ?? {}).length || all?.AutoMap === AutoMap.Enabled) {
51
+ reject('All', 'fromEvery/all subscriptions require a kernel-backed test');
52
+ }
53
+
54
+ const readModel = compiled.readModels.find(candidate => candidate.Type.Identifier === model)
55
+ ?? reject('ReadModel', 'a single registered read-model schema is required');
56
+ let schema: JsonSchema;
57
+ try {
58
+ schema = JSON.parse(readModel.Schema) as JsonSchema;
59
+ } catch {
60
+ reject('ReadModel.Schema', 'read-model schema is not valid JSON');
61
+ }
62
+ const identifier = schema!.properties?.id ?? schema!.properties?.Id ?? { type: 'string' };
63
+ if (!((identifier.type === 'string' && (!identifier.format || identifier.format === 'guid')) ||
64
+ (identifier.type === 'number' && (!identifier.format || identifier.format === 'double')) ||
65
+ (identifier.type === 'integer' && ['int32', 'uint32'].includes(identifier.format ?? '')))) {
66
+ reject('ReadModel.Schema.id', 'identifier schema must be string, number, or GUID (int32/uint32 identifiers are supported)');
67
+ }
68
+ let initial: unknown;
69
+ try { initial = JSON.parse(String(wire.InitialModelState ?? '{}')); } catch { reject('InitialModelState', 'initial values must be a JSON object'); }
70
+ if (initial === null || typeof initial !== 'object' || Array.isArray(initial)) reject('InitialModelState', 'initial values must be a JSON object');
71
+ for (const [property, value] of Object.entries(initial as Record<string, unknown>)) {
72
+ const target = schema!.properties?.[property]
73
+ ?? reject(`InitialModelState.${property}`, 'initial value has no read-model schema');
74
+ this.checkSchema(target, `InitialModelState.${property}`, reject);
75
+ if (typeof value === 'number') {
76
+ const outsideIntegerRange = !Number.isSafeInteger(value) ||
77
+ (target.format === 'int32' && (value < -2147483648 || value > 2147483647)) ||
78
+ (target.format === 'uint32' && (value < 0 || value > 4294967295));
79
+ if (!Number.isFinite(value) || (target.type === 'integer' && outsideIntegerRange)) {
80
+ reject(`InitialModelState.${property}`, 'initial numeric value is outside the supported finite/integer range');
81
+ }
82
+ }
83
+ }
84
+ const generations = new Map<string, number>();
85
+ const requireEventSchema = (eventType: ContractEventType, path: string): JsonSchema => {
86
+ const previous = generations.get(eventType.Id);
87
+ if (previous !== undefined && previous !== eventType.Generation) reject(path, 'multiple generations of the same event-type id require a kernel-backed test');
88
+ generations.set(eventType.Id, eventType.Generation);
89
+ const event = catalog.get(getEventTypeMapKey(eventType)) ?? reject(path, 'participating event schema is unavailable');
90
+ return event.schema;
91
+ };
92
+ const from = wire.From as FromRecord[] ?? [];
93
+ const removedWith = wire.RemovedWith as RemovedWithRecord[] ?? [];
94
+ for (const entry of [...from, ...removedWith]) {
95
+ const section = from.includes(entry as FromRecord) ? 'From' : 'RemovedWith';
96
+ const path = eventContractPath(section, entry.Key);
97
+ const eventSchema = requireEventSchema(entry.Key, path);
98
+ if (entry.Value.Key !== '$eventSourceId') reject(`${path}.Key`, 'only $eventSourceId keys are supported');
99
+ if (entry.Value.ParentKey) reject(`${path}.ParentKey`, 'parent keys require a kernel-backed test');
100
+ if (section === 'RemovedWith') continue;
101
+ const properties = (entry as FromRecord).Value.Properties ?? {};
102
+ for (const [property, expression] of Object.entries(properties)) {
103
+ const mappingPath = `${path}.Properties.${property}`;
104
+ this.checkMapping(schema!, eventSchema, property, expression, mappingPath, reject);
105
+ }
106
+ const expressions = Object.values(properties);
107
+ const aggregateOnly = expressions.length > 0 && expressions.every(expression =>
108
+ /^(?:\$add\([^()]+\)|\$subtract\([^()]+\)|\$count|\$increment|\$decrement)$/.test(expression));
109
+ if (wire.AutoMap === AutoMap.Enabled && !aggregateOnly) {
110
+ this.checkAutoMap(schema!, eventSchema, properties, wire.NoAutoMapProperties as string[] ?? [], path, reject);
111
+ }
112
+ }
113
+ }
114
+
115
+ private static checkAutoMap(
116
+ modelSchema: JsonSchema, eventSchema: JsonSchema, explicit: Record<string, string>, exclusions: string[],
117
+ path: string, reject: (path: string, reason: string) => never
118
+ ): void {
119
+ for (const [destination] of Object.entries(modelSchema.properties ?? {})) {
120
+ if (destination in explicit || exclusions.includes(destination)) continue;
121
+ const candidates = Object.keys(eventSchema.properties ?? {}).filter(source => source.toLowerCase() === destination.toLowerCase());
122
+ if (candidates.length > 1) reject(`${path}.AutoMap.${destination}`, `inferred AutoMap source is ambiguous: ${candidates.join(', ')}`);
123
+ if (candidates.length) this.checkMapping(modelSchema, eventSchema, destination, candidates[0], `${path}.AutoMap.${destination}`, reject);
124
+ }
125
+ }
126
+
127
+ private static checkMapping(
128
+ modelSchema: JsonSchema, eventSchema: JsonSchema, destination: string, expression: string,
129
+ path: string, reject: (path: string, reason: string) => never
130
+ ): void {
131
+ const fail = (reason: string): never => reject(path, `expression '${expression}': ${reason}`);
132
+ const target = modelSchema.properties?.[destination]
133
+ ?? fail('dynamic or unknown destination paths require a kernel-backed test');
134
+ if (destination.includes('.') || destination.includes('$')) fail('dynamic or unknown destination paths require a kernel-backed test');
135
+ this.checkSchema(target, path, (_path, reason) => fail(reason));
136
+ if (expression === '$eventSourceId' || expression === '$null') return;
137
+ if (expression.startsWith('$eventContext(')) {
138
+ if (/^\$eventContext\(.+\(\)\)$/.test(expression)) fail('derived event-context functions require a kernel-backed test');
139
+ const path = /^\$eventContext\(([^()]*)\)$/.exec(expression)?.[1];
140
+ if (path) {
141
+ try {
142
+ if (eventContextPropertyExpression(path) === expression) return;
143
+ } catch (error) {
144
+ if (!(error instanceof InvalidEventContextPropertyError)) throw error;
145
+ }
146
+ }
147
+ fail('event-context path is not supported by the client');
148
+ }
149
+ if (/^\$value\([\p{L}\p{Mn}\p{Nd}\p{Pc} ._/:*+-]*\)$/u.test(expression)) {
150
+ const text = expression.slice(7, -1);
151
+ if (target.format === 'date-time') fail('$value date-time literals require a kernel-backed test');
152
+ if (this.numeric(target)) {
153
+ if (!/^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$/.test(text) || !Number.isFinite(Number(text)) ||
154
+ (target.type === 'integer' && (!Number.isSafeInteger(Number(text)) ||
155
+ (target.format === 'int32' && (Number(text) < -2147483648 || Number(text) > 2147483647)) ||
156
+ (target.format === 'uint32' && (Number(text) < 0 || Number(text) > 4294967295))))) {
157
+ fail('$value numeric literal is outside the supported finite/integer range');
158
+ }
159
+ } else if (target.type === 'boolean' && !/^(?:true|false)$/i.test(text)) {
160
+ fail('$value boolean literal must be true or false');
161
+ } else if (target.format === 'guid' && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(text)) {
162
+ fail('$value GUID literal is not canonical');
163
+ } else if (target.type === 'object' || target.type === 'array') {
164
+ fail('$value object/array literals require a kernel-backed test');
165
+ }
166
+ return;
167
+ }
168
+ const operation = /^(\$add|\$subtract)\(([^()]+)\)$/.exec(expression);
169
+ const arithmetic = operation || ['$count', '$increment', '$decrement'].includes(expression);
170
+ if (arithmetic) {
171
+ if (!this.numeric(target)) fail('arithmetic requires a supported finite number/double or int32/uint32 schema (not float, decimal, duration, or int64)');
172
+ if (operation) {
173
+ const operand = this.propertyAt(eventSchema, operation[2]);
174
+ if (!operand || !this.numeric(operand)) fail('arithmetic operand requires a supported numeric event schema');
175
+ }
176
+ return;
177
+ }
178
+ if (/^[A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)*$/.test(expression)) {
179
+ const source = this.propertyAt(eventSchema, expression);
180
+ if (!source) return fail(`event property '${expression}' is absent from the participating event schema`);
181
+ this.checkSchema(source, path, (_path, reason) => fail(reason));
182
+ return;
183
+ }
184
+ if (expression.startsWith('$context.')) {
185
+ return fail('is a legacy expression the kernel does not resolve; event-context mappings use $eventContext(...)');
186
+ }
187
+ fail('requires a kernel-backed test');
188
+ }
189
+
190
+ private static propertyAt(schema: JsonSchema, path: string): JsonSchema | undefined {
191
+ return path.split('.').reduce<JsonSchema | undefined>((current, segment) => current?.properties?.[segment], schema);
192
+ }
193
+
194
+ private static numeric(schema: JsonSchema): boolean {
195
+ return (schema.type === 'number' && (!schema.format || schema.format === 'double')) ||
196
+ (schema.type === 'integer' && ['int32', 'uint32'].includes(schema.format ?? ''));
197
+ }
198
+
199
+ private static checkSchema(schema: JsonSchema, path: string, reject: (path: string, reason: string) => never): void {
200
+ if (!schema.type || schema.type === 'null' || (schema.format && !['guid', 'date-time', 'double', 'int32', 'uint32'].includes(schema.format)) ||
201
+ (schema.format === 'date-time' && schema.type !== 'string') ||
202
+ (schema.format === 'guid' && schema.type !== 'string') ||
203
+ (schema.format === 'double' && schema.type !== 'number') ||
204
+ (['int32', 'uint32'].includes(schema.format ?? '') && schema.type !== 'integer')) {
205
+ reject(path, `schema type/format '${schema.type ?? 'unknown'}${schema.format ? `/${schema.format}` : ''}' requires a kernel-backed test`);
206
+ }
207
+ if (schema.type === 'integer' && !this.numeric(schema)) reject(path, 'integer width requires a kernel-backed test');
208
+ if (schema.type === 'array' && schema.items) this.checkSchema(schema.items, path, reject);
209
+ if (schema.type === 'object') {
210
+ for (const property of Object.values(schema.properties ?? {})) this.checkSchema(property, path, reject);
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,16 @@
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
+ /** A compiled projection uses a kernel feature outside the infrastructure-free scenario boundary. */
5
+ export class UnsupportedProjectionOperation extends Error {
6
+ /**
7
+ * @param model - The read model being evaluated.
8
+ * @param contractPath - Location of the unsupported operation in the compiled contract.
9
+ * @param declaration - Source declaration responsible for the operation.
10
+ * @param reason - Why an infrastructure-free evaluation cannot represent it.
11
+ */
12
+ constructor(model: string, contractPath: string, declaration: string, reason: string) {
13
+ super(`UnsupportedProjectionOperation: ${model}, ${contractPath} (${declaration}): ${reason}`);
14
+ this.name = 'UnsupportedProjectionOperation';
15
+ }
16
+ }
@@ -0,0 +1,12 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { field } from '@cratis/fundamentals';
5
+ import { eventType } from '../../../../events/eventTypeDecorator.js';
6
+
7
+ export class Changed { name!: string; quantity!: number; labels!: string[]; details!: object; }
8
+ field(String)(Changed.prototype, 'name');
9
+ field(Number)(Changed.prototype, 'quantity');
10
+ field(Array)(Changed.prototype, 'labels');
11
+ field(Object)(Changed.prototype, 'details');
12
+ eventType('capability-changed')(Changed);
@@ -0,0 +1,13 @@
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 { field } from '@cratis/fundamentals';
5
+
6
+ export class Model { id!: string; name!: string; quantity!: number; total!: number; state!: string; labels!: string[]; details!: object; }
7
+ field(String)(Model.prototype, 'id');
8
+ field(String)(Model.prototype, 'name');
9
+ field(Number)(Model.prototype, 'quantity');
10
+ field(Number)(Model.prototype, 'total');
11
+ field(String)(Model.prototype, 'state');
12
+ field(Array)(Model.prototype, 'labels');
13
+ field(Object)(Model.prototype, 'details');
@@ -0,0 +1,7 @@
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 { eventType } from '../../../../events/eventTypeDecorator.js';
5
+
6
+ export class Removed {}
7
+ eventType('capability-removed')(Removed);
@@ -0,0 +1,39 @@
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 { Constructor } from '@cratis/fundamentals';
5
+ import type { IClientArtifactsProvider } from '../../../../artifacts/index.js';
6
+ import { IProjectionBuilderFor } from '../../../../projections/declarative/IProjectionBuilderFor.js';
7
+ import { IProjectionFor } from '../../../../projections/declarative/IProjectionFor.js';
8
+ import { projection } from '../../../../projections/declarative/projection.js';
9
+ import { fromEvent } from '../../../../projections/modelBound/fromEvent.js';
10
+ import { ProjectionDefinitionCompiler } from '../../../../projections/ProjectionDefinitionCompiler.js';
11
+ import { Changed } from './Changed.js';
12
+ import { Model } from './Model.js';
13
+ import { Removed } from './Removed.js';
14
+
15
+ function artifacts(model: Constructor, projections: Constructor[], eventTypes: Constructor[]): IClientArtifactsProvider {
16
+ return {
17
+ readModels: [model], projections, eventTypes: [Changed, Removed, ...eventTypes],
18
+ globalForHandlers: [], reducers: [], reactors: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
19
+ };
20
+ }
21
+
22
+ export function compileModelBound(configure: (model: typeof Model) => void = () => {}, eventTypes: Constructor[] = []) {
23
+ class Candidate extends Model {}
24
+ fromEvent(Changed)(Candidate);
25
+ configure(Candidate);
26
+ const compiled = new ProjectionDefinitionCompiler(artifacts(Candidate, [], eventTypes), 'test-sink').compile([], [Candidate]);
27
+ return { compiled, definition: compiled.definitions[0] };
28
+ }
29
+
30
+ export function compileDeclarative(define: (builder: IProjectionBuilderFor<Model>) => void, eventTypes: Constructor[] = []) {
31
+ class Candidate extends Model {}
32
+ class CandidateProjection implements IProjectionFor<Model> {
33
+ define(builder: IProjectionBuilderFor<Model>): void { define(builder); }
34
+ }
35
+ projection('capability-projection', Candidate)(CandidateProjection);
36
+ const compiled = new ProjectionDefinitionCompiler(artifacts(Candidate, [CandidateProjection], eventTypes), 'test-sink')
37
+ .compile([CandidateProjection], []);
38
+ return { compiled, definition: compiled.definitions[0] };
39
+ }