@cratis/chronicle 6.28.0 → 6.30.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 (72) hide show
  1. package/ChronicleClient.ts +1 -1
  2. package/ChronicleOptions.ts +14 -1
  3. package/EventStore.ts +6 -4
  4. package/dist/ChronicleClient.js +1 -1
  5. package/dist/ChronicleClient.js.map +1 -1
  6. package/dist/ChronicleOptions.d.ts +9 -0
  7. package/dist/ChronicleOptions.d.ts.map +1 -1
  8. package/dist/ChronicleOptions.js +10 -1
  9. package/dist/ChronicleOptions.js.map +1 -1
  10. package/dist/EventStore.d.ts +2 -1
  11. package/dist/EventStore.d.ts.map +1 -1
  12. package/dist/EventStore.js +4 -4
  13. package/dist/EventStore.js.map +1 -1
  14. package/dist/projections/ProjectionDefinitionCompiler.d.ts +4 -1
  15. package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -1
  16. package/dist/projections/ProjectionDefinitionCompiler.js +6 -2
  17. package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -1
  18. package/dist/projections/Projections.d.ts +4 -1
  19. package/dist/projections/Projections.d.ts.map +1 -1
  20. package/dist/projections/Projections.js +5 -2
  21. package/dist/projections/Projections.js.map +1 -1
  22. package/dist/readModels/ReadModelNamingPolicy.d.ts +13 -0
  23. package/dist/readModels/ReadModelNamingPolicy.d.ts.map +1 -0
  24. package/dist/readModels/ReadModelNamingPolicy.js +4 -0
  25. package/dist/readModels/ReadModelNamingPolicy.js.map +1 -0
  26. package/dist/readModels/ReadModels.d.ts +3 -1
  27. package/dist/readModels/ReadModels.d.ts.map +1 -1
  28. package/dist/readModels/ReadModels.js +5 -2
  29. package/dist/readModels/ReadModels.js.map +1 -1
  30. package/dist/readModels/buildReadModelDefinition.d.ts +3 -0
  31. package/dist/readModels/buildReadModelDefinition.d.ts.map +1 -1
  32. package/dist/readModels/buildReadModelDefinition.js +1 -1
  33. package/dist/readModels/buildReadModelDefinition.js.map +1 -1
  34. package/dist/readModels/for_ReadModelNamingPolicy/given/registered_read_models.d.ts +47 -0
  35. package/dist/readModels/for_ReadModelNamingPolicy/given/registered_read_models.d.ts.map +1 -0
  36. package/dist/readModels/for_ReadModelNamingPolicy/given/registered_read_models.js +115 -0
  37. package/dist/readModels/for_ReadModelNamingPolicy/given/registered_read_models.js.map +1 -0
  38. package/dist/readModels/index.d.ts +1 -0
  39. package/dist/readModels/index.d.ts.map +1 -1
  40. package/dist/readModels/index.js.map +1 -1
  41. package/dist/reducers/Reducers.d.ts +6 -1
  42. package/dist/reducers/Reducers.d.ts.map +1 -1
  43. package/dist/reducers/Reducers.js +8 -2
  44. package/dist/reducers/Reducers.js.map +1 -1
  45. package/dist/testing/ReactorScenario.d.ts +3 -0
  46. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  47. package/dist/testing/ReactorScenario.js +2 -0
  48. package/dist/testing/ReactorScenario.js.map +1 -1
  49. package/dist/testing/ReadModelScenario.d.ts +11 -0
  50. package/dist/testing/ReadModelScenario.d.ts.map +1 -1
  51. package/dist/testing/ReadModelScenario.js +37 -8
  52. package/dist/testing/ReadModelScenario.js.map +1 -1
  53. package/dist/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +1 -1
  55. package/projections/ProjectionDefinitionCompiler.ts +6 -2
  56. package/projections/Projections.ts +5 -2
  57. package/readModels/ReadModelNamingPolicy.ts +16 -0
  58. package/readModels/ReadModels.ts +5 -2
  59. package/readModels/buildReadModelDefinition.ts +5 -2
  60. package/readModels/for_ReadModelNamingPolicy/given/registered_read_models.ts +137 -0
  61. package/readModels/for_ReadModelNamingPolicy/when_configuring_options/with_a_naming_policy.spec.ts +96 -0
  62. package/readModels/for_ReadModelNamingPolicy/when_registering/a_declarative_projection.spec.ts +40 -0
  63. package/readModels/for_ReadModelNamingPolicy/when_registering/a_model_bound_projection.spec.ts +40 -0
  64. package/readModels/for_ReadModelNamingPolicy/when_registering/a_model_bound_read_model_directly.spec.ts +40 -0
  65. package/readModels/for_ReadModelNamingPolicy/when_registering/a_projection_directly.spec.ts +40 -0
  66. package/readModels/for_ReadModelNamingPolicy/when_registering/a_reducer.spec.ts +40 -0
  67. package/readModels/for_ReadModelNamingPolicy/when_registering/a_reducer_read_model_directly.spec.ts +40 -0
  68. package/readModels/index.ts +1 -0
  69. package/reducers/Reducers.ts +8 -2
  70. package/testing/ReactorScenario.ts +2 -0
  71. package/testing/ReadModelScenario.composition.spec.ts +111 -0
  72. package/testing/ReadModelScenario.ts +41 -7
@@ -0,0 +1,111 @@
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 { chai, describe, it } from 'vitest';
6
+ import { eventType } from '../events/eventTypeDecorator.js';
7
+ import { fromEvent } from '../projections/modelBound/fromEvent.js';
8
+ import { reactor } from '../reactors/reactor.js';
9
+ import type { ReactorServices } from '../reactors/ReactorServices.js';
10
+ import { reducer } from '../reducers/reducer.js';
11
+ import { EventScenario, ReactorScenario, ReadModelScenario, UnsupportedProjectionOperation } from './index.js';
12
+
13
+ chai.should();
14
+
15
+ @eventType('composition-member-joined')
16
+ class MemberJoined {
17
+ @field(String) name: string;
18
+ constructor(name: string) { this.name = name; }
19
+ }
20
+
21
+ @eventType('composition-member-welcomed')
22
+ class MemberWelcomed {
23
+ @field(String) name: string;
24
+ constructor(name: string) { this.name = name; }
25
+ }
26
+
27
+ @fromEvent(MemberJoined)
28
+ class Member {
29
+ @field(String) id = '';
30
+ @field(String) name = '';
31
+ }
32
+
33
+ class MemberCount { joined = 0; }
34
+ class MemberCountReducer {
35
+ memberJoined(_event: MemberJoined, current: MemberCount | undefined): MemberCount {
36
+ return { joined: (current?.joined ?? 0) + 1 };
37
+ }
38
+ }
39
+ reducer('composition-member-count', undefined, MemberCount)(MemberCountReducer);
40
+
41
+ @fromEvent(MemberWelcomed)
42
+ class Welcome {
43
+ @field(String) id = '';
44
+ @field(String) name = '';
45
+ }
46
+
47
+ @reactor('composition-welcomer')
48
+ class Welcomer {
49
+ async memberJoined(event: MemberJoined, context: { eventSourceId: string }, services: ReactorServices) {
50
+ await services.eventStore.eventLog.append(context.eventSourceId, new MemberWelcomed(event.name));
51
+ return new MemberWelcomed(`returned ${event.name}`);
52
+ }
53
+ }
54
+
55
+ const eventTypes = [MemberJoined, MemberWelcomed];
56
+ const catalog = { eventTypes, reducers: [MemberCountReducer], projections: [] };
57
+
58
+ describe('ReadModelScenario observing an EventScenario', () => {
59
+ it('projects the committed history at the time of each read', async () => {
60
+ const events = new EventScenario({ artifacts: { eventTypes }, constraints: 'disabled' });
61
+ const members = new ReadModelScenario(Member, catalog).observe(events);
62
+ await events.append('member-1', new MemberJoined('Ada'));
63
+ (await members.instanceForEventSourceId('member-1'))!.name.should.equal('Ada');
64
+ await events.appendMany('member-2', [new MemberJoined('Grace')]);
65
+ (await members.instanceForEventSourceId('member-2'))!.name.should.equal('Grace');
66
+ });
67
+
68
+ it('folds setup and action events through a reducer', async () => {
69
+ const events = new EventScenario({ artifacts: { eventTypes }, constraints: 'disabled' });
70
+ const counts = new ReadModelScenario(MemberCount, catalog).observe(events);
71
+ await events.given.forEventSource('club').events(new MemberJoined('Ada'));
72
+ await events.when.forEventSource('club').events(new MemberJoined('Grace'));
73
+ (await counts.instance)!.joined.should.equal(2);
74
+ });
75
+
76
+ it('rejects an explicitly routed event instead of guessing its projection semantics', async () => {
77
+ const events = new EventScenario({ artifacts: { eventTypes }, constraints: 'disabled' });
78
+ const members = new ReadModelScenario(Member, catalog).observe(events);
79
+ await events.appendMany([{ eventSourceId: 'member-1', event: new MemberJoined('Ada'), eventSourceType: 'Members' }]);
80
+ const error = await members.instanceForEventSourceId('member-1').then(() => undefined, (reason: unknown) => reason);
81
+ error.should.be.instanceOf(UnsupportedProjectionOperation);
82
+ (error as Error).message.should.contain('routed events');
83
+ });
84
+
85
+ it('rejects seeding events into an observing scenario', () => {
86
+ const members = new ReadModelScenario(Member, catalog).observe(new EventScenario({ artifacts: { eventTypes }, constraints: 'disabled' }));
87
+ (() => members.given.forEventSource('member-1').events(new MemberJoined('Ada'))).should.throw(UnsupportedProjectionOperation, 'not both');
88
+ });
89
+
90
+ it('rejects observing after seeding or observing twice', () => {
91
+ const events = new EventScenario({ artifacts: { eventTypes }, constraints: 'disabled' });
92
+ const seeded = new ReadModelScenario(Member, catalog);
93
+ seeded.given.forEventSource('member-1').events(new MemberJoined('Ada'));
94
+ (() => seeded.observe(events)).should.throw(UnsupportedProjectionOperation, 'not both');
95
+ const observing = new ReadModelScenario(Member, catalog).observe(events);
96
+ (() => observing.observe(events)).should.throw(UnsupportedProjectionOperation, 'not both');
97
+ });
98
+ });
99
+
100
+ describe('ReadModelScenario observing a ReactorScenario', () => {
101
+ it('reads input events and the reactor\'s explicit appends, but not its recorded returned effects', async () => {
102
+ const reactorScenario = new ReactorScenario(Welcomer, { artifacts: { eventTypes }, constraints: 'disabled' });
103
+ const welcomes = new ReadModelScenario(Welcome, catalog).observe(reactorScenario);
104
+ const members = new ReadModelScenario(Member, catalog).observe(reactorScenario);
105
+ await reactorScenario.when.forEventSource('member-1').events(new MemberJoined('Ada'));
106
+ (await members.instanceForEventSourceId('member-1'))!.name.should.equal('Ada');
107
+ (await welcomes.instanceForEventSourceId('member-1'))!.name.should.equal('Ada');
108
+ reactorScenario.appendedEvents.length.should.equal(2);
109
+ reactorScenario.shouldHaveProduced(MemberWelcomed, welcome => welcome.name === 'returned Ada');
110
+ });
111
+ });
@@ -23,6 +23,8 @@ import type { IReadModelProcessor } from './IReadModelProcessor.js';
23
23
  import type { ReadModelState } from './ReadModelState.js';
24
24
  import { ReducerReadModelProcessor } from './ReducerReadModelProcessor.js';
25
25
  import type { ScenarioEvent } from './ScenarioEvent.js';
26
+ import type { EventScenario } from './EventScenario.js';
27
+ import type { ReactorScenario } from './ReactorScenario.js';
26
28
  import { ProjectionReadModelProcessor } from './projections/ProjectionReadModelProcessor.js';
27
29
  import { UnsupportedProjectionOperation } from './projections/UnsupportedProjectionOperation.js';
28
30
 
@@ -39,6 +41,7 @@ export class ReadModelScenario<TReadModel extends object> {
39
41
  private readonly _modelName: string;
40
42
  private readonly _events: ScenarioEvent[] = [];
41
43
  private _results: Promise<Map<string, ReadModelState<TReadModel>>> | undefined;
44
+ private _observed: EventScenario | ReactorScenario | undefined;
42
45
 
43
46
  /** Selects the reducer (when present) or a single applicable compiled projection. */
44
47
  constructor(readModelType: Constructor<TReadModel>, artifacts?: ScenarioArtifacts) {
@@ -109,8 +112,20 @@ export class ReadModelScenario<TReadModel extends object> {
109
112
  return (await this.process()).get(id)?.deleted ?? false;
110
113
  }
111
114
 
115
+ /**
116
+ * Reads the committed history of an EventScenario or ReactorScenario instead of seeded events.
117
+ * Every read replays that shared sequence as it is at the time of the read.
118
+ */
119
+ observe(source: EventScenario | ReactorScenario): this {
120
+ if (this._observed || this._events.length) throw this.mixedHistory();
121
+ this._observed = source;
122
+ this._results = undefined;
123
+ return this;
124
+ }
125
+
112
126
  /** Collects events for a source; subsequent reads replay the complete seeded history. */
113
127
  collectEventsFor(id: string, events: readonly object[]): void {
128
+ if (this._observed) throw this.mixedHistory();
114
129
  for (const event of events) {
115
130
  const metadata = getEventTypeMetadata(event.constructor);
116
131
  if (!metadata) throw new Error(`Event '${event.constructor.name}' has no @eventType metadata.`);
@@ -139,13 +154,32 @@ export class ReadModelScenario<TReadModel extends object> {
139
154
  }
140
155
 
141
156
  private process(): Promise<Map<string, ReadModelState<TReadModel>>> {
142
- if (!this._results) {
143
- const events = [...this._events];
144
- this._results = this._processor.process(events).catch(error => {
145
- if (!(this._processor instanceof ProjectionReadModelProcessor) || error instanceof UnsupportedProjectionOperation) throw error;
146
- throw new Error(`Projection replay for read model '${this._modelName}' failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
147
- });
148
- }
157
+ if (this._observed) return this.replay(this.observedEvents(this._observed));
158
+ this._results ??= this.replay([...this._events]);
149
159
  return this._results;
150
160
  }
161
+
162
+ private replay(events: ScenarioEvent[]): Promise<Map<string, ReadModelState<TReadModel>>> {
163
+ return this._processor.process(events).catch(error => {
164
+ if (!(this._processor instanceof ProjectionReadModelProcessor) || error instanceof UnsupportedProjectionOperation) throw error;
165
+ throw new Error(`Projection replay for read model '${this._modelName}' failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
166
+ });
167
+ }
168
+
169
+ private observedEvents(source: EventScenario | ReactorScenario): ScenarioEvent[] {
170
+ return source.appendedEvents.map(event => {
171
+ const { eventSourceType = 'Default', eventStreamType = 'All', eventStreamId = 'Default' } = event.context;
172
+ // No fixture establishes how the evaluator should treat an explicitly routed event; the kernel decides.
173
+ if (eventSourceType !== 'Default' || eventStreamType !== 'All' || eventStreamId !== 'Default') {
174
+ throw new UnsupportedProjectionOperation(this._modelName, 'observe', `${eventSourceType}/${eventStreamType}/${eventStreamId}`,
175
+ 'observed events must use the default event source type and stream; routed events require a kernel-backed test');
176
+ }
177
+ return { sourceId: event.context.eventSourceId, content: event.content, context: event.context };
178
+ });
179
+ }
180
+
181
+ private mixedHistory(): UnsupportedProjectionOperation {
182
+ return new UnsupportedProjectionOperation(this._modelName, 'observe', 'given.events',
183
+ 'a scenario replays either its own seeded events or one observed event sequence, not both');
184
+ }
151
185
  }