@cratis/chronicle 6.14.0 → 6.14.2
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 +11 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +7 -0
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/projections/CompiledProjectionDefinitions.d.ts +10 -0
- package/dist/projections/CompiledProjectionDefinitions.d.ts.map +1 -0
- package/dist/projections/CompiledProjectionDefinitions.js +4 -0
- package/dist/projections/CompiledProjectionDefinitions.js.map +1 -0
- package/dist/projections/ProjectionDefinitionCompiler.d.ts +35 -0
- package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -0
- package/dist/projections/ProjectionDefinitionCompiler.js +355 -0
- package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -0
- package/dist/projections/Projections.d.ts +0 -21
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +9 -339
- package/dist/projections/Projections.js.map +1 -1
- package/dist/projections/ResolvedModelBoundMetadata.d.ts +8 -0
- package/dist/projections/ResolvedModelBoundMetadata.d.ts.map +1 -0
- package/dist/projections/ResolvedModelBoundMetadata.js +4 -0
- package/dist/projections/ResolvedModelBoundMetadata.js.map +1 -0
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.js +7 -3
- package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
- package/dist/projections/modelBound/isModelBoundProjection.d.ts +3 -0
- package/dist/projections/modelBound/isModelBoundProjection.d.ts.map +1 -0
- package/dist/projections/modelBound/isModelBoundProjection.js +9 -0
- package/dist/projections/modelBound/isModelBoundProjection.js.map +1 -0
- package/dist/readModels/ReadModels.js +1 -1
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/readModels/rootReadModelTypes.d.ts +2 -0
- package/dist/readModels/rootReadModelTypes.d.ts.map +1 -1
- package/dist/readModels/rootReadModelTypes.js +10 -2
- package/dist/readModels/rootReadModelTypes.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/TypeDiscoverer.d.ts.map +1 -1
- package/dist/types/TypeDiscoverer.js +7 -4
- package/dist/types/TypeDiscoverer.js.map +1 -1
- package/dist/types/modelBoundProperty.d.ts.map +1 -1
- package/dist/types/modelBoundProperty.js +7 -2
- package/dist/types/modelBoundProperty.js.map +1 -1
- package/dist/types/modelBoundPropertyMetadata.d.ts +9 -0
- package/dist/types/modelBoundPropertyMetadata.d.ts.map +1 -0
- package/dist/types/modelBoundPropertyMetadata.js +66 -0
- package/dist/types/modelBoundPropertyMetadata.js.map +1 -0
- package/for_ChronicleClient/when_registering_artifacts/with_legacy_property_mappings.legacy.spec.ts +31 -0
- package/for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts +138 -0
- package/package.json +2 -2
- package/projections/CompiledProjectionDefinitions.ts +13 -0
- package/projections/ProjectionDefinitionCompiler.registration.spec.ts +279 -0
- package/projections/ProjectionDefinitionCompiler.ts +386 -0
- package/projections/Projections.childrenAndNested.spec.ts +47 -2
- package/projections/Projections.ts +14 -413
- package/projections/ResolvedModelBoundMetadata.ts +11 -0
- package/projections/modelBound/childrenAndNestedBuilder.ts +7 -3
- package/projections/modelBound/decorators.standard.spec.ts +2 -2
- package/projections/modelBound/isModelBoundProjection.ts +10 -0
- package/readModels/ReadModels.ts +1 -1
- package/readModels/discovery.spec.ts +2 -2
- package/readModels/registration.legacy.spec.ts +38 -0
- package/readModels/registration.standard.spec.ts +108 -0
- package/readModels/rootReadModelTypes.ts +12 -2
- package/types/TypeDiscoverer.ts +6 -5
- package/types/modelBoundProperty.ts +5 -2
- package/types/modelBoundPropertyMetadata.ts +67 -0
|
@@ -0,0 +1,138 @@
|
|
|
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 { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { ChronicleClient } from '../../ChronicleClient.js';
|
|
7
|
+
import { ChronicleOptions } from '../../ChronicleOptions.js';
|
|
8
|
+
import type { EventStore } from '../../EventStore.js';
|
|
9
|
+
import { childrenFrom } from '../../projections/modelBound/childrenFrom.js';
|
|
10
|
+
import { fromEvent } from '../../projections/modelBound/fromEvent.js';
|
|
11
|
+
import { setFrom } from '../../projections/modelBound/setFrom.js';
|
|
12
|
+
import { DecoratorType } from '../../types/DecoratorType.js';
|
|
13
|
+
import { TypeDiscoverer } from '../../types/TypeDiscoverer.js';
|
|
14
|
+
|
|
15
|
+
class Changed {}
|
|
16
|
+
|
|
17
|
+
function createClient() {
|
|
18
|
+
const client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { discoveryPatterns: [] }));
|
|
19
|
+
const warn = vi.spyOn(client['_logger'], 'warn');
|
|
20
|
+
const registerArtifacts = vi.fn().mockResolvedValue(undefined);
|
|
21
|
+
const store = { name: { value: 'store' }, namespace: { value: 'Default' }, registerArtifacts } as unknown as EventStore;
|
|
22
|
+
return { client, warn, store, registerArtifacts };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
TypeDiscoverer.default.clear();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('when registering artifacts with standard property mappings', () => {
|
|
30
|
+
it('should warn once about orphan mappings with their property and event type', async () => {
|
|
31
|
+
const discoverer = new TypeDiscoverer(async () => ['model.ts'], async () => {
|
|
32
|
+
class Orphan {
|
|
33
|
+
@setFrom(Changed) value = '';
|
|
34
|
+
@setFrom(Changed) name = '';
|
|
35
|
+
}
|
|
36
|
+
class OtherOrphan { @setFrom(Changed) value = ''; }
|
|
37
|
+
void Orphan;
|
|
38
|
+
void OtherOrphan;
|
|
39
|
+
return {};
|
|
40
|
+
});
|
|
41
|
+
await discoverer.discover('model.ts');
|
|
42
|
+
const { client, warn, store, registerArtifacts } = createClient();
|
|
43
|
+
try {
|
|
44
|
+
await client['registerArtifactsForStore'](store, 'new-store');
|
|
45
|
+
await client['registerArtifactsForStore'](store, 'new-store');
|
|
46
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
47
|
+
expect(warn.mock.calls[0][0]).toContain('{value <- Changed, name <- Changed}, {value <- Changed}');
|
|
48
|
+
expect(warn.mock.calls[0][0]).toContain('@fromEvent(...)');
|
|
49
|
+
expect(warn.mock.calls[0][0]).toContain('discoveryPatterns');
|
|
50
|
+
expect(warn.mock.calls[0][0]).toContain('register the class explicitly');
|
|
51
|
+
expect(registerArtifacts).toHaveBeenCalledTimes(2);
|
|
52
|
+
} finally {
|
|
53
|
+
client.dispose();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should not warn after the first instance registers the model before store creation', async () => {
|
|
58
|
+
class Constructed { @setFrom(Changed) value = ''; }
|
|
59
|
+
new Constructed();
|
|
60
|
+
const { client, warn, store } = createClient();
|
|
61
|
+
try {
|
|
62
|
+
await client['registerArtifactsForStore'](store, 'new-store');
|
|
63
|
+
expect(warn).not.toHaveBeenCalled();
|
|
64
|
+
} finally {
|
|
65
|
+
client.dispose();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should not warn for a discovered export', async () => {
|
|
70
|
+
class Discovered { @setFrom(Changed) value = ''; }
|
|
71
|
+
const discoverer = new TypeDiscoverer(async () => ['model.ts'], async () => ({ Discovered }));
|
|
72
|
+
await discoverer.discover('model.ts');
|
|
73
|
+
const { client, warn, store } = createClient();
|
|
74
|
+
try {
|
|
75
|
+
await client['registerArtifactsForStore'](store, 'new-store');
|
|
76
|
+
expect(warn).not.toHaveBeenCalled();
|
|
77
|
+
} finally {
|
|
78
|
+
client.dispose();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should not warn for an unexported base class of a discovered model', async () => {
|
|
83
|
+
class Base { @setFrom(Changed) value = ''; }
|
|
84
|
+
class Derived extends Base {}
|
|
85
|
+
const discoverer = new TypeDiscoverer(async () => ['model.ts'], async () => ({ Derived }));
|
|
86
|
+
await discoverer.discover('model.ts');
|
|
87
|
+
const { client, warn, store } = createClient();
|
|
88
|
+
try {
|
|
89
|
+
await client['registerArtifactsForStore'](store, 'new-store');
|
|
90
|
+
expect(warn).not.toHaveBeenCalled();
|
|
91
|
+
} finally {
|
|
92
|
+
client.dispose();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should not warn for an unexported child of a discovered model', async () => {
|
|
97
|
+
class Line { @setFrom(Changed) value = ''; }
|
|
98
|
+
class Cart {
|
|
99
|
+
@childrenFrom(Changed)
|
|
100
|
+
@field(Array, { genericArguments: [Line] })
|
|
101
|
+
lines: Line[] = [];
|
|
102
|
+
}
|
|
103
|
+
const discoverer = new TypeDiscoverer(async () => ['model.ts'], async () => ({ Cart }));
|
|
104
|
+
await discoverer.discover('model.ts');
|
|
105
|
+
const { client, warn, store } = createClient();
|
|
106
|
+
try {
|
|
107
|
+
await client['registerArtifactsForStore'](store, 'new-store');
|
|
108
|
+
expect(warn).not.toHaveBeenCalled();
|
|
109
|
+
} finally {
|
|
110
|
+
client.dispose();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should not warn for an explicitly registered class', async () => {
|
|
115
|
+
class Explicit { @setFrom(Changed) value = ''; }
|
|
116
|
+
TypeDiscoverer.default.register(DecoratorType.ReadModel, Explicit);
|
|
117
|
+
const { client, warn, store } = createClient();
|
|
118
|
+
try {
|
|
119
|
+
await client['registerArtifactsForStore'](store, 'new-store');
|
|
120
|
+
expect(warn).not.toHaveBeenCalled();
|
|
121
|
+
} finally {
|
|
122
|
+
client.dispose();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should not warn for a class-level model mapping', async () => {
|
|
127
|
+
@fromEvent(Changed)
|
|
128
|
+
class ClassMapped { @setFrom(Changed) value = ''; }
|
|
129
|
+
const { client, warn, store } = createClient();
|
|
130
|
+
try {
|
|
131
|
+
await client['registerArtifactsForStore'](store, 'new-store');
|
|
132
|
+
expect(warn).not.toHaveBeenCalled();
|
|
133
|
+
void ClassMapped;
|
|
134
|
+
} finally {
|
|
135
|
+
client.dispose();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/chronicle",
|
|
3
|
-
"version": "6.14.
|
|
3
|
+
"version": "6.14.2",
|
|
4
4
|
"description": "Event sourcing for TypeScript and Node.js — the idiomatic client for Cratis Chronicle, the open-source event-sourcing database",
|
|
5
5
|
"author": "Cratis",
|
|
6
6
|
"license": "MIT",
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"build": "tsc -b",
|
|
168
168
|
"clean": "rm -rf dist",
|
|
169
169
|
"test": "vitest run",
|
|
170
|
-
"test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts",
|
|
170
|
+
"test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts",
|
|
171
171
|
"test:native": "node ./scripts/check-native-esm.mjs"
|
|
172
172
|
},
|
|
173
173
|
"dependencies": {
|
|
@@ -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 type { ProjectionDefinition } from '@cratis/chronicle.contracts';
|
|
5
|
+
import type { buildReadModelDefinition } from '../readModels/buildReadModelDefinition.js';
|
|
6
|
+
|
|
7
|
+
/** Connection-independent contracts produced for projection registration. */
|
|
8
|
+
export interface CompiledProjectionDefinitions {
|
|
9
|
+
/** The final wire definitions, including variant cross-wiring and LastUpdated hashes. */
|
|
10
|
+
readonly definitions: ProjectionDefinition[];
|
|
11
|
+
/** Read models registered before the projections. */
|
|
12
|
+
readonly readModels: ReturnType<typeof buildReadModelDefinition>[];
|
|
13
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
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 { readFileSync } from 'node:fs';
|
|
6
|
+
import { field, Constructor } from '@cratis/fundamentals';
|
|
7
|
+
import { chai, describe, it, vi } from 'vitest';
|
|
8
|
+
import type { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
9
|
+
import type { ChronicleConnection } from '../connection/index.js';
|
|
10
|
+
import { eventType } from '../events/eventTypeDecorator.js';
|
|
11
|
+
import { IProjectionBuilderFor } from './declarative/IProjectionBuilderFor.js';
|
|
12
|
+
import { IProjectionFor } from './declarative/IProjectionFor.js';
|
|
13
|
+
import { projection } from './declarative/projection.js';
|
|
14
|
+
import { addFrom } from './modelBound/addFrom.js';
|
|
15
|
+
import { childrenFrom } from './modelBound/childrenFrom.js';
|
|
16
|
+
import { clearWith } from './modelBound/clearWith.js';
|
|
17
|
+
import { count } from './modelBound/count.js';
|
|
18
|
+
import { decrement } from './modelBound/decrement.js';
|
|
19
|
+
import { entersOn } from './modelBound/entersOn.js';
|
|
20
|
+
import { eventSequence } from './modelBound/eventSequence.js';
|
|
21
|
+
import { fromAll } from './modelBound/fromAll.js';
|
|
22
|
+
import { fromEvent } from './modelBound/fromEvent.js';
|
|
23
|
+
import { fromEvery } from './modelBound/fromEvery.js';
|
|
24
|
+
import { globalFor } from './modelBound/globalFor.js';
|
|
25
|
+
import { increment } from './modelBound/increment.js';
|
|
26
|
+
import { isModelBoundProjection } from './modelBound/isModelBoundProjection.js';
|
|
27
|
+
import { join } from './modelBound/join.js';
|
|
28
|
+
import { nested } from './modelBound/nested.js';
|
|
29
|
+
import { noAutoMap } from './modelBound/noAutoMap.js';
|
|
30
|
+
import { notRewindable } from './modelBound/notRewindable.js';
|
|
31
|
+
import { passive } from './modelBound/passive.js';
|
|
32
|
+
import { removedWith } from './modelBound/removedWith.js';
|
|
33
|
+
import { removedWithJoin } from './modelBound/removedWithJoin.js';
|
|
34
|
+
import { setFrom } from './modelBound/setFrom.js';
|
|
35
|
+
import { setFromContext } from './modelBound/setFromContext.js';
|
|
36
|
+
import { setValue } from './modelBound/setValue.js';
|
|
37
|
+
import { subtractFrom } from './modelBound/subtractFrom.js';
|
|
38
|
+
import { variantOf } from './modelBound/variantOf.js';
|
|
39
|
+
import { ProjectionDefinitionCompiler } from './ProjectionDefinitionCompiler.js';
|
|
40
|
+
import { Projections } from './Projections.js';
|
|
41
|
+
|
|
42
|
+
class ItemCreated { name!: string; quantity!: number; }
|
|
43
|
+
eventType()(ItemCreated);
|
|
44
|
+
class ItemUpdated { name!: string; quantity!: number; }
|
|
45
|
+
eventType()(ItemUpdated);
|
|
46
|
+
class ItemRemoved {}
|
|
47
|
+
eventType()(ItemRemoved);
|
|
48
|
+
class DescriptionCleared {}
|
|
49
|
+
eventType()(DescriptionCleared);
|
|
50
|
+
class LineAdded { productId!: string; }
|
|
51
|
+
eventType()(LineAdded);
|
|
52
|
+
class Joined { name!: string; }
|
|
53
|
+
eventType()(Joined);
|
|
54
|
+
class Opened { title!: string; }
|
|
55
|
+
eventType()(Opened);
|
|
56
|
+
class Published { url!: string; }
|
|
57
|
+
eventType()(Published);
|
|
58
|
+
class TitleChanged { title!: string; }
|
|
59
|
+
eventType()(TitleChanged);
|
|
60
|
+
|
|
61
|
+
class Flat { id!: string; name!: string; }
|
|
62
|
+
setFrom(ItemCreated, 'name')(Flat.prototype, 'name');
|
|
63
|
+
fromEvent(ItemCreated)(Flat);
|
|
64
|
+
class AutoMapped { id!: string; name!: string; quantity!: number; }
|
|
65
|
+
noAutoMap(AutoMapped.prototype, 'quantity');
|
|
66
|
+
fromEvent(ItemCreated)(AutoMapped);
|
|
67
|
+
class Totals { id!: string; quantity!: number; removed!: number; count!: number; }
|
|
68
|
+
addFrom(ItemCreated, 'quantity')(Totals.prototype, 'quantity');
|
|
69
|
+
subtractFrom(ItemUpdated, 'quantity')(Totals.prototype, 'removed');
|
|
70
|
+
count(ItemCreated)(Totals.prototype, 'count');
|
|
71
|
+
fromEvent(ItemCreated)(Totals);
|
|
72
|
+
class Removable { id!: string; name!: string; }
|
|
73
|
+
fromEvent(ItemCreated)(Removable);
|
|
74
|
+
removedWith(ItemRemoved)(Removable);
|
|
75
|
+
class Line { productId!: string; }
|
|
76
|
+
class WithChildren { id!: string; lines!: Line[]; }
|
|
77
|
+
childrenFrom(LineAdded, undefined, 'productId')(WithChildren.prototype, 'lines');
|
|
78
|
+
field(Array, { enumerable: true, genericArguments: [Line] })(WithChildren.prototype, 'lines');
|
|
79
|
+
fromEvent(ItemCreated)(WithChildren);
|
|
80
|
+
class WithJoin { id!: string; name!: string; }
|
|
81
|
+
join(Joined, 'name')(WithJoin.prototype, 'name');
|
|
82
|
+
fromEvent(ItemCreated)(WithJoin);
|
|
83
|
+
class Every { id!: string; name!: string; }
|
|
84
|
+
fromEvery('name')(Every.prototype, 'name');
|
|
85
|
+
fromEvent(ItemCreated)(Every);
|
|
86
|
+
|
|
87
|
+
class PassiveModel { id!: string; name!: string; }
|
|
88
|
+
field(String)(PassiveModel.prototype, 'name');
|
|
89
|
+
setFrom(ItemCreated, 'name')(PassiveModel.prototype, 'name');
|
|
90
|
+
fromEvent(ItemCreated)(PassiveModel);
|
|
91
|
+
passive(PassiveModel);
|
|
92
|
+
|
|
93
|
+
class Detail { description!: string; }
|
|
94
|
+
setFrom(ItemCreated, 'name')(Detail.prototype, 'description');
|
|
95
|
+
clearWith(DescriptionCleared)(Detail.prototype, 'description');
|
|
96
|
+
fromEvent(ItemCreated)(Detail);
|
|
97
|
+
clearWith(ItemRemoved)(Detail);
|
|
98
|
+
class WithNested { id!: string; detail!: Detail; title!: string; }
|
|
99
|
+
field(Detail)(WithNested.prototype, 'detail');
|
|
100
|
+
nested(WithNested.prototype, 'detail');
|
|
101
|
+
clearWith(ItemUpdated)(WithNested.prototype, 'detail');
|
|
102
|
+
clearWith(ItemRemoved)(WithNested.prototype, 'title');
|
|
103
|
+
fromEvent(ItemCreated)(WithNested);
|
|
104
|
+
|
|
105
|
+
class WithRemovedJoin { id!: string; name!: string; }
|
|
106
|
+
removedWithJoin(ItemRemoved, 'productId')(WithRemovedJoin);
|
|
107
|
+
removedWithJoin(TitleChanged, 'title')(WithRemovedJoin.prototype, 'name');
|
|
108
|
+
fromEvent(ItemCreated)(WithRemovedJoin);
|
|
109
|
+
|
|
110
|
+
class CustomKeys { id!: string; }
|
|
111
|
+
fromEvent(ItemCreated, { key: 'productId', parentKey: 'ownerId' })(CustomKeys);
|
|
112
|
+
class ConstantKeys { id!: string; }
|
|
113
|
+
fromEvent(ItemCreated, { constantKey: 'one', parentKey: 'ownerId' })(ConstantKeys);
|
|
114
|
+
|
|
115
|
+
class NoMapping { id!: string; name!: string; }
|
|
116
|
+
noAutoMap(NoMapping);
|
|
117
|
+
fromEvent(ItemCreated)(NoMapping);
|
|
118
|
+
class NoRewind { id!: string; }
|
|
119
|
+
notRewindable(NoRewind);
|
|
120
|
+
fromEvent(ItemCreated)(NoRewind);
|
|
121
|
+
class OtherSequence { id!: string; }
|
|
122
|
+
eventSequence('custom-sequence')(OtherSequence);
|
|
123
|
+
fromEvent(ItemCreated)(OtherSequence);
|
|
124
|
+
|
|
125
|
+
class MappingOptions { id!: string; context!: string; state!: string; increments!: number; decrements!: number; all!: string; }
|
|
126
|
+
field(String)(MappingOptions.prototype, 'context');
|
|
127
|
+
field(Number)(MappingOptions.prototype, 'increments');
|
|
128
|
+
setFromContext(ItemCreated, 'eventSourceId')(MappingOptions.prototype, 'context');
|
|
129
|
+
setValue(ItemUpdated, 'ready')(MappingOptions.prototype, 'state');
|
|
130
|
+
increment(ItemCreated)(MappingOptions.prototype, 'increments');
|
|
131
|
+
decrement(ItemUpdated)(MappingOptions.prototype, 'decrements');
|
|
132
|
+
fromAll('name')(MappingOptions.prototype, 'all');
|
|
133
|
+
fromEvent(ItemCreated)(MappingOptions);
|
|
134
|
+
|
|
135
|
+
class Identity {}
|
|
136
|
+
class Draft { id!: string; title!: string; }
|
|
137
|
+
setFrom(Opened, 'title')(Draft.prototype, 'title');
|
|
138
|
+
variantOf(Identity, 'id')(Draft);
|
|
139
|
+
entersOn(Opened)(Draft);
|
|
140
|
+
fromEvent(Opened)(Draft);
|
|
141
|
+
class Public { id!: string; url!: string; title!: string; }
|
|
142
|
+
setFrom(Published, 'url')(Public.prototype, 'url');
|
|
143
|
+
setFrom(TitleChanged, 'title')(Public.prototype, 'title');
|
|
144
|
+
variantOf(Identity, 'id')(Public);
|
|
145
|
+
entersOn(Published)(Public);
|
|
146
|
+
fromEvent(Published)(Public);
|
|
147
|
+
fromEvent(TitleChanged)(Public);
|
|
148
|
+
class KeyedVariant { id!: string; title!: string; }
|
|
149
|
+
variantOf(Identity, 'id')(KeyedVariant);
|
|
150
|
+
entersOn(Opened, 'productId')(KeyedVariant);
|
|
151
|
+
fromEvent(Opened)(KeyedVariant);
|
|
152
|
+
class SharedTitle { title!: string; }
|
|
153
|
+
setFrom(TitleChanged, 'title')(SharedTitle.prototype, 'title');
|
|
154
|
+
globalFor(Identity)(SharedTitle);
|
|
155
|
+
|
|
156
|
+
class Declarative { id!: string; name!: string; quantity!: number; lines!: Line[]; }
|
|
157
|
+
class DeclarativeProjection implements IProjectionFor<Declarative> {
|
|
158
|
+
define(builder: IProjectionBuilderFor<Declarative>): void {
|
|
159
|
+
builder.from(ItemCreated, from => from.set(model => model.name).to(event => event.name))
|
|
160
|
+
.join(Joined, joined => joined.on(model => model.name).set(model => model.name).to(event => event.name))
|
|
161
|
+
.fromEvery(every => every.set(model => model.name).toEventContextProperty('eventType'))
|
|
162
|
+
.removedWith(ItemRemoved)
|
|
163
|
+
.children<Line>(model => model.lines, children => children.from(LineAdded))
|
|
164
|
+
.autoMap();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
projection('Declarative', Declarative)(DeclarativeProjection);
|
|
168
|
+
class AnotherDeclarativeProjection implements IProjectionFor<Declarative> {
|
|
169
|
+
define(builder: IProjectionBuilderFor<Declarative>): void {
|
|
170
|
+
builder.from(ItemUpdated);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
projection('AnotherDeclarative', Declarative)(AnotherDeclarativeProjection);
|
|
174
|
+
|
|
175
|
+
class DeclarativeDraft { id!: string; title!: string; }
|
|
176
|
+
class DeclarativeDraftProjection implements IProjectionFor<DeclarativeDraft> {
|
|
177
|
+
define(builder: IProjectionBuilderFor<DeclarativeDraft>): void {
|
|
178
|
+
builder.variantOf(Identity, model => model.id).entersOn(Opened).from(Opened);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
projection('DeclarativeDraft', DeclarativeDraft)(DeclarativeDraftProjection);
|
|
182
|
+
class DeclarativePublic { id!: string; url!: string; }
|
|
183
|
+
class DeclarativePublicProjection implements IProjectionFor<DeclarativePublic> {
|
|
184
|
+
define(builder: IProjectionBuilderFor<DeclarativePublic>): void {
|
|
185
|
+
builder.variantOf(Identity, model => model.id).entersOn(Published).from(Published).from(TitleChanged);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
projection('DeclarativePublic', DeclarativePublic)(DeclarativePublicProjection);
|
|
189
|
+
|
|
190
|
+
class Inferred { uniqueTitle!: string; }
|
|
191
|
+
field(String)(Inferred.prototype, 'uniqueTitle');
|
|
192
|
+
class InferredProjection implements IProjectionFor<Inferred> {
|
|
193
|
+
define(builder: IProjectionBuilderFor<Inferred>): void {
|
|
194
|
+
builder.from(Opened, from => from.set(model => model.uniqueTitle).to(event => event.title));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
projection()(InferredProjection);
|
|
198
|
+
|
|
199
|
+
const cases = [
|
|
200
|
+
{ name: 'flat', readModels: [Flat], projections: [], globalForHandlers: [] },
|
|
201
|
+
{ name: 'passive-with-fields', readModels: [PassiveModel], projections: [], globalForHandlers: [] },
|
|
202
|
+
{ name: 'nested-and-clear', readModels: [WithNested], projections: [], globalForHandlers: [] },
|
|
203
|
+
{ name: 'removed-with-join', readModels: [WithRemovedJoin], projections: [], globalForHandlers: [] },
|
|
204
|
+
{ name: 'custom-keys', readModels: [CustomKeys, ConstantKeys], projections: [], globalForHandlers: [] },
|
|
205
|
+
{ name: 'no-auto-map', readModels: [NoMapping], projections: [], globalForHandlers: [] },
|
|
206
|
+
{ name: 'not-rewindable', readModels: [NoRewind], projections: [], globalForHandlers: [] },
|
|
207
|
+
{ name: 'event-sequence', readModels: [OtherSequence], projections: [], globalForHandlers: [] },
|
|
208
|
+
{ name: 'mapping-options-with-fields', readModels: [MappingOptions], projections: [], globalForHandlers: [] },
|
|
209
|
+
{ name: 'enters-on-key', readModels: [KeyedVariant], projections: [], globalForHandlers: [] },
|
|
210
|
+
{ name: 'declarative-inferred-with-fields', readModels: [Inferred], projections: [InferredProjection], globalForHandlers: [] },
|
|
211
|
+
{ name: 'automap-exclusion', readModels: [AutoMapped], projections: [], globalForHandlers: [] },
|
|
212
|
+
{ name: 'arithmetic', readModels: [Totals], projections: [], globalForHandlers: [] },
|
|
213
|
+
{ name: 'removed-with', readModels: [Removable], projections: [], globalForHandlers: [] },
|
|
214
|
+
{ name: 'children', readModels: [WithChildren], projections: [], globalForHandlers: [] },
|
|
215
|
+
{ name: 'join', readModels: [WithJoin], projections: [], globalForHandlers: [] },
|
|
216
|
+
{ name: 'from-every', readModels: [Every], projections: [], globalForHandlers: [] },
|
|
217
|
+
{ name: 'model-bound-variants', readModels: [Draft, Public], projections: [], globalForHandlers: [SharedTitle] },
|
|
218
|
+
{ name: 'declarative', readModels: [Declarative], projections: [DeclarativeProjection], globalForHandlers: [] },
|
|
219
|
+
{ name: 'declarative-variants', readModels: [DeclarativeDraft, DeclarativePublic], projections: [DeclarativeDraftProjection, DeclarativePublicProjection], globalForHandlers: [] }
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
function artifactsFor(testCase: typeof cases[number]): IClientArtifactsProvider {
|
|
223
|
+
return {
|
|
224
|
+
projections: testCase.projections as Constructor[],
|
|
225
|
+
readModels: testCase.readModels as Constructor[],
|
|
226
|
+
globalForHandlers: testCase.globalForHandlers as Constructor[],
|
|
227
|
+
eventTypes: [ItemCreated, ItemUpdated, ItemRemoved, LineAdded, Joined, Opened, Published, TitleChanged],
|
|
228
|
+
reactors: [], reducers: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function serializeContract(value: unknown): string {
|
|
233
|
+
return JSON.stringify(value, (_key, member: unknown) => typeof member === 'bigint' ? member.toString() : member);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function captureRegistration(artifacts: IClientArtifactsProvider): Promise<string> {
|
|
237
|
+
const registerMock = vi.fn().mockResolvedValue(undefined);
|
|
238
|
+
const registerManyMock = vi.fn().mockResolvedValue(undefined);
|
|
239
|
+
const connection = {
|
|
240
|
+
projections: { register: registerMock },
|
|
241
|
+
readModels: { registerMany: registerManyMock }
|
|
242
|
+
} as unknown as ChronicleConnection;
|
|
243
|
+
await new Projections('test-store', 'test-namespace', connection, artifacts, 'test-sink').register();
|
|
244
|
+
return serializeContract({
|
|
245
|
+
readModels: registerManyMock.mock.calls.map(call => call[0].ReadModels).flat(),
|
|
246
|
+
projections: registerMock.mock.calls.map(call => call[0].Projections).flat()
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
chai.should();
|
|
251
|
+
|
|
252
|
+
const goldenUrl = new URL('./ProjectionDefinitionCompiler.registration.golden.json', import.meta.url);
|
|
253
|
+
|
|
254
|
+
describe('projection registration payload', () => {
|
|
255
|
+
for (const testCase of cases) {
|
|
256
|
+
it(`should match the registration golden for ${testCase.name}`, async () => {
|
|
257
|
+
const actual = await captureRegistration(artifactsFor(testCase));
|
|
258
|
+
const goldens = JSON.parse(readFileSync(goldenUrl, 'utf8')) as Array<{ name: string; payload: string }>;
|
|
259
|
+
actual.should.equal(goldens.find(candidate => candidate.name === testCase.name)?.payload);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it(`should send the compiler's definitions and read-model schemas for ${testCase.name}`, async () => {
|
|
263
|
+
const artifacts = artifactsFor(testCase);
|
|
264
|
+
const compiled = new ProjectionDefinitionCompiler(artifacts, 'test-sink').compile(
|
|
265
|
+
artifacts.projections,
|
|
266
|
+
artifacts.readModels.filter(isModelBoundProjection)
|
|
267
|
+
);
|
|
268
|
+
const compiledPayload = serializeContract({ readModels: compiled.readModels, projections: compiled.definitions });
|
|
269
|
+
compiledPayload.should.equal(await captureRegistration(artifacts));
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
it('should reject multiple projections for one read model', () => {
|
|
274
|
+
const artifacts = artifactsFor(cases.find(testCase => testCase.name === 'declarative')!);
|
|
275
|
+
(() => new ProjectionDefinitionCompiler(artifacts, 'test-sink').compile(
|
|
276
|
+
[DeclarativeProjection, AnotherDeclarativeProjection], []
|
|
277
|
+
)).should.throw("Read model id 'Declarative' has multiple projections.");
|
|
278
|
+
});
|
|
279
|
+
});
|