@cratis/chronicle 3.1.1 → 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.
- package/EventStore.ts +1 -1
- package/compliance/ComplianceContracts.ts +59 -0
- package/compliance/IPIIManager.ts +12 -0
- package/compliance/PIIManager.spec.ts +50 -0
- package/compliance/PIIManager.ts +9 -0
- package/compliance/PIINotSupportedOnEventSourceId.ts +21 -0
- package/compliance/index.ts +2 -0
- package/compliance/pii.spec.ts +37 -0
- package/compliance/pii.ts +16 -1
- package/compliance/subject.spec.ts +45 -0
- package/compliance/subject.ts +64 -0
- package/dist/EventStore.js +1 -1
- package/dist/EventStore.js.map +1 -1
- package/dist/compliance/ComplianceContracts.d.ts +31 -0
- package/dist/compliance/ComplianceContracts.d.ts.map +1 -1
- package/dist/compliance/ComplianceContracts.js +52 -0
- package/dist/compliance/ComplianceContracts.js.map +1 -1
- package/dist/compliance/IPIIManager.d.ts +11 -0
- package/dist/compliance/IPIIManager.d.ts.map +1 -1
- package/dist/compliance/PIIManager.d.ts +2 -0
- package/dist/compliance/PIIManager.d.ts.map +1 -1
- package/dist/compliance/PIIManager.js +8 -0
- package/dist/compliance/PIIManager.js.map +1 -1
- package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts +11 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.d.ts.map +1 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.js +19 -0
- package/dist/compliance/PIINotSupportedOnEventSourceId.js.map +1 -0
- package/dist/compliance/index.d.ts +2 -0
- package/dist/compliance/index.d.ts.map +1 -1
- package/dist/compliance/index.js +2 -0
- package/dist/compliance/index.js.map +1 -1
- package/dist/compliance/pii.d.ts.map +1 -1
- package/dist/compliance/pii.js +13 -1
- package/dist/compliance/pii.js.map +1 -1
- package/dist/compliance/subject.d.ts +39 -0
- package/dist/compliance/subject.d.ts.map +1 -0
- package/dist/compliance/subject.js +58 -0
- package/dist/compliance/subject.js.map +1 -0
- package/dist/eventSequences/AppendOptions.d.ts +8 -0
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +7 -0
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +17 -6
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/events/EventContext.d.ts +3 -0
- package/dist/events/EventContext.d.ts.map +1 -1
- package/dist/events/Tag.d.ts +10 -0
- package/dist/events/Tag.d.ts.map +1 -0
- package/dist/events/Tag.js +16 -0
- package/dist/events/Tag.js.map +1 -0
- package/dist/events/filterEventsByTagDecorator.d.ts +35 -0
- package/dist/events/filterEventsByTagDecorator.d.ts.map +1 -0
- package/dist/events/filterEventsByTagDecorator.js +48 -0
- package/dist/events/filterEventsByTagDecorator.js.map +1 -0
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +4 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/mergeTags.d.ts +12 -0
- package/dist/events/mergeTags.d.ts.map +1 -0
- package/dist/events/mergeTags.js +26 -0
- package/dist/events/mergeTags.js.map +1 -0
- package/dist/events/tagDecorator.d.ts +38 -0
- package/dist/events/tagDecorator.d.ts.map +1 -0
- package/dist/events/tagDecorator.js +54 -0
- package/dist/events/tagDecorator.js.map +1 -0
- package/dist/observation/ObserverRunningState.d.ts +9 -15
- package/dist/observation/ObserverRunningState.d.ts.map +1 -1
- package/dist/observation/ObserverRunningState.js +8 -14
- package/dist/observation/ObserverRunningState.js.map +1 -1
- package/dist/observation/index.d.ts +1 -0
- package/dist/observation/index.d.ts.map +1 -1
- package/dist/observation/index.js +1 -0
- package/dist/observation/index.js.map +1 -1
- package/dist/observation/toObserverRunningState.d.ts +9 -0
- package/dist/observation/toObserverRunningState.d.ts.map +1 -0
- package/dist/observation/toObserverRunningState.js +26 -0
- package/dist/observation/toObserverRunningState.js.map +1 -0
- package/dist/projections/IProjections.d.ts +71 -1
- package/dist/projections/IProjections.d.ts.map +1 -1
- package/dist/projections/ProjectionQueryResult.d.ts +8 -0
- package/dist/projections/ProjectionQueryResult.d.ts.map +1 -0
- package/dist/projections/ProjectionQueryResult.js +4 -0
- package/dist/projections/ProjectionQueryResult.js.map +1 -0
- package/dist/projections/ProjectionState.d.ts +18 -0
- package/dist/projections/ProjectionState.d.ts.map +1 -0
- package/dist/projections/ProjectionState.js +4 -0
- package/dist/projections/ProjectionState.js.map +1 -0
- package/dist/projections/Projections.d.ts +46 -1
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +153 -6
- package/dist/projections/Projections.js.map +1 -1
- package/dist/projections/UnableToQueryProjection.d.ts +12 -0
- package/dist/projections/UnableToQueryProjection.d.ts.map +1 -0
- package/dist/projections/UnableToQueryProjection.js +17 -0
- package/dist/projections/UnableToQueryProjection.js.map +1 -0
- package/dist/projections/index.d.ts +3 -0
- package/dist/projections/index.d.ts.map +1 -1
- package/dist/projections/index.js +1 -0
- package/dist/projections/index.js.map +1 -1
- package/dist/projections/modelBound/eventSequence.d.ts +22 -0
- package/dist/projections/modelBound/eventSequence.d.ts.map +1 -0
- package/dist/projections/modelBound/eventSequence.js +34 -0
- package/dist/projections/modelBound/eventSequence.js.map +1 -0
- package/dist/projections/modelBound/fromAll.d.ts +26 -0
- package/dist/projections/modelBound/fromAll.d.ts.map +1 -0
- package/dist/projections/modelBound/fromAll.js +31 -0
- package/dist/projections/modelBound/fromAll.js.map +1 -0
- package/dist/projections/modelBound/index.d.ts +4 -0
- package/dist/projections/modelBound/index.d.ts.map +1 -1
- package/dist/projections/modelBound/index.js +3 -0
- package/dist/projections/modelBound/index.js.map +1 -1
- package/dist/projections/modelBound/noAutoMap.d.ts +26 -0
- package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -0
- package/dist/projections/modelBound/noAutoMap.js +44 -0
- package/dist/projections/modelBound/noAutoMap.js.map +1 -0
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +7 -3
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/readModels/MaterializedReadModels.d.ts +0 -1
- package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
- package/dist/readModels/MaterializedReadModels.js +2 -8
- package/dist/readModels/MaterializedReadModels.js.map +1 -1
- package/dist/readModels/ReadModelSubjectResolver.d.ts +22 -0
- package/dist/readModels/ReadModelSubjectResolver.d.ts.map +1 -0
- package/dist/readModels/ReadModelSubjectResolver.js +38 -0
- package/dist/readModels/ReadModelSubjectResolver.js.map +1 -0
- package/dist/readModels/ReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.js +7 -7
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/readModels/index.d.ts +1 -0
- package/dist/readModels/index.d.ts.map +1 -1
- package/dist/readModels/index.js +1 -0
- package/dist/readModels/index.js.map +1 -1
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +21 -3
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/schemas/JsonSchema.d.ts +14 -0
- package/dist/schemas/JsonSchema.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.d.ts +61 -5
- package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.js +130 -26
- package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.js +3 -1
- package/dist/seeding/EventSeeding.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/AppendOptions.ts +9 -0
- package/eventSequences/EventForEventSourceId.ts +9 -0
- package/eventSequences/EventSequence.ts +20 -6
- package/events/EventContext.ts +4 -0
- package/events/Tag.ts +14 -0
- package/events/filterEventsByTagDecorator.ts +51 -0
- package/events/index.ts +4 -0
- package/events/mergeTags.spec.ts +50 -0
- package/events/mergeTags.ts +27 -0
- package/events/tagDecorator.spec.ts +115 -0
- package/events/tagDecorator.ts +59 -0
- package/observation/ObserverRunningState.ts +9 -18
- package/observation/index.ts +1 -0
- package/observation/toObserverRunningState.ts +27 -0
- package/package.json +8 -8
- package/projections/IProjections.ts +81 -1
- package/projections/ProjectionQueryResult.ts +10 -0
- package/projections/ProjectionState.ts +25 -0
- package/projections/Projections.childrenAndNested.spec.ts +1 -1
- package/projections/Projections.modelBoundCompleteness.spec.ts +224 -0
- package/projections/Projections.operationalSurface.spec.ts +257 -0
- package/projections/Projections.spec.ts +1 -1
- package/projections/Projections.ts +180 -5
- package/projections/UnableToQueryProjection.ts +17 -0
- package/projections/index.ts +3 -0
- package/projections/modelBound/eventSequence.ts +38 -0
- package/projections/modelBound/fromAll.ts +42 -0
- package/projections/modelBound/index.ts +4 -0
- package/projections/modelBound/noAutoMap.ts +47 -0
- package/reactors/Reactors.ts +7 -3
- package/readModels/MaterializedReadModels.spec.ts +98 -0
- package/readModels/MaterializedReadModels.ts +2 -8
- package/readModels/ReadModelSubjectResolver.spec.ts +84 -0
- package/readModels/ReadModelSubjectResolver.ts +44 -0
- package/readModels/ReadModels.spec.ts +118 -0
- package/readModels/ReadModels.ts +7 -7
- package/readModels/index.ts +1 -0
- package/reducers/Reducers.spec.ts +80 -0
- package/reducers/Reducers.ts +22 -3
- package/schemas/JsonSchema.ts +16 -0
- package/schemas/JsonSchemaGenerator.spec.ts +195 -0
- package/schemas/JsonSchemaGenerator.ts +145 -27
- package/schemas/index.ts +1 -1
- package/seeding/EventSeeding.ts +3 -1
|
@@ -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
|
-
|
|
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:
|
|
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
|
+
}
|
package/projections/index.ts
CHANGED
|
@@ -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';
|
|
@@ -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 'reflect-metadata';
|
|
5
|
+
import { TypeIntrospector } from '../../types';
|
|
6
|
+
|
|
7
|
+
const CLASS_METADATA_KEY = 'chronicle:projection:noAutoMap:class';
|
|
8
|
+
const PROPERTY_METADATA_KEY = 'chronicle:projection:noAutoMap:property';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Class or property decorator that disables AutoMap for a model-bound projection.
|
|
12
|
+
* Applied to a class, it prevents AutoMap from mapping any property automatically.
|
|
13
|
+
* Applied to a property, it excludes only that single property from AutoMap while every other
|
|
14
|
+
* property keeps mapping - use it to stop an unrelated event that carries an identically named
|
|
15
|
+
* property from silently overwriting a property whose value is set explicitly (for example via
|
|
16
|
+
* `setFrom`).
|
|
17
|
+
* @param target - The class constructor, or the class prototype when used on a property.
|
|
18
|
+
* @param propertyKey - The property name, when used as a property decorator.
|
|
19
|
+
*/
|
|
20
|
+
export function noAutoMap(target: object, propertyKey?: string | symbol): void {
|
|
21
|
+
if (propertyKey !== undefined) {
|
|
22
|
+
const key = propertyKey.toString();
|
|
23
|
+
TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
|
|
24
|
+
Reflect.defineMetadata(PROPERTY_METADATA_KEY, true, target, key);
|
|
25
|
+
} else {
|
|
26
|
+
Reflect.defineMetadata(CLASS_METADATA_KEY, true, target as Function);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Checks whether the given class has AutoMap disabled entirely.
|
|
32
|
+
* @param target - The class constructor.
|
|
33
|
+
* @returns True if the class is marked with {@link noAutoMap}; false otherwise.
|
|
34
|
+
*/
|
|
35
|
+
export function isNoAutoMap(target: Function): boolean {
|
|
36
|
+
return Reflect.hasMetadata(CLASS_METADATA_KEY, target);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Checks whether a specific property is excluded from AutoMap.
|
|
41
|
+
* @param target - The class prototype.
|
|
42
|
+
* @param propertyKey - The property name.
|
|
43
|
+
* @returns True if the property is marked with {@link noAutoMap}; false otherwise.
|
|
44
|
+
*/
|
|
45
|
+
export function isPropertyNoAutoMap(target: object, propertyKey: string): boolean {
|
|
46
|
+
return Reflect.hasMetadata(PROPERTY_METADATA_KEY, target, propertyKey);
|
|
47
|
+
}
|
package/reactors/Reactors.ts
CHANGED
|
@@ -12,6 +12,9 @@ import { getEventTypeMetadata } from '../events/eventTypeDecorator';
|
|
|
12
12
|
import { EventContext } from '../events/EventContext';
|
|
13
13
|
import { EventTypeId } from '../events/EventTypeId';
|
|
14
14
|
import { EventTypeGeneration } from '../events/EventTypeGeneration';
|
|
15
|
+
import { Tag } from '../events/Tag';
|
|
16
|
+
import { getTagsFor } from '../events/tagDecorator';
|
|
17
|
+
import { getFilterTagsFor } from '../events/filterEventsByTagDecorator';
|
|
15
18
|
import { EventSequenceId } from '../eventSequences/EventSequenceId';
|
|
16
19
|
import type { IEventLog } from '../eventSequences/IEventLog';
|
|
17
20
|
import { notifyReplayLifecycle } from '../observation/notifyReplayLifecycle';
|
|
@@ -239,9 +242,9 @@ export class Reactors implements IReactors {
|
|
|
239
242
|
Key: EVENT_SOURCE_ID_KEY
|
|
240
243
|
})),
|
|
241
244
|
IsReplayable: false,
|
|
242
|
-
Tags:
|
|
245
|
+
Tags: getTagsFor(reactorType).map(t => t.value),
|
|
243
246
|
Filters: {
|
|
244
|
-
FilterTags:
|
|
247
|
+
FilterTags: getFilterTagsFor(reactorType).map(t => t.value),
|
|
245
248
|
EventSourceType: '',
|
|
246
249
|
EventStreamType: 'All'
|
|
247
250
|
}
|
|
@@ -303,7 +306,8 @@ export class Reactors implements IReactors {
|
|
|
303
306
|
},
|
|
304
307
|
occurred: new Date(event.Context!.Occurred?.Value ?? ''),
|
|
305
308
|
correlationId: event.Context?.CorrelationId ? `${event.Context.CorrelationId.lo}-${event.Context.CorrelationId.hi}` : '',
|
|
306
|
-
causation: []
|
|
309
|
+
causation: [],
|
|
310
|
+
tags: (event.Context!.Tags ?? []).map(value => new Tag(value))
|
|
307
311
|
};
|
|
308
312
|
|
|
309
313
|
this._logger.info('Invoking reactor handler', {
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { field } from '@cratis/fundamentals';
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import type { ChronicleConnection } from '../connection';
|
|
8
|
+
import { pii } from '../compliance/pii';
|
|
9
|
+
import { subject } from '../compliance/subject';
|
|
10
|
+
import { readModel } from './readModel';
|
|
11
|
+
import { MaterializedReadModels } from './MaterializedReadModels';
|
|
12
|
+
|
|
13
|
+
// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
|
|
14
|
+
// fixtures don't depend on the test runner's decorator-syntax support.
|
|
15
|
+
|
|
16
|
+
function createMaterializedReadModels(instancesJson: string[], releaseResponse: Record<string, unknown> = { HasError: false, Payload: '{}' }) {
|
|
17
|
+
const release = vi.fn().mockResolvedValue(releaseResponse);
|
|
18
|
+
const getInstances = vi.fn().mockResolvedValue({ Instances: instancesJson });
|
|
19
|
+
const connection = {
|
|
20
|
+
compliance: { release },
|
|
21
|
+
materializedReadModels: { getInstances }
|
|
22
|
+
} as unknown as ChronicleConnection;
|
|
23
|
+
|
|
24
|
+
const readModels = new MaterializedReadModels('test-store', 'test-namespace', connection);
|
|
25
|
+
return { readModels, release };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('MaterializedReadModels', () => {
|
|
29
|
+
describe('when a read model has a property decorated with @subject()', () => {
|
|
30
|
+
class Employee {
|
|
31
|
+
id = '';
|
|
32
|
+
personId = '';
|
|
33
|
+
ssn = '';
|
|
34
|
+
}
|
|
35
|
+
field(String)(Employee.prototype, 'id');
|
|
36
|
+
field(String)(Employee.prototype, 'personId');
|
|
37
|
+
field(String)(Employee.prototype, 'ssn');
|
|
38
|
+
pii()(Employee.prototype, 'ssn');
|
|
39
|
+
subject()(Employee.prototype, 'personId');
|
|
40
|
+
readModel('EmployeeWithSubjectMaterialized')(Employee);
|
|
41
|
+
|
|
42
|
+
it('should release using the decorated property as the subject', async () => {
|
|
43
|
+
const json = JSON.stringify({ id: 'employee-1', personId: 'person-42', ssn: '123-45-6789' });
|
|
44
|
+
const { readModels, release } = createMaterializedReadModels([json]);
|
|
45
|
+
|
|
46
|
+
await readModels.getInstances(Employee);
|
|
47
|
+
|
|
48
|
+
expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'person-42' }));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('when a read model has no property decorated with @subject() but has an id property', () => {
|
|
53
|
+
class Customer {
|
|
54
|
+
id = '';
|
|
55
|
+
ssn = '';
|
|
56
|
+
}
|
|
57
|
+
field(String)(Customer.prototype, 'id');
|
|
58
|
+
field(String)(Customer.prototype, 'ssn');
|
|
59
|
+
pii()(Customer.prototype, 'ssn');
|
|
60
|
+
readModel('CustomerWithIdOnlyMaterialized')(Customer);
|
|
61
|
+
|
|
62
|
+
it('should fall back to the id property as the subject, unchanged from today', async () => {
|
|
63
|
+
const json = JSON.stringify({ id: 'customer-7', ssn: '987-65-4321' });
|
|
64
|
+
const { readModels, release } = createMaterializedReadModels([json]);
|
|
65
|
+
|
|
66
|
+
await readModels.getInstances(Customer);
|
|
67
|
+
|
|
68
|
+
expect(release).toHaveBeenCalledWith(expect.objectContaining({ Subject: 'customer-7' }));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('when a read model has neither a decorated property nor an id property', () => {
|
|
73
|
+
class Anonymous {
|
|
74
|
+
ssn = '';
|
|
75
|
+
}
|
|
76
|
+
field(String)(Anonymous.prototype, 'ssn');
|
|
77
|
+
pii()(Anonymous.prototype, 'ssn');
|
|
78
|
+
readModel('AnonymousMaterialized')(Anonymous);
|
|
79
|
+
|
|
80
|
+
it('should not call release, unchanged from today', async () => {
|
|
81
|
+
const json = JSON.stringify({ ssn: '000-00-0000' });
|
|
82
|
+
const { readModels, release } = createMaterializedReadModels([json]);
|
|
83
|
+
|
|
84
|
+
await readModels.getInstances(Anonymous);
|
|
85
|
+
|
|
86
|
+
expect(release).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return the instance unreleased', async () => {
|
|
90
|
+
const json = JSON.stringify({ ssn: '000-00-0000' });
|
|
91
|
+
const { readModels } = createMaterializedReadModels([json]);
|
|
92
|
+
|
|
93
|
+
const [instance] = await readModels.getInstances(Anonymous);
|
|
94
|
+
|
|
95
|
+
expect(instance.ssn).toBe('000-00-0000');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -6,6 +6,7 @@ import { JsonSerializer } from '@cratis/fundamentals';
|
|
|
6
6
|
import { ChronicleConnection } from '../connection';
|
|
7
7
|
import { JsonSchemaGenerator } from '../schemas';
|
|
8
8
|
import { getReadModelMetadata } from './readModel';
|
|
9
|
+
import { ReadModelSubjectResolver } from './ReadModelSubjectResolver';
|
|
9
10
|
import type { IMaterializedReadModels } from './IMaterializedReadModels';
|
|
10
11
|
|
|
11
12
|
const defaultTake = 50;
|
|
@@ -97,7 +98,7 @@ export class MaterializedReadModels implements IMaterializedReadModels {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
private async releaseInstance<TReadModel>(readModelType: Constructor<TReadModel>, instance: TReadModel, schema: string): Promise<TReadModel> {
|
|
100
|
-
const subject =
|
|
101
|
+
const subject = ReadModelSubjectResolver.resolveFrom(readModelType, instance);
|
|
101
102
|
if (!subject) {
|
|
102
103
|
return instance;
|
|
103
104
|
}
|
|
@@ -141,11 +142,4 @@ export class MaterializedReadModels implements IMaterializedReadModels {
|
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
private extractSubject<TReadModel>(instance: TReadModel): string | undefined {
|
|
145
|
-
const anyInstance = instance as Record<string, unknown>;
|
|
146
|
-
if (anyInstance.id !== undefined && anyInstance.id !== null) {
|
|
147
|
-
return String(anyInstance.id);
|
|
148
|
-
}
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
145
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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 } from 'vitest';
|
|
6
|
+
import { subject } from '../compliance/subject';
|
|
7
|
+
import { ReadModelSubjectResolver } from './ReadModelSubjectResolver';
|
|
8
|
+
|
|
9
|
+
// Decorators are applied as plain function calls (rather than `@decorator` syntax) so these
|
|
10
|
+
// fixtures don't depend on the test runner's decorator-syntax support.
|
|
11
|
+
|
|
12
|
+
describe('ReadModelSubjectResolver', () => {
|
|
13
|
+
describe('when the read model has a property decorated with @subject()', () => {
|
|
14
|
+
class Employee {
|
|
15
|
+
id = '';
|
|
16
|
+
personId = '';
|
|
17
|
+
}
|
|
18
|
+
subject()(Employee.prototype, 'personId');
|
|
19
|
+
|
|
20
|
+
it('should resolve to the decorated property value', () => {
|
|
21
|
+
const instance = new Employee();
|
|
22
|
+
instance.id = 'employee-1';
|
|
23
|
+
instance.personId = 'person-42';
|
|
24
|
+
|
|
25
|
+
expect(ReadModelSubjectResolver.resolveFrom(Employee, instance)).toBe('person-42');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should take precedence over the id property', () => {
|
|
29
|
+
const instance = new Employee();
|
|
30
|
+
instance.id = 'employee-1';
|
|
31
|
+
instance.personId = 'person-42';
|
|
32
|
+
|
|
33
|
+
expect(ReadModelSubjectResolver.resolveFrom(Employee, instance)).not.toBe('employee-1');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should fall back to id when the decorated property has no value', () => {
|
|
37
|
+
const instance = new Employee();
|
|
38
|
+
instance.id = 'employee-1';
|
|
39
|
+
instance.personId = '';
|
|
40
|
+
|
|
41
|
+
expect(ReadModelSubjectResolver.resolveFrom(Employee, instance)).toBe('employee-1');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('when the read model has no property decorated with @subject()', () => {
|
|
46
|
+
class Customer {
|
|
47
|
+
id = '';
|
|
48
|
+
name = '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it('should fall back to the id property', () => {
|
|
52
|
+
const instance = new Customer();
|
|
53
|
+
instance.id = 'customer-7';
|
|
54
|
+
|
|
55
|
+
expect(ReadModelSubjectResolver.resolveFrom(Customer, instance)).toBe('customer-7');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('when the read model has neither a decorated property nor an id property', () => {
|
|
60
|
+
class Anonymous {
|
|
61
|
+
name = '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
it('should not resolve a subject', () => {
|
|
65
|
+
const instance = new Anonymous();
|
|
66
|
+
|
|
67
|
+
expect(ReadModelSubjectResolver.resolveFrom(Anonymous, instance)).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('when the instance does not exist', () => {
|
|
72
|
+
class Customer {
|
|
73
|
+
id = '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
it('should not resolve a subject for null', () => {
|
|
77
|
+
expect(ReadModelSubjectResolver.resolveFrom(Customer, null as unknown as Customer)).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should not resolve a subject for undefined', () => {
|
|
81
|
+
expect(ReadModelSubjectResolver.resolveFrom(Customer, undefined as unknown as Customer)).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|