@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
|
@@ -0,0 +1,273 @@
|
|
|
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 { Constructor, field } from '@cratis/fundamentals';
|
|
6
|
+
import { chai, describe, expect, it } from 'vitest';
|
|
7
|
+
import type { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
8
|
+
import type { ChronicleConnection } from '../connection/index.js';
|
|
9
|
+
import { InvalidEventContextPropertyError } from '../index.js';
|
|
10
|
+
import { eventType } from '../events/eventTypeDecorator.js';
|
|
11
|
+
import { ProjectionBuilderFor } from './declarative/ProjectionBuilderFor.js';
|
|
12
|
+
import { projection } from './declarative/projection.js';
|
|
13
|
+
import { childrenFrom } from './modelBound/childrenFrom.js';
|
|
14
|
+
import { fromEvent } from './modelBound/fromEvent.js';
|
|
15
|
+
import { fromEvery } from './modelBound/fromEvery.js';
|
|
16
|
+
import { nested } from './modelBound/nested.js';
|
|
17
|
+
import { setFromContext } from './modelBound/setFromContext.js';
|
|
18
|
+
import { ProjectionDefinitionCompiler } from './ProjectionDefinitionCompiler.js';
|
|
19
|
+
import { Projections } from './Projections.js';
|
|
20
|
+
|
|
21
|
+
class Recorded { reference!: string; }
|
|
22
|
+
eventType()(Recorded);
|
|
23
|
+
class Removed {}
|
|
24
|
+
eventType()(Removed);
|
|
25
|
+
class Child { happened!: Date; }
|
|
26
|
+
setFromContext(Recorded, 'occurred')(Child.prototype, 'happened');
|
|
27
|
+
class Model { occurred!: Date; happened!: Date; viaAccessor!: Date; children!: Child[]; detail!: Child; }
|
|
28
|
+
setFromContext(Recorded)(Model.prototype, 'occurred');
|
|
29
|
+
setFromContext(Recorded, 'occurred')(Model.prototype, 'happened');
|
|
30
|
+
setFromContext(Recorded, context => context.occurred)(Model.prototype, 'viaAccessor');
|
|
31
|
+
childrenFrom(Recorded, Child)(Model.prototype, 'children');
|
|
32
|
+
field(Child)(Model.prototype, 'detail');
|
|
33
|
+
nested(Model.prototype, 'detail');
|
|
34
|
+
fromEvery(undefined, 'sequenceNumber')(Model.prototype, 'happened');
|
|
35
|
+
fromEvent(Recorded)(Model);
|
|
36
|
+
|
|
37
|
+
interface FromRecord { Value: { Properties: Record<string, string>; Key: string; ParentKey: string } }
|
|
38
|
+
interface JoinRecord { Value: { Properties: Record<string, string>; Key: string } }
|
|
39
|
+
interface Definition {
|
|
40
|
+
From: FromRecord[];
|
|
41
|
+
Join: JoinRecord[];
|
|
42
|
+
All: { Properties: Record<string, string> };
|
|
43
|
+
RemovedWith: Array<{ Value: { Key: string } }>;
|
|
44
|
+
Children: Record<string, { From: FromRecord[] }>;
|
|
45
|
+
Nested: Record<string, { From: FromRecord[] }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function declarative(configure: (builder: ProjectionBuilderFor<Model>) => void): Definition {
|
|
49
|
+
const builder = new ProjectionBuilderFor<Model>();
|
|
50
|
+
configure(builder);
|
|
51
|
+
return builder.build('context-spec', 'Model') as unknown as Definition;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function modelBound(readModel: Constructor = Model): Definition {
|
|
55
|
+
const artifacts: IClientArtifactsProvider = {
|
|
56
|
+
projections: [], readModels: [readModel], globalForHandlers: [], eventTypes: [Recorded],
|
|
57
|
+
reactors: [], reducers: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
|
|
58
|
+
};
|
|
59
|
+
return new ProjectionDefinitionCompiler(artifacts, 'test-sink').compile([], [readModel]).definitions[0] as unknown as Definition;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function compileDeclarative(configure: (builder: ProjectionBuilderFor<Model>) => void): void {
|
|
63
|
+
class InvalidDeclarativeProjection {
|
|
64
|
+
define(builder: ProjectionBuilderFor<Model>): void { configure(builder); }
|
|
65
|
+
}
|
|
66
|
+
projection('invalid-context', Model)(InvalidDeclarativeProjection);
|
|
67
|
+
const artifacts: IClientArtifactsProvider = {
|
|
68
|
+
projections: [InvalidDeclarativeProjection], readModels: [Model], globalForHandlers: [], eventTypes: [Recorded],
|
|
69
|
+
reactors: [], reducers: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
|
|
70
|
+
};
|
|
71
|
+
new ProjectionDefinitionCompiler(artifacts, 'test-sink').compile([InvalidDeclarativeProjection], []);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function register(projections: Constructor[], readModels: Constructor[]): Promise<void> {
|
|
75
|
+
const artifacts: IClientArtifactsProvider = {
|
|
76
|
+
projections, readModels, globalForHandlers: [], eventTypes: [Recorded],
|
|
77
|
+
reactors: [], reducers: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
|
|
78
|
+
};
|
|
79
|
+
return new Projections('test-store', 'test-namespace', {} as ChronicleConnection, artifacts, 'test-sink').register();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
chai.should();
|
|
83
|
+
|
|
84
|
+
describe('event context expressions', () => {
|
|
85
|
+
it('should map a model-bound implicit context property to the kernel expression', () => {
|
|
86
|
+
modelBound().From[0].Value.Properties.occurred.should.equal('$eventContext(Occurred)');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should map a model-bound explicit context property to the kernel expression', () => {
|
|
90
|
+
modelBound().From[0].Value.Properties.happened.should.equal('$eventContext(Occurred)');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should map a model-bound context accessor to the kernel expression', () => {
|
|
94
|
+
modelBound().From[0].Value.Properties.viaAccessor.should.equal('$eventContext(Occurred)');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should map a child model-bound context property to the kernel expression', () => {
|
|
98
|
+
modelBound().Children.children.From[0].Value.Properties.happened.should.equal('$eventContext(Occurred)');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should map a nested model-bound context property to the kernel expression', () => {
|
|
102
|
+
modelBound().Nested.detail.From[0].Value.Properties.happened.should.equal('$eventContext(Occurred)');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should map a model-bound fromEvery context property to the kernel expression', () => {
|
|
106
|
+
modelBound().All.Properties.happened.should.equal('$eventContext(SequenceNumber)');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should set an event context property on a declarative from mapping', () => {
|
|
110
|
+
declarative(builder => builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('occurred')))
|
|
111
|
+
.From[0].Value.Properties.happened.should.equal('$eventContext(Occurred)');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should set an event context property on a declarative join mapping', () => {
|
|
115
|
+
declarative(builder => builder.join(Recorded, join => join.on(model => model.occurred).set(model => model.happened).toEventContextProperty('occurred')))
|
|
116
|
+
.Join[0].Value.Properties.happened.should.equal('$eventContext(Occurred)');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should set an event context property on a declarative fromEvery mapping', () => {
|
|
120
|
+
declarative(builder => builder.fromEvery(all => all.set(model => model.happened).toEventContextProperty('sequenceNumber')))
|
|
121
|
+
.All.Properties.happened.should.equal('$eventContext(SequenceNumber)');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should preserve PascalCase and capitalize every nested context segment', () => {
|
|
125
|
+
declarative(builder => builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('CausedBy.subject')))
|
|
126
|
+
.From[0].Value.Properties.happened.should.equal('$eventContext(CausedBy.Subject)');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should allow a kernel-derived function on a known context property', () => {
|
|
130
|
+
declarative(builder => builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('occurred.week()')))
|
|
131
|
+
.From[0].Value.Properties.happened.should.equal('$eventContext(Occurred.Week())');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should normalize a mixed-case kernel-derived function', () => {
|
|
135
|
+
declarative(builder => builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('occurred.WEEK()')))
|
|
136
|
+
.From[0].Value.Properties.happened.should.equal('$eventContext(Occurred.Week())');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should reject the week function on a non-date property, in a nonterminal position, or without parentheses', () => {
|
|
140
|
+
for (const path of ['eventSourceId.week()', 'occurred.week.day', 'occurred.week', 'causedBy.subject.week()']) {
|
|
141
|
+
expect(() => compileDeclarative(builder => builder.fromEvery(all =>
|
|
142
|
+
all.set(model => model.happened).toEventContextProperty(path))))
|
|
143
|
+
.toThrow(`Invalid event context property '${path}'`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should reject a derived function the kernel does not recognize', () => {
|
|
148
|
+
expect(() => compileDeclarative(builder => builder.fromEvery(all =>
|
|
149
|
+
all.set(model => model.happened).toEventContextProperty('occurred.ISOWeek()'))))
|
|
150
|
+
.toThrow(/Invalid event context property 'occurred.ISOWeek\(\)'.*'InvalidDeclarativeProjection'/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should map the on-behalf-of identity as a whole', () => {
|
|
154
|
+
declarative(builder => builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('causedBy.onBehalfOf')))
|
|
155
|
+
.From[0].Value.Properties.happened.should.equal('$eventContext(CausedBy.OnBehalfOf)');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should reject unknown identity members and deeper onBehalfOf chains', () => {
|
|
159
|
+
for (const path of ['causedBy.unknown', 'causedBy.onBehalfOf.unknown', 'causedBy.subject.name',
|
|
160
|
+
'causedBy.onBehalfOf.userName', 'causedBy.onBehalfOf.onBehalfOf.userName', 'causedBy.onBehalfOf.onBehalfOf']) {
|
|
161
|
+
expect(() => compileDeclarative(builder => builder.fromEvery(all =>
|
|
162
|
+
all.set(model => model.happened).toEventContextProperty(path))))
|
|
163
|
+
.toThrow(`Invalid event context property '${path}'`);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should map the whole causation collection', () => {
|
|
168
|
+
declarative(builder => builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('causation')))
|
|
169
|
+
.From[0].Value.Properties.happened.should.equal('$eventContext(Causation)');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should reject paths into the causation and tags collections', () => {
|
|
173
|
+
for (const path of ['causation.type', 'causation.unknown', 'causation.properties.unknown', 'tags.name']) {
|
|
174
|
+
expect(() => compileDeclarative(builder => builder.fromEvery(all =>
|
|
175
|
+
all.set(model => model.happened).toEventContextProperty(path))))
|
|
176
|
+
.toThrow(`Invalid event context property '${path}'`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should use a context property as a from key', () => {
|
|
181
|
+
declarative(builder => builder.from(Recorded, from => from.usingKeyFromContext('eventSourceId')))
|
|
182
|
+
.From[0].Value.Key.should.equal('$eventContext(EventSourceId)');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should use a context property as a from parent key', () => {
|
|
186
|
+
declarative(builder => builder.from(Recorded, from => from.usingParentKeyFromContext('eventSourceId')))
|
|
187
|
+
.From[0].Value.ParentKey.should.equal('$eventContext(EventSourceId)');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should use a context property as a join key', () => {
|
|
191
|
+
declarative(builder => builder.join(Recorded, join => join.on(model => model.occurred).usingKeyFromContext('eventSourceId')))
|
|
192
|
+
.Join[0].Value.Key.should.equal('$eventContext(EventSourceId)');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should leave a join parent key unset because JoinDefinition has no parent key', () => {
|
|
196
|
+
declarative(builder => builder.join(Recorded, join => join.on(model => model.occurred).usingParentKeyFromContext('eventSourceId')))
|
|
197
|
+
.Join[0].Value.Key.should.equal('$eventSourceId');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should use a context property as a removal key', () => {
|
|
201
|
+
declarative(builder => builder.removedWith(Removed, removed => removed.usingKeyFromContext('eventSourceId')))
|
|
202
|
+
.RemovedWith[0].Value.Key.should.equal('$eventContext(EventSourceId)');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should name the model-bound projection and read model identifier on registration failure', async () => {
|
|
206
|
+
class InvalidModel { static readonly readModelId = 'invalid-model-id'; happened!: Date; }
|
|
207
|
+
setFromContext(Recorded, 'invalidProperty')(InvalidModel.prototype, 'happened');
|
|
208
|
+
fromEvent(Recorded)(InvalidModel);
|
|
209
|
+
const attempt = register([], [InvalidModel]);
|
|
210
|
+
await expect(attempt).rejects.toThrow("Invalid event context property 'invalidProperty' in projection 'InvalidModel' (read model 'invalid-model-id').");
|
|
211
|
+
await expect(attempt).rejects.toMatchObject({ cause: expect.any(InvalidEventContextPropertyError) });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should reject an unknown nested identity member in a model-bound projection', () => {
|
|
215
|
+
class InvalidIdentityModel { happened!: string; }
|
|
216
|
+
setFromContext(Recorded, 'causedBy.missing')(InvalidIdentityModel.prototype, 'happened');
|
|
217
|
+
fromEvent(Recorded)(InvalidIdentityModel);
|
|
218
|
+
expect(() => modelBound(InvalidIdentityModel))
|
|
219
|
+
.toThrow(/Invalid event context property 'causedBy.missing'.*'InvalidIdentityModel'/);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should reject an invalid implicit model-bound context property at registration', () => {
|
|
223
|
+
class ImplicitInvalidModel { unknown!: string; }
|
|
224
|
+
setFromContext(Recorded)(ImplicitInvalidModel.prototype, 'unknown');
|
|
225
|
+
fromEvent(Recorded)(ImplicitInvalidModel);
|
|
226
|
+
expect(() => modelBound(ImplicitInvalidModel)).toThrow(/Invalid event context property 'unknown'.*'ImplicitInvalidModel'/);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should name the declarative projection and read model identifier on registration failure', async () => {
|
|
230
|
+
class InvalidDeclarativeModel { static readonly readModelId = 'declarative-model-id'; happened!: Date; }
|
|
231
|
+
class InvalidDeclarativeProjection {
|
|
232
|
+
define(builder: ProjectionBuilderFor<InvalidDeclarativeModel>): void {
|
|
233
|
+
builder.from(Recorded, from => from.set(model => model.happened).toEventContextProperty('unknown'));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
projection('invalid-context', InvalidDeclarativeModel)(InvalidDeclarativeProjection);
|
|
237
|
+
const attempt = register([InvalidDeclarativeProjection], [InvalidDeclarativeModel]);
|
|
238
|
+
await expect(attempt).rejects.toThrow("Invalid event context property 'unknown' in projection 'InvalidDeclarativeProjection' (read model 'declarative-model-id').");
|
|
239
|
+
await expect(attempt).rejects.toMatchObject({ cause: expect.any(InvalidEventContextPropertyError) });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should reject an invalid declarative all-set context property at registration', () => {
|
|
243
|
+
expect(() => compileDeclarative(builder => builder.fromEvery(all =>
|
|
244
|
+
all.set(model => model.happened).toEventContextProperty('unknown'))))
|
|
245
|
+
.toThrow(/Invalid event context property 'unknown'.*'InvalidDeclarativeProjection'.*'Model'/);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should reject invalid context paths that the kernel cannot parse', () => {
|
|
249
|
+
expect(() => compileDeclarative(builder => builder.fromEvery(all =>
|
|
250
|
+
all.set(model => model.happened).toEventContextProperty('causedBy.subject[0]'))))
|
|
251
|
+
.toThrow(/Invalid event context property 'causedBy.subject\[0\]'.*'InvalidDeclarativeProjection'/);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should reject invalid context properties in key and parent-key APIs at registration', () => {
|
|
255
|
+
const mappings = [
|
|
256
|
+
(builder: ProjectionBuilderFor<Model>) => builder.from(Recorded, from => from.usingKeyFromContext('missingKey')),
|
|
257
|
+
(builder: ProjectionBuilderFor<Model>) => builder.from(Recorded, from => from.usingParentKeyFromContext('missingParent')),
|
|
258
|
+
(builder: ProjectionBuilderFor<Model>) => builder.join(Recorded, join => join.on(model => model.occurred).usingKeyFromContext('missingJoinKey')),
|
|
259
|
+
(builder: ProjectionBuilderFor<Model>) => builder.removedWith(Removed, removed => removed.usingKeyFromContext('missingRemovalKey'))
|
|
260
|
+
];
|
|
261
|
+
for (const configure of mappings) {
|
|
262
|
+
expect(() => compileDeclarative(configure)).toThrow(/Invalid event context property 'missing.*' in projection 'InvalidDeclarativeProjection'/);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should use a context property on a declarative child and child parent key', () => {
|
|
267
|
+
const child = declarative(builder => builder.children<Child>(model => model.children, children => children
|
|
268
|
+
.from(Recorded, from => from.usingParentKeyFromContext('eventSourceId')
|
|
269
|
+
.set(model => model.happened).toEventContextProperty('occurred'))));
|
|
270
|
+
child.Children.children.From[0].Value.ParentKey.should.equal('$eventContext(EventSourceId)');
|
|
271
|
+
child.Children.children.From[0].Value.Properties.happened.should.equal('$eventContext(Occurred)');
|
|
272
|
+
});
|
|
273
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
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 type { EventContext } from '../events/EventContext.js';
|
|
5
|
+
import { InvalidEventContextPropertyError } from './InvalidEventContextPropertyError.js';
|
|
6
|
+
|
|
7
|
+
// EventContext is an interface (erased at runtime). Keep this exhaustive against its keys so
|
|
8
|
+
// additions/removals to the SDK type cannot silently leave registration validation out of date.
|
|
9
|
+
const contextProperties = {
|
|
10
|
+
sequenceNumber: true,
|
|
11
|
+
eventSourceId: true,
|
|
12
|
+
eventStore: true,
|
|
13
|
+
namespace: true,
|
|
14
|
+
eventSourceType: true,
|
|
15
|
+
eventStreamType: true,
|
|
16
|
+
eventStreamId: true,
|
|
17
|
+
subject: true,
|
|
18
|
+
hash: true,
|
|
19
|
+
causedBy: true,
|
|
20
|
+
observationState: true,
|
|
21
|
+
eventType: true,
|
|
22
|
+
occurred: true,
|
|
23
|
+
correlationId: true,
|
|
24
|
+
causation: true,
|
|
25
|
+
tags: true
|
|
26
|
+
} satisfies Record<keyof EventContext, true>;
|
|
27
|
+
|
|
28
|
+
// Mirror the kernel's DerivedPropertyFunctions registry: Week() applies to dates only.
|
|
29
|
+
const derivedFunctions: Record<string, string> = { week: 'Week' };
|
|
30
|
+
const dateProperties = new Set(['occurred']);
|
|
31
|
+
const identityProperties = new Set(['subject', 'name', 'userName']);
|
|
32
|
+
|
|
33
|
+
function lower(segment: string): string {
|
|
34
|
+
return segment.charAt(0).toLowerCase() + segment.slice(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Accept only paths the kernel can resolve: a root property or an identity member of causedBy.
|
|
38
|
+
// causedBy.onBehalfOf is allowed only as a whole value: traversing it when it is missing makes the
|
|
39
|
+
// kernel construct identities recursively. Nothing inside the causation or tags collections, which a
|
|
40
|
+
// dotted path cannot traverse.
|
|
41
|
+
function validNestedPath(root: string, segments: string[]): boolean {
|
|
42
|
+
if (segments.length === 0) return true;
|
|
43
|
+
if (root !== 'causedBy') return false;
|
|
44
|
+
const members = segments.map(lower);
|
|
45
|
+
return members.length === 1 && (identityProperties.has(members[0]) || members[0] === 'onBehalfOf');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Build the expression read by Chronicle's event-context resolver, using CLR property casing. */
|
|
49
|
+
export function eventContextPropertyExpression(propertyPath: string): string {
|
|
50
|
+
// The kernel accepts dotted CLR paths (and terminal derived-property functions) using
|
|
51
|
+
// [A-Za-z.()]. Reject unsupported characters and unknown root properties before registration.
|
|
52
|
+
const segments = propertyPath.split('.');
|
|
53
|
+
const contextProperty = segments[0].charAt(0).toLowerCase() + segments[0].slice(1);
|
|
54
|
+
if (!/^[A-Za-z]+(?:\.[A-Za-z]+)*(?:\(\))?$/.test(propertyPath) ||
|
|
55
|
+
!Object.hasOwn(contextProperties, contextProperty)) {
|
|
56
|
+
throw new InvalidEventContextPropertyError(propertyPath);
|
|
57
|
+
}
|
|
58
|
+
const lastIndex = segments.length - 1;
|
|
59
|
+
const isFunction = segments[lastIndex].endsWith('()');
|
|
60
|
+
if (isFunction) {
|
|
61
|
+
const functionName = segments[lastIndex].slice(0, -2).toLowerCase();
|
|
62
|
+
if (!Object.hasOwn(derivedFunctions, functionName) || lastIndex !== 1 || !dateProperties.has(contextProperty)) {
|
|
63
|
+
throw new InvalidEventContextPropertyError(propertyPath);
|
|
64
|
+
}
|
|
65
|
+
segments[lastIndex] = derivedFunctions[functionName];
|
|
66
|
+
} else if (segments.some(segment => Object.hasOwn(derivedFunctions, segment.toLowerCase())) ||
|
|
67
|
+
!validNestedPath(contextProperty, segments.slice(1))) {
|
|
68
|
+
throw new InvalidEventContextPropertyError(propertyPath);
|
|
69
|
+
}
|
|
70
|
+
const clrPath = segments.map(segment => segment.charAt(0).toUpperCase() + segment.slice(1)).join('.');
|
|
71
|
+
return `$eventContext(${clrPath}${propertyPath.endsWith('()') ? '()' : ''})`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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 type { ContractEventType } from './declarative/ProjectionBuilderCore.js';
|
|
5
|
+
|
|
6
|
+
/** Stable, generation-aware path shared by provenance and capability diagnostics. */
|
|
7
|
+
export function eventContractPath(section: string, eventType: ContractEventType): string {
|
|
8
|
+
return `${section}[${eventType.Id}:${eventType.Generation}]`;
|
|
9
|
+
}
|
package/projections/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { Projections } from './Projections.js';
|
|
|
7
7
|
export type { ProjectionState } from './ProjectionState.js';
|
|
8
8
|
export type { ProjectionQueryResult } from './ProjectionQueryResult.js';
|
|
9
9
|
export { UnableToQueryProjection } from './UnableToQueryProjection.js';
|
|
10
|
+
export { InvalidEventContextPropertyError } from './InvalidEventContextPropertyError.js';
|
|
10
11
|
export { VariantMustDeclareEntersOnEvent } from './VariantMustDeclareEntersOnEvent.js';
|
|
11
12
|
export { GlobalHandlerPropertyNotOnVariant } from './GlobalHandlerPropertyNotOnVariant.js';
|
|
12
13
|
export * from './declarative/index.js';
|
|
@@ -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 { InvalidEventContextPropertyError } from './InvalidEventContextPropertyError.js';
|
|
5
|
+
|
|
6
|
+
/** Add projection and read-model context to a rejected event-context path. */
|
|
7
|
+
export function invalidEventContextPropertyInProjection(
|
|
8
|
+
error: InvalidEventContextPropertyError,
|
|
9
|
+
projectionName: string,
|
|
10
|
+
readModelId: string
|
|
11
|
+
): Error {
|
|
12
|
+
return new Error(`Invalid event context property '${error.propertyPath}' in projection '${projectionName}' (read model '${readModelId}').`, { cause: error });
|
|
13
|
+
}
|
|
@@ -6,6 +6,7 @@ import { AutoMap } from '@cratis/chronicle.contracts';
|
|
|
6
6
|
import { Constructor, Fields } from '@cratis/fundamentals';
|
|
7
7
|
import { TypeIntrospector } from '../../types/index.js';
|
|
8
8
|
import { constantValueExpression } from '../constantValueExpression.js';
|
|
9
|
+
import { eventContextPropertyExpression } from '../eventContextPropertyExpression.js';
|
|
9
10
|
import { notSetPropertyPath } from '../notSetPropertyPath.js';
|
|
10
11
|
import { getEventTypeFor } from '../../events/eventTypeDecorator.js';
|
|
11
12
|
import { getAddFromMetadata } from './addFrom.js';
|
|
@@ -109,7 +110,7 @@ export function applyPropertyMappings(prototype: object, property: string, fromB
|
|
|
109
110
|
|
|
110
111
|
for (const mapping of getSetFromContextMetadata(prototype, property)) {
|
|
111
112
|
const entry = ensureFromEntry(fromByEventType, mapping.eventType);
|
|
112
|
-
entry.Value.Properties[property] = mapping.contextPropertyName ?? property;
|
|
113
|
+
entry.Value.Properties[property] = eventContextPropertyExpression(mapping.contextPropertyName ?? property);
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
for (const mapping of getSetValueMetadata(prototype, property)) {
|
package/reactors/Reactors.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type { ActivatedArtifact } from '../artifacts/ActivatedArtifact.js';
|
|
|
28
28
|
import { ArtifactKind } from '../artifacts/ArtifactKind.js';
|
|
29
29
|
import { ArtifactDelivery } from '../artifacts/ArtifactDelivery.js';
|
|
30
30
|
import { withActivatedArtifact, runActivated } from '../artifacts/withActivatedArtifact.js';
|
|
31
|
+
import { ArtifactCompletionFailed } from '../artifacts/ArtifactCompletionFailed.js';
|
|
31
32
|
|
|
32
33
|
/** Expression used to partition reactor observations by event source ID. */
|
|
33
34
|
const EVENT_SOURCE_ID_KEY = '$eventSourceId';
|
|
@@ -306,12 +307,16 @@ export class Reactors implements IReactors {
|
|
|
306
307
|
signal: controller.signal, delivery: ArtifactDelivery.ReplayNotification,
|
|
307
308
|
replayState: eventsToObserve.ReplayState
|
|
308
309
|
}, this._artifactActivator, artifact => runActivated(artifact, () =>
|
|
309
|
-
notifyReplayLifecycle(artifact.instance, eventsToObserve.ReplayState, eventsToObserve.Partition)
|
|
310
|
+
notifyReplayLifecycle(artifact.instance, eventsToObserve.ReplayState, eventsToObserve.Partition),
|
|
311
|
+
{ delivery: ArtifactDelivery.ReplayNotification, replayState: eventsToObserve.ReplayState }));
|
|
310
312
|
} else if (reactorInstance) {
|
|
311
313
|
await notifyReplayLifecycle(reactorInstance, eventsToObserve.ReplayState, eventsToObserve.Partition);
|
|
312
314
|
}
|
|
313
315
|
} catch (err) {
|
|
314
316
|
this._logger.error('Error notifying reactor of replay lifecycle transition', { reactorId: id, error: String(err) });
|
|
317
|
+
if (err instanceof ArtifactCompletionFailed && err.processingError !== undefined) {
|
|
318
|
+
exceptionMessages.push(String(err.processingError));
|
|
319
|
+
}
|
|
315
320
|
exceptionMessages.push(String(err));
|
|
316
321
|
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
317
322
|
state = ObservationState.Failed;
|
|
@@ -368,7 +373,7 @@ export class Reactors implements IReactors {
|
|
|
368
373
|
const handlerResult = await artifact.instance[methodName](content, context, services);
|
|
369
374
|
await dispatchReactorSideEffects(this._eventLog, handlerResult, context, reactorType as Function,
|
|
370
375
|
this._eventStoreName, this._namespace, this._resultHandler);
|
|
371
|
-
});
|
|
376
|
+
}, { delivery: ArtifactDelivery.Events, eventContext: context, methodName });
|
|
372
377
|
|
|
373
378
|
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
374
379
|
} catch (err) {
|
|
@@ -398,8 +403,13 @@ export class Reactors implements IReactors {
|
|
|
398
403
|
} catch (err) {
|
|
399
404
|
this._logger.error('Error activating reactor', { reactorId: id, error: String(err) });
|
|
400
405
|
exceptionMessages.push(String(err));
|
|
401
|
-
|
|
406
|
+
const completionStack = err instanceof Error ? (err.stack ?? '') : '';
|
|
407
|
+
exceptionStackTrace = err instanceof ArtifactCompletionFailed && exceptionStackTrace
|
|
408
|
+
? `${exceptionStackTrace}\n${completionStack}` : completionStack;
|
|
402
409
|
state = ObservationState.Failed;
|
|
410
|
+
if (err instanceof ArtifactCompletionFailed) {
|
|
411
|
+
lastSuccessfullyObservedEvent = SEQUENCE_NUMBER_UNAVAILABLE;
|
|
412
|
+
}
|
|
403
413
|
}
|
|
404
414
|
} else {
|
|
405
415
|
await processEvents({ instance: reactorInstance ?? {} });
|
|
@@ -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
|
|
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);
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import { type Constructor } from '@cratis/fundamentals';
|
|
5
5
|
import type { IClientArtifactsProvider } from '../artifacts/IClientArtifactsProvider.js';
|
|
6
6
|
import { getProjectionMetadata } from '../projections/declarative/projection.js';
|
|
7
|
+
import { InvalidEventContextPropertyError } from '../projections/InvalidEventContextPropertyError.js';
|
|
8
|
+
import { invalidEventContextPropertyInProjection } from '../projections/invalidEventContextPropertyInProjection.js';
|
|
9
|
+
import { getReadModelId } from './readModel.js';
|
|
7
10
|
import { ProjectionBuilderFor } from '../projections/declarative/ProjectionBuilderFor.js';
|
|
8
11
|
import type { ChildrenDefinitionLike } from '../projections/declarative/ProjectionBuilderCore.js';
|
|
9
12
|
import type { IProjectionFor } from '../projections/declarative/IProjectionFor.js';
|
|
@@ -53,7 +56,14 @@ function inspectReadModelTypes(artifacts: IClientArtifactsProvider): { models: C
|
|
|
53
56
|
if (!model) continue;
|
|
54
57
|
inspect(model);
|
|
55
58
|
const builder = new ProjectionBuilderFor<unknown>();
|
|
56
|
-
|
|
59
|
+
try {
|
|
60
|
+
(new type() as IProjectionFor<unknown>).define(builder);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof InvalidEventContextPropertyError) {
|
|
63
|
+
throw invalidEventContextPropertyInProjection(error, type.name, getReadModelId(model));
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
57
67
|
inspectDeclarative(model, builder.getSubobjectDefinitions());
|
|
58
68
|
}
|
|
59
69
|
for (const type of artifacts.reducers) {
|