@cratis/chronicle 6.13.0 → 6.14.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.
Files changed (70) hide show
  1. package/ChronicleClient.ts +11 -0
  2. package/dist/ChronicleClient.d.ts.map +1 -1
  3. package/dist/ChronicleClient.js +7 -0
  4. package/dist/ChronicleClient.js.map +1 -1
  5. package/dist/readModels/ReadModels.js +1 -1
  6. package/dist/readModels/ReadModels.js.map +1 -1
  7. package/dist/readModels/rootReadModelTypes.d.ts +2 -0
  8. package/dist/readModels/rootReadModelTypes.d.ts.map +1 -1
  9. package/dist/readModels/rootReadModelTypes.js +10 -2
  10. package/dist/readModels/rootReadModelTypes.js.map +1 -1
  11. package/dist/reducers/ReducerEventDispatcher.d.ts +14 -0
  12. package/dist/reducers/ReducerEventDispatcher.d.ts.map +1 -0
  13. package/dist/reducers/ReducerEventDispatcher.js +35 -0
  14. package/dist/reducers/ReducerEventDispatcher.js.map +1 -0
  15. package/dist/reducers/ReducerEventHandler.d.ts +7 -0
  16. package/dist/reducers/ReducerEventHandler.d.ts.map +1 -0
  17. package/dist/reducers/ReducerEventHandler.js +4 -0
  18. package/dist/reducers/ReducerEventHandler.js.map +1 -0
  19. package/dist/reducers/Reducers.d.ts +0 -1
  20. package/dist/reducers/Reducers.d.ts.map +1 -1
  21. package/dist/reducers/Reducers.js +11 -30
  22. package/dist/reducers/Reducers.js.map +1 -1
  23. package/dist/testing/ReadModelScenario.d.ts +33 -0
  24. package/dist/testing/ReadModelScenario.d.ts.map +1 -0
  25. package/dist/testing/ReadModelScenario.js +137 -0
  26. package/dist/testing/ReadModelScenario.js.map +1 -0
  27. package/dist/testing/ReadModelScenarioGiven.d.ts +11 -0
  28. package/dist/testing/ReadModelScenarioGiven.d.ts.map +1 -0
  29. package/dist/testing/ReadModelScenarioGiven.js +18 -0
  30. package/dist/testing/ReadModelScenarioGiven.js.map +1 -0
  31. package/dist/testing/ReadModelScenarioGivenBuilder.d.ts +10 -0
  32. package/dist/testing/ReadModelScenarioGivenBuilder.d.ts.map +1 -0
  33. package/dist/testing/ReadModelScenarioGivenBuilder.js +15 -0
  34. package/dist/testing/ReadModelScenarioGivenBuilder.js.map +1 -0
  35. package/dist/testing/index.d.ts +4 -0
  36. package/dist/testing/index.d.ts.map +1 -0
  37. package/dist/testing/index.js +6 -0
  38. package/dist/testing/index.js.map +1 -0
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/dist/types/TypeDiscoverer.d.ts.map +1 -1
  41. package/dist/types/TypeDiscoverer.js +7 -4
  42. package/dist/types/TypeDiscoverer.js.map +1 -1
  43. package/dist/types/modelBoundProperty.d.ts.map +1 -1
  44. package/dist/types/modelBoundProperty.js +7 -2
  45. package/dist/types/modelBoundProperty.js.map +1 -1
  46. package/dist/types/modelBoundPropertyMetadata.d.ts +9 -0
  47. package/dist/types/modelBoundPropertyMetadata.d.ts.map +1 -0
  48. package/dist/types/modelBoundPropertyMetadata.js +66 -0
  49. package/dist/types/modelBoundPropertyMetadata.js.map +1 -0
  50. package/for_ChronicleClient/when_registering_artifacts/with_legacy_property_mappings.legacy.spec.ts +31 -0
  51. package/for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts +138 -0
  52. package/package.json +7 -2
  53. package/projections/modelBound/decorators.standard.spec.ts +2 -2
  54. package/readModels/ReadModels.ts +1 -1
  55. package/readModels/discovery.spec.ts +2 -2
  56. package/readModels/registration.legacy.spec.ts +38 -0
  57. package/readModels/registration.standard.spec.ts +108 -0
  58. package/readModels/rootReadModelTypes.ts +12 -2
  59. package/reducers/ReducerEventDispatcher.ts +47 -0
  60. package/reducers/ReducerEventHandler.ts +9 -0
  61. package/reducers/Reducers.ts +13 -42
  62. package/testing/ReadModelScenario.example.ts +29 -0
  63. package/testing/ReadModelScenario.spec.ts +191 -0
  64. package/testing/ReadModelScenario.ts +151 -0
  65. package/testing/ReadModelScenarioGiven.ts +16 -0
  66. package/testing/ReadModelScenarioGivenBuilder.ts +15 -0
  67. package/testing/index.ts +6 -0
  68. package/types/TypeDiscoverer.ts +6 -5
  69. package/types/modelBoundProperty.ts +5 -2
  70. package/types/modelBoundPropertyMetadata.ts +67 -0
@@ -0,0 +1,66 @@
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
+ import 'reflect-metadata';
4
+ import { getStandardMetadata } from './standardDecoratorMetadata.js';
5
+ // The standard field decorator has no constructor, but its metadata object is attached to
6
+ // the declaring class after evaluation. Discovery can match that object to a module export.
7
+ const mappedMetadata = new WeakMap();
8
+ const pendingMetadata = new Set();
9
+ const finalizedMetadata = new FinalizationRegistry(reference => pendingMetadata.delete(reference));
10
+ export const modelBoundPropertyKeys = [
11
+ 'setFrom', 'setFromContext', 'setValue', 'addFrom', 'subtractFrom',
12
+ 'increment', 'decrement', 'count', 'childrenFrom', 'join', 'fromEvery', 'fromAll'
13
+ ].map(name => `chronicle:projection:${name}`);
14
+ /** Records a standard-decorated class with an event mapping on one of its fields. */
15
+ export function trackModelBoundMetadata(metadata, property) {
16
+ if (!modelBoundPropertyKeys.some(key => Reflect.hasOwnMetadata(key, metadata, property)))
17
+ return;
18
+ let properties = mappedMetadata.get(metadata);
19
+ if (!properties) {
20
+ properties = new Set();
21
+ mappedMetadata.set(metadata, properties);
22
+ const reference = new WeakRef(metadata);
23
+ pendingMetadata.add(reference);
24
+ finalizedMetadata.register(metadata, reference, reference);
25
+ }
26
+ properties.add(property);
27
+ }
28
+ /** Whether a class's standard decorator metadata contains a mapped property. */
29
+ export function hasModelBoundMetadata(metadata) {
30
+ return mappedMetadata.has(metadata);
31
+ }
32
+ /** Returns unresolved mappings grouped by class metadata, dropping resolved and collected entries. */
33
+ export function takeUnregisteredModelBoundMappings(registeredTypes) {
34
+ const registeredMetadata = new Set();
35
+ for (const type of registeredTypes) {
36
+ for (let metadata = getStandardMetadata(type); metadata; metadata = Object.getPrototypeOf(metadata)) {
37
+ registeredMetadata.add(metadata);
38
+ }
39
+ }
40
+ const unmapped = [];
41
+ for (const reference of pendingMetadata) {
42
+ const metadata = reference.deref();
43
+ pendingMetadata.delete(reference);
44
+ finalizedMetadata.unregister(reference);
45
+ if (!metadata || registeredMetadata.has(metadata))
46
+ continue;
47
+ const mappingsForClass = new Set();
48
+ for (const property of mappedMetadata.get(metadata) ?? []) {
49
+ for (const key of modelBoundPropertyKeys) {
50
+ const value = Reflect.getOwnMetadata(key, metadata, property);
51
+ if (value === undefined)
52
+ continue;
53
+ const mappings = Array.isArray(value) ? value : [value];
54
+ for (const mapping of mappings) {
55
+ const eventType = mapping && typeof mapping === 'object' && 'eventType' in mapping ? mapping.eventType : undefined;
56
+ const eventName = typeof eventType === 'function' ? eventType.name : 'all events';
57
+ mappingsForClass.add(`${property} <- ${eventName}`);
58
+ }
59
+ }
60
+ }
61
+ if (mappingsForClass.size > 0)
62
+ unmapped.push(`{${[...mappingsForClass].join(', ')}}`);
63
+ }
64
+ return unmapped;
65
+ }
66
+ //# sourceMappingURL=modelBoundPropertyMetadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelBoundPropertyMetadata.js","sourceRoot":"","sources":["../../types/modelBoundPropertyMetadata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
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 { describe, expect, it, vi } from 'vitest';
5
+ import { ChronicleClient } from '../../ChronicleClient.js';
6
+ import { ChronicleOptions } from '../../ChronicleOptions.js';
7
+ import type { EventStore } from '../../EventStore.js';
8
+ import { setFrom } from '../../projections/modelBound/setFrom.js';
9
+
10
+ class Changed {}
11
+
12
+ class Legacy {
13
+ @setFrom(Changed) value = '';
14
+ }
15
+
16
+ describe('when registering artifacts with legacy property mappings', () => {
17
+ it('should not warn for the model registered on import', async () => {
18
+ const client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { discoveryPatterns: [] }));
19
+ const warn = vi.spyOn(client['_logger'], 'warn');
20
+ const store = {
21
+ name: { value: 'store' }, namespace: { value: 'Default' }, registerArtifacts: vi.fn().mockResolvedValue(undefined)
22
+ } as unknown as EventStore;
23
+ try {
24
+ await client['registerArtifactsForStore'](store, 'new-store');
25
+ expect(warn).not.toHaveBeenCalled();
26
+ expect(Legacy).toBeDefined();
27
+ } finally {
28
+ client.dispose();
29
+ }
30
+ });
31
+ });
@@ -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.13.0",
3
+ "version": "6.14.1",
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",
@@ -76,6 +76,11 @@
76
76
  "import": "./dist/reducers/index.js",
77
77
  "default": "./dist/reducers/index.js"
78
78
  },
79
+ "./testing": {
80
+ "types": "./dist/testing/index.d.ts",
81
+ "import": "./dist/testing/index.js",
82
+ "default": "./dist/testing/index.js"
83
+ },
79
84
  "./seeding": {
80
85
  "types": "./dist/seeding/index.d.ts",
81
86
  "import": "./dist/seeding/index.js",
@@ -162,7 +167,7 @@
162
167
  "build": "tsc -b",
163
168
  "clean": "rm -rf dist",
164
169
  "test": "vitest run",
165
- "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",
166
171
  "test:native": "node ./scripts/check-native-esm.mjs"
167
172
  },
168
173
  "dependencies": {
@@ -84,11 +84,11 @@ describe('standard model-bound decorators', () => {
84
84
  }
85
85
  });
86
86
 
87
- it('should explain that an unconstructed property-only model was never registered', async () => {
87
+ it('should explain how to register an unconstructed model not exported for discovery', async () => {
88
88
  const getInstanceByKey = vi.fn().mockResolvedValue({ ReadModel: '{"value":"stored"}' });
89
89
  const connection = { readModels: { getInstanceByKey } } as unknown as ChronicleConnection;
90
90
  const readModels = new ReadModels('store', 'Default', connection, DefaultClientArtifactsProvider.default, 'sink');
91
- await expect(readModels.findInstanceById(UnconstructedMappedOnly, 'id')).rejects.toThrow(/add a class-level @fromEvent/);
91
+ await expect(readModels.findInstanceById(UnconstructedMappedOnly, 'id')).rejects.toThrow(/register it explicitly before creating the event store/);
92
92
  expect(getInstanceByKey).not.toHaveBeenCalled();
93
93
  });
94
94
  it('stores class and property annotations without constructing an instance', () => {
@@ -333,7 +333,7 @@ export class ReadModels implements IReadModels {
333
333
  const neverRegistered = resolved?.isModelBound && this._isModelBoundProjectionRegistered && !this._isModelBoundProjectionRegistered(readModelType);
334
334
  if (!resolved || neverRegistered) {
335
335
  throw new Error(hasModelBoundProperties(readModelType)
336
- ? `Unknown read model '${readModelType.name}'. It has model-bound property mappings but was not registered when the event store was created. With standard decorators, a class whose mappings are all on properties is only registered once an instance exists; add a class-level @fromEvent(...) decorator so it registers when its module loads.`
336
+ ? `Unknown read model '${readModelType.name}'. It has model-bound property mappings but was not registered when the event store was created. Export the class from a module matched by discoveryPatterns, or register it explicitly before creating the event store.`
337
337
  : `Unknown read model '${readModelType.name}'. Make sure it is discoverable through a projection, reducer, or model-bound mapping.`);
338
338
  }
339
339
  return resolved;
@@ -82,7 +82,7 @@ describe('when discovering read models from observers', () => {
82
82
  setFrom(Event)(RegisteredLate.prototype, 'value');
83
83
  const { connection } = createConnection();
84
84
  const readModels = new ReadModels('store', 'Default', connection, DefaultClientArtifactsProvider.default, 'sink', () => false);
85
- await expect(readModels.findInstanceById(RegisteredLate, 'key')).rejects.toThrow(/add a class-level @fromEvent/);
85
+ await expect(readModels.findInstanceById(RegisteredLate, 'key')).rejects.toThrow(/register it explicitly before creating the event store/);
86
86
  });
87
87
 
88
88
  it('should explain how to register an unregistered property-bound model', async () => {
@@ -91,7 +91,7 @@ describe('when discovering read models from observers', () => {
91
91
  Reflect.defineMetadata('chronicle:typeIntrospection:properties', ['value'], NeverRegistered);
92
92
  const { connection } = createConnection();
93
93
  await expect(new ReadModels('store', 'Default', connection, DefaultClientArtifactsProvider.default, 'sink').findInstanceById(NeverRegistered, 'key'))
94
- .rejects.toThrow(/add a class-level @fromEvent/);
94
+ .rejects.toThrow(/register it explicitly before creating the event store/);
95
95
  });
96
96
 
97
97
  it('should deduplicate inferred types and retain custom identifiers', async () => {
@@ -0,0 +1,38 @@
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 { setFrom } from '../projections/modelBound/setFrom.js';
10
+ import { Projections } from '../projections/Projections.js';
11
+
12
+ @eventType('legacy-cold-registration-event')
13
+ class Changed { @field(String) value!: string; }
14
+
15
+ let constructions = 0;
16
+ class LegacyPropertyOnly {
17
+ @setFrom(Changed)
18
+ @field(String)
19
+ value = '';
20
+
21
+ constructor() { constructions++; }
22
+ }
23
+
24
+ describe('when registering a legacy-decorated property-only read model', () => {
25
+ it('should discover and register it without constructing an instance first', async () => {
26
+ expect(constructions).toBe(0);
27
+ const provider = DefaultClientArtifactsProvider.default;
28
+ expect(provider.readModels).toContain(LegacyPropertyOnly);
29
+ const registerMany = vi.fn().mockResolvedValue({});
30
+ const register = vi.fn().mockResolvedValue({});
31
+ const connection = { readModels: { registerMany }, projections: { register } } as unknown as ChronicleConnection;
32
+
33
+ await new Projections('store', 'Default', connection, provider, 'sink').register();
34
+
35
+ expect(registerMany.mock.calls[0][0].ReadModels.some((model: { Type: { Identifier: string } }) => model.Type.Identifier === 'LegacyPropertyOnly')).toBe(true);
36
+ expect(register.mock.calls.some(call => call[0].Projections[0].ReadModel === 'LegacyPropertyOnly')).toBe(true);
37
+ });
38
+ });
@@ -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
- /** Excludes models used as children or nested objects from root projection registration. */
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
+ }
@@ -0,0 +1,47 @@
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 { Constructor } from '@cratis/fundamentals';
5
+ import { getEventTypeMetadata } from '../events/eventTypeDecorator.js';
6
+ import type { EventContext } from '../events/EventContext.js';
7
+ import type { ReducerEventHandler } from './ReducerEventHandler.js';
8
+
9
+ /** Selects and invokes the same reducer handlers for kernel deliveries and in-process scenarios. */
10
+ export class ReducerEventDispatcher {
11
+ readonly handlers: ReducerEventHandler[];
12
+
13
+ /** Discovers event methods from the artifact event types. */
14
+ constructor(reducerType: Constructor, eventTypes: readonly Constructor[]) {
15
+ const proto = reducerType.prototype as Record<string, unknown>;
16
+ this.handlers = [];
17
+ for (const eventTypeClass of eventTypes) {
18
+ const metadata = getEventTypeMetadata(eventTypeClass);
19
+ if (!metadata) continue;
20
+ const className = (eventTypeClass as Function).name;
21
+ const methodName = className.charAt(0).toLowerCase() + className.slice(1);
22
+ if (typeof proto[methodName] === 'function') {
23
+ this.handlers.push({
24
+ id: metadata.eventType.id.value,
25
+ generation: metadata.eventType.generation.value,
26
+ methodName
27
+ });
28
+ }
29
+ }
30
+ }
31
+
32
+ /** Resolves a registered event type, or returns undefined for an unsubscribed event. */
33
+ handlerFor(eventTypeId: string): ReducerEventHandler | undefined {
34
+ return this.handlers.find(handler => handler.id === eventTypeId);
35
+ }
36
+
37
+ /** Calls the selected handler with the event, current state, and context. */
38
+ async invoke(
39
+ instance: Record<string, Function>,
40
+ handler: ReducerEventHandler,
41
+ event: unknown,
42
+ currentState: unknown,
43
+ context: EventContext
44
+ ): Promise<unknown> {
45
+ return await instance[handler.methodName](event, currentState, context) as unknown;
46
+ }
47
+ }
@@ -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
+ /** Event type and camel-case method selected for a reducer observation. */
5
+ export interface ReducerEventHandler {
6
+ readonly id: string;
7
+ readonly generation: number;
8
+ readonly methodName: string;
9
+ }