@cratis/chronicle 6.14.1 → 6.14.2
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/dist/projections/CompiledProjectionDefinitions.d.ts +10 -0
- package/dist/projections/CompiledProjectionDefinitions.d.ts.map +1 -0
- package/dist/projections/CompiledProjectionDefinitions.js +4 -0
- package/dist/projections/CompiledProjectionDefinitions.js.map +1 -0
- package/dist/projections/ProjectionDefinitionCompiler.d.ts +35 -0
- package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -0
- package/dist/projections/ProjectionDefinitionCompiler.js +355 -0
- package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -0
- package/dist/projections/Projections.d.ts +0 -21
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +9 -339
- package/dist/projections/Projections.js.map +1 -1
- package/dist/projections/ResolvedModelBoundMetadata.d.ts +8 -0
- package/dist/projections/ResolvedModelBoundMetadata.d.ts.map +1 -0
- package/dist/projections/ResolvedModelBoundMetadata.js +4 -0
- package/dist/projections/ResolvedModelBoundMetadata.js.map +1 -0
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.js +7 -3
- package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
- package/dist/projections/modelBound/isModelBoundProjection.d.ts +3 -0
- package/dist/projections/modelBound/isModelBoundProjection.d.ts.map +1 -0
- package/dist/projections/modelBound/isModelBoundProjection.js +9 -0
- package/dist/projections/modelBound/isModelBoundProjection.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/projections/CompiledProjectionDefinitions.ts +13 -0
- package/projections/ProjectionDefinitionCompiler.registration.spec.ts +279 -0
- package/projections/ProjectionDefinitionCompiler.ts +386 -0
- package/projections/Projections.childrenAndNested.spec.ts +47 -2
- package/projections/Projections.ts +14 -413
- package/projections/ResolvedModelBoundMetadata.ts +11 -0
- package/projections/modelBound/childrenAndNestedBuilder.ts +7 -3
- package/projections/modelBound/isModelBoundProjection.ts +10 -0
|
@@ -2,72 +2,29 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import { diag } from '@opentelemetry/api';
|
|
5
|
-
import {
|
|
6
|
-
AutoMap,
|
|
7
|
-
ProjectionOwner,
|
|
8
|
-
ReadModelObserverType
|
|
9
|
-
} from '@cratis/chronicle.contracts';
|
|
5
|
+
import { ProjectionOwner, type ProjectionDefinition } from '@cratis/chronicle.contracts';
|
|
10
6
|
import { Constructor, Guid } from '@cratis/fundamentals';
|
|
11
7
|
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
12
8
|
import { ChronicleConnection } from '../connection/index.js';
|
|
13
|
-
import { WellKnownSinks } from '../sinks/index.js';
|
|
14
9
|
import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
|
|
15
10
|
import { EventSequenceNumber } from '../eventSequences/EventSequenceNumber.js';
|
|
16
11
|
import { JobId } from '../jobs/JobId.js';
|
|
17
12
|
import { FailedPartition } from '../observation/FailedPartition.js';
|
|
18
13
|
import { FailedPartitions } from '../observation/FailedPartitions.js';
|
|
19
14
|
import { toObserverRunningState } from '../observation/toObserverRunningState.js';
|
|
20
|
-
import { getReadModelMetadata } from '../readModels/index.js';
|
|
21
15
|
import { getReadModelId } from '../readModels/readModel.js';
|
|
22
|
-
import { buildReadModelDefinition } from '../readModels/buildReadModelDefinition.js';
|
|
23
16
|
import { assertUniqueReadModelIds } from '../readModels/assertUniqueReadModelIds.js';
|
|
24
17
|
import { rootReadModelTypes } from '../readModels/rootReadModelTypes.js';
|
|
25
|
-
import { JsonSchemaGenerator } from '../schemas/index.js';
|
|
26
|
-
import { TypeIntrospector } from '../types/index.js';
|
|
27
|
-
import { hasModelBoundProperties } from '../types/TypeDiscoverer.js';
|
|
28
18
|
import { IProjections } from './IProjections.js';
|
|
29
|
-
import { constantValueExpression } from './constantValueExpression.js';
|
|
30
19
|
import { getProjectionMetadata } from './declarative/projection.js';
|
|
31
|
-
import { ProjectionBuilderFor } from './declarative/ProjectionBuilderFor.js';
|
|
32
|
-
import type { IProjectionFor } from './declarative/IProjectionFor.js';
|
|
33
|
-
import {
|
|
34
|
-
applyPropertyMappings,
|
|
35
|
-
buildChildrenEntry,
|
|
36
|
-
buildNestedEntry,
|
|
37
|
-
ChildrenDefinitionLike,
|
|
38
|
-
ContractEventType,
|
|
39
|
-
ensureFromEntry,
|
|
40
|
-
FromRecord,
|
|
41
|
-
getEventTypeMapKey,
|
|
42
|
-
toContractEventType
|
|
43
|
-
} from './modelBound/childrenAndNestedBuilder.js';
|
|
44
|
-
import { getChildrenFromMetadata } from './modelBound/childrenFrom.js';
|
|
45
|
-
import { getClearWithPropertyMetadata } from './modelBound/clearWith.js';
|
|
46
20
|
import { getEventSequenceMetadata } from './modelBound/eventSequence.js';
|
|
47
|
-
import {
|
|
48
|
-
import { getFromEveryMetadata } from './modelBound/fromEvery.js';
|
|
49
|
-
import { getFromEventMetadata, hasFromEventMetadata } from './modelBound/fromEvent.js';
|
|
50
|
-
import { getJoinMetadata } from './modelBound/join.js';
|
|
51
|
-
import { isNoAutoMap, isPropertyNoAutoMap } from './modelBound/noAutoMap.js';
|
|
21
|
+
import { isModelBoundProjection } from './modelBound/isModelBoundProjection.js';
|
|
52
22
|
import { ProjectionId } from './ProjectionId.js';
|
|
53
23
|
import { ProjectionQueryResult } from './ProjectionQueryResult.js';
|
|
54
24
|
import { ProjectionState } from './ProjectionState.js';
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
import { isPassive } from './modelBound/passive.js';
|
|
58
|
-
import { getRemovedWithClassMetadata, getRemovedWithPropertyMetadata } from './modelBound/removedWith.js';
|
|
59
|
-
import { getRemovedWithJoinClassMetadata, getRemovedWithJoinPropertyMetadata } from './modelBound/removedWithJoin.js';
|
|
60
|
-
import { getVariantOfMetadata } from './modelBound/variantOf.js';
|
|
61
|
-
import { getEntersOnMetadata } from './modelBound/entersOn.js';
|
|
62
|
-
import { getGlobalForMetadata } from './modelBound/globalFor.js';
|
|
25
|
+
import { ProjectionDefinitionCompiler } from './ProjectionDefinitionCompiler.js';
|
|
26
|
+
import type { ResolvedModelBoundMetadata } from './ResolvedModelBoundMetadata.js';
|
|
63
27
|
import { UnableToQueryProjection } from './UnableToQueryProjection.js';
|
|
64
|
-
import { BuiltProjection, crossWireGroups, mergeGlobalHandlers, reclassify, VariantDeclaration } from './VariantReclassifier.js';
|
|
65
|
-
|
|
66
|
-
interface ResolvedModelBoundMetadata {
|
|
67
|
-
id: ProjectionId;
|
|
68
|
-
eventSequenceId: string | undefined;
|
|
69
|
-
readModelIdentifier: string;
|
|
70
|
-
}
|
|
71
28
|
|
|
72
29
|
/**
|
|
73
30
|
* Implements {@link IProjections}, managing discovery and registration of projections
|
|
@@ -116,7 +73,7 @@ export class Projections implements IProjections {
|
|
|
116
73
|
}
|
|
117
74
|
|
|
118
75
|
for (const type of readModelTypes) {
|
|
119
|
-
if (!
|
|
76
|
+
if (!isModelBoundProjection(type)) {
|
|
120
77
|
continue;
|
|
121
78
|
}
|
|
122
79
|
|
|
@@ -147,27 +104,16 @@ export class Projections implements IProjections {
|
|
|
147
104
|
.map(type => getProjectionMetadata(type)?.readModelType)
|
|
148
105
|
.filter((type): type is Constructor => type !== undefined);
|
|
149
106
|
assertUniqueReadModelIds([...rootReadModelTypes(this._clientArtifacts), ...inferred]);
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
];
|
|
154
|
-
|
|
155
|
-
// Cross-wiring can only run once every variant in this registration call is known, so it
|
|
156
|
-
// runs here rather than as each definition is built - and LastUpdated is computed only
|
|
157
|
-
// afterward, so a RemovedWith entry it adds is reflected in the hash sent to the kernel.
|
|
158
|
-
crossWireGroups(builtProjections);
|
|
159
|
-
for (const built of builtProjections) {
|
|
160
|
-
built.definition.LastUpdated = { Value: this.computeStableLastUpdated(built.definition) };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const projections = builtProjections.map(built => built.definition);
|
|
107
|
+
const compiled = new ProjectionDefinitionCompiler(this._clientArtifacts, this._defaultSinkTypeId)
|
|
108
|
+
.compile(this._declarative.values(), this._modelBound.values());
|
|
109
|
+
const projections = compiled.definitions;
|
|
164
110
|
|
|
165
111
|
if (projections.length === 0) {
|
|
166
112
|
this._logger.info('No projections to register');
|
|
167
113
|
return;
|
|
168
114
|
}
|
|
169
115
|
|
|
170
|
-
const readModels =
|
|
116
|
+
const readModels = compiled.readModels;
|
|
171
117
|
if (readModels.length > 0) {
|
|
172
118
|
await this._connection.readModels.registerMany({
|
|
173
119
|
EventStore: this._eventStore,
|
|
@@ -178,7 +124,7 @@ export class Projections implements IProjections {
|
|
|
178
124
|
}
|
|
179
125
|
|
|
180
126
|
for (const projection of projections) {
|
|
181
|
-
const identifier = String(
|
|
127
|
+
const identifier = String(projection.Identifier ?? '<unknown>');
|
|
182
128
|
await this.registerWithRetry(projection, identifier);
|
|
183
129
|
}
|
|
184
130
|
}
|
|
@@ -325,15 +271,15 @@ export class Projections implements IProjections {
|
|
|
325
271
|
return EventSequenceId.eventLog.value;
|
|
326
272
|
}
|
|
327
273
|
|
|
328
|
-
private async registerWithRetry(projection:
|
|
274
|
+
private async registerWithRetry(projection: ProjectionDefinition, identifier: string, maxAttempts = 5): Promise<void> {
|
|
329
275
|
let delay = 2000;
|
|
330
276
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
331
277
|
try {
|
|
332
|
-
this._logger.info('Registering projection', { identifier, readModel:
|
|
278
|
+
this._logger.info('Registering projection', { identifier, readModel: projection.ReadModel, attempt });
|
|
333
279
|
await this._connection.projections.register({
|
|
334
280
|
EventStore: this._eventStore,
|
|
335
281
|
Owner: ProjectionOwner.PROJECTION_OWNER_Client,
|
|
336
|
-
Projections: [projection
|
|
282
|
+
Projections: [projection]
|
|
337
283
|
});
|
|
338
284
|
this._logger.info('Projection registered successfully', { identifier });
|
|
339
285
|
return;
|
|
@@ -355,314 +301,8 @@ export class Projections implements IProjections {
|
|
|
355
301
|
}
|
|
356
302
|
}
|
|
357
303
|
|
|
358
|
-
private buildReadModelDefinitions(projections: any[]): any[] {
|
|
359
|
-
const byReadModel = new Map<string, any>();
|
|
360
|
-
|
|
361
|
-
for (const projection of projections) {
|
|
362
|
-
const readModelIdentifier = projection.ReadModel;
|
|
363
|
-
if (!readModelIdentifier) {
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
const existing = byReadModel.get(readModelIdentifier);
|
|
367
|
-
if (existing) {
|
|
368
|
-
if (existing.ObserverIdentifier !== projection.Identifier) {
|
|
369
|
-
throw new Error(`Read model id '${readModelIdentifier}' has multiple projections.`);
|
|
370
|
-
}
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const readModelType = this.getReadModelType(readModelIdentifier);
|
|
375
|
-
byReadModel.set(readModelIdentifier, buildReadModelDefinition({
|
|
376
|
-
identifier: readModelIdentifier,
|
|
377
|
-
type: readModelType,
|
|
378
|
-
schema: readModelType
|
|
379
|
-
? JSON.stringify(getReadModelMetadata(readModelType)?.schema ?? JsonSchemaGenerator.generate(readModelType))
|
|
380
|
-
: '{}',
|
|
381
|
-
// Passive projections never write to a materialized sink, so they register with
|
|
382
|
-
// the None sink. This lets the kernel fall through to immediate projection when
|
|
383
|
-
// resolving the instance by key instead of reading an empty sink and returning null.
|
|
384
|
-
sinkTypeId: projection.IsActive === false ? WellKnownSinks.None : this._defaultSinkTypeId,
|
|
385
|
-
observerType: ReadModelObserverType.Projection,
|
|
386
|
-
observerIdentifier: projection.Identifier
|
|
387
|
-
}));
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return Array.from(byReadModel.values());
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
private getReadModelType(readModelIdentifier: string): Constructor | undefined {
|
|
394
|
-
const types = [
|
|
395
|
-
...rootReadModelTypes(this._clientArtifacts),
|
|
396
|
-
...this._clientArtifacts.projections
|
|
397
|
-
.map(projectionType => getProjectionMetadata(projectionType)?.readModelType)
|
|
398
|
-
.filter((type): type is Constructor => type !== undefined)
|
|
399
|
-
];
|
|
400
|
-
for (const type of types) {
|
|
401
|
-
if (getReadModelId(type) === readModelIdentifier) {
|
|
402
|
-
return type;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return undefined;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
private buildDeclarativeDefinition(type: Constructor): BuiltProjection {
|
|
410
|
-
const metadata = getProjectionMetadata(type);
|
|
411
|
-
if (!metadata) {
|
|
412
|
-
throw new Error(`Type '${type.name}' is missing declarative projection metadata.`);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const builder = new ProjectionBuilderFor<unknown>();
|
|
416
|
-
const instance = new type() as IProjectionFor<unknown>;
|
|
417
|
-
instance.define(builder);
|
|
418
|
-
const definition = builder.build(metadata.id.value, type.name) as Record<string, unknown>;
|
|
419
|
-
|
|
420
|
-
const explicitReadModelIdentifier = definition.ReadModel as string;
|
|
421
|
-
if (explicitReadModelIdentifier === type.name) {
|
|
422
|
-
// Use explicit readModelType from decorator if provided
|
|
423
|
-
if (metadata.readModelType) {
|
|
424
|
-
definition.ReadModel = getReadModelId(metadata.readModelType);
|
|
425
|
-
} else {
|
|
426
|
-
const inferredReadModelIdentifier = this.inferReadModelIdentifier(builder.getMappedReadModelProperties());
|
|
427
|
-
if (inferredReadModelIdentifier) {
|
|
428
|
-
definition.ReadModel = inferredReadModelIdentifier;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
let variant: VariantDeclaration | undefined;
|
|
434
|
-
const variantDeclaration = builder.getVariantDeclaration();
|
|
435
|
-
if (variantDeclaration) {
|
|
436
|
-
const reclassified = reclassify(
|
|
437
|
-
type.name,
|
|
438
|
-
definition.From as any,
|
|
439
|
-
definition.Join as any,
|
|
440
|
-
variantDeclaration.enteringEventTypes,
|
|
441
|
-
variantDeclaration.key);
|
|
442
|
-
definition.From = reclassified.from;
|
|
443
|
-
definition.Join = reclassified.join;
|
|
444
|
-
variant = variantDeclaration;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return { typeName: type.name, definition, variant };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
private inferReadModelIdentifier(mappedProperties: string[]): string | undefined {
|
|
451
|
-
if (mappedProperties.length === 0) {
|
|
452
|
-
return undefined;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const matchingReadModels = rootReadModelTypes(this._clientArtifacts)
|
|
456
|
-
.map(type => ({ type, metadata: getReadModelMetadata(type) }))
|
|
457
|
-
.filter(candidate => candidate.metadata)
|
|
458
|
-
.filter(candidate => {
|
|
459
|
-
const readModelProperties = Array.from(candidate.metadata!.members.keys());
|
|
460
|
-
return mappedProperties.every(property => readModelProperties.includes(property));
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
if (matchingReadModels.length !== 1) {
|
|
464
|
-
return undefined;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return matchingReadModels[0].metadata!.id.value;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
private buildModelBoundDefinition(type: Constructor): BuiltProjection {
|
|
471
|
-
const metadata = this.resolveModelBoundMetadata(type);
|
|
472
|
-
if (!metadata) {
|
|
473
|
-
throw new Error(`Type '${type.name}' is missing model-bound projection metadata.`);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const properties = TypeIntrospector.getTrackedProperties(type);
|
|
477
|
-
const prototype = type.prototype;
|
|
478
|
-
|
|
479
|
-
const fromByEventType = new Map<string, FromRecord>();
|
|
480
|
-
const joinByEventType = new Map<string, { Key: ContractEventType; Value: { On: string; Properties: Record<string, string>; Key: string } }>();
|
|
481
|
-
const removedWithByEventType = new Map<string, { Key: ContractEventType; Value: { Key: string; ParentKey: string } }>();
|
|
482
|
-
const removedWithJoinByEventType = new Map<string, { Key: ContractEventType; Value: { Key: string } }>();
|
|
483
|
-
const childrenByProperty: Record<string, ChildrenDefinitionLike> = {};
|
|
484
|
-
const nestedByProperty: Record<string, ChildrenDefinitionLike> = {};
|
|
485
|
-
|
|
486
|
-
const fromEvents = getFromEventMetadata(type);
|
|
487
|
-
for (const fromEvent of fromEvents) {
|
|
488
|
-
const eventType = toContractEventType(fromEvent.eventType);
|
|
489
|
-
const eventKey = getEventTypeMapKey(eventType);
|
|
490
|
-
fromByEventType.set(eventKey, {
|
|
491
|
-
Key: eventType,
|
|
492
|
-
Value: {
|
|
493
|
-
Properties: {},
|
|
494
|
-
Key: fromEvent.constantKey ? constantValueExpression(fromEvent.constantKey) : (fromEvent.key ?? '$eventSourceId'),
|
|
495
|
-
ParentKey: fromEvent.parentKey ?? ''
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const removedWithClass = getRemovedWithClassMetadata(type);
|
|
501
|
-
for (const removed of removedWithClass) {
|
|
502
|
-
const eventType = toContractEventType(removed.eventType);
|
|
503
|
-
removedWithByEventType.set(getEventTypeMapKey(eventType), {
|
|
504
|
-
Key: eventType,
|
|
505
|
-
Value: { Key: removed.key ?? '$eventSourceId', ParentKey: removed.parentKey ?? '' }
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const removedWithJoinClass = getRemovedWithJoinClassMetadata(type);
|
|
510
|
-
for (const removed of removedWithJoinClass) {
|
|
511
|
-
const eventType = toContractEventType(removed.eventType);
|
|
512
|
-
removedWithJoinByEventType.set(getEventTypeMapKey(eventType), {
|
|
513
|
-
Key: eventType,
|
|
514
|
-
Value: { Key: removed.key ?? '$eventSourceId' }
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
for (const property of properties) {
|
|
519
|
-
applyPropertyMappings(prototype, property, fromByEventType);
|
|
520
|
-
|
|
521
|
-
for (const mapping of getJoinMetadata(prototype, property)) {
|
|
522
|
-
const entry = this.ensureJoinEntry(joinByEventType, mapping.eventType);
|
|
523
|
-
entry.Value.On = mapping.on ?? (entry.Value.On || property);
|
|
524
|
-
entry.Value.Properties[property] = mapping.eventPropertyName ?? property;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
for (const removed of getRemovedWithPropertyMetadata(prototype, property)) {
|
|
528
|
-
const eventType = toContractEventType(removed.eventType);
|
|
529
|
-
removedWithByEventType.set(getEventTypeMapKey(eventType), {
|
|
530
|
-
Key: eventType,
|
|
531
|
-
Value: { Key: removed.key ?? '$eventSourceId', ParentKey: removed.parentKey ?? '' }
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
for (const removed of getRemovedWithJoinPropertyMetadata(prototype, property)) {
|
|
536
|
-
const eventType = toContractEventType(removed.eventType);
|
|
537
|
-
removedWithJoinByEventType.set(getEventTypeMapKey(eventType), {
|
|
538
|
-
Key: eventType,
|
|
539
|
-
Value: { Key: removed.key ?? '$eventSourceId' }
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const childrenFromList = getChildrenFromMetadata(prototype, property);
|
|
544
|
-
if (childrenFromList.length > 0) {
|
|
545
|
-
childrenByProperty[property] = buildChildrenEntry(type, property, childrenFromList);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const propertyIsNested = isNested(prototype, property);
|
|
549
|
-
if (propertyIsNested) {
|
|
550
|
-
nestedByProperty[property] = buildNestedEntry(type, property);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// A scalar (non-nested, non-children-collection) root property clears back to no value
|
|
554
|
-
// every time the given event is observed. A nested single-object property's clearWith is
|
|
555
|
-
// handled by buildNestedEntry instead, which clears the whole nested object.
|
|
556
|
-
if (childrenFromList.length === 0 && !propertyIsNested) {
|
|
557
|
-
for (const clearWith of getClearWithPropertyMetadata(prototype, property)) {
|
|
558
|
-
const entry = ensureFromEntry(fromByEventType, clearWith.eventType);
|
|
559
|
-
entry.Value.Properties[property] = '$null';
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const allProperties: Record<string, string> = {};
|
|
565
|
-
for (const property of properties) {
|
|
566
|
-
const fromEvery = getFromEveryMetadata(prototype, property) ?? getFromAllMetadata(prototype, property);
|
|
567
|
-
if (fromEvery) {
|
|
568
|
-
allProperties[property] = fromEvery.contextProperty
|
|
569
|
-
? fromEvery.contextProperty
|
|
570
|
-
: (fromEvery.property ?? property);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
let from = Array.from(fromByEventType.values());
|
|
575
|
-
let join = Array.from(joinByEventType.values());
|
|
576
|
-
let variant: VariantDeclaration | undefined;
|
|
577
|
-
|
|
578
|
-
const variantMetadata = getVariantOfMetadata(type);
|
|
579
|
-
if (variantMetadata) {
|
|
580
|
-
const entersOnList = getEntersOnMetadata(type);
|
|
581
|
-
const enteringEventTypes = entersOnList.map(entersOn => {
|
|
582
|
-
const contractType = toContractEventType(entersOn.eventType);
|
|
583
|
-
if (entersOn.key) {
|
|
584
|
-
const entry = ensureFromEntry(fromByEventType, entersOn.eventType);
|
|
585
|
-
entry.Value.Key = entersOn.key;
|
|
586
|
-
}
|
|
587
|
-
return contractType;
|
|
588
|
-
});
|
|
589
|
-
from = Array.from(fromByEventType.values());
|
|
590
|
-
|
|
591
|
-
const globalHandlers = new Map<string, FromRecord[]>();
|
|
592
|
-
for (const handlerType of this._clientArtifacts.globalForHandlers) {
|
|
593
|
-
const globalForMetadata = getGlobalForMetadata(handlerType);
|
|
594
|
-
if (globalForMetadata?.identity === variantMetadata.identity) {
|
|
595
|
-
globalHandlers.set(handlerType.name, this.buildFromRecordsForType(handlerType));
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const memberNames = new Set(getReadModelMetadata(type)?.members.keys() ?? TypeIntrospector.getMembers(type).keys());
|
|
600
|
-
const merged = mergeGlobalHandlers(type.name, memberNames, from, globalHandlers);
|
|
601
|
-
const reclassified = reclassify(type.name, merged, join, enteringEventTypes, variantMetadata.key);
|
|
602
|
-
from = reclassified.from;
|
|
603
|
-
join = reclassified.join;
|
|
604
|
-
variant = { identity: variantMetadata.identity, key: variantMetadata.key, enteringEventTypes };
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
const definition: Record<string, unknown> = {
|
|
608
|
-
EventSequenceId: metadata.eventSequenceId ?? EventSequenceId.eventLog.value,
|
|
609
|
-
Identifier: metadata.id.value,
|
|
610
|
-
ReadModel: metadata.readModelIdentifier,
|
|
611
|
-
IsActive: !isPassive(type),
|
|
612
|
-
IsRewindable: !isNotRewindable(type),
|
|
613
|
-
InitialModelState: '{}',
|
|
614
|
-
From: from,
|
|
615
|
-
Join: join,
|
|
616
|
-
Children: childrenByProperty,
|
|
617
|
-
FromEvery: [],
|
|
618
|
-
All: {
|
|
619
|
-
Properties: allProperties,
|
|
620
|
-
IncludeChildren: false,
|
|
621
|
-
AutoMap: AutoMap.Inherit
|
|
622
|
-
},
|
|
623
|
-
RemovedWith: Array.from(removedWithByEventType.values()),
|
|
624
|
-
RemovedWithJoin: Array.from(removedWithJoinByEventType.values()),
|
|
625
|
-
LastUpdated: { Value: '' },
|
|
626
|
-
Tags: [],
|
|
627
|
-
AutoMap: isNoAutoMap(type) ? AutoMap.Disabled : AutoMap.Enabled,
|
|
628
|
-
NoAutoMapProperties: properties.filter(property => isPropertyNoAutoMap(prototype, property)),
|
|
629
|
-
Nested: nestedByProperty
|
|
630
|
-
};
|
|
631
|
-
return { typeName: type.name, definition, variant };
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Builds the raw From records for an arbitrary type using the same property-mapping rules as
|
|
636
|
-
* a model-bound read model - used to fold a globalFor shared handler's mappings into every
|
|
637
|
-
* variant of its identity.
|
|
638
|
-
* @param type - The type to build From records for.
|
|
639
|
-
* @returns The built From records.
|
|
640
|
-
*/
|
|
641
|
-
private buildFromRecordsForType(type: Constructor): FromRecord[] {
|
|
642
|
-
const fromByEventType = new Map<string, FromRecord>();
|
|
643
|
-
const fromEvents = getFromEventMetadata(type);
|
|
644
|
-
for (const fromEvent of fromEvents) {
|
|
645
|
-
const eventType = toContractEventType(fromEvent.eventType);
|
|
646
|
-
fromByEventType.set(getEventTypeMapKey(eventType), {
|
|
647
|
-
Key: eventType,
|
|
648
|
-
Value: {
|
|
649
|
-
Properties: {},
|
|
650
|
-
Key: fromEvent.constantKey ? constantValueExpression(fromEvent.constantKey) : (fromEvent.key ?? '$eventSourceId'),
|
|
651
|
-
ParentKey: fromEvent.parentKey ?? ''
|
|
652
|
-
}
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
const prototype = type.prototype;
|
|
657
|
-
for (const property of TypeIntrospector.getTrackedProperties(type)) {
|
|
658
|
-
applyPropertyMappings(prototype, property, fromByEventType);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
return Array.from(fromByEventType.values());
|
|
662
|
-
}
|
|
663
|
-
|
|
664
304
|
private resolveModelBoundMetadata(type: Constructor): ResolvedModelBoundMetadata | undefined {
|
|
665
|
-
if (
|
|
305
|
+
if (isModelBoundProjection(type)) {
|
|
666
306
|
const identifier = getReadModelId(type);
|
|
667
307
|
return {
|
|
668
308
|
id: new ProjectionId(identifier),
|
|
@@ -673,43 +313,4 @@ export class Projections implements IProjections {
|
|
|
673
313
|
|
|
674
314
|
return undefined;
|
|
675
315
|
}
|
|
676
|
-
|
|
677
|
-
private ensureJoinEntry(
|
|
678
|
-
joinByEventType: Map<string, { Key: ContractEventType; Value: { On: string; Properties: Record<string, string>; Key: string } }>,
|
|
679
|
-
eventTypeConstructor: Function
|
|
680
|
-
): { Key: ContractEventType; Value: { On: string; Properties: Record<string, string>; Key: string } } {
|
|
681
|
-
const eventType = toContractEventType(eventTypeConstructor);
|
|
682
|
-
const key = getEventTypeMapKey(eventType);
|
|
683
|
-
const existing = joinByEventType.get(key);
|
|
684
|
-
if (existing) {
|
|
685
|
-
return existing;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const created = {
|
|
689
|
-
Key: eventType,
|
|
690
|
-
Value: {
|
|
691
|
-
On: '',
|
|
692
|
-
Properties: {},
|
|
693
|
-
Key: '$eventSourceId'
|
|
694
|
-
}
|
|
695
|
-
};
|
|
696
|
-
joinByEventType.set(key, created);
|
|
697
|
-
return created;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Computes a stable, deterministic ISO timestamp from the projection definition content,
|
|
702
|
-
* excluding the LastUpdated field itself. This ensures the server does not interpret
|
|
703
|
-
* a repeated registration of an unchanged definition as a definition change, which
|
|
704
|
-
* would otherwise trigger an unnecessary auto-replay.
|
|
705
|
-
*/
|
|
706
|
-
private computeStableLastUpdated(definition: Record<string, unknown>): string {
|
|
707
|
-
const { LastUpdated: _omit, ...rest } = definition;
|
|
708
|
-
const content = JSON.stringify(rest, Object.keys(rest).sort());
|
|
709
|
-
let hash = 5381;
|
|
710
|
-
for (let i = 0; i < content.length; i++) {
|
|
711
|
-
hash = ((hash << 5) + hash + content.charCodeAt(i)) >>> 0;
|
|
712
|
-
}
|
|
713
|
-
return new Date(hash * 1000).toISOString();
|
|
714
|
-
}
|
|
715
316
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import type { ProjectionId } from './ProjectionId.js';
|
|
5
|
+
|
|
6
|
+
/** Metadata resolved during model-bound projection discovery. */
|
|
7
|
+
export interface ResolvedModelBoundMetadata {
|
|
8
|
+
id: ProjectionId;
|
|
9
|
+
eventSequenceId: string | undefined;
|
|
10
|
+
readModelIdentifier: string;
|
|
11
|
+
}
|
|
@@ -274,9 +274,13 @@ function populateFromType(definition: ChildrenDefinitionLike, childType: Functio
|
|
|
274
274
|
for (const property of TypeIntrospector.getTrackedProperties(childType)) {
|
|
275
275
|
applyPropertyMappings(prototype, property, fromByEventType);
|
|
276
276
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
// A clear on the member carrying @nested removes that nested object; a clear on
|
|
278
|
+
// any other member maps the member to null in this child/nested definition.
|
|
279
|
+
if (!isNested(prototype, property)) {
|
|
280
|
+
for (const clearWith of getClearWithPropertyMetadata(prototype, property)) {
|
|
281
|
+
const entry = ensureFromEntry(fromByEventType, clearWith.eventType);
|
|
282
|
+
entry.Value.Properties[property] = '$null';
|
|
283
|
+
}
|
|
280
284
|
}
|
|
281
285
|
|
|
282
286
|
for (const removed of getRemovedWithPropertyMetadata(prototype, property)) {
|
|
@@ -0,0 +1,10 @@
|
|
|
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 { hasModelBoundProperties } from '../../types/TypeDiscoverer.js';
|
|
5
|
+
import { hasFromEventMetadata } from './fromEvent.js';
|
|
6
|
+
|
|
7
|
+
/** Returns whether a read model declares model-bound projection metadata. */
|
|
8
|
+
export function isModelBoundProjection(type: Function): boolean {
|
|
9
|
+
return hasFromEventMetadata(type) || hasModelBoundProperties(type);
|
|
10
|
+
}
|