@cratis/chronicle 6.16.0 → 6.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/artifacts/ActivatedArtifact.ts +5 -1
- package/artifacts/ArtifactCompletionFailed.ts +20 -0
- package/artifacts/ArtifactInvocationContext.ts +21 -0
- package/artifacts/for_withActivatedArtifact/when_completing/after_processing_failure.spec.ts +34 -0
- package/artifacts/index.ts +2 -0
- package/artifacts/withActivatedArtifact.ts +20 -3
- package/dist/artifacts/ActivatedArtifact.d.ts +4 -1
- package/dist/artifacts/ActivatedArtifact.d.ts.map +1 -1
- package/dist/artifacts/ArtifactCompletionFailed.d.ts +9 -0
- package/dist/artifacts/ArtifactCompletionFailed.d.ts.map +1 -0
- package/dist/artifacts/ArtifactCompletionFailed.js +19 -0
- package/dist/artifacts/ArtifactCompletionFailed.js.map +1 -0
- package/dist/artifacts/ArtifactInvocationContext.d.ts +18 -0
- package/dist/artifacts/ArtifactInvocationContext.d.ts.map +1 -0
- package/dist/artifacts/ArtifactInvocationContext.js +4 -0
- package/dist/artifacts/ArtifactInvocationContext.js.map +1 -0
- package/dist/artifacts/index.d.ts +2 -0
- package/dist/artifacts/index.d.ts.map +1 -1
- package/dist/artifacts/index.js +1 -0
- package/dist/artifacts/index.js.map +1 -1
- package/dist/artifacts/withActivatedArtifact.d.ts +2 -1
- package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -1
- package/dist/artifacts/withActivatedArtifact.js +22 -3
- package/dist/artifacts/withActivatedArtifact.js.map +1 -1
- package/dist/projections/CompiledProjectionDefinitions.d.ts +6 -0
- package/dist/projections/CompiledProjectionDefinitions.d.ts.map +1 -1
- package/dist/projections/InvalidEventContextPropertyError.d.ts +10 -0
- package/dist/projections/InvalidEventContextPropertyError.d.ts.map +1 -0
- package/dist/projections/InvalidEventContextPropertyError.js +16 -0
- package/dist/projections/InvalidEventContextPropertyError.js.map +1 -0
- package/dist/projections/ProjectionCapabilityProvenance.d.ts +8 -0
- package/dist/projections/ProjectionCapabilityProvenance.d.ts.map +1 -0
- package/dist/projections/ProjectionCapabilityProvenance.js +4 -0
- package/dist/projections/ProjectionCapabilityProvenance.js.map +1 -0
- package/dist/projections/ProjectionDefinitionCompiler.d.ts +2 -0
- package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -1
- package/dist/projections/ProjectionDefinitionCompiler.js +148 -7
- package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -1
- package/dist/projections/ProjectionEventSchema.d.ts +10 -0
- package/dist/projections/ProjectionEventSchema.d.ts.map +1 -0
- package/dist/projections/ProjectionEventSchema.js +4 -0
- package/dist/projections/ProjectionEventSchema.js.map +1 -0
- package/dist/projections/VariantReclassifier.d.ts +2 -0
- package/dist/projections/VariantReclassifier.d.ts.map +1 -1
- package/dist/projections/VariantReclassifier.js.map +1 -1
- package/dist/projections/captureProjectionProvenance.d.ts +4 -0
- package/dist/projections/captureProjectionProvenance.d.ts.map +1 -0
- package/dist/projections/captureProjectionProvenance.js +56 -0
- package/dist/projections/captureProjectionProvenance.js.map +1 -0
- package/dist/projections/declarative/AllSetBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/AllSetBuilder.js +2 -1
- package/dist/projections/declarative/AllSetBuilder.js.map +1 -1
- package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/FromBuilder.js +12 -2
- package/dist/projections/declarative/FromBuilder.js.map +1 -1
- package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/JoinBuilder.js +2 -1
- package/dist/projections/declarative/JoinBuilder.js.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderCore.js +8 -0
- package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
- package/dist/projections/declarative/RemovedWithBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/RemovedWithBuilder.js +2 -1
- package/dist/projections/declarative/RemovedWithBuilder.js.map +1 -1
- package/dist/projections/declarative/SetBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/SetBuilder.js +2 -1
- package/dist/projections/declarative/SetBuilder.js.map +1 -1
- package/dist/projections/declarative/projectionBuilderProvenance.d.ts +16 -0
- package/dist/projections/declarative/projectionBuilderProvenance.d.ts.map +1 -0
- package/dist/projections/declarative/projectionBuilderProvenance.js +28 -0
- package/dist/projections/declarative/projectionBuilderProvenance.js.map +1 -0
- package/dist/projections/eventContextPropertyExpression.d.ts +3 -0
- package/dist/projections/eventContextPropertyExpression.d.ts.map +1 -0
- package/dist/projections/eventContextPropertyExpression.js +69 -0
- package/dist/projections/eventContextPropertyExpression.js.map +1 -0
- package/dist/projections/eventContractPath.d.ts +4 -0
- package/dist/projections/eventContractPath.d.ts.map +1 -0
- package/dist/projections/eventContractPath.js +7 -0
- package/dist/projections/eventContractPath.js.map +1 -0
- package/dist/projections/index.d.ts +1 -0
- package/dist/projections/index.d.ts.map +1 -1
- package/dist/projections/index.js +1 -0
- package/dist/projections/index.js.map +1 -1
- package/dist/projections/invalidEventContextPropertyInProjection.d.ts +4 -0
- package/dist/projections/invalidEventContextPropertyInProjection.d.ts.map +1 -0
- package/dist/projections/invalidEventContextPropertyInProjection.js +7 -0
- package/dist/projections/invalidEventContextPropertyInProjection.js.map +1 -0
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.js +2 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +12 -3
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/readModels/rootReadModelTypes.d.ts.map +1 -1
- package/dist/readModels/rootReadModelTypes.js +12 -1
- package/dist/readModels/rootReadModelTypes.js.map +1 -1
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +12 -3
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/testing/projections/ProjectionCapabilities.d.ts +17 -0
- package/dist/testing/projections/ProjectionCapabilities.d.ts.map +1 -0
- package/dist/testing/projections/ProjectionCapabilities.js +233 -0
- package/dist/testing/projections/ProjectionCapabilities.js.map +1 -0
- package/dist/testing/projections/UnsupportedProjectionOperation.d.ts +11 -0
- package/dist/testing/projections/UnsupportedProjectionOperation.d.ts.map +1 -0
- package/dist/testing/projections/UnsupportedProjectionOperation.js +16 -0
- package/dist/testing/projections/UnsupportedProjectionOperation.js.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.d.ts +7 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.d.ts.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.js +16 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.js.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Model.d.ts +10 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Model.d.ts.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Model.js +20 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Model.js.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.d.ts +3 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.d.ts.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.js +7 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.js.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/compile.d.ts +12 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/compile.d.ts.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/compile.js +34 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/compile.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/projections/CompiledProjectionDefinitions.ts +6 -0
- package/projections/InvalidEventContextPropertyError.ts +14 -0
- package/projections/ProjectionCapabilityProvenance.ts +10 -0
- package/projections/ProjectionDefinitionCompiler.ts +146 -7
- package/projections/ProjectionEventSchema.ts +13 -0
- package/projections/VariantReclassifier.ts +2 -0
- package/projections/captureProjectionProvenance.ts +50 -0
- package/projections/declarative/AllSetBuilder.ts +2 -1
- package/projections/declarative/CompositeKeyBuilder.spec.ts +1 -1
- package/projections/declarative/FromBuilder.ts +12 -2
- package/projections/declarative/JoinBuilder.ts +2 -1
- package/projections/declarative/ProjectionBuilderCore.ts +7 -0
- package/projections/declarative/ProjectionBuilderFor.spec.ts +4 -4
- package/projections/declarative/RemovedWithBuilder.ts +2 -1
- package/projections/declarative/SetBuilder.ts +2 -1
- package/projections/declarative/projectionBuilderProvenance.ts +40 -0
- package/projections/eventContextExpressions.spec.ts +273 -0
- package/projections/eventContextPropertyExpression.ts +72 -0
- package/projections/eventContractPath.ts +9 -0
- package/projections/index.ts +1 -0
- package/projections/invalidEventContextPropertyInProjection.ts +13 -0
- package/projections/modelBound/childrenAndNestedBuilder.ts +2 -1
- package/reactors/Reactors.ts +13 -3
- package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +93 -2
- package/readModels/rootReadModelTypes.ts +11 -1
- package/reducers/Reducers.ts +14 -3
- package/reducers/for_Reducers/when_delivering/with_activation.spec.ts +67 -1
- package/testing/projections/ProjectionCapabilities.ts +213 -0
- package/testing/projections/UnsupportedProjectionOperation.ts +16 -0
- package/testing/projections/for_ProjectionCapabilities/given/Changed.ts +12 -0
- package/testing/projections/for_ProjectionCapabilities/given/Model.ts +13 -0
- package/testing/projections/for_ProjectionCapabilities/given/Removed.ts +7 -0
- package/testing/projections/for_ProjectionCapabilities/given/compile.ts +39 -0
- package/testing/projections/for_ProjectionCapabilities/when_compiling_capability_evidence.spec.ts +68 -0
- package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +230 -0
- package/testing/projections/for_ProjectionCapabilities/when_validating_numeric_schemas.spec.ts +73 -0
- package/testing/projections/for_ProjectionCapabilities/when_validating_schema_boundaries.spec.ts +118 -0
- package/testing/projections/for_ProjectionCapabilities/when_validating_supported_operations.spec.ts +103 -0
package/reducers/Reducers.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/testing/projections/for_ProjectionCapabilities/when_compiling_capability_evidence.spec.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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 { chai, describe, it } from 'vitest';
|
|
6
|
+
import { childrenFrom } from '../../../projections/modelBound/childrenFrom.js';
|
|
7
|
+
import { entersOn } from '../../../projections/modelBound/entersOn.js';
|
|
8
|
+
import { fromEvent } from '../../../projections/modelBound/fromEvent.js';
|
|
9
|
+
import { variantOf } from '../../../projections/modelBound/variantOf.js';
|
|
10
|
+
import { getEventTypeJsonSchemaFor } from '../../../events/eventTypeDecorator.js';
|
|
11
|
+
import { ProjectionDefinitionCompiler } from '../../../projections/ProjectionDefinitionCompiler.js';
|
|
12
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
13
|
+
import { Changed } from './given/Changed.js';
|
|
14
|
+
import { Removed } from './given/Removed.js';
|
|
15
|
+
import { Model } from './given/Model.js';
|
|
16
|
+
import { compileDeclarative, compileModelBound } from './given/compile.js';
|
|
17
|
+
|
|
18
|
+
chai.should();
|
|
19
|
+
|
|
20
|
+
describe('when compiling capability evidence', () => {
|
|
21
|
+
it('should retain model-bound variant declarations before joins and sibling removal obscure them', () => {
|
|
22
|
+
class Identity {}
|
|
23
|
+
class First extends Model {}
|
|
24
|
+
class Second extends Model {}
|
|
25
|
+
fromEvent(Changed)(First);
|
|
26
|
+
fromEvent(Removed)(Second);
|
|
27
|
+
variantOf(Identity, 'id')(First);
|
|
28
|
+
variantOf(Identity, 'id')(Second);
|
|
29
|
+
entersOn(Changed)(First);
|
|
30
|
+
entersOn(Removed)(Second);
|
|
31
|
+
const artifacts: IClientArtifactsProvider = {
|
|
32
|
+
readModels: [First, Second], projections: [], eventTypes: [Changed, Removed], globalForHandlers: [],
|
|
33
|
+
reactors: [], reducers: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
|
|
34
|
+
};
|
|
35
|
+
const compiled = new ProjectionDefinitionCompiler(artifacts, 'test-sink').compile([], [First, Second]);
|
|
36
|
+
const first = compiled.definitions[0];
|
|
37
|
+
compiled.provenance.get(first)!.should.deep.include({ contractPath: 'Variant', declaration: '@variantOf' });
|
|
38
|
+
compiled.provenance.get(first)!.should.deep.include({ contractPath: 'EntersOn[capability-changed:1]', declaration: '@entersOn' });
|
|
39
|
+
compiled.eventSchemas.get(first)!.has('capability-removed:1:0').should.be.true;
|
|
40
|
+
(first.RemovedWith?.length ?? 0).should.equal(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should retain declarative variant provenance before update-only joins replace From', () => {
|
|
44
|
+
class Identity {}
|
|
45
|
+
const { compiled, definition } = compileDeclarative(builder => builder
|
|
46
|
+
.variantOf(Identity, model => model.id).entersOn(Changed).from(Changed).from(Removed));
|
|
47
|
+
compiled.provenance.get(definition)!.should.deep.include({ contractPath: 'Variant', declaration: '.variantOf' });
|
|
48
|
+
compiled.provenance.get(definition)!.should.deep.include({ contractPath: 'From[capability-removed:1]', declaration: '.from' });
|
|
49
|
+
(definition.Join?.length ?? 0).should.equal(1);
|
|
50
|
+
compiled.eventSchemas.get(definition)!.has('capability-removed:1:0').should.be.true;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should catalog child events even though no root From entry refers to them', () => {
|
|
54
|
+
const { compiled, definition } = compileModelBound(model => childrenFrom(Removed)(model.prototype, 'name'));
|
|
55
|
+
compiled.eventSchemas.get(definition)!.has('capability-removed:1:0').should.be.true;
|
|
56
|
+
compiled.provenance.get(definition)!.should.deep.include({ contractPath: 'Children.name', declaration: '@childrenFrom' });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should isolate event schemas from later compiles and registration metadata', () => {
|
|
60
|
+
const first = compileModelBound();
|
|
61
|
+
const second = compileModelBound();
|
|
62
|
+
const firstSchema = first.compiled.eventSchemas.get(first.definition)!.get('capability-changed:1:0')!.schema;
|
|
63
|
+
const secondSchema = second.compiled.eventSchemas.get(second.definition)!.get('capability-changed:1:0')!.schema;
|
|
64
|
+
firstSchema.properties!.name.type = 'boolean';
|
|
65
|
+
secondSchema.properties!.name.type.should.equal('string');
|
|
66
|
+
getEventTypeJsonSchemaFor(Changed).properties!.name.type.should.equal('string');
|
|
67
|
+
});
|
|
68
|
+
});
|