@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,108 @@
|
|
|
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 { describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { DefaultClientArtifactsProvider } from '../artifacts/DefaultClientArtifactsProvider.js';
|
|
7
|
+
import type { ChronicleConnection } from '../connection/ChronicleConnection.js';
|
|
8
|
+
import { eventType } from '../events/eventTypeDecorator.js';
|
|
9
|
+
import { Projections } from '../projections/Projections.js';
|
|
10
|
+
import { setFrom } from '../projections/modelBound/setFrom.js';
|
|
11
|
+
import { DecoratorType } from '../types/DecoratorType.js';
|
|
12
|
+
import { TypeDiscoverer } from '../types/TypeDiscoverer.js';
|
|
13
|
+
import { ReadModels } from './ReadModels.js';
|
|
14
|
+
|
|
15
|
+
@eventType('cold-registration-event')
|
|
16
|
+
class Changed { @field(String) value!: string; }
|
|
17
|
+
|
|
18
|
+
let constructions = 0;
|
|
19
|
+
class PropertyOnly {
|
|
20
|
+
@setFrom(Changed, 'value')
|
|
21
|
+
@field(String)
|
|
22
|
+
value = '';
|
|
23
|
+
|
|
24
|
+
constructor() { constructions++; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('when discovering a standard-decorated property-only read model', () => {
|
|
28
|
+
it('should register and query the model before any instance has been constructed', async () => {
|
|
29
|
+
expect(constructions).toBe(0);
|
|
30
|
+
const discoverer = new TypeDiscoverer(async () => ['models.ts'], async () => ({ PropertyOnly }));
|
|
31
|
+
await discoverer.discover('models.ts');
|
|
32
|
+
expect(constructions).toBe(0);
|
|
33
|
+
|
|
34
|
+
const provider = new DefaultClientArtifactsProvider(discoverer);
|
|
35
|
+
const registerMany = vi.fn().mockResolvedValue({});
|
|
36
|
+
const register = vi.fn().mockResolvedValue({});
|
|
37
|
+
const getInstanceByKey = vi.fn().mockResolvedValue({ ReadModel: '{"value":"stored"}' });
|
|
38
|
+
const connection = { readModels: { registerMany, getInstanceByKey }, projections: { register } } as unknown as ChronicleConnection;
|
|
39
|
+
const projections = new Projections('store', 'Default', connection, provider, 'sink');
|
|
40
|
+
await projections.register();
|
|
41
|
+
const readModels = new ReadModels('store', 'Default', connection, provider, 'sink', type => projections.hasForModel(type));
|
|
42
|
+
const result = await readModels.findInstanceById(PropertyOnly, 'id');
|
|
43
|
+
|
|
44
|
+
expect(registerMany.mock.calls[0][0].ReadModels.some((model: { Type: { Identifier: string } }) => model.Type.Identifier === 'PropertyOnly')).toBe(true);
|
|
45
|
+
expect(register.mock.calls.some(call => call[0].Projections[0].ReadModel === 'PropertyOnly')).toBe(true);
|
|
46
|
+
expect(getInstanceByKey).toHaveBeenCalledWith(expect.objectContaining({ ReadModelIdentifier: 'PropertyOnly', ReadModelKey: 'id' }));
|
|
47
|
+
expect(result?.value).toBe('stored');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should register an explicitly supplied model without glob discovery or construction', async () => {
|
|
51
|
+
class ExplicitModel {
|
|
52
|
+
@setFrom(Changed)
|
|
53
|
+
@field(String)
|
|
54
|
+
value = '';
|
|
55
|
+
}
|
|
56
|
+
const discoverer = new TypeDiscoverer();
|
|
57
|
+
discoverer.register(DecoratorType.ReadModel, ExplicitModel);
|
|
58
|
+
const provider = new DefaultClientArtifactsProvider(discoverer);
|
|
59
|
+
const registerMany = vi.fn().mockResolvedValue({});
|
|
60
|
+
const register = vi.fn().mockResolvedValue({});
|
|
61
|
+
const connection = { readModels: { registerMany }, projections: { register } } as unknown as ChronicleConnection;
|
|
62
|
+
|
|
63
|
+
await new Projections('store', 'Default', connection, provider, 'sink').register();
|
|
64
|
+
|
|
65
|
+
expect(registerMany.mock.calls[0][0].ReadModels.some((model: { Type: { Identifier: string } }) => model.Type.Identifier === 'ExplicitModel')).toBe(true);
|
|
66
|
+
expect(register.mock.calls.some(call => call[0].Projections[0].ReadModel === 'ExplicitModel')).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should not register the same model twice when constructed before discovery', async () => {
|
|
70
|
+
class ConstructedFirst {
|
|
71
|
+
@setFrom(Changed)
|
|
72
|
+
@field(String)
|
|
73
|
+
value = '';
|
|
74
|
+
}
|
|
75
|
+
new ConstructedFirst();
|
|
76
|
+
const discoverer = new TypeDiscoverer(async () => ['models.ts'], async () => ({ ConstructedFirst }));
|
|
77
|
+
await discoverer.discover('models.ts');
|
|
78
|
+
const provider = new DefaultClientArtifactsProvider(discoverer);
|
|
79
|
+
expect(provider.readModels.filter(type => type === ConstructedFirst)).toHaveLength(1);
|
|
80
|
+
|
|
81
|
+
const registerMany = vi.fn().mockResolvedValue({});
|
|
82
|
+
const register = vi.fn().mockResolvedValue({});
|
|
83
|
+
const connection = { readModels: { registerMany }, projections: { register } } as unknown as ChronicleConnection;
|
|
84
|
+
await new Projections('store', 'Default', connection, provider, 'sink').register();
|
|
85
|
+
|
|
86
|
+
expect(registerMany.mock.calls[0][0].ReadModels.filter((model: { Type: { Identifier: string } }) => model.Type.Identifier === 'ConstructedFirst')).toHaveLength(1);
|
|
87
|
+
expect(register.mock.calls.filter(call => call[0].Projections[0].ReadModel === 'ConstructedFirst')).toHaveLength(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should not register the same model again after its first construction', async () => {
|
|
91
|
+
const discoverer = new TypeDiscoverer(async () => ['models.ts'], async () => ({ PropertyOnly }));
|
|
92
|
+
await discoverer.discover('models.ts');
|
|
93
|
+
const provider = new DefaultClientArtifactsProvider(discoverer);
|
|
94
|
+
const registerMany = vi.fn().mockResolvedValue({});
|
|
95
|
+
const register = vi.fn().mockResolvedValue({});
|
|
96
|
+
const connection = { readModels: { registerMany }, projections: { register } } as unknown as ChronicleConnection;
|
|
97
|
+
await new Projections('store', 'Default', connection, provider, 'sink').register();
|
|
98
|
+
const registered = registerMany.mock.calls.length;
|
|
99
|
+
const registeredProjections = register.mock.calls.length;
|
|
100
|
+
|
|
101
|
+
new PropertyOnly();
|
|
102
|
+
new PropertyOnly();
|
|
103
|
+
|
|
104
|
+
expect(provider.readModels.filter(type => type === PropertyOnly)).toHaveLength(1);
|
|
105
|
+
expect(registerMany).toHaveBeenCalledTimes(registered);
|
|
106
|
+
expect(register).toHaveBeenCalledTimes(registeredProjections);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -13,8 +13,7 @@ import { isNested } from '../projections/modelBound/nested.js';
|
|
|
13
13
|
import { getReducerMetadata } from '../reducers/reducer.js';
|
|
14
14
|
import { TypeIntrospector } from '../types/TypeIntrospector.js';
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
export function rootReadModelTypes(artifacts: IClientArtifactsProvider): Constructor[] {
|
|
16
|
+
function inspectReadModelTypes(artifacts: IClientArtifactsProvider): { models: Constructor[]; usedAsChildren: Set<Function>; visited: Set<Function> } {
|
|
18
17
|
const models = artifacts.readModels;
|
|
19
18
|
const usedAsChildren = new Set<Function>();
|
|
20
19
|
const visited = new Set<Function>();
|
|
@@ -61,5 +60,16 @@ export function rootReadModelTypes(artifacts: IClientArtifactsProvider): Constru
|
|
|
61
60
|
const model = getReducerMetadata(type)?.readModel;
|
|
62
61
|
if (model) inspect(model);
|
|
63
62
|
}
|
|
63
|
+
return { models, usedAsChildren, visited };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Excludes models used as children or nested objects from root projection registration. */
|
|
67
|
+
export function rootReadModelTypes(artifacts: IClientArtifactsProvider): Constructor[] {
|
|
68
|
+
const { models, usedAsChildren } = inspectReadModelTypes(artifacts);
|
|
64
69
|
return models.filter(type => !usedAsChildren.has(type));
|
|
65
70
|
}
|
|
71
|
+
|
|
72
|
+
/** Includes all read-model types reached from registered roots, including children and nested models. */
|
|
73
|
+
export function reachableReadModelTypes(artifacts: IClientArtifactsProvider): Function[] {
|
|
74
|
+
return [...inspectReadModelTypes(artifacts).visited];
|
|
75
|
+
}
|
package/types/TypeDiscoverer.ts
CHANGED
|
@@ -6,17 +6,18 @@ import { DecoratorType } from './DecoratorType.js';
|
|
|
6
6
|
import { Constructor } from '@cratis/fundamentals';
|
|
7
7
|
import { TypeIntrospector } from './TypeIntrospector.js';
|
|
8
8
|
import { hasPropertyMetadata } from './propertyDecoratorMetadata.js';
|
|
9
|
+
import { hasModelBoundMetadata, modelBoundPropertyKeys } from './modelBoundPropertyMetadata.js';
|
|
10
|
+
import { getStandardMetadata, hasOwnStandardMetadata } from './standardDecoratorMetadata.js';
|
|
9
11
|
|
|
10
12
|
type GlobFunction = (pattern: string | string[], options?: { ignore: string[] }) => Promise<string[]>;
|
|
11
13
|
type FileImporter = (filePath: string) => Promise<unknown>;
|
|
12
14
|
|
|
13
|
-
const modelBoundPropertyKeys = [
|
|
14
|
-
'setFrom', 'setFromContext', 'setValue', 'addFrom', 'subtractFrom',
|
|
15
|
-
'increment', 'decrement', 'count', 'childrenFrom', 'join', 'fromEvery', 'fromAll'
|
|
16
|
-
].map(name => `chronicle:projection:${name}`);
|
|
17
|
-
|
|
18
15
|
/** Identifies models whose event mappings live on their properties rather than on @fromEvent. */
|
|
19
16
|
export function hasModelBoundProperties(type: Function): boolean {
|
|
17
|
+
// A standard field decorator can mark its metadata before any instances exist. Once the
|
|
18
|
+
// class is evaluated, Symbol.metadata links that object back to the exported constructor.
|
|
19
|
+
const metadata = hasOwnStandardMetadata(type) ? getStandardMetadata(type) : undefined;
|
|
20
|
+
if (metadata && hasModelBoundMetadata(metadata)) return true;
|
|
20
21
|
return TypeIntrospector.getTrackedProperties(type).some(property =>
|
|
21
22
|
modelBoundPropertyKeys.some(key => hasPropertyMetadata(key, type.prototype, property)));
|
|
22
23
|
}
|
|
@@ -5,6 +5,7 @@ import type { Constructor } from '@cratis/fundamentals';
|
|
|
5
5
|
import { TypeDiscoverer } from './TypeDiscoverer.js';
|
|
6
6
|
import { ChronicleClassOrPropertyDecorator, ChroniclePropertyDecorator, decorateClassOrProperty, decorateProperty } from './propertyDecoratorMetadata.js';
|
|
7
7
|
import { getStandardMetadata, hasOwnStandardMetadata } from './standardDecoratorMetadata.js';
|
|
8
|
+
import { trackModelBoundMetadata } from './modelBoundPropertyMetadata.js';
|
|
8
9
|
|
|
9
10
|
const registered = new WeakSet<Function>();
|
|
10
11
|
|
|
@@ -28,9 +29,10 @@ export function decorateModelBoundProperty(legacy: PropertyDecorator): Chronicle
|
|
|
28
29
|
return (target: object | undefined, keyOrContext: string | symbol | ClassFieldDecoratorContext) => {
|
|
29
30
|
decorate(target as object, keyOrContext as string);
|
|
30
31
|
if (typeof keyOrContext === 'object') {
|
|
31
|
-
// Standard field decorators do not receive the class constructor.
|
|
32
|
-
//
|
|
32
|
+
// Standard field decorators do not receive the class constructor. Discovery resolves
|
|
33
|
+
// the metadata against exported classes; the initializer remains a fallback.
|
|
33
34
|
const metadata = keyOrContext.metadata;
|
|
35
|
+
if (metadata) trackModelBoundMetadata(metadata, keyOrContext.name as string);
|
|
34
36
|
keyOrContext.addInitializer(function () { if (metadata) registerDeclaringClass(this as object, metadata); });
|
|
35
37
|
} else {
|
|
36
38
|
register((target as { constructor: Function }).constructor);
|
|
@@ -45,6 +47,7 @@ export function decorateModelBoundClassOrProperty(legacy: (target: object, prope
|
|
|
45
47
|
decorate(target as object, keyOrContext as string);
|
|
46
48
|
if (typeof keyOrContext === 'object' && keyOrContext?.kind === 'field') {
|
|
47
49
|
const metadata = keyOrContext.metadata;
|
|
50
|
+
if (metadata) trackModelBoundMetadata(metadata, keyOrContext.name as string);
|
|
48
51
|
keyOrContext.addInitializer(function () { if (metadata) registerDeclaringClass(this as object, metadata); });
|
|
49
52
|
} else if (typeof keyOrContext === 'string' || typeof keyOrContext === 'symbol') {
|
|
50
53
|
register((target as { constructor: Function }).constructor);
|
|
@@ -0,0 +1,67 @@
|
|
|
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 { getStandardMetadata } from './standardDecoratorMetadata.js';
|
|
6
|
+
|
|
7
|
+
// The standard field decorator has no constructor, but its metadata object is attached to
|
|
8
|
+
// the declaring class after evaluation. Discovery can match that object to a module export.
|
|
9
|
+
const mappedMetadata = new WeakMap<object, Set<string>>();
|
|
10
|
+
const pendingMetadata = new Set<WeakRef<object>>();
|
|
11
|
+
const finalizedMetadata = new FinalizationRegistry<WeakRef<object>>(reference => pendingMetadata.delete(reference));
|
|
12
|
+
|
|
13
|
+
export const modelBoundPropertyKeys = [
|
|
14
|
+
'setFrom', 'setFromContext', 'setValue', 'addFrom', 'subtractFrom',
|
|
15
|
+
'increment', 'decrement', 'count', 'childrenFrom', 'join', 'fromEvery', 'fromAll'
|
|
16
|
+
].map(name => `chronicle:projection:${name}`);
|
|
17
|
+
|
|
18
|
+
/** Records a standard-decorated class with an event mapping on one of its fields. */
|
|
19
|
+
export function trackModelBoundMetadata(metadata: object, property: string): void {
|
|
20
|
+
if (!modelBoundPropertyKeys.some(key => Reflect.hasOwnMetadata(key, metadata, property))) return;
|
|
21
|
+
let properties = mappedMetadata.get(metadata);
|
|
22
|
+
if (!properties) {
|
|
23
|
+
properties = new Set<string>();
|
|
24
|
+
mappedMetadata.set(metadata, properties);
|
|
25
|
+
const reference = new WeakRef(metadata);
|
|
26
|
+
pendingMetadata.add(reference);
|
|
27
|
+
finalizedMetadata.register(metadata, reference, reference);
|
|
28
|
+
}
|
|
29
|
+
properties.add(property);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Whether a class's standard decorator metadata contains a mapped property. */
|
|
33
|
+
export function hasModelBoundMetadata(metadata: object): boolean {
|
|
34
|
+
return mappedMetadata.has(metadata);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Returns unresolved mappings grouped by class metadata, dropping resolved and collected entries. */
|
|
38
|
+
export function takeUnregisteredModelBoundMappings(registeredTypes: Function[]): string[] {
|
|
39
|
+
const registeredMetadata = new Set<object>();
|
|
40
|
+
for (const type of registeredTypes) {
|
|
41
|
+
for (let metadata: object | null | undefined = getStandardMetadata(type); metadata; metadata = Object.getPrototypeOf(metadata) as object | null) {
|
|
42
|
+
registeredMetadata.add(metadata);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const unmapped: string[] = [];
|
|
46
|
+
for (const reference of pendingMetadata) {
|
|
47
|
+
const metadata = reference.deref();
|
|
48
|
+
pendingMetadata.delete(reference);
|
|
49
|
+
finalizedMetadata.unregister(reference);
|
|
50
|
+
if (!metadata || registeredMetadata.has(metadata)) continue;
|
|
51
|
+
const mappingsForClass = new Set<string>();
|
|
52
|
+
for (const property of mappedMetadata.get(metadata) ?? []) {
|
|
53
|
+
for (const key of modelBoundPropertyKeys) {
|
|
54
|
+
const value = Reflect.getOwnMetadata(key, metadata, property) as unknown;
|
|
55
|
+
if (value === undefined) continue;
|
|
56
|
+
const mappings: unknown[] = Array.isArray(value) ? value : [value];
|
|
57
|
+
for (const mapping of mappings) {
|
|
58
|
+
const eventType = mapping && typeof mapping === 'object' && 'eventType' in mapping ? mapping.eventType : undefined;
|
|
59
|
+
const eventName = typeof eventType === 'function' ? eventType.name : 'all events';
|
|
60
|
+
mappingsForClass.add(`${property} <- ${eventName}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (mappingsForClass.size > 0) unmapped.push(`{${[...mappingsForClass].join(', ')}}`);
|
|
65
|
+
}
|
|
66
|
+
return unmapped;
|
|
67
|
+
}
|