@cratis/chronicle 3.1.0 → 4.0.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 (217) hide show
  1. package/ChronicleClient.ts +4 -3
  2. package/EventStore.ts +4 -3
  3. package/README.md +40 -28
  4. package/compliance/ComplianceContracts.ts +59 -0
  5. package/compliance/IPIIManager.ts +12 -0
  6. package/compliance/PIIManager.spec.ts +50 -0
  7. package/compliance/PIIManager.ts +9 -0
  8. package/compliance/PIINotSupportedOnEventSourceId.ts +21 -0
  9. package/compliance/index.ts +2 -0
  10. package/compliance/pii.spec.ts +37 -0
  11. package/compliance/pii.ts +16 -1
  12. package/compliance/subject.spec.ts +45 -0
  13. package/compliance/subject.ts +64 -0
  14. package/connection/callResults.spec.ts +90 -0
  15. package/connection/callResults.ts +112 -0
  16. package/dist/ChronicleClient.d.ts.map +1 -1
  17. package/dist/ChronicleClient.js +4 -3
  18. package/dist/ChronicleClient.js.map +1 -1
  19. package/dist/EventStore.d.ts.map +1 -1
  20. package/dist/EventStore.js +4 -3
  21. package/dist/EventStore.js.map +1 -1
  22. package/dist/compliance/ComplianceContracts.d.ts +31 -0
  23. package/dist/compliance/ComplianceContracts.d.ts.map +1 -1
  24. package/dist/compliance/ComplianceContracts.js +52 -0
  25. package/dist/compliance/ComplianceContracts.js.map +1 -1
  26. package/dist/compliance/IPIIManager.d.ts +11 -0
  27. package/dist/compliance/IPIIManager.d.ts.map +1 -1
  28. package/dist/compliance/PIIManager.d.ts +2 -0
  29. package/dist/compliance/PIIManager.d.ts.map +1 -1
  30. package/dist/compliance/PIIManager.js +8 -0
  31. package/dist/compliance/PIIManager.js.map +1 -1
  32. package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts +11 -0
  33. package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts.map +1 -0
  34. package/dist/compliance/PIINotSupportedOnEventSourceId.js +19 -0
  35. package/dist/compliance/PIINotSupportedOnEventSourceId.js.map +1 -0
  36. package/dist/compliance/index.d.ts +2 -0
  37. package/dist/compliance/index.d.ts.map +1 -1
  38. package/dist/compliance/index.js +2 -0
  39. package/dist/compliance/index.js.map +1 -1
  40. package/dist/compliance/pii.d.ts.map +1 -1
  41. package/dist/compliance/pii.js +13 -1
  42. package/dist/compliance/pii.js.map +1 -1
  43. package/dist/compliance/subject.d.ts +39 -0
  44. package/dist/compliance/subject.d.ts.map +1 -0
  45. package/dist/compliance/subject.js +58 -0
  46. package/dist/compliance/subject.js.map +1 -0
  47. package/dist/connection/callResults.d.ts +69 -0
  48. package/dist/connection/callResults.d.ts.map +1 -0
  49. package/dist/connection/callResults.js +74 -0
  50. package/dist/connection/callResults.js.map +1 -0
  51. package/dist/eventSequences/AppendOptions.d.ts +8 -0
  52. package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
  53. package/dist/eventSequences/EventForEventSourceId.d.ts +7 -0
  54. package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
  55. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  56. package/dist/eventSequences/EventSequence.js +17 -6
  57. package/dist/eventSequences/EventSequence.js.map +1 -1
  58. package/dist/events/EventContext.d.ts +3 -0
  59. package/dist/events/EventContext.d.ts.map +1 -1
  60. package/dist/events/Tag.d.ts +10 -0
  61. package/dist/events/Tag.d.ts.map +1 -0
  62. package/dist/events/Tag.js +16 -0
  63. package/dist/events/Tag.js.map +1 -0
  64. package/dist/events/constraints/Constraints.js +3 -3
  65. package/dist/events/constraints/Constraints.js.map +1 -1
  66. package/dist/events/filterEventsByTagDecorator.d.ts +35 -0
  67. package/dist/events/filterEventsByTagDecorator.d.ts.map +1 -0
  68. package/dist/events/filterEventsByTagDecorator.js +48 -0
  69. package/dist/events/filterEventsByTagDecorator.js.map +1 -0
  70. package/dist/events/index.d.ts +4 -0
  71. package/dist/events/index.d.ts.map +1 -1
  72. package/dist/events/index.js +4 -0
  73. package/dist/events/index.js.map +1 -1
  74. package/dist/events/mergeTags.d.ts +12 -0
  75. package/dist/events/mergeTags.d.ts.map +1 -0
  76. package/dist/events/mergeTags.js +26 -0
  77. package/dist/events/mergeTags.js.map +1 -0
  78. package/dist/events/tagDecorator.d.ts +38 -0
  79. package/dist/events/tagDecorator.d.ts.map +1 -0
  80. package/dist/events/tagDecorator.js +54 -0
  81. package/dist/events/tagDecorator.js.map +1 -0
  82. package/dist/jobs/IJobs.d.ts +4 -4
  83. package/dist/jobs/IJobs.d.ts.map +1 -1
  84. package/dist/jobs/Jobs.d.ts +4 -4
  85. package/dist/jobs/Jobs.d.ts.map +1 -1
  86. package/dist/jobs/Jobs.js +12 -10
  87. package/dist/jobs/Jobs.js.map +1 -1
  88. package/dist/observation/ObserverRunningState.d.ts +9 -15
  89. package/dist/observation/ObserverRunningState.d.ts.map +1 -1
  90. package/dist/observation/ObserverRunningState.js +8 -14
  91. package/dist/observation/ObserverRunningState.js.map +1 -1
  92. package/dist/observation/index.d.ts +1 -0
  93. package/dist/observation/index.d.ts.map +1 -1
  94. package/dist/observation/index.js +1 -0
  95. package/dist/observation/index.js.map +1 -1
  96. package/dist/observation/toObserverRunningState.d.ts +9 -0
  97. package/dist/observation/toObserverRunningState.d.ts.map +1 -0
  98. package/dist/observation/toObserverRunningState.js +26 -0
  99. package/dist/observation/toObserverRunningState.js.map +1 -0
  100. package/dist/projections/IProjections.d.ts +71 -1
  101. package/dist/projections/IProjections.d.ts.map +1 -1
  102. package/dist/projections/ProjectionQueryResult.d.ts +8 -0
  103. package/dist/projections/ProjectionQueryResult.d.ts.map +1 -0
  104. package/dist/projections/ProjectionQueryResult.js +4 -0
  105. package/dist/projections/ProjectionQueryResult.js.map +1 -0
  106. package/dist/projections/ProjectionState.d.ts +18 -0
  107. package/dist/projections/ProjectionState.d.ts.map +1 -0
  108. package/dist/projections/ProjectionState.js +4 -0
  109. package/dist/projections/ProjectionState.js.map +1 -0
  110. package/dist/projections/Projections.d.ts +46 -1
  111. package/dist/projections/Projections.d.ts.map +1 -1
  112. package/dist/projections/Projections.js +153 -6
  113. package/dist/projections/Projections.js.map +1 -1
  114. package/dist/projections/UnableToQueryProjection.d.ts +12 -0
  115. package/dist/projections/UnableToQueryProjection.d.ts.map +1 -0
  116. package/dist/projections/UnableToQueryProjection.js +17 -0
  117. package/dist/projections/UnableToQueryProjection.js.map +1 -0
  118. package/dist/projections/index.d.ts +3 -0
  119. package/dist/projections/index.d.ts.map +1 -1
  120. package/dist/projections/index.js +1 -0
  121. package/dist/projections/index.js.map +1 -1
  122. package/dist/projections/modelBound/eventSequence.d.ts +22 -0
  123. package/dist/projections/modelBound/eventSequence.d.ts.map +1 -0
  124. package/dist/projections/modelBound/eventSequence.js +34 -0
  125. package/dist/projections/modelBound/eventSequence.js.map +1 -0
  126. package/dist/projections/modelBound/fromAll.d.ts +26 -0
  127. package/dist/projections/modelBound/fromAll.d.ts.map +1 -0
  128. package/dist/projections/modelBound/fromAll.js +31 -0
  129. package/dist/projections/modelBound/fromAll.js.map +1 -0
  130. package/dist/projections/modelBound/index.d.ts +4 -0
  131. package/dist/projections/modelBound/index.d.ts.map +1 -1
  132. package/dist/projections/modelBound/index.js +3 -0
  133. package/dist/projections/modelBound/index.js.map +1 -1
  134. package/dist/projections/modelBound/noAutoMap.d.ts +26 -0
  135. package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -0
  136. package/dist/projections/modelBound/noAutoMap.js +44 -0
  137. package/dist/projections/modelBound/noAutoMap.js.map +1 -0
  138. package/dist/reactors/Reactors.d.ts.map +1 -1
  139. package/dist/reactors/Reactors.js +7 -3
  140. package/dist/reactors/Reactors.js.map +1 -1
  141. package/dist/readModels/MaterializedReadModels.d.ts +0 -1
  142. package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
  143. package/dist/readModels/MaterializedReadModels.js +2 -8
  144. package/dist/readModels/MaterializedReadModels.js.map +1 -1
  145. package/dist/readModels/ReadModelSubjectResolver.d.ts +22 -0
  146. package/dist/readModels/ReadModelSubjectResolver.d.ts.map +1 -0
  147. package/dist/readModels/ReadModelSubjectResolver.js +38 -0
  148. package/dist/readModels/ReadModelSubjectResolver.js.map +1 -0
  149. package/dist/readModels/ReadModels.d.ts.map +1 -1
  150. package/dist/readModels/ReadModels.js +7 -7
  151. package/dist/readModels/ReadModels.js.map +1 -1
  152. package/dist/readModels/index.d.ts +1 -0
  153. package/dist/readModels/index.d.ts.map +1 -1
  154. package/dist/readModels/index.js +1 -0
  155. package/dist/readModels/index.js.map +1 -1
  156. package/dist/reducers/Reducers.d.ts.map +1 -1
  157. package/dist/reducers/Reducers.js +21 -3
  158. package/dist/reducers/Reducers.js.map +1 -1
  159. package/dist/schemas/JsonSchema.d.ts +14 -0
  160. package/dist/schemas/JsonSchema.d.ts.map +1 -1
  161. package/dist/schemas/JsonSchemaGenerator.d.ts +61 -5
  162. package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
  163. package/dist/schemas/JsonSchemaGenerator.js +130 -26
  164. package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
  165. package/dist/schemas/index.d.ts +1 -1
  166. package/dist/schemas/index.d.ts.map +1 -1
  167. package/dist/seeding/EventSeeding.d.ts.map +1 -1
  168. package/dist/seeding/EventSeeding.js +3 -1
  169. package/dist/seeding/EventSeeding.js.map +1 -1
  170. package/dist/tsconfig.tsbuildinfo +1 -1
  171. package/eventSequences/AppendOptions.ts +9 -0
  172. package/eventSequences/EventForEventSourceId.ts +9 -0
  173. package/eventSequences/EventSequence.ts +20 -6
  174. package/events/EventContext.ts +4 -0
  175. package/events/Tag.ts +14 -0
  176. package/events/constraints/Constraints.ts +3 -3
  177. package/events/filterEventsByTagDecorator.ts +51 -0
  178. package/events/index.ts +4 -0
  179. package/events/mergeTags.spec.ts +50 -0
  180. package/events/mergeTags.ts +27 -0
  181. package/events/tagDecorator.spec.ts +115 -0
  182. package/events/tagDecorator.ts +59 -0
  183. package/jobs/IJobs.ts +4 -4
  184. package/jobs/Jobs.ts +16 -14
  185. package/observation/ObserverRunningState.ts +9 -18
  186. package/observation/index.ts +1 -0
  187. package/observation/toObserverRunningState.ts +27 -0
  188. package/package.json +25 -10
  189. package/projections/IProjections.ts +81 -1
  190. package/projections/ProjectionQueryResult.ts +10 -0
  191. package/projections/ProjectionState.ts +25 -0
  192. package/projections/Projections.childrenAndNested.spec.ts +1 -1
  193. package/projections/Projections.modelBoundCompleteness.spec.ts +224 -0
  194. package/projections/Projections.operationalSurface.spec.ts +257 -0
  195. package/projections/Projections.spec.ts +1 -1
  196. package/projections/Projections.ts +180 -5
  197. package/projections/UnableToQueryProjection.ts +17 -0
  198. package/projections/index.ts +3 -0
  199. package/projections/modelBound/eventSequence.ts +38 -0
  200. package/projections/modelBound/fromAll.ts +42 -0
  201. package/projections/modelBound/index.ts +4 -0
  202. package/projections/modelBound/noAutoMap.ts +47 -0
  203. package/reactors/Reactors.ts +7 -3
  204. package/readModels/MaterializedReadModels.spec.ts +98 -0
  205. package/readModels/MaterializedReadModels.ts +2 -8
  206. package/readModels/ReadModelSubjectResolver.spec.ts +84 -0
  207. package/readModels/ReadModelSubjectResolver.ts +44 -0
  208. package/readModels/ReadModels.spec.ts +118 -0
  209. package/readModels/ReadModels.ts +7 -7
  210. package/readModels/index.ts +1 -0
  211. package/reducers/Reducers.spec.ts +80 -0
  212. package/reducers/Reducers.ts +22 -3
  213. package/schemas/JsonSchema.ts +16 -0
  214. package/schemas/JsonSchemaGenerator.spec.ts +195 -0
  215. package/schemas/JsonSchemaGenerator.ts +145 -27
  216. package/schemas/index.ts +1 -1
  217. package/seeding/EventSeeding.ts +3 -1
@@ -0,0 +1,257 @@
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 { describe, expect, it, vi } from 'vitest';
6
+ import { IClientArtifactsProvider } from '../artifacts';
7
+ import { ChronicleConnection } from '../connection';
8
+ import { ObserverRunningState } from '../observation/ObserverRunningState';
9
+ import { eventType } from '../events/eventTypeDecorator';
10
+ import { readModel } from '../readModels/readModel';
11
+ import { eventSequence } from './modelBound/eventSequence';
12
+ import { fromEvent } from './modelBound/fromEvent';
13
+ import { projection } from './declarative/projection';
14
+ import type { IProjectionBuilderFor } from './declarative/IProjectionBuilderFor';
15
+ import type { IProjectionFor } from './declarative/IProjectionFor';
16
+ import { Projections } from './Projections';
17
+ import { UnableToQueryProjection } from './UnableToQueryProjection';
18
+
19
+ // Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
20
+ // fixtures don't depend on the test runner's decorator-syntax support.
21
+
22
+ class OpStateChanged {
23
+ value!: string;
24
+ }
25
+ eventType()(OpStateChanged);
26
+
27
+ class OpSummary {
28
+ id!: string;
29
+ value!: string;
30
+ }
31
+ fromEvent(OpStateChanged)(OpSummary);
32
+ eventSequence('custom-op-sequence')(OpSummary);
33
+ readModel()(OpSummary);
34
+
35
+ class UndiscoveredSummary {
36
+ id!: string;
37
+ }
38
+ readModel()(UndiscoveredSummary);
39
+
40
+ class DeclarativeReadModel {
41
+ id!: string;
42
+ }
43
+ readModel()(DeclarativeReadModel);
44
+
45
+ class DeclarativeSummaryProjection implements IProjectionFor<DeclarativeReadModel> {
46
+ // discover() never calls define() - only register() does - so a no-op body is sufficient here.
47
+ define(_builder: IProjectionBuilderFor<DeclarativeReadModel>): void {}
48
+ }
49
+ projection('DeclarativeSummary', DeclarativeReadModel)(DeclarativeSummaryProjection);
50
+
51
+ function wireObserverInformation() {
52
+ return {
53
+ RunningState: 1, // Active
54
+ IsSubscribed: true,
55
+ NextEventSequenceNumber: 5n,
56
+ LastHandledEventSequenceNumber: 4n,
57
+ TailEventSequenceNumber: 4n
58
+ };
59
+ }
60
+
61
+ async function createDiscoveredProjections() {
62
+ const getObserverInformation = vi.fn().mockResolvedValue(wireObserverInformation());
63
+ const replay = vi.fn().mockResolvedValue({ JobId: '11111111-1111-1111-1111-111111111111' });
64
+ const getFailedPartitions = vi.fn().mockResolvedValue({ items: [] });
65
+ const preview = vi.fn().mockResolvedValue({ Value0: { ReadModelEntries: ['{"id":"1"}'] } });
66
+ const registerMock = vi.fn().mockResolvedValue(undefined);
67
+ const registerManyMock = vi.fn().mockResolvedValue(undefined);
68
+
69
+ const connection = {
70
+ observers: { getObserverInformation, replay },
71
+ failedPartitions: { getFailedPartitions },
72
+ projections: { register: registerMock, preview },
73
+ readModels: { registerMany: registerManyMock }
74
+ } as unknown as ChronicleConnection;
75
+
76
+ const clientArtifacts: IClientArtifactsProvider = {
77
+ eventTypes: [],
78
+ readModels: [OpSummary, UndiscoveredSummary, DeclarativeReadModel] as unknown as IClientArtifactsProvider['readModels'],
79
+ reactors: [],
80
+ reducers: [],
81
+ seeders: [],
82
+ constraints: [],
83
+ projections: [DeclarativeSummaryProjection] as unknown as IClientArtifactsProvider['projections'],
84
+ webhooks: [],
85
+ eventTypeMigrations: []
86
+ };
87
+
88
+ const projections = new Projections('test-store', 'test-namespace', connection, clientArtifacts, 'test-sink');
89
+ await projections.discover();
90
+
91
+ return { projections, getObserverInformation, replay, getFailedPartitions, preview };
92
+ }
93
+
94
+ describe('Projections operational surface', () => {
95
+ describe('when checking hasFor with a discovered projection identifier', () => {
96
+ it('should return true', async () => {
97
+ const { projections } = await createDiscoveredProjections();
98
+ expect(projections.hasFor('OpSummary')).toBe(true);
99
+ });
100
+ });
101
+
102
+ describe('when checking hasFor with an unknown projection identifier', () => {
103
+ it('should return false', async () => {
104
+ const { projections } = await createDiscoveredProjections();
105
+ expect(projections.hasFor('unknown-projection')).toBe(false);
106
+ });
107
+ });
108
+
109
+ describe('when checking hasForModel for a discovered model-bound read model', () => {
110
+ it('should return true', async () => {
111
+ const { projections } = await createDiscoveredProjections();
112
+ expect(projections.hasForModel(OpSummary)).toBe(true);
113
+ });
114
+ });
115
+
116
+ describe('when checking hasForModel for an undiscovered read model', () => {
117
+ it('should return false', async () => {
118
+ const { projections } = await createDiscoveredProjections();
119
+ expect(projections.hasForModel(UndiscoveredSummary)).toBe(false);
120
+ });
121
+ });
122
+
123
+ describe('when getting the projection identifier for a model-bound read model', () => {
124
+ it('should resolve the read model identifier', async () => {
125
+ const { projections } = await createDiscoveredProjections();
126
+ expect(projections.getProjectionIdFor(OpSummary).value).toBe('OpSummary');
127
+ });
128
+ });
129
+
130
+ describe('when getting the projection identifier for a declarative projection with an explicit read model type', () => {
131
+ it('should resolve the declared projection identifier', async () => {
132
+ const { projections } = await createDiscoveredProjections();
133
+ expect(projections.getProjectionIdFor(DeclarativeReadModel).value).toBe('DeclarativeSummary');
134
+ });
135
+ });
136
+
137
+ describe('when getting the projection identifier for an undiscovered read model', () => {
138
+ it('should throw', async () => {
139
+ const { projections } = await createDiscoveredProjections();
140
+ expect(() => projections.getProjectionIdFor(UndiscoveredSummary)).toThrow();
141
+ });
142
+ });
143
+
144
+ describe('when getting state for a projection by identifier', () => {
145
+ it('should call GetObserverInformation and map the response', async () => {
146
+ const { projections, getObserverInformation } = await createDiscoveredProjections();
147
+
148
+ const state = await projections.getStateFor('OpSummary');
149
+
150
+ expect(getObserverInformation).toHaveBeenCalledTimes(1);
151
+ const request = getObserverInformation.mock.calls[0][0];
152
+ expect(request.EventStore).toEqual('test-store');
153
+ expect(request.Namespace).toEqual('test-namespace');
154
+ expect(request.ObserverId).toEqual('OpSummary');
155
+
156
+ expect(state.runningState).toBe(ObserverRunningState.Active);
157
+ expect(state.isSubscribed).toBe(true);
158
+ expect(state.nextEventSequenceNumber.value).toEqual(5n);
159
+ expect(state.lastHandledEventSequenceNumber.value).toEqual(4n);
160
+ expect(state.tailEventSequenceNumber.value).toEqual(4n);
161
+ });
162
+ });
163
+
164
+ describe('when getting state for a model-bound read model with a custom event sequence', () => {
165
+ it('should resolve the identifier and pass the model-declared event sequence', async () => {
166
+ const { projections, getObserverInformation } = await createDiscoveredProjections();
167
+
168
+ await projections.getStateForModel(OpSummary);
169
+
170
+ const request = getObserverInformation.mock.calls[0][0];
171
+ expect(request.ObserverId).toEqual('OpSummary');
172
+ expect(request.EventSequenceId).toEqual('custom-op-sequence');
173
+ });
174
+ });
175
+
176
+ describe('when getting failed partitions for a model-bound read model', () => {
177
+ it('should call GetFailedPartitions with the resolved projection identifier', async () => {
178
+ const { projections, getFailedPartitions } = await createDiscoveredProjections();
179
+
180
+ await projections.getFailedPartitionsForModel(OpSummary);
181
+
182
+ expect(getFailedPartitions).toHaveBeenCalledTimes(1);
183
+ const request = getFailedPartitions.mock.calls[0][0];
184
+ expect(request.ObserverId).toEqual('OpSummary');
185
+ });
186
+ });
187
+
188
+ describe('when replaying a projection by identifier', () => {
189
+ it('should call Replay and parse the returned job id', async () => {
190
+ const { projections, replay } = await createDiscoveredProjections();
191
+
192
+ const jobId = await projections.replay('OpSummary');
193
+
194
+ expect(replay).toHaveBeenCalledTimes(1);
195
+ const request = replay.mock.calls[0][0];
196
+ expect(request.ObserverId).toEqual('OpSummary');
197
+ expect(jobId.toString()).toEqual('11111111-1111-1111-1111-111111111111');
198
+ });
199
+ });
200
+
201
+ describe('when replaying a projection that is not replayable', () => {
202
+ it('should resolve to a not-set job id', async () => {
203
+ const { projections, replay } = await createDiscoveredProjections();
204
+ replay.mockResolvedValueOnce({ JobId: '' });
205
+
206
+ const jobId = await projections.replay('OpSummary');
207
+
208
+ expect(jobId.value.toString()).toEqual('00000000-0000-0000-0000-000000000000');
209
+ });
210
+ });
211
+
212
+ describe('when replaying a model-bound read model', () => {
213
+ it('should resolve the identifier and replay it', async () => {
214
+ const { projections, replay } = await createDiscoveredProjections();
215
+
216
+ await projections.replayForModel(OpSummary);
217
+
218
+ const request = replay.mock.calls[0][0];
219
+ expect(request.ObserverId).toEqual('OpSummary');
220
+ });
221
+ });
222
+
223
+ describe('when querying a projection declaration', () => {
224
+ it('should call Preview and return the read model entries', async () => {
225
+ const { projections, preview } = await createDiscoveredProjections();
226
+
227
+ const result = await projections.query('projection Orders\n from OpStateChanged');
228
+
229
+ expect(preview).toHaveBeenCalledTimes(1);
230
+ const request = preview.mock.calls[0][0];
231
+ expect(request.EventSequenceId).toEqual('event-log');
232
+ expect(result.readModelEntries).toEqual(['{"id":"1"}']);
233
+ });
234
+ });
235
+
236
+ describe('when querying a projection declaration with a custom event sequence', () => {
237
+ it('should pass the given event sequence identifier', async () => {
238
+ const { projections, preview } = await createDiscoveredProjections();
239
+
240
+ await projections.query('projection Orders\n from OpStateChanged', 'inbox');
241
+
242
+ const request = preview.mock.calls[0][0];
243
+ expect(request.EventSequenceId).toEqual('inbox');
244
+ });
245
+ });
246
+
247
+ describe('when querying a projection declaration that fails to parse', () => {
248
+ it('should throw UnableToQueryProjection with the syntax errors', async () => {
249
+ const { projections, preview } = await createDiscoveredProjections();
250
+ preview.mockResolvedValueOnce({
251
+ Value1: { Errors: [{ Message: 'Unexpected token', Line: 2, Column: 3 }] }
252
+ });
253
+
254
+ await expect(projections.query('projection Bad')).rejects.toThrow(UnableToQueryProjection);
255
+ });
256
+ });
257
+ });
@@ -85,7 +85,7 @@ function createProjections(readModels: (new (...args: unknown[]) => unknown)[])
85
85
  eventTypeMigrations: []
86
86
  };
87
87
 
88
- const projections = new Projections('test-store', connection, clientArtifacts, 'test-sink');
88
+ const projections = new Projections('test-store', 'test-namespace', connection, clientArtifacts, 'test-sink');
89
89
  return { projections, registerMock };
90
90
  }
91
91
 
@@ -12,6 +12,11 @@ import { ChronicleConnection } from '../connection';
12
12
  import { toContractsGuid } from '../connection/Guid';
13
13
  import { WellKnownSinks } from '../sinks';
14
14
  import { EventSequenceId } from '../eventSequences/EventSequenceId';
15
+ import { EventSequenceNumber } from '../eventSequences/EventSequenceNumber';
16
+ import { JobId } from '../jobs/JobId';
17
+ import { FailedPartition } from '../observation/FailedPartition';
18
+ import { FailedPartitions } from '../observation/FailedPartitions';
19
+ import { toObserverRunningState } from '../observation/toObserverRunningState';
15
20
  import { getReadModelMetadata } from '../readModels';
16
21
  import { TypeIntrospector } from '../types';
17
22
  import { IProjections } from './IProjections';
@@ -24,20 +29,28 @@ import {
24
29
  buildNestedEntry,
25
30
  ChildrenDefinitionLike,
26
31
  ContractEventType,
32
+ ensureFromEntry,
27
33
  FromRecord,
28
34
  getEventTypeMapKey,
29
35
  toContractEventType
30
36
  } from './modelBound/childrenAndNestedBuilder';
31
37
  import { getChildrenFromMetadata } from './modelBound/childrenFrom';
38
+ import { getClearWithPropertyMetadata } from './modelBound/clearWith';
39
+ import { getEventSequenceMetadata } from './modelBound/eventSequence';
40
+ import { getFromAllMetadata } from './modelBound/fromAll';
32
41
  import { getFromEveryMetadata } from './modelBound/fromEvery';
33
42
  import { getFromEventMetadata, hasFromEventMetadata } from './modelBound/fromEvent';
34
43
  import { getJoinMetadata } from './modelBound/join';
44
+ import { isNoAutoMap, isPropertyNoAutoMap } from './modelBound/noAutoMap';
35
45
  import { ProjectionId } from './ProjectionId';
46
+ import { ProjectionQueryResult } from './ProjectionQueryResult';
47
+ import { ProjectionState } from './ProjectionState';
36
48
  import { isNested } from './modelBound/nested';
37
49
  import { isNotRewindable } from './modelBound/notRewindable';
38
50
  import { isPassive } from './modelBound/passive';
39
51
  import { getRemovedWithClassMetadata, getRemovedWithPropertyMetadata } from './modelBound/removedWith';
40
52
  import { getRemovedWithJoinClassMetadata, getRemovedWithJoinPropertyMetadata } from './modelBound/removedWithJoin';
53
+ import { UnableToQueryProjection } from './UnableToQueryProjection';
41
54
 
42
55
  interface ResolvedModelBoundMetadata {
43
56
  id: ProjectionId;
@@ -52,19 +65,27 @@ interface ResolvedModelBoundMetadata {
52
65
  export class Projections implements IProjections {
53
66
  private readonly _declarative = new Map<string, Constructor>();
54
67
  private readonly _modelBound = new Map<string, Constructor>();
68
+ private readonly _failedPartitions: FailedPartitions;
55
69
 
56
70
  private readonly _logger = diag.createComponentLogger({ namespace: '@cratis/chronicle/projections' });
57
71
 
58
72
  /**
59
73
  * Creates a new {@link Projections} instance.
74
+ * @param _eventStore - The event store name.
75
+ * @param _namespace - The event store namespace.
76
+ * @param _connection - Chronicle connection.
60
77
  * @param _clientArtifacts - Provider for discovered client artifact types.
78
+ * @param _defaultSinkTypeId - The identifier of the default read model sink.
61
79
  */
62
80
  constructor(
63
81
  private readonly _eventStore: string,
82
+ private readonly _namespace: string,
64
83
  private readonly _connection: ChronicleConnection,
65
84
  private readonly _clientArtifacts: IClientArtifactsProvider,
66
85
  private readonly _defaultSinkTypeId: string
67
- ) {}
86
+ ) {
87
+ this._failedPartitions = new FailedPartitions(_eventStore, _namespace, _connection);
88
+ }
68
89
 
69
90
  /** @inheritdoc */
70
91
  async discover(): Promise<void> {
@@ -137,6 +158,148 @@ export class Projections implements IProjections {
137
158
  }
138
159
  }
139
160
 
161
+ /** @inheritdoc */
162
+ hasFor(projectionId: ProjectionId | string): boolean {
163
+ const id = this.toProjectionIdValue(projectionId);
164
+ return this._declarative.has(id) || this._modelBound.has(id);
165
+ }
166
+
167
+ /** @inheritdoc */
168
+ hasForModel(readModelType: Constructor): boolean {
169
+ try {
170
+ this.resolveProjectionIdForModel(readModelType);
171
+ return true;
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
176
+
177
+ /** @inheritdoc */
178
+ getProjectionIdFor(readModelType: Constructor): ProjectionId {
179
+ return this.resolveProjectionIdForModel(readModelType);
180
+ }
181
+
182
+ /** @inheritdoc */
183
+ async getStateFor(projectionId: ProjectionId | string): Promise<ProjectionState> {
184
+ const id = this.toProjectionIdValue(projectionId);
185
+ const eventSequenceId = this.resolveEventSequenceIdFor(id);
186
+
187
+ const response = await this._connection.observers.getObserverInformation({
188
+ EventStore: this._eventStore,
189
+ Namespace: this._namespace,
190
+ ObserverId: id,
191
+ EventSequenceId: eventSequenceId
192
+ });
193
+
194
+ return {
195
+ runningState: toObserverRunningState(response.RunningState),
196
+ isSubscribed: response.IsSubscribed,
197
+ nextEventSequenceNumber: new EventSequenceNumber(response.NextEventSequenceNumber),
198
+ lastHandledEventSequenceNumber: new EventSequenceNumber(response.LastHandledEventSequenceNumber),
199
+ tailEventSequenceNumber: new EventSequenceNumber(response.TailEventSequenceNumber)
200
+ };
201
+ }
202
+
203
+ /** @inheritdoc */
204
+ getStateForModel(readModelType: Constructor): Promise<ProjectionState> {
205
+ return this.getStateFor(this.resolveProjectionIdForModel(readModelType));
206
+ }
207
+
208
+ /** @inheritdoc */
209
+ getFailedPartitionsForModel(readModelType: Constructor): Promise<FailedPartition[]> {
210
+ const projectionId = this.resolveProjectionIdForModel(readModelType);
211
+ return this._failedPartitions.getFailedPartitionsFor(projectionId.value);
212
+ }
213
+
214
+ /** @inheritdoc */
215
+ async replay(projectionId: ProjectionId | string): Promise<JobId> {
216
+ const id = this.toProjectionIdValue(projectionId);
217
+
218
+ const response = await this._connection.observers.replay({
219
+ EventStore: this._eventStore,
220
+ Namespace: this._namespace,
221
+ ObserverId: id,
222
+
223
+ // The kernel resolves the observer's own event sequence from its identifier - it does not
224
+ // need to be told which one to replay.
225
+ EventSequenceId: ''
226
+ });
227
+
228
+ return response.JobId ? JobId.from(response.JobId) : JobId.from(Guid.empty);
229
+ }
230
+
231
+ /** @inheritdoc */
232
+ replayForModel(readModelType: Constructor): Promise<JobId> {
233
+ return this.replay(this.resolveProjectionIdForModel(readModelType));
234
+ }
235
+
236
+ /** @inheritdoc */
237
+ async query(declaration: string, eventSequenceId: string = EventSequenceId.eventLog.value): Promise<ProjectionQueryResult> {
238
+ const result = await this._connection.projections.preview({
239
+ EventStore: this._eventStore,
240
+ Namespace: this._namespace,
241
+ EventSequenceId: eventSequenceId,
242
+ Declaration: declaration
243
+ });
244
+
245
+ if (result.Value1) {
246
+ throw new UnableToQueryProjection(result.Value1.Errors.map(error => error.Message));
247
+ }
248
+
249
+ return {
250
+ readModelEntries: result.Value0?.ReadModelEntries ?? []
251
+ };
252
+ }
253
+
254
+ private toProjectionIdValue(projectionId: ProjectionId | string): string {
255
+ return typeof projectionId === 'string' ? projectionId : projectionId.value;
256
+ }
257
+
258
+ /**
259
+ * Resolves the {@link ProjectionId} of the projection that maintains a specific read model type,
260
+ * from what has already been discovered.
261
+ * @param readModelType - The read model type to resolve for.
262
+ * @returns The resolved {@link ProjectionId}.
263
+ */
264
+ private resolveProjectionIdForModel(readModelType: Constructor): ProjectionId {
265
+ for (const [id, type] of this._modelBound) {
266
+ if (type === readModelType) {
267
+ return new ProjectionId(id);
268
+ }
269
+ }
270
+
271
+ for (const [id, type] of this._declarative) {
272
+ const metadata = getProjectionMetadata(type);
273
+ if (metadata?.readModelType === readModelType) {
274
+ return new ProjectionId(id);
275
+ }
276
+ }
277
+
278
+ throw new Error(
279
+ `No projection found for read model '${readModelType.name}'. Make sure discover() has run and the ` +
280
+ 'read model is model-bound, or its declarative projection() decorator specifies an explicit readModelType.');
281
+ }
282
+
283
+ /**
284
+ * Resolves the event sequence identifier a discovered projection reads from, without requiring
285
+ * its full definition to have been built.
286
+ * @param projectionId - The projection identifier to resolve for.
287
+ * @returns The resolved event sequence identifier.
288
+ */
289
+ private resolveEventSequenceIdFor(projectionId: string): string {
290
+ const modelBoundType = this._modelBound.get(projectionId);
291
+ if (modelBoundType) {
292
+ return getEventSequenceMetadata(modelBoundType) ?? EventSequenceId.eventLog.value;
293
+ }
294
+
295
+ const declarativeType = this._declarative.get(projectionId);
296
+ if (declarativeType) {
297
+ return getProjectionMetadata(declarativeType)?.eventSequenceId ?? EventSequenceId.eventLog.value;
298
+ }
299
+
300
+ return EventSequenceId.eventLog.value;
301
+ }
302
+
140
303
  private async registerWithRetry(projection: unknown, identifier: string, maxAttempts = 5): Promise<void> {
141
304
  let delay = 2000;
142
305
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -344,14 +507,25 @@ export class Projections implements IProjections {
344
507
  childrenByProperty[property] = buildChildrenEntry(type, property, childrenFromList);
345
508
  }
346
509
 
347
- if (isNested(prototype, property)) {
510
+ const propertyIsNested = isNested(prototype, property);
511
+ if (propertyIsNested) {
348
512
  nestedByProperty[property] = buildNestedEntry(type, property);
349
513
  }
514
+
515
+ // A scalar (non-nested, non-children-collection) root property clears back to no value
516
+ // every time the given event is observed. A nested single-object property's clearWith is
517
+ // handled by buildNestedEntry instead, which clears the whole nested object.
518
+ if (childrenFromList.length === 0 && !propertyIsNested) {
519
+ for (const clearWith of getClearWithPropertyMetadata(prototype, property)) {
520
+ const entry = ensureFromEntry(fromByEventType, clearWith.eventType);
521
+ entry.Value.Properties[property] = '$null';
522
+ }
523
+ }
350
524
  }
351
525
 
352
526
  const allProperties: Record<string, string> = {};
353
527
  for (const property of properties) {
354
- const fromEvery = getFromEveryMetadata(prototype, property);
528
+ const fromEvery = getFromEveryMetadata(prototype, property) ?? getFromAllMetadata(prototype, property);
355
529
  if (fromEvery) {
356
530
  allProperties[property] = fromEvery.contextProperty
357
531
  ? fromEvery.contextProperty
@@ -379,7 +553,8 @@ export class Projections implements IProjections {
379
553
  RemovedWithJoin: Array.from(removedWithJoinByEventType.values()),
380
554
  LastUpdated: { Value: '' },
381
555
  Tags: [],
382
- AutoMap: AutoMap.Enabled,
556
+ AutoMap: isNoAutoMap(type) ? AutoMap.Disabled : AutoMap.Enabled,
557
+ NoAutoMapProperties: properties.filter(property => isPropertyNoAutoMap(prototype, property)),
383
558
  Nested: nestedByProperty
384
559
  };
385
560
  definition.LastUpdated = { Value: this.computeStableLastUpdated(definition) };
@@ -392,7 +567,7 @@ export class Projections implements IProjections {
392
567
  if (readModelMetadata && hasFromEventMetadata(type)) {
393
568
  return {
394
569
  id: new ProjectionId(readModelMetadata.id.value),
395
- eventSequenceId: undefined,
570
+ eventSequenceId: getEventSequenceMetadata(type),
396
571
  readModelIdentifier: readModelMetadata.id.value
397
572
  };
398
573
  }
@@ -0,0 +1,17 @@
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
+ /**
5
+ * Error thrown when a projection query fails because the Projection Declaration Language
6
+ * declaration contains errors.
7
+ */
8
+ export class UnableToQueryProjection extends Error {
9
+ /**
10
+ * Creates a new {@link UnableToQueryProjection}.
11
+ * @param errors - The collection of error messages describing why the query failed.
12
+ */
13
+ constructor(errors: ReadonlyArray<string>) {
14
+ super(`Unable to query projection. Errors:\n${errors.join('\n')}`);
15
+ this.name = 'UnableToQueryProjection';
16
+ }
17
+ }
@@ -4,5 +4,8 @@
4
4
  export { ProjectionId } from './ProjectionId';
5
5
  export type { IProjections } from './IProjections';
6
6
  export { Projections } from './Projections';
7
+ export type { ProjectionState } from './ProjectionState';
8
+ export type { ProjectionQueryResult } from './ProjectionQueryResult';
9
+ export { UnableToQueryProjection } from './UnableToQueryProjection';
7
10
  export * from './declarative';
8
11
  export * from './modelBound';
@@ -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 'reflect-metadata';
5
+ import { EventSequenceId } from '../../eventSequences/EventSequenceId';
6
+
7
+ const METADATA_KEY = 'chronicle:projection:eventSequence';
8
+
9
+ /**
10
+ * Class decorator that overrides the event sequence a model-bound projection reads from.
11
+ * When applied, auto-inbox routing is suppressed - the explicit value is always honored.
12
+ * @param sequence - The event sequence identifier to read from.
13
+ * @returns A class decorator.
14
+ */
15
+ export function eventSequence(sequence: string): ClassDecorator {
16
+ return (target: object) => {
17
+ Reflect.defineMetadata(METADATA_KEY, sequence, target);
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Convenience class decorator that pins a model-bound projection to the default event log
23
+ * sequence. Equivalent to `eventSequence(EventSequenceId.eventLog.value)`, but more expressive
24
+ * about the intent to read from the local event log rather than an inbox or another sequence.
25
+ * @param target - The class constructor.
26
+ */
27
+ export function eventLog(target: Function): void {
28
+ Reflect.defineMetadata(METADATA_KEY, EventSequenceId.eventLog.value, target);
29
+ }
30
+
31
+ /**
32
+ * Retrieves the explicit event sequence identifier stored on a class, if any.
33
+ * @param target - The class constructor.
34
+ * @returns The event sequence identifier, or undefined when the class has no explicit override.
35
+ */
36
+ export function getEventSequenceMetadata(target: Function): string | undefined {
37
+ return Reflect.getMetadata(METADATA_KEY, target);
38
+ }
@@ -0,0 +1,42 @@
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 { TypeIntrospector } from '../../types';
6
+
7
+ /** Metadata stored by the fromAll property decorator. */
8
+ export interface FromAllMetadata {
9
+ /** The event property name to read the value from. */
10
+ readonly property?: string;
11
+ /** The event context property name to read the value from. */
12
+ readonly contextProperty?: string;
13
+ }
14
+
15
+ const METADATA_KEY = 'chronicle:projection:fromAll';
16
+
17
+ /**
18
+ * Property decorator that sets the decorated read model property from a property present on every
19
+ * event type the projection is built from - the convention-based counterpart to declaring the same
20
+ * mapping on every individual `fromEvent`. Equivalent to {@link fromEvery} for model-bound
21
+ * projections; use whichever name reads better for the property being mapped.
22
+ * @param property - Optional event property name. If not specified, uses the model property name.
23
+ * @param contextProperty - Optional event context property name.
24
+ * @returns A property decorator.
25
+ */
26
+ export function fromAll(property?: string, contextProperty?: string): PropertyDecorator {
27
+ return (target: object, propertyKey: string | symbol) => {
28
+ TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, propertyKey.toString());
29
+ const metadata: FromAllMetadata = { property, contextProperty };
30
+ Reflect.defineMetadata(METADATA_KEY, metadata, target, propertyKey.toString());
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Retrieves fromAll metadata stored on the given property.
36
+ * @param target - The class prototype.
37
+ * @param propertyKey - The property name.
38
+ * @returns The fromAll metadata, or undefined if not decorated.
39
+ */
40
+ export function getFromAllMetadata(target: object, propertyKey: string): FromAllMetadata | undefined {
41
+ return Reflect.getMetadata(METADATA_KEY, target, propertyKey);
42
+ }
@@ -35,3 +35,7 @@ export { setValue, getSetValueMetadata } from './setValue';
35
35
  export type { SetValueMetadata } from './setValue';
36
36
  export { fromEvery, getFromEveryMetadata } from './fromEvery';
37
37
  export type { FromEveryMetadata } from './fromEvery';
38
+ export { fromAll, getFromAllMetadata } from './fromAll';
39
+ export type { FromAllMetadata } from './fromAll';
40
+ export { noAutoMap, isNoAutoMap, isPropertyNoAutoMap } from './noAutoMap';
41
+ export { eventSequence, eventLog, getEventSequenceMetadata } from './eventSequence';