@cratis/chronicle 6.15.0 → 6.16.1
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/ChronicleClient.ts +1 -1
- package/ChronicleOptions.ts +9 -1
- package/EventStore.ts +5 -3
- package/artifacts/ActivatedArtifact.ts +12 -0
- package/artifacts/ArtifactActivationContext.ts +37 -0
- package/artifacts/ArtifactDelivery.ts +8 -0
- package/artifacts/ArtifactKind.ts +8 -0
- package/artifacts/ClientArtifactsActivator.ts +10 -0
- package/artifacts/for_ChronicleOptions/when_configuring/with_an_artifact_activator.spec.ts +21 -0
- package/artifacts/index.ts +6 -0
- package/artifacts/withActivatedArtifact.ts +32 -0
- package/dist/ChronicleClient.js +1 -1
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/ChronicleOptions.d.ts +4 -0
- package/dist/ChronicleOptions.d.ts.map +1 -1
- package/dist/ChronicleOptions.js +5 -1
- package/dist/ChronicleOptions.js.map +1 -1
- package/dist/EventStore.d.ts +2 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +3 -3
- package/dist/EventStore.js.map +1 -1
- package/dist/artifacts/ActivatedArtifact.d.ts +10 -0
- package/dist/artifacts/ActivatedArtifact.d.ts.map +1 -0
- package/dist/artifacts/ActivatedArtifact.js +4 -0
- package/dist/artifacts/ActivatedArtifact.js.map +1 -0
- package/dist/artifacts/ArtifactActivationContext.d.ts +34 -0
- package/dist/artifacts/ArtifactActivationContext.d.ts.map +1 -0
- package/dist/artifacts/ArtifactActivationContext.js +4 -0
- package/dist/artifacts/ArtifactActivationContext.js.map +1 -0
- package/dist/artifacts/ArtifactDelivery.d.ts +6 -0
- package/dist/artifacts/ArtifactDelivery.d.ts.map +1 -0
- package/dist/artifacts/ArtifactDelivery.js +9 -0
- package/dist/artifacts/ArtifactDelivery.js.map +1 -0
- package/dist/artifacts/ArtifactKind.d.ts +6 -0
- package/dist/artifacts/ArtifactKind.d.ts.map +1 -0
- package/dist/artifacts/ArtifactKind.js +9 -0
- package/dist/artifacts/ArtifactKind.js.map +1 -0
- package/dist/artifacts/ClientArtifactsActivator.d.ts +6 -0
- package/dist/artifacts/ClientArtifactsActivator.d.ts.map +1 -0
- package/dist/artifacts/ClientArtifactsActivator.js +4 -0
- package/dist/artifacts/ClientArtifactsActivator.js.map +1 -0
- package/dist/artifacts/index.d.ts +5 -0
- package/dist/artifacts/index.d.ts.map +1 -1
- package/dist/artifacts/index.js +2 -0
- package/dist/artifacts/index.js.map +1 -1
- package/dist/artifacts/withActivatedArtifact.d.ts +9 -0
- package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -0
- package/dist/artifacts/withActivatedArtifact.js +24 -0
- package/dist/artifacts/withActivatedArtifact.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.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/ProjectionDefinitionCompiler.d.ts +1 -0
- package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -1
- package/dist/projections/ProjectionDefinitionCompiler.js +18 -3
- package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -1
- 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 +3 -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/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/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/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/ReactorServices.d.ts +12 -0
- package/dist/reactors/ReactorServices.d.ts.map +1 -0
- package/dist/reactors/ReactorServices.js +4 -0
- package/dist/reactors/ReactorServices.js.map +1 -0
- package/dist/reactors/Reactors.d.ts +5 -1
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +99 -39
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/reactors/index.d.ts +1 -0
- package/dist/reactors/index.d.ts.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 +5 -1
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +73 -28
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +1 -0
- package/package.json +1 -1
- package/projections/InvalidEventContextPropertyError.ts +14 -0
- package/projections/ProjectionDefinitionCompiler.ts +19 -3
- package/projections/declarative/AllSetBuilder.ts +2 -1
- package/projections/declarative/CompositeKeyBuilder.spec.ts +1 -1
- package/projections/declarative/FromBuilder.ts +3 -2
- package/projections/declarative/JoinBuilder.ts +2 -1
- package/projections/declarative/ProjectionBuilderFor.spec.ts +4 -4
- package/projections/declarative/RemovedWithBuilder.ts +2 -1
- package/projections/declarative/SetBuilder.ts +2 -1
- package/projections/eventContextExpressions.spec.ts +273 -0
- package/projections/eventContextPropertyExpression.ts +72 -0
- package/projections/index.ts +1 -0
- package/projections/invalidEventContextPropertyInProjection.ts +13 -0
- package/projections/modelBound/childrenAndNestedBuilder.ts +2 -1
- package/reactors/ReactorServices.ts +15 -0
- package/reactors/Reactors.ts +101 -42
- package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +386 -0
- package/reactors/index.ts +1 -0
- package/readModels/rootReadModelTypes.ts +11 -1
- package/reducers/Reducers.ts +75 -31
- package/reducers/for_Reducers/when_delivering/with_activation.spec.ts +177 -0
|
@@ -101,7 +101,7 @@ describe('CompositeKeyBuilder', () => {
|
|
|
101
101
|
expect(() => builder.join(OrderLineJoined, join => join
|
|
102
102
|
.on(model => model.id)
|
|
103
103
|
.usingParentKey(event => event.orderId)
|
|
104
|
-
.usingParentKeyFromContext('
|
|
104
|
+
.usingParentKeyFromContext('eventSourceId')
|
|
105
105
|
.usingParentCompositeKey<CompositeKey>(key => key.set(target => target.orderId, event => event.orderId))
|
|
106
106
|
.usingConstantParentKey('constant')
|
|
107
107
|
)).not.toThrow();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals';
|
|
5
5
|
import { constantValueExpression } from '../constantValueExpression.js';
|
|
6
|
+
import { eventContextPropertyExpression } from '../eventContextPropertyExpression.js';
|
|
6
7
|
import { AddBuilder } from './AddBuilder.js';
|
|
7
8
|
import { AddChildBuilder, ChildAdditionEntry } from './AddChildBuilder.js';
|
|
8
9
|
import { CompositeKeyBuilder } from './CompositeKeyBuilder.js';
|
|
@@ -62,7 +63,7 @@ export class FromBuilder<TReadModel, TEvent> implements IFromBuilder<TReadModel,
|
|
|
62
63
|
|
|
63
64
|
/** @inheritdoc */
|
|
64
65
|
usingKeyFromContext(contextPropertyName: string): this {
|
|
65
|
-
this.entry.key =
|
|
66
|
+
this.entry.key = eventContextPropertyExpression(contextPropertyName);
|
|
66
67
|
return this;
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -77,7 +78,7 @@ export class FromBuilder<TReadModel, TEvent> implements IFromBuilder<TReadModel,
|
|
|
77
78
|
|
|
78
79
|
/** @inheritdoc */
|
|
79
80
|
usingParentKeyFromContext(contextPropertyName: string): this {
|
|
80
|
-
this.entry.parentKey =
|
|
81
|
+
this.entry.parentKey = eventContextPropertyExpression(contextPropertyName);
|
|
81
82
|
return this;
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals';
|
|
5
5
|
import { constantValueExpression } from '../constantValueExpression.js';
|
|
6
|
+
import { eventContextPropertyExpression } from '../eventContextPropertyExpression.js';
|
|
6
7
|
import { AddBuilder } from './AddBuilder.js';
|
|
7
8
|
import { AddChildBuilder, ChildAdditionEntry } from './AddChildBuilder.js';
|
|
8
9
|
import { CompositeKeyBuilder } from './CompositeKeyBuilder.js';
|
|
@@ -72,7 +73,7 @@ export class JoinBuilder<TReadModel, TEvent> implements IJoinBuilder<TReadModel,
|
|
|
72
73
|
|
|
73
74
|
/** @inheritdoc */
|
|
74
75
|
usingKeyFromContext(contextPropertyName: string): this {
|
|
75
|
-
this.entry.key =
|
|
76
|
+
this.entry.key = eventContextPropertyExpression(contextPropertyName);
|
|
76
77
|
return this;
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -227,19 +227,19 @@ describe('ProjectionBuilderFor', () => {
|
|
|
227
227
|
};
|
|
228
228
|
|
|
229
229
|
it('should change the hash when a From property expression changes', () => {
|
|
230
|
-
expect(hashFor(false, 'one', '
|
|
230
|
+
expect(hashFor(false, 'one', 'eventSourceId')).not.toBe(hashFor(true, 'one', 'eventSourceId'));
|
|
231
231
|
});
|
|
232
232
|
|
|
233
233
|
it('should change the hash when a Join key changes', () => {
|
|
234
|
-
expect(hashFor(false, 'one', '
|
|
234
|
+
expect(hashFor(false, 'one', 'eventSourceId')).not.toBe(hashFor(false, 'two', 'eventSourceId'));
|
|
235
235
|
});
|
|
236
236
|
|
|
237
237
|
it('should change the hash when a RemovedWith key changes', () => {
|
|
238
|
-
expect(hashFor(false, 'one', '
|
|
238
|
+
expect(hashFor(false, 'one', 'eventSourceId')).not.toBe(hashFor(false, 'one', 'correlationId'));
|
|
239
239
|
});
|
|
240
240
|
|
|
241
241
|
it('should give identical definitions identical hashes', () => {
|
|
242
|
-
expect(hashFor(false, 'one', '
|
|
242
|
+
expect(hashFor(false, 'one', 'eventSourceId')).toBe(hashFor(false, 'one', 'eventSourceId'));
|
|
243
243
|
});
|
|
244
244
|
|
|
245
245
|
it('should give reordered property mappings the same hash', () => {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals';
|
|
5
5
|
import { IRemovedWithBuilder } from './IRemovedWithBuilder.js';
|
|
6
|
+
import { eventContextPropertyExpression } from '../eventContextPropertyExpression.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Accumulated removal configuration for a removedWith clause.
|
|
@@ -34,7 +35,7 @@ export class RemovedWithBuilder<TReadModel, TEvent> implements IRemovedWithBuild
|
|
|
34
35
|
|
|
35
36
|
/** @inheritdoc */
|
|
36
37
|
usingKeyFromContext(contextPropertyName: string): this {
|
|
37
|
-
this.entry.key =
|
|
38
|
+
this.entry.key = eventContextPropertyExpression(contextPropertyName);
|
|
38
39
|
return this;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals';
|
|
5
5
|
import { ISetBuilder } from './ISetBuilder.js';
|
|
6
6
|
import { constantValueExpression } from '../constantValueExpression.js';
|
|
7
|
+
import { eventContextPropertyExpression } from '../eventContextPropertyExpression.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Concrete implementation of {@link ISetBuilder} that records the property mapping
|
|
@@ -35,7 +36,7 @@ export class SetBuilder<TEvent, TParentBuilder> implements ISetBuilder<TEvent, T
|
|
|
35
36
|
|
|
36
37
|
/** @inheritdoc */
|
|
37
38
|
toEventContextProperty(contextPropertyName: string): TParentBuilder {
|
|
38
|
-
this._setProperty(this._readModelProperty,
|
|
39
|
+
this._setProperty(this._readModelProperty, eventContextPropertyExpression(contextPropertyName));
|
|
39
40
|
return this._parent;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -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
|
+
}
|
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)) {
|
|
@@ -0,0 +1,15 @@
|
|
|
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 { IEventStore } from '../IEventStore.js';
|
|
5
|
+
import type { IReadModels } from '../readModels/IReadModels.js';
|
|
6
|
+
|
|
7
|
+
/** Services for the observation's owning store and namespace, not an application default store. */
|
|
8
|
+
export interface ReactorServices {
|
|
9
|
+
/** The event store that owns this reactor observation. */
|
|
10
|
+
readonly eventStore: IEventStore;
|
|
11
|
+
/** The read models from the same event store (`eventStore.readModels`). */
|
|
12
|
+
readonly readModels: IReadModels;
|
|
13
|
+
/** Aborted when the observation disconnects or the client is disposed. */
|
|
14
|
+
readonly signal: AbortSignal;
|
|
15
|
+
}
|