@cratis/chronicle 6.13.0 → 6.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/reducers/ReducerEventDispatcher.d.ts +14 -0
  2. package/dist/reducers/ReducerEventDispatcher.d.ts.map +1 -0
  3. package/dist/reducers/ReducerEventDispatcher.js +35 -0
  4. package/dist/reducers/ReducerEventDispatcher.js.map +1 -0
  5. package/dist/reducers/ReducerEventHandler.d.ts +7 -0
  6. package/dist/reducers/ReducerEventHandler.d.ts.map +1 -0
  7. package/dist/reducers/ReducerEventHandler.js +4 -0
  8. package/dist/reducers/ReducerEventHandler.js.map +1 -0
  9. package/dist/reducers/Reducers.d.ts +0 -1
  10. package/dist/reducers/Reducers.d.ts.map +1 -1
  11. package/dist/reducers/Reducers.js +11 -30
  12. package/dist/reducers/Reducers.js.map +1 -1
  13. package/dist/testing/ReadModelScenario.d.ts +33 -0
  14. package/dist/testing/ReadModelScenario.d.ts.map +1 -0
  15. package/dist/testing/ReadModelScenario.js +137 -0
  16. package/dist/testing/ReadModelScenario.js.map +1 -0
  17. package/dist/testing/ReadModelScenarioGiven.d.ts +11 -0
  18. package/dist/testing/ReadModelScenarioGiven.d.ts.map +1 -0
  19. package/dist/testing/ReadModelScenarioGiven.js +18 -0
  20. package/dist/testing/ReadModelScenarioGiven.js.map +1 -0
  21. package/dist/testing/ReadModelScenarioGivenBuilder.d.ts +10 -0
  22. package/dist/testing/ReadModelScenarioGivenBuilder.d.ts.map +1 -0
  23. package/dist/testing/ReadModelScenarioGivenBuilder.js +15 -0
  24. package/dist/testing/ReadModelScenarioGivenBuilder.js.map +1 -0
  25. package/dist/testing/index.d.ts +4 -0
  26. package/dist/testing/index.d.ts.map +1 -0
  27. package/dist/testing/index.js +6 -0
  28. package/dist/testing/index.js.map +1 -0
  29. package/dist/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +6 -1
  31. package/reducers/ReducerEventDispatcher.ts +47 -0
  32. package/reducers/ReducerEventHandler.ts +9 -0
  33. package/reducers/Reducers.ts +13 -42
  34. package/testing/ReadModelScenario.example.ts +29 -0
  35. package/testing/ReadModelScenario.spec.ts +191 -0
  36. package/testing/ReadModelScenario.ts +151 -0
  37. package/testing/ReadModelScenarioGiven.ts +16 -0
  38. package/testing/ReadModelScenarioGivenBuilder.ts +15 -0
  39. package/testing/index.ts +6 -0
@@ -0,0 +1,151 @@
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 { JsonSerializer } from '@cratis/fundamentals';
6
+ import type { Constructor } from '@cratis/fundamentals';
7
+ import type { IClientArtifactsProvider } from '../artifacts/IClientArtifactsProvider.js';
8
+ import type { EventContext } from '../events/EventContext.js';
9
+ import { getEventTypeMetadata } from '../events/eventTypeDecorator.js';
10
+ import { getFromEventMetadata } from '../projections/modelBound/fromEvent.js';
11
+ import { getRemovedWithClassMetadata } from '../projections/modelBound/removedWith.js';
12
+ import { getRemovedWithJoinClassMetadata } from '../projections/modelBound/removedWithJoin.js';
13
+ import { getClearWithClassMetadata } from '../projections/modelBound/clearWith.js';
14
+ import { getVariantOfMetadata } from '../projections/modelBound/variantOf.js';
15
+ import { getEntersOnMetadata } from '../projections/modelBound/entersOn.js';
16
+ import { getProjectionMetadata } from '../projections/declarative/projection.js';
17
+ import { getReducerMetadata } from '../reducers/reducer.js';
18
+ import { ReducerEventDispatcher } from '../reducers/ReducerEventDispatcher.js';
19
+ import { DecoratorType } from '../types/DecoratorType.js';
20
+ import { hasModelBoundProperties, TypeDiscoverer } from '../types/TypeDiscoverer.js';
21
+ import { ReadModelScenarioGivenBuilder } from './ReadModelScenarioGivenBuilder.js';
22
+
23
+ type ScenarioArtifacts = Pick<IClientArtifactsProvider, 'reducers' | 'eventTypes' | 'projections'>;
24
+
25
+ type SeededEvent = { sourceId: string; content: unknown; context: EventContext };
26
+ type ReducedState<T> = { instance: T | null; deleted: boolean };
27
+
28
+ /**
29
+ * Folds seeded events through a reducer in-process, without a Chronicle kernel.
30
+ * Projections, observer scheduling, storage, migrations, and compliance are not simulated.
31
+ * Supply an artifact catalog to isolate a scenario from process-wide decorator discovery.
32
+ */
33
+ export class ReadModelScenario<TReadModel extends object> {
34
+ private readonly _readModelType: Constructor<TReadModel>;
35
+ private readonly _reducerType: Constructor;
36
+ private readonly _dispatcher: ReducerEventDispatcher;
37
+ private readonly _events: SeededEvent[] = [];
38
+ private _results: Promise<Map<string, ReducedState<TReadModel>>> | undefined;
39
+
40
+ /** Selects the reducer associated with the read model type. */
41
+ constructor(readModelType: Constructor<TReadModel>, artifacts?: ScenarioArtifacts) {
42
+ this._readModelType = readModelType;
43
+ const registered = artifacts ?? {
44
+ reducers: TypeDiscoverer.default.getTypesByDecoratorType(DecoratorType.Reducer),
45
+ eventTypes: TypeDiscoverer.default.getTypesByDecoratorType(DecoratorType.EventType),
46
+ projections: TypeDiscoverer.default.getTypesByDecoratorType(DecoratorType.Projection)
47
+ };
48
+ const reducerTypes = registered.reducers.filter(type => getReducerMetadata(type)?.readModel === readModelType);
49
+ if (reducerTypes.length > 1) {
50
+ throw new Error(`Multiple reducers found for read model '${readModelType.name}'.`);
51
+ }
52
+ if (reducerTypes.length === 0) {
53
+ const projected = getFromEventMetadata(readModelType).length > 0 || hasModelBoundProperties(readModelType) ||
54
+ getRemovedWithClassMetadata(readModelType).length > 0 || getRemovedWithJoinClassMetadata(readModelType).length > 0 ||
55
+ getClearWithClassMetadata(readModelType).length > 0 || getVariantOfMetadata(readModelType) !== undefined ||
56
+ getEntersOnMetadata(readModelType).length > 0 ||
57
+ registered.projections.some(type => getProjectionMetadata(type)?.readModelType === readModelType);
58
+ if (projected) {
59
+ throw new Error(`Projection-backed read model '${readModelType.name}' is not supported yet; use a kernel-backed test.`);
60
+ }
61
+ throw new Error(`No reducer found for read model '${readModelType.name}'. Pass the read model type to @reducer; projections are not supported yet (use a kernel-backed test).`);
62
+ }
63
+ this._reducerType = reducerTypes[0];
64
+ this._dispatcher = new ReducerEventDispatcher(this._reducerType, registered.eventTypes);
65
+ }
66
+
67
+ /** Fluent entry point for event history. */
68
+ get given(): ReadModelScenarioGivenBuilder<TReadModel> {
69
+ return new ReadModelScenarioGivenBuilder(this);
70
+ }
71
+
72
+ /** Returns the sole materialized model, or null when no model exists. */
73
+ get instance(): Promise<TReadModel | null> {
74
+ return this.process().then(results => {
75
+ const materialized = [...results.values()].filter(result => result.instance !== null);
76
+ if (materialized.length > 1) {
77
+ throw new Error('Multiple read model instances materialized; use instanceForEventSourceId(id).');
78
+ }
79
+ return materialized[0]?.instance ?? null;
80
+ });
81
+ }
82
+
83
+ /** Returns the model for one source, or null if no handler created it or it was deleted. */
84
+ async instanceForEventSourceId(id: string): Promise<TReadModel | null> {
85
+ return (await this.process()).get(id)?.instance ?? null;
86
+ }
87
+
88
+ /** Distinguishes a model removed by a reducer from one never created. */
89
+ async wasDeletedForEventSourceId(id: string): Promise<boolean> {
90
+ return (await this.process()).get(id)?.deleted ?? false;
91
+ }
92
+
93
+ /** Collects events for a source; subsequent reads replay the complete seeded history. */
94
+ collectEventsFor(id: string, events: readonly object[]): void {
95
+ for (const event of events) {
96
+ const metadata = getEventTypeMetadata(event.constructor);
97
+ if (!metadata) throw new Error(`Event '${event.constructor.name}' has no @eventType metadata.`);
98
+ // The kernel delivers JSON objects, not instances of decorated event classes.
99
+ const content: unknown = JSON.parse(JsonSerializer.serialize(event));
100
+ this._events.push({
101
+ sourceId: id,
102
+ content,
103
+ context: {
104
+ eventSourceId: id,
105
+ eventSourceType: 'Default',
106
+ eventStreamType: 'All',
107
+ eventStreamId: 'Default',
108
+ eventStore: '[NotSet]',
109
+ namespace: '[NotSet]',
110
+ sequenceNumber: BigInt(this._events.length),
111
+ eventType: metadata.eventType,
112
+ occurred: new Date(),
113
+ correlationId: '00000000-0000-0000-0000-000000000000',
114
+ causation: [],
115
+ tags: []
116
+ }
117
+ });
118
+ }
119
+ this._results = undefined;
120
+ }
121
+
122
+ private process(): Promise<Map<string, ReducedState<TReadModel>>> {
123
+ if (!this._results) {
124
+ const events = [...this._events];
125
+ this._results = this.reduce(events);
126
+ }
127
+ return this._results;
128
+ }
129
+
130
+ private async reduce(events: readonly SeededEvent[]): Promise<Map<string, ReducedState<TReadModel>>> {
131
+ const results = new Map<string, ReducedState<TReadModel>>();
132
+ const reducer = new (this._reducerType as new () => Record<string, Function>)();
133
+ for (const event of events) {
134
+ const handler = this._dispatcher.handlerFor(event.context.eventType.id.value);
135
+ if (!handler) continue;
136
+ const prior = results.get(event.sourceId);
137
+ const previous = prior?.deleted ? undefined : prior?.instance;
138
+ let next: unknown;
139
+ try {
140
+ next = await this._dispatcher.invoke(reducer, handler, event.content, previous, event.context);
141
+ } catch (cause) {
142
+ throw new Error(`Reducer '${this._reducerType.name}' for read model '${this._readModelType.name}' failed`, { cause });
143
+ }
144
+ results.set(event.sourceId, {
145
+ instance: next === undefined ? null : next as TReadModel | null,
146
+ deleted: next === undefined
147
+ });
148
+ }
149
+ return results;
150
+ }
151
+ }
@@ -0,0 +1,16 @@
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 { ReadModelScenario } from './ReadModelScenario.js';
5
+
6
+ /** Seeds events belonging to one event source, in the order provided. */
7
+ export class ReadModelScenarioGiven<TReadModel extends object> {
8
+ /** Creates a builder for the selected event source. */
9
+ constructor(private readonly _scenario: ReadModelScenario<TReadModel>, private readonly _eventSourceId: string) {}
10
+
11
+ /** Adds events to the scenario without connecting to a Chronicle kernel. */
12
+ events(...events: object[]): ReadModelScenario<TReadModel> {
13
+ this._scenario.collectEventsFor(this._eventSourceId, events);
14
+ return this._scenario;
15
+ }
16
+ }
@@ -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 { ReadModelScenario } from './ReadModelScenario.js';
5
+ import { ReadModelScenarioGiven } from './ReadModelScenarioGiven.js';
6
+
7
+ /** Selects the event source for events seeded into a read model scenario. */
8
+ export class ReadModelScenarioGivenBuilder<TReadModel extends object> {
9
+ constructor(private readonly _scenario: ReadModelScenario<TReadModel>) {}
10
+
11
+ /** Creates a builder for the selected event source. */
12
+ forEventSource(id: string): ReadModelScenarioGiven<TReadModel> {
13
+ return new ReadModelScenarioGiven(this._scenario, id);
14
+ }
15
+ }
@@ -0,0 +1,6 @@
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
+ export { ReadModelScenario } from './ReadModelScenario.js';
5
+ export { ReadModelScenarioGiven } from './ReadModelScenarioGiven.js';
6
+ export { ReadModelScenarioGivenBuilder } from './ReadModelScenarioGivenBuilder.js';