@cratis/chronicle 6.16.1 → 6.18.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/artifacts/ActivatedArtifact.ts +5 -1
- package/artifacts/ArtifactCompletionFailed.ts +20 -0
- package/artifacts/ArtifactInvocationContext.ts +21 -0
- package/artifacts/for_withActivatedArtifact/when_completing/after_processing_failure.spec.ts +34 -0
- package/artifacts/index.ts +2 -0
- package/artifacts/withActivatedArtifact.ts +20 -3
- package/dist/artifacts/ActivatedArtifact.d.ts +4 -1
- package/dist/artifacts/ActivatedArtifact.d.ts.map +1 -1
- package/dist/artifacts/ArtifactCompletionFailed.d.ts +9 -0
- package/dist/artifacts/ArtifactCompletionFailed.d.ts.map +1 -0
- package/dist/artifacts/ArtifactCompletionFailed.js +19 -0
- package/dist/artifacts/ArtifactCompletionFailed.js.map +1 -0
- package/dist/artifacts/ArtifactInvocationContext.d.ts +18 -0
- package/dist/artifacts/ArtifactInvocationContext.d.ts.map +1 -0
- package/dist/artifacts/ArtifactInvocationContext.js +4 -0
- package/dist/artifacts/ArtifactInvocationContext.js.map +1 -0
- package/dist/artifacts/index.d.ts +2 -0
- package/dist/artifacts/index.d.ts.map +1 -1
- package/dist/artifacts/index.js +1 -0
- package/dist/artifacts/index.js.map +1 -1
- package/dist/artifacts/withActivatedArtifact.d.ts +2 -1
- package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -1
- package/dist/artifacts/withActivatedArtifact.js +22 -3
- package/dist/artifacts/withActivatedArtifact.js.map +1 -1
- package/dist/projections/CompiledProjectionDefinitions.d.ts +6 -0
- package/dist/projections/CompiledProjectionDefinitions.d.ts.map +1 -1
- package/dist/projections/ProjectionCapabilityProvenance.d.ts +8 -0
- package/dist/projections/ProjectionCapabilityProvenance.d.ts.map +1 -0
- package/dist/projections/ProjectionCapabilityProvenance.js +4 -0
- package/dist/projections/ProjectionCapabilityProvenance.js.map +1 -0
- package/dist/projections/ProjectionDefinitionCompiler.d.ts +1 -0
- package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -1
- package/dist/projections/ProjectionDefinitionCompiler.js +130 -4
- package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -1
- package/dist/projections/ProjectionEventSchema.d.ts +10 -0
- package/dist/projections/ProjectionEventSchema.d.ts.map +1 -0
- package/dist/projections/ProjectionEventSchema.js +4 -0
- package/dist/projections/ProjectionEventSchema.js.map +1 -0
- package/dist/projections/VariantReclassifier.d.ts +2 -0
- package/dist/projections/VariantReclassifier.d.ts.map +1 -1
- package/dist/projections/VariantReclassifier.js.map +1 -1
- package/dist/projections/captureProjectionProvenance.d.ts +4 -0
- package/dist/projections/captureProjectionProvenance.d.ts.map +1 -0
- package/dist/projections/captureProjectionProvenance.js +56 -0
- package/dist/projections/captureProjectionProvenance.js.map +1 -0
- package/dist/projections/declarative/AddChildBuilder.d.ts +13 -2
- package/dist/projections/declarative/AddChildBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/AddChildBuilder.js +27 -1
- package/dist/projections/declarative/AddChildBuilder.js.map +1 -1
- package/dist/projections/declarative/FromBuilder.d.ts +4 -0
- package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/FromBuilder.js +10 -2
- package/dist/projections/declarative/FromBuilder.js.map +1 -1
- package/dist/projections/declarative/IAddChildBuilder.d.ts +12 -0
- package/dist/projections/declarative/IAddChildBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/IReadModelPropertiesBuilder.d.ts +16 -1
- package/dist/projections/declarative/IReadModelPropertiesBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/JoinBuilder.d.ts +4 -0
- package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
- package/dist/projections/declarative/JoinBuilder.js +1 -2
- package/dist/projections/declarative/JoinBuilder.js.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderCore.js +9 -1
- package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
- package/dist/projections/declarative/projectionBuilderProvenance.d.ts +16 -0
- package/dist/projections/declarative/projectionBuilderProvenance.d.ts.map +1 -0
- package/dist/projections/declarative/projectionBuilderProvenance.js +28 -0
- package/dist/projections/declarative/projectionBuilderProvenance.js.map +1 -0
- package/dist/projections/eventContractPath.d.ts +4 -0
- package/dist/projections/eventContractPath.d.ts.map +1 -0
- package/dist/projections/eventContractPath.js +7 -0
- package/dist/projections/eventContractPath.js.map +1 -0
- package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
- package/dist/projections/modelBound/childrenAndNestedBuilder.js +41 -10
- package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +12 -3
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +12 -3
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/testing/projections/ProjectionCapabilities.d.ts +17 -0
- package/dist/testing/projections/ProjectionCapabilities.d.ts.map +1 -0
- package/dist/testing/projections/ProjectionCapabilities.js +233 -0
- package/dist/testing/projections/ProjectionCapabilities.js.map +1 -0
- package/dist/testing/projections/UnsupportedProjectionOperation.d.ts +11 -0
- package/dist/testing/projections/UnsupportedProjectionOperation.d.ts.map +1 -0
- package/dist/testing/projections/UnsupportedProjectionOperation.js +16 -0
- package/dist/testing/projections/UnsupportedProjectionOperation.js.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.d.ts +7 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.d.ts.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.js +16 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.js.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Model.d.ts +10 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Model.d.ts.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Model.js +20 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Model.js.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.d.ts +3 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.d.ts.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.js +7 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.js.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/compile.d.ts +12 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/compile.d.ts.map +1 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/compile.js +34 -0
- package/dist/testing/projections/for_ProjectionCapabilities/given/compile.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/projections/CompiledProjectionDefinitions.ts +6 -0
- package/projections/ProjectionCapabilityProvenance.ts +10 -0
- package/projections/ProjectionDefinitionCompiler.registration.spec.ts +4 -2
- package/projections/ProjectionDefinitionCompiler.ts +127 -4
- package/projections/ProjectionEventSchema.ts +13 -0
- package/projections/Projections.childrenAndNested.spec.ts +250 -1
- package/projections/VariantReclassifier.ts +2 -0
- package/projections/captureProjectionProvenance.ts +50 -0
- package/projections/declarative/AddChildBuilder.ts +33 -2
- package/projections/declarative/FromBuilder.ts +18 -1
- package/projections/declarative/IAddChildBuilder.ts +14 -0
- package/projections/declarative/IReadModelPropertiesBuilder.ts +18 -1
- package/projections/declarative/JoinBuilder.ts +9 -1
- package/projections/declarative/ProjectionBuilderCore.ts +8 -1
- package/projections/declarative/addChildCompatibility.standard.spec.ts +80 -0
- package/projections/declarative/projectionBuilderProvenance.ts +40 -0
- package/projections/eventContextExpressions.spec.ts +20 -0
- package/projections/eventContractPath.ts +9 -0
- package/projections/modelBound/childrenAndNestedBuilder.ts +45 -11
- package/reactors/Reactors.ts +13 -3
- package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +93 -2
- package/reducers/Reducers.ts +14 -3
- package/reducers/for_Reducers/when_delivering/with_activation.spec.ts +67 -1
- package/testing/projections/ProjectionCapabilities.ts +213 -0
- package/testing/projections/UnsupportedProjectionOperation.ts +16 -0
- package/testing/projections/for_ProjectionCapabilities/given/Changed.ts +12 -0
- package/testing/projections/for_ProjectionCapabilities/given/Model.ts +13 -0
- package/testing/projections/for_ProjectionCapabilities/given/Removed.ts +7 -0
- package/testing/projections/for_ProjectionCapabilities/given/compile.ts +39 -0
- package/testing/projections/for_ProjectionCapabilities/when_compiling_capability_evidence.spec.ts +68 -0
- package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +230 -0
- package/testing/projections/for_ProjectionCapabilities/when_validating_numeric_schemas.spec.ts +73 -0
- package/testing/projections/for_ProjectionCapabilities/when_validating_schema_boundaries.spec.ts +118 -0
- package/testing/projections/for_ProjectionCapabilities/when_validating_supported_operations.spec.ts +103 -0
|
@@ -5,6 +5,7 @@ import { AutoMap, ReadModelObserverType, type ProjectionDefinition } from '@crat
|
|
|
5
5
|
import { Constructor } from '@cratis/fundamentals';
|
|
6
6
|
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
7
7
|
import { EventSequenceId } from '../eventSequences/EventSequenceId.js';
|
|
8
|
+
import { getEventTypeFor, getEventTypeJsonSchemaFor } from '../events/eventTypeDecorator.js';
|
|
8
9
|
import { getReadModelMetadata } from '../readModels/index.js';
|
|
9
10
|
import { getReadModelId } from '../readModels/readModel.js';
|
|
10
11
|
import { buildReadModelDefinition } from '../readModels/buildReadModelDefinition.js';
|
|
@@ -14,6 +15,11 @@ import { WellKnownSinks } from '../sinks/index.js';
|
|
|
14
15
|
import { TypeIntrospector } from '../types/index.js';
|
|
15
16
|
import { canonicalStringify } from './canonicalStringify.js';
|
|
16
17
|
import { CompiledProjectionDefinitions } from './CompiledProjectionDefinitions.js';
|
|
18
|
+
import { captureProjectionProvenance } from './captureProjectionProvenance.js';
|
|
19
|
+
import { eventContractPath } from './eventContractPath.js';
|
|
20
|
+
import { getProjectionBuilderProvenance } from './declarative/projectionBuilderProvenance.js';
|
|
21
|
+
import type { ProjectionCapabilityProvenance } from './ProjectionCapabilityProvenance.js';
|
|
22
|
+
import type { ProjectionEventSchema } from './ProjectionEventSchema.js';
|
|
17
23
|
import { constantValueExpression } from './constantValueExpression.js';
|
|
18
24
|
import { eventContextPropertyExpression } from './eventContextPropertyExpression.js';
|
|
19
25
|
import { InvalidEventContextPropertyError } from './InvalidEventContextPropertyError.js';
|
|
@@ -42,6 +48,14 @@ import { getRemovedWithJoinClassMetadata, getRemovedWithJoinPropertyMetadata } f
|
|
|
42
48
|
import { getVariantOfMetadata } from './modelBound/variantOf.js';
|
|
43
49
|
import { getEntersOnMetadata } from './modelBound/entersOn.js';
|
|
44
50
|
import { getGlobalForMetadata } from './modelBound/globalFor.js';
|
|
51
|
+
import { getSetFromMetadata } from './modelBound/setFrom.js';
|
|
52
|
+
import { getSetFromContextMetadata } from './modelBound/setFromContext.js';
|
|
53
|
+
import { getSetValueMetadata } from './modelBound/setValue.js';
|
|
54
|
+
import { getAddFromMetadata } from './modelBound/addFrom.js';
|
|
55
|
+
import { getSubtractFromMetadata } from './modelBound/subtractFrom.js';
|
|
56
|
+
import { getCountMetadata } from './modelBound/count.js';
|
|
57
|
+
import { getIncrementMetadata } from './modelBound/increment.js';
|
|
58
|
+
import { getDecrementMetadata } from './modelBound/decrement.js';
|
|
45
59
|
import type { JoinRecord } from './declarative/ProjectionBuilderCore.js';
|
|
46
60
|
import { BuiltProjection, crossWireGroups, mergeGlobalHandlers, reclassify, VariantDeclaration } from './VariantReclassifier.js';
|
|
47
61
|
|
|
@@ -79,12 +93,53 @@ export class ProjectionDefinitionCompiler {
|
|
|
79
93
|
// existing sparse wire objects: adding those default-valued fields would change the payload.
|
|
80
94
|
// Evaluators must tolerate absent optional contract fields (for example NoAutoMapProperties).
|
|
81
95
|
const definitions = builtProjections.map(built => built.definition as unknown as ProjectionDefinition);
|
|
96
|
+
const provenance = new Map<ProjectionDefinition, readonly ProjectionCapabilityProvenance[]>();
|
|
97
|
+
const eventSchemas = new Map<ProjectionDefinition, ReadonlyMap<string, ProjectionEventSchema>>();
|
|
98
|
+
const availableEvents = new Map<string, Constructor>(this._clientArtifacts.eventTypes.map(type => {
|
|
99
|
+
const event = getEventTypeFor(type);
|
|
100
|
+
return [`${event.id.value}:${event.generation.value}`, type] as const;
|
|
101
|
+
}));
|
|
102
|
+
for (let index = 0; index < definitions.length; index++) {
|
|
103
|
+
provenance.set(definitions[index], builtProjections[index].provenance ?? []);
|
|
104
|
+
eventSchemas.set(definitions[index], this.buildEventSchemaCatalog(definitions[index], availableEvents));
|
|
105
|
+
}
|
|
82
106
|
return {
|
|
83
107
|
definitions,
|
|
84
|
-
readModels: this.buildReadModelDefinitions(definitions)
|
|
108
|
+
readModels: this.buildReadModelDefinitions(definitions),
|
|
109
|
+
provenance,
|
|
110
|
+
eventSchemas
|
|
85
111
|
};
|
|
86
112
|
}
|
|
87
113
|
|
|
114
|
+
private buildEventSchemaCatalog(definition: ProjectionDefinition, availableEvents: ReadonlyMap<string, Constructor>): ReadonlyMap<string, ProjectionEventSchema> {
|
|
115
|
+
const catalog = new Map<string, ProjectionEventSchema>();
|
|
116
|
+
const collect = (node: Record<string, unknown>) => {
|
|
117
|
+
for (const section of ['From', 'Join', 'RemovedWith', 'RemovedWithJoin'] as const) {
|
|
118
|
+
for (const entry of node[section] as Array<{ Key: ContractEventType }> ?? []) {
|
|
119
|
+
const eventType = entry.Key;
|
|
120
|
+
const key = getEventTypeMapKey(eventType);
|
|
121
|
+
const type = availableEvents.get(`${eventType.Id}:${eventType.Generation}`);
|
|
122
|
+
if (type && !catalog.has(key)) {
|
|
123
|
+
// Registration uses this exact schema path. Clone it so a compile or consumer
|
|
124
|
+
// cannot mutate metadata or another compile's catalog.
|
|
125
|
+
catalog.set(key, { eventType: { ...eventType }, schema: structuredClone(getEventTypeJsonSchemaFor(type)) });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const fromEventProperty = node.FromEventProperty as { Event?: ContractEventType } | undefined;
|
|
130
|
+
if (fromEventProperty?.Event) {
|
|
131
|
+
const key = getEventTypeMapKey(fromEventProperty.Event);
|
|
132
|
+
const type = availableEvents.get(`${fromEventProperty.Event.Id}:${fromEventProperty.Event.Generation}`);
|
|
133
|
+
if (type && !catalog.has(key)) catalog.set(key, { eventType: { ...fromEventProperty.Event }, schema: structuredClone(getEventTypeJsonSchemaFor(type)) });
|
|
134
|
+
}
|
|
135
|
+
for (const section of ['Children', 'Nested'] as const) {
|
|
136
|
+
for (const child of Object.values(node[section] as Record<string, Record<string, unknown>> ?? {})) collect(child);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
collect(definition as unknown as Record<string, unknown>);
|
|
140
|
+
return catalog;
|
|
141
|
+
}
|
|
142
|
+
|
|
88
143
|
private buildWithContextValidation(type: Constructor, build: () => BuiltProjection, readModelType?: Constructor): BuiltProjection {
|
|
89
144
|
try {
|
|
90
145
|
return build();
|
|
@@ -165,6 +220,12 @@ export class ProjectionDefinitionCompiler {
|
|
|
165
220
|
}
|
|
166
221
|
}
|
|
167
222
|
|
|
223
|
+
const declarations = getProjectionBuilderProvenance(builder);
|
|
224
|
+
const provenance = captureProjectionProvenance(definition, false,
|
|
225
|
+
new Map([...declarations.keys, ...declarations.children]));
|
|
226
|
+
if (declarations.fromEvery && !provenance.some(entry => entry.contractPath === 'All')) {
|
|
227
|
+
provenance.push({ contractPath: 'All', declaration: '.fromEvery' });
|
|
228
|
+
}
|
|
168
229
|
let variant: VariantDeclaration | undefined;
|
|
169
230
|
const variantDeclaration = builder.getVariantDeclaration();
|
|
170
231
|
if (variantDeclaration) {
|
|
@@ -174,8 +235,11 @@ export class ProjectionDefinitionCompiler {
|
|
|
174
235
|
definition.From = reclassified.from;
|
|
175
236
|
definition.Join = reclassified.join;
|
|
176
237
|
variant = variantDeclaration;
|
|
238
|
+
provenance.push({ contractPath: 'Variant', declaration: '.variantOf' });
|
|
239
|
+
provenance.push({ contractPath: 'Variant.Key', declaration: '.variantOf' });
|
|
240
|
+
for (const entering of variant.enteringEventTypes) provenance.push({ contractPath: eventContractPath('EntersOn', entering), declaration: '.entersOn' });
|
|
177
241
|
}
|
|
178
|
-
return { typeName: type.name, definition, variant };
|
|
242
|
+
return { typeName: type.name, definition, variant, provenance };
|
|
179
243
|
}
|
|
180
244
|
|
|
181
245
|
private inferReadModelIdentifier(mappedProperties: string[]): string | undefined {
|
|
@@ -203,6 +267,16 @@ export class ProjectionDefinitionCompiler {
|
|
|
203
267
|
const eventSequenceId = getEventSequenceMetadata(type);
|
|
204
268
|
const properties = TypeIntrospector.getTrackedProperties(type);
|
|
205
269
|
const prototype = type.prototype;
|
|
270
|
+
const overrides = new Map<string, string>();
|
|
271
|
+
const declaredFrom = new Set(getFromEventMetadata(type).map(entry => getEventTypeMapKey(toContractEventType(entry.eventType))));
|
|
272
|
+
const recordMappings = (property: string, metadata: Array<{ eventType: Function }>, declaration: string) => {
|
|
273
|
+
for (const mapping of metadata) {
|
|
274
|
+
const eventType = toContractEventType(mapping.eventType);
|
|
275
|
+
const path = eventContractPath('From', eventType);
|
|
276
|
+
overrides.set(`${path}.Properties.${property}`, declaration);
|
|
277
|
+
if (!declaredFrom.has(getEventTypeMapKey(eventType)) && !overrides.has(path)) overrides.set(path, declaration);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
206
280
|
|
|
207
281
|
const fromByEventType = new Map<string, FromRecord>();
|
|
208
282
|
const joinByEventType = new Map<string, { Key: ContractEventType; Value: { On: string; Properties: Record<string, string>; Key: string } }>();
|
|
@@ -244,6 +318,23 @@ export class ProjectionDefinitionCompiler {
|
|
|
244
318
|
}
|
|
245
319
|
|
|
246
320
|
for (const property of properties) {
|
|
321
|
+
recordMappings(property, getSetFromMetadata(prototype, property), '@setFrom');
|
|
322
|
+
recordMappings(property, getSetFromContextMetadata(prototype, property), '@setFromContext');
|
|
323
|
+
recordMappings(property, getSetValueMetadata(prototype, property), '@setValue');
|
|
324
|
+
recordMappings(property, getAddFromMetadata(prototype, property), '@addFrom');
|
|
325
|
+
recordMappings(property, getSubtractFromMetadata(prototype, property), '@subtractFrom');
|
|
326
|
+
recordMappings(property, getIncrementMetadata(prototype, property), '@increment');
|
|
327
|
+
recordMappings(property, getDecrementMetadata(prototype, property), '@decrement');
|
|
328
|
+
recordMappings(property, getCountMetadata(prototype, property), '@count');
|
|
329
|
+
for (const [declaration, mappings] of [
|
|
330
|
+
['@increment', getIncrementMetadata(prototype, property)],
|
|
331
|
+
['@decrement', getDecrementMetadata(prototype, property)],
|
|
332
|
+
['@count', getCountMetadata(prototype, property)]
|
|
333
|
+
] as const) {
|
|
334
|
+
for (const mapping of mappings) {
|
|
335
|
+
if (mapping.constantKey) overrides.set(`${eventContractPath('From', toContractEventType(mapping.eventType))}.Key`, declaration);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
247
338
|
applyPropertyMappings(prototype, property, fromByEventType);
|
|
248
339
|
for (const mapping of getJoinMetadata(prototype, property)) {
|
|
249
340
|
const entry = this.ensureJoinEntry(joinByEventType, mapping.eventType);
|
|
@@ -278,6 +369,7 @@ export class ProjectionDefinitionCompiler {
|
|
|
278
369
|
for (const clearWith of getClearWithPropertyMetadata(prototype, property)) {
|
|
279
370
|
const entry = ensureFromEntry(fromByEventType, clearWith.eventType);
|
|
280
371
|
entry.Value.Properties[property] = '$null';
|
|
372
|
+
overrides.set(`${eventContractPath('From', toContractEventType(clearWith.eventType))}.Properties.${property}`, '@clearWith');
|
|
281
373
|
}
|
|
282
374
|
}
|
|
283
375
|
}
|
|
@@ -294,6 +386,8 @@ export class ProjectionDefinitionCompiler {
|
|
|
294
386
|
|
|
295
387
|
let from = Array.from(fromByEventType.values());
|
|
296
388
|
let join = Array.from(joinByEventType.values());
|
|
389
|
+
let preLoweringFrom = from;
|
|
390
|
+
let preLoweringJoin = join;
|
|
297
391
|
let variant: VariantDeclaration | undefined;
|
|
298
392
|
const variantMetadata = getVariantOfMetadata(type);
|
|
299
393
|
if (variantMetadata) {
|
|
@@ -303,6 +397,7 @@ export class ProjectionDefinitionCompiler {
|
|
|
303
397
|
if (entersOn.key) {
|
|
304
398
|
const entry = ensureFromEntry(fromByEventType, entersOn.eventType);
|
|
305
399
|
entry.Value.Key = entersOn.key;
|
|
400
|
+
overrides.set(`${eventContractPath('From', contractType)}.Key`, '@entersOn');
|
|
306
401
|
}
|
|
307
402
|
return contractType;
|
|
308
403
|
});
|
|
@@ -311,11 +406,21 @@ export class ProjectionDefinitionCompiler {
|
|
|
311
406
|
for (const handlerType of this._clientArtifacts.globalForHandlers) {
|
|
312
407
|
const globalForMetadata = getGlobalForMetadata(handlerType);
|
|
313
408
|
if (globalForMetadata?.identity === variantMetadata.identity) {
|
|
314
|
-
|
|
409
|
+
const records = this.buildFromRecordsForType(handlerType);
|
|
410
|
+
globalHandlers.set(handlerType.name, records);
|
|
411
|
+
for (const record of records) {
|
|
412
|
+
const path = eventContractPath('From', record.Key);
|
|
413
|
+
overrides.set(path, `@globalFor(${handlerType.name})`);
|
|
414
|
+
for (const property of Object.keys(record.Value.Properties)) {
|
|
415
|
+
overrides.set(`${path}.Properties.${property}`, `@globalFor(${handlerType.name})`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
315
418
|
}
|
|
316
419
|
}
|
|
317
420
|
const memberNames = new Set(getReadModelMetadata(type)?.members.keys() ?? TypeIntrospector.getMembers(type).keys());
|
|
318
421
|
const merged = mergeGlobalHandlers(type.name, memberNames, from, globalHandlers);
|
|
422
|
+
preLoweringFrom = merged;
|
|
423
|
+
preLoweringJoin = join;
|
|
319
424
|
const reclassified = reclassify(type.name, merged, join, enteringEventTypes, variantMetadata.key);
|
|
320
425
|
from = reclassified.from;
|
|
321
426
|
join = reclassified.join;
|
|
@@ -346,7 +451,25 @@ export class ProjectionDefinitionCompiler {
|
|
|
346
451
|
NoAutoMapProperties: properties.filter(property => isPropertyNoAutoMap(prototype, property)),
|
|
347
452
|
Nested: nestedByProperty
|
|
348
453
|
};
|
|
349
|
-
|
|
454
|
+
const provenance = captureProjectionProvenance({ ...definition, From: preLoweringFrom, Join: preLoweringJoin }, true, overrides);
|
|
455
|
+
if (Object.keys(allProperties).length && Object.keys(allProperties).every(property =>
|
|
456
|
+
getFromAllMetadata(prototype, property) && !getFromEveryMetadata(prototype, property))) {
|
|
457
|
+
const index = provenance.findIndex(entry => entry.contractPath === 'All');
|
|
458
|
+
if (index >= 0) provenance[index] = { contractPath: 'All', declaration: '@fromAll' };
|
|
459
|
+
}
|
|
460
|
+
for (const property of Object.keys(allProperties)) {
|
|
461
|
+
if (getFromAllMetadata(prototype, property) && !getFromEveryMetadata(prototype, property)) {
|
|
462
|
+
const path = `All.Properties.${property}`;
|
|
463
|
+
const index = provenance.findIndex(entry => entry.contractPath === path);
|
|
464
|
+
if (index >= 0) provenance[index] = { contractPath: path, declaration: '@fromAll' };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (variant) {
|
|
468
|
+
provenance.push({ contractPath: 'Variant', declaration: '@variantOf' });
|
|
469
|
+
provenance.push({ contractPath: 'Variant.Key', declaration: '@variantOf' });
|
|
470
|
+
for (const entering of variant.enteringEventTypes) provenance.push({ contractPath: eventContractPath('EntersOn', entering), declaration: '@entersOn' });
|
|
471
|
+
}
|
|
472
|
+
return { typeName: type.name, definition, variant, provenance };
|
|
350
473
|
}
|
|
351
474
|
|
|
352
475
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
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 { JsonSchema } from '../schemas/JsonSchema.js';
|
|
5
|
+
import type { ContractEventType } from './declarative/ProjectionBuilderCore.js';
|
|
6
|
+
|
|
7
|
+
/** Schema registered for one participating event type and generation. */
|
|
8
|
+
export interface ProjectionEventSchema {
|
|
9
|
+
/** Event identifier, generation, and tombstone status. */
|
|
10
|
+
readonly eventType: ContractEventType;
|
|
11
|
+
/** Isolated copy of the schema registered for this event. */
|
|
12
|
+
readonly schema: JsonSchema;
|
|
13
|
+
}
|
|
@@ -10,7 +10,10 @@ import { ChronicleConnection } from '../connection/index.js';
|
|
|
10
10
|
import { eventType } from '../events/eventTypeDecorator.js';
|
|
11
11
|
import { childrenFrom } from './modelBound/childrenFrom.js';
|
|
12
12
|
import { clearWith } from './modelBound/clearWith.js';
|
|
13
|
+
import { count } from './modelBound/count.js';
|
|
14
|
+
import { decrement } from './modelBound/decrement.js';
|
|
13
15
|
import { fromEvent } from './modelBound/fromEvent.js';
|
|
16
|
+
import { increment } from './modelBound/increment.js';
|
|
14
17
|
import { nested } from './modelBound/nested.js';
|
|
15
18
|
import { noAutoMap } from './modelBound/noAutoMap.js';
|
|
16
19
|
import { setFrom } from './modelBound/setFrom.js';
|
|
@@ -22,6 +25,7 @@ import { Projections } from './Projections.js';
|
|
|
22
25
|
class LineAdded {
|
|
23
26
|
productId!: string;
|
|
24
27
|
quantity!: number;
|
|
28
|
+
label!: string;
|
|
25
29
|
}
|
|
26
30
|
eventType()(LineAdded);
|
|
27
31
|
|
|
@@ -30,6 +34,11 @@ class LineQuantityChanged {
|
|
|
30
34
|
}
|
|
31
35
|
eventType()(LineQuantityChanged);
|
|
32
36
|
|
|
37
|
+
class AlternateLineAdded {
|
|
38
|
+
alternateId!: string;
|
|
39
|
+
}
|
|
40
|
+
eventType()(AlternateLineAdded);
|
|
41
|
+
|
|
33
42
|
class LineQuantityCleared {}
|
|
34
43
|
eventType()(LineQuantityCleared);
|
|
35
44
|
|
|
@@ -71,6 +80,117 @@ setFrom(LineQuantityChanged)(OrderLine.prototype, 'quantity');
|
|
|
71
80
|
noAutoMap(OrderLine.prototype, 'quantity');
|
|
72
81
|
clearWith(LineQuantityCleared)(OrderLine.prototype, 'quantity');
|
|
73
82
|
|
|
83
|
+
class ChildWithId {
|
|
84
|
+
id = '';
|
|
85
|
+
label = '';
|
|
86
|
+
}
|
|
87
|
+
class ChildWithUppercaseId {
|
|
88
|
+
Id = '';
|
|
89
|
+
}
|
|
90
|
+
class ChildWithExplicitId {
|
|
91
|
+
employeeNumber = '';
|
|
92
|
+
label = '';
|
|
93
|
+
}
|
|
94
|
+
class ChildWithMatchingEventKey {
|
|
95
|
+
productId = '';
|
|
96
|
+
}
|
|
97
|
+
class ChildWithMappedId {
|
|
98
|
+
id = '';
|
|
99
|
+
}
|
|
100
|
+
setFrom(LineAdded, 'productId')(ChildWithMappedId.prototype, 'id');
|
|
101
|
+
class ChildWithoutAutoMap {
|
|
102
|
+
id = '';
|
|
103
|
+
}
|
|
104
|
+
noAutoMap(ChildWithoutAutoMap);
|
|
105
|
+
class ChildWithNoAutoMappedId {
|
|
106
|
+
id = '';
|
|
107
|
+
}
|
|
108
|
+
noAutoMap(ChildWithNoAutoMappedId.prototype, 'id');
|
|
109
|
+
class ChildWithConstantCount {
|
|
110
|
+
id = '';
|
|
111
|
+
total = 0;
|
|
112
|
+
label = '';
|
|
113
|
+
}
|
|
114
|
+
count(LineAdded, 'all')(ChildWithConstantCount.prototype, 'total');
|
|
115
|
+
class ChildWithConstantIncrement {
|
|
116
|
+
id = '';
|
|
117
|
+
total = 0;
|
|
118
|
+
label = '';
|
|
119
|
+
}
|
|
120
|
+
increment(LineAdded, 'all')(ChildWithConstantIncrement.prototype, 'total');
|
|
121
|
+
class ChildWithConstantDecrement {
|
|
122
|
+
id = '';
|
|
123
|
+
total = 0;
|
|
124
|
+
label = '';
|
|
125
|
+
}
|
|
126
|
+
decrement(LineAdded, 'all')(ChildWithConstantDecrement.prototype, 'total');
|
|
127
|
+
class ChildWithPlainCount {
|
|
128
|
+
id = '';
|
|
129
|
+
total = 0;
|
|
130
|
+
label = '';
|
|
131
|
+
}
|
|
132
|
+
count(LineAdded)(ChildWithPlainCount.prototype, 'total');
|
|
133
|
+
class ChildWithMappedLabelAndCount {
|
|
134
|
+
id = '';
|
|
135
|
+
total = 0;
|
|
136
|
+
label = '';
|
|
137
|
+
}
|
|
138
|
+
count(LineAdded)(ChildWithMappedLabelAndCount.prototype, 'total');
|
|
139
|
+
setFrom(LineAdded)(ChildWithMappedLabelAndCount.prototype, 'label');
|
|
140
|
+
class ChildWithEmptyKey {
|
|
141
|
+
[''] = '';
|
|
142
|
+
}
|
|
143
|
+
class ChildWithoutMatchingEmptyKey {
|
|
144
|
+
label = '';
|
|
145
|
+
}
|
|
146
|
+
class IdentifiedChildren {
|
|
147
|
+
byConvention!: ChildWithId[];
|
|
148
|
+
byUppercaseId!: ChildWithUppercaseId[];
|
|
149
|
+
byExplicitId!: ChildWithExplicitId[];
|
|
150
|
+
byExplicitIdWithoutChildType!: ChildWithId[];
|
|
151
|
+
byInferredEventKey!: ChildWithMatchingEventKey[];
|
|
152
|
+
byEventKey!: ChildWithId[];
|
|
153
|
+
byMatchingKey!: ChildWithId[];
|
|
154
|
+
withExplicitMapping!: ChildWithMappedId[];
|
|
155
|
+
withoutAutoMap!: ChildWithoutAutoMap[];
|
|
156
|
+
withNoAutoMappedId!: ChildWithNoAutoMappedId[];
|
|
157
|
+
withDifferentEventKeys!: ChildWithId[];
|
|
158
|
+
withConstantCount!: ChildWithConstantCount[];
|
|
159
|
+
withConstantIncrement!: ChildWithConstantIncrement[];
|
|
160
|
+
withConstantDecrement!: ChildWithConstantDecrement[];
|
|
161
|
+
withPlainCount!: ChildWithPlainCount[];
|
|
162
|
+
withMappedLabelAndCount!: ChildWithMappedLabelAndCount[];
|
|
163
|
+
withEmptyEventKey!: ChildWithEmptyKey[];
|
|
164
|
+
withoutMatchingEmptyKey!: ChildWithoutMatchingEmptyKey[];
|
|
165
|
+
}
|
|
166
|
+
childrenFrom(LineAdded, ChildWithId)(IdentifiedChildren.prototype, 'byConvention');
|
|
167
|
+
childrenFrom(LineAdded, ChildWithUppercaseId)(IdentifiedChildren.prototype, 'byUppercaseId');
|
|
168
|
+
childrenFrom(LineAdded, ChildWithExplicitId, 'employeeNumber')(IdentifiedChildren.prototype, 'byExplicitId');
|
|
169
|
+
childrenFrom(LineAdded, undefined, 'id')(IdentifiedChildren.prototype, 'byExplicitIdWithoutChildType');
|
|
170
|
+
childrenFrom(LineAdded, 'productId')(IdentifiedChildren.prototype, 'byInferredEventKey');
|
|
171
|
+
childrenFrom(LineAdded, 'productId', 'id')(IdentifiedChildren.prototype, 'byEventKey');
|
|
172
|
+
childrenFrom(LineAdded, 'id', 'id')(IdentifiedChildren.prototype, 'byMatchingKey');
|
|
173
|
+
childrenFrom(LineAdded, ChildWithMappedId)(IdentifiedChildren.prototype, 'withExplicitMapping');
|
|
174
|
+
childrenFrom(LineAdded, ChildWithoutAutoMap)(IdentifiedChildren.prototype, 'withoutAutoMap');
|
|
175
|
+
childrenFrom(LineAdded, ChildWithNoAutoMappedId)(IdentifiedChildren.prototype, 'withNoAutoMappedId');
|
|
176
|
+
childrenFrom(LineAdded, 'productId')(IdentifiedChildren.prototype, 'withDifferentEventKeys');
|
|
177
|
+
childrenFrom(AlternateLineAdded, 'alternateId')(IdentifiedChildren.prototype, 'withDifferentEventKeys');
|
|
178
|
+
childrenFrom(LineAdded, ChildWithConstantCount)(IdentifiedChildren.prototype, 'withConstantCount');
|
|
179
|
+
childrenFrom(LineAdded, 'productId')(IdentifiedChildren.prototype, 'withConstantIncrement');
|
|
180
|
+
field(Array, { enumerable: true, genericArguments: [ChildWithConstantIncrement] })(IdentifiedChildren.prototype, 'withConstantIncrement');
|
|
181
|
+
childrenFrom(LineAdded, ChildWithConstantDecrement)(IdentifiedChildren.prototype, 'withConstantDecrement');
|
|
182
|
+
childrenFrom(LineAdded, ChildWithPlainCount)(IdentifiedChildren.prototype, 'withPlainCount');
|
|
183
|
+
childrenFrom(LineAdded, ChildWithMappedLabelAndCount)(IdentifiedChildren.prototype, 'withMappedLabelAndCount');
|
|
184
|
+
childrenFrom(LineAdded, '')(IdentifiedChildren.prototype, 'withEmptyEventKey');
|
|
185
|
+
childrenFrom(LineAdded, '')(IdentifiedChildren.prototype, 'withoutMatchingEmptyKey');
|
|
186
|
+
field(Array, { enumerable: true, genericArguments: [ChildWithMatchingEventKey] })(IdentifiedChildren.prototype, 'byInferredEventKey');
|
|
187
|
+
field(Array, { enumerable: true, genericArguments: [ChildWithId] })(IdentifiedChildren.prototype, 'byEventKey');
|
|
188
|
+
field(Array, { enumerable: true, genericArguments: [ChildWithId] })(IdentifiedChildren.prototype, 'byMatchingKey');
|
|
189
|
+
field(Array, { enumerable: true, genericArguments: [ChildWithId] })(IdentifiedChildren.prototype, 'withDifferentEventKeys');
|
|
190
|
+
field(Array, { enumerable: true, genericArguments: [ChildWithEmptyKey] })(IdentifiedChildren.prototype, 'withEmptyEventKey');
|
|
191
|
+
field(Array, { enumerable: true, genericArguments: [ChildWithoutMatchingEmptyKey] })(IdentifiedChildren.prototype, 'withoutMatchingEmptyKey');
|
|
192
|
+
fromEvent(OrderCreated)(IdentifiedChildren);
|
|
193
|
+
|
|
74
194
|
class OrderSummary {
|
|
75
195
|
total!: number;
|
|
76
196
|
}
|
|
@@ -140,10 +260,11 @@ noAutoMap(OrderWithoutAutoMap);
|
|
|
140
260
|
fromEvent(OrderCreated)(OrderWithoutAutoMap);
|
|
141
261
|
|
|
142
262
|
class Grandchild {
|
|
143
|
-
id
|
|
263
|
+
id = '';
|
|
144
264
|
}
|
|
145
265
|
|
|
146
266
|
class ChildWithGrandchildren {
|
|
267
|
+
id = '';
|
|
147
268
|
grandchildren!: Grandchild[];
|
|
148
269
|
detail!: Grandchild | undefined;
|
|
149
270
|
}
|
|
@@ -248,6 +369,130 @@ describe('Projections with childrenFrom, nested and clearWith', () => {
|
|
|
248
369
|
expect(updateEntry.Value.Properties.quantity).toBe('quantity');
|
|
249
370
|
});
|
|
250
371
|
|
|
372
|
+
it('should map a convention or explicit child identifier from the creating event source id', async () => {
|
|
373
|
+
const { projections, registerMock } = createProjections([IdentifiedChildren]);
|
|
374
|
+
await projections.register();
|
|
375
|
+
|
|
376
|
+
const children = (registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition).Children;
|
|
377
|
+
expect(children.byConvention.IdentifiedBy).toBe('id');
|
|
378
|
+
expect(children.byConvention.From[0].Value.Properties).toEqual({ id: '$eventContext(EventSourceId)' });
|
|
379
|
+
expect(children.byUppercaseId.IdentifiedBy).toBe('Id');
|
|
380
|
+
expect(children.byUppercaseId.From[0].Value.Properties).toEqual({ Id: '$eventContext(EventSourceId)' });
|
|
381
|
+
expect(children.byExplicitId.IdentifiedBy).toBe('employeeNumber');
|
|
382
|
+
expect(children.byExplicitId.From[0].Value.Properties).toEqual({ employeeNumber: '$eventContext(EventSourceId)' });
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should map an explicit identifier without child-type metadata from the creating event source id', async () => {
|
|
386
|
+
const { projections, registerMock } = createProjections([IdentifiedChildren]);
|
|
387
|
+
await projections.register();
|
|
388
|
+
|
|
389
|
+
const child = (registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition).Children.byExplicitIdWithoutChildType;
|
|
390
|
+
expect(child.IdentifiedBy).toBe('id');
|
|
391
|
+
expect(child.From[0].Value.Properties).toEqual({ id: '$eventContext(EventSourceId)' });
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should map the child identifier from an explicit event key when it differs from the property name', async () => {
|
|
395
|
+
const { projections, registerMock } = createProjections([IdentifiedChildren]);
|
|
396
|
+
await projections.register();
|
|
397
|
+
|
|
398
|
+
const children = (registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition).Children;
|
|
399
|
+
expect(children.byEventKey.From[0].Value.Properties.id).toBe('productId');
|
|
400
|
+
expect(children.byInferredEventKey.IdentifiedBy).toBe('productId');
|
|
401
|
+
expect(children.byInferredEventKey.From[0].Value.Properties).toEqual({});
|
|
402
|
+
expect(children.byMatchingKey.From[0].Value.Properties).toEqual({});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should leave explicitly mapped identifiers and class-level disabled auto-mapping unchanged', async () => {
|
|
406
|
+
const { projections, registerMock } = createProjections([IdentifiedChildren]);
|
|
407
|
+
await projections.register();
|
|
408
|
+
|
|
409
|
+
const children = (registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition).Children;
|
|
410
|
+
expect(children.withExplicitMapping.From[0].Value.Properties).toEqual({ id: 'productId' });
|
|
411
|
+
expect(children.withoutAutoMap.AutoMap).toBe(AutoMap.Disabled);
|
|
412
|
+
expect(children.withoutAutoMap.From[0].Value.Properties).toEqual({});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should map an identifier with property-level noAutoMap from the creating event source id', async () => {
|
|
416
|
+
const { projections, registerMock } = createProjections([IdentifiedChildren]);
|
|
417
|
+
await projections.register();
|
|
418
|
+
|
|
419
|
+
const child = (registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition).Children.withNoAutoMappedId;
|
|
420
|
+
expect(child.AutoMap).toBe(AutoMap.Enabled);
|
|
421
|
+
expect(child.NoAutoMapProperties).toContain('id');
|
|
422
|
+
expect(child.From[0].Value.Properties.id).toBe('$eventContext(EventSourceId)');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should map each creating event using its own key on a shared children collection', async () => {
|
|
426
|
+
const { projections, registerMock } = createProjections([IdentifiedChildren]);
|
|
427
|
+
await projections.register();
|
|
428
|
+
|
|
429
|
+
const child = (registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition).Children.withDifferentEventKeys;
|
|
430
|
+
expect(child.IdentifiedBy).toBe('id');
|
|
431
|
+
expect(child.From).toHaveLength(2);
|
|
432
|
+
expect(child.From.find(candidate => candidate.Key.Id === 'LineAdded')?.Value).toEqual({
|
|
433
|
+
Key: 'productId', ParentKey: '$eventSourceId', Properties: { id: 'productId' }
|
|
434
|
+
});
|
|
435
|
+
expect(child.From.find(candidate => candidate.Key.Id === 'AlternateLineAdded')?.Value).toEqual({
|
|
436
|
+
Key: 'alternateId', ParentKey: '$eventSourceId', Properties: { id: 'alternateId' }
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('should preserve aggregate-only child count, increment and decrement with constant keys', async () => {
|
|
441
|
+
const { projections, registerMock } = createProjections([IdentifiedChildren]);
|
|
442
|
+
await projections.register();
|
|
443
|
+
|
|
444
|
+
const children = (registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition).Children;
|
|
445
|
+
for (const [name, operation] of [
|
|
446
|
+
['withConstantCount', '$count'],
|
|
447
|
+
['withConstantIncrement', '$increment'],
|
|
448
|
+
['withConstantDecrement', '$decrement']
|
|
449
|
+
]) {
|
|
450
|
+
const child = children[name];
|
|
451
|
+
expect(child.IdentifiedBy).toBe('id');
|
|
452
|
+
expect(child.From[0].Value).toEqual({
|
|
453
|
+
Key: '$value(all)', ParentKey: '$eventSourceId', Properties: { total: operation }
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should preserve aggregate-only child count without a constant key', async () => {
|
|
459
|
+
const { projections, registerMock } = createProjections([IdentifiedChildren]);
|
|
460
|
+
await projections.register();
|
|
461
|
+
|
|
462
|
+
const child = (registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition).Children.withPlainCount;
|
|
463
|
+
expect(child.IdentifiedBy).toBe('id');
|
|
464
|
+
expect(child.From[0].Value).toEqual({
|
|
465
|
+
Key: '$eventSourceId', ParentKey: '$eventSourceId', Properties: { total: '$count' }
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should still map the identifier when a child count also explicitly maps a label', async () => {
|
|
470
|
+
const { projections, registerMock } = createProjections([IdentifiedChildren]);
|
|
471
|
+
await projections.register();
|
|
472
|
+
|
|
473
|
+
const child = (registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition).Children.withMappedLabelAndCount;
|
|
474
|
+
expect(child.From[0].Value.Properties).toEqual({ total: '$count', label: 'label', id: '$eventContext(EventSourceId)' });
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should discover a child property matching an explicitly empty event key', async () => {
|
|
478
|
+
const { projections, registerMock } = createProjections([IdentifiedChildren]);
|
|
479
|
+
await projections.register();
|
|
480
|
+
|
|
481
|
+
const child = (registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition).Children.withEmptyEventKey;
|
|
482
|
+
expect(child.IdentifiedBy).toBe('');
|
|
483
|
+
expect(child.From[0].Value.Key).toBe('');
|
|
484
|
+
expect(child.From[0].Value.Properties).toEqual({});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should not infer an identifier from an empty event key without a matching child property', async () => {
|
|
488
|
+
const { projections, registerMock } = createProjections([IdentifiedChildren]);
|
|
489
|
+
await projections.register();
|
|
490
|
+
|
|
491
|
+
const child = (registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition).Children.withoutMatchingEmptyKey;
|
|
492
|
+
expect(child.IdentifiedBy).toBe('$eventSourceId');
|
|
493
|
+
expect(child.From[0].Value.Key).toBe('');
|
|
494
|
+
});
|
|
495
|
+
|
|
251
496
|
it('should clear only the child member through a From mapping', async () => {
|
|
252
497
|
const { projections, registerMock } = createProjections([Order]);
|
|
253
498
|
await projections.register();
|
|
@@ -324,6 +569,7 @@ describe('Projections with childrenFrom, nested and clearWith', () => {
|
|
|
324
569
|
const child = definition.Children.children;
|
|
325
570
|
expect(child.AutoMap).toBe(AutoMap.Disabled);
|
|
326
571
|
expect(child.Children.grandchildren.AutoMap).toBe(AutoMap.Disabled);
|
|
572
|
+
expect(child.Children.grandchildren.From[0].Value.Properties).toEqual({});
|
|
327
573
|
expect(child.Nested.detail.AutoMap).toBe(AutoMap.Disabled);
|
|
328
574
|
});
|
|
329
575
|
|
|
@@ -334,7 +580,10 @@ describe('Projections with childrenFrom, nested and clearWith', () => {
|
|
|
334
580
|
const definition = registerMock.mock.calls[0][0].Projections[0] as BuiltDefinition;
|
|
335
581
|
const child = definition.Children.children;
|
|
336
582
|
expect(child.AutoMap).toBe(AutoMap.Disabled);
|
|
583
|
+
expect(child.From[0].Value.Properties).toEqual({});
|
|
337
584
|
expect(child.Children.grandchildren.AutoMap).toBe(AutoMap.Enabled);
|
|
585
|
+
expect(child.Children.grandchildren.IdentifiedBy).toBe('id');
|
|
586
|
+
expect(child.Children.grandchildren.From[0].Value.Properties.id).toBe('$eventContext(EventSourceId)');
|
|
338
587
|
expect(child.Nested.detail.AutoMap).toBe(AutoMap.Enabled);
|
|
339
588
|
});
|
|
340
589
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { ContractEventType, FromRecord, JoinRecord } from './declarative/ProjectionBuilderCore.js';
|
|
5
5
|
import { GlobalHandlerPropertyNotOnVariant } from './GlobalHandlerPropertyNotOnVariant.js';
|
|
6
|
+
import type { ProjectionCapabilityProvenance } from './ProjectionCapabilityProvenance.js';
|
|
6
7
|
import { VariantMustDeclareEntersOnEvent } from './VariantMustDeclareEntersOnEvent.js';
|
|
7
8
|
|
|
8
9
|
const EVENT_SOURCE_ID_KEY = '$eventSourceId';
|
|
@@ -28,6 +29,7 @@ export interface BuiltProjection {
|
|
|
28
29
|
readonly typeName: string;
|
|
29
30
|
readonly definition: Record<string, unknown>;
|
|
30
31
|
readonly variant?: VariantDeclaration;
|
|
32
|
+
readonly provenance?: ProjectionCapabilityProvenance[];
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
function eventTypeKey(eventType: ContractEventType): string {
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { ProjectionCapabilityProvenance } from './ProjectionCapabilityProvenance.js';
|
|
5
|
+
import type { ContractEventType } from './declarative/ProjectionBuilderCore.js';
|
|
6
|
+
import { eventContractPath } from './eventContractPath.js';
|
|
7
|
+
|
|
8
|
+
/** Captures pre-lowering declarations without changing the registration contract. */
|
|
9
|
+
export function captureProjectionProvenance(definition: Record<string, unknown>, modelBound: boolean, overrides: ReadonlyMap<string, string> = new Map()): ProjectionCapabilityProvenance[] {
|
|
10
|
+
const result: ProjectionCapabilityProvenance[] = [];
|
|
11
|
+
const add = (contractPath: string, declaration: string) => result.push({ contractPath, declaration: overrides.get(contractPath) ?? declaration });
|
|
12
|
+
const prefix = modelBound ? '@' : '.';
|
|
13
|
+
const from = definition.From as Array<{ Key: ContractEventType; Value: { Properties: Record<string, string>; Key: string; ParentKey: string } }> ?? [];
|
|
14
|
+
for (const entry of from) {
|
|
15
|
+
const path = eventContractPath('From', entry.Key);
|
|
16
|
+
add(path, `${prefix}${modelBound ? 'fromEvent' : 'from'}`);
|
|
17
|
+
add(`${path}.Key`, `${prefix}${modelBound ? 'fromEvent' : 'from().usingKey'}`);
|
|
18
|
+
add(`${path}.ParentKey`, `${prefix}${modelBound ? 'fromEvent' : 'from().usingParentKey'}`);
|
|
19
|
+
for (const [property, expression] of Object.entries(entry.Value.Properties ?? {})) {
|
|
20
|
+
const operation = expression.startsWith('$add(') ? 'addFrom' : expression.startsWith('$subtract(') ? 'subtractFrom'
|
|
21
|
+
: expression === '$count' ? 'count' : expression === '$increment' ? 'increment'
|
|
22
|
+
: expression === '$decrement' ? 'decrement' : expression === '$null' ? 'clearWith'
|
|
23
|
+
: expression.startsWith('$value(') ? 'setValue' : expression.startsWith('$eventContext(') ? 'setFromContext' : 'setFrom';
|
|
24
|
+
add(`${path}.Properties.${property}`, `${prefix}${modelBound ? operation : `from().${operation}`}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const section of ['Join', 'RemovedWith', 'RemovedWithJoin'] as const) {
|
|
28
|
+
for (const entry of definition[section] as Array<{ Key: ContractEventType; Value: { Properties?: Record<string, string>; Key?: string; ParentKey?: string } }> ?? []) {
|
|
29
|
+
const path = eventContractPath(section, entry.Key);
|
|
30
|
+
const declaration = modelBound ? `@${section[0].toLowerCase()}${section.slice(1)}` : `.${section[0].toLowerCase()}${section.slice(1)}`;
|
|
31
|
+
add(path, declaration);
|
|
32
|
+
if (entry.Value.Key !== undefined) add(`${path}.Key`, declaration);
|
|
33
|
+
if (entry.Value.ParentKey !== undefined) add(`${path}.ParentKey`, declaration);
|
|
34
|
+
for (const property of Object.keys(entry.Value.Properties ?? {})) add(`${path}.Properties.${property}`, declaration);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const section of ['Children', 'Nested'] as const) {
|
|
38
|
+
for (const property of Object.keys(definition[section] as Record<string, unknown> ?? {})) {
|
|
39
|
+
add(`${section}.${property}`, modelBound ? (section === 'Children' ? '@childrenFrom' : '@nested') : `.${section.toLowerCase()}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const all = definition.All as { Properties?: Record<string, string>; IncludeChildren?: boolean } | undefined;
|
|
43
|
+
if (all?.IncludeChildren || Object.keys(all?.Properties ?? {}).length) add('All', modelBound ? '@fromEvery' : '.fromEvery');
|
|
44
|
+
for (const property of Object.keys(all?.Properties ?? {})) add(`All.Properties.${property}`, modelBound ? '@fromEvery' : '.fromEvery');
|
|
45
|
+
if ((definition.FromEvery as unknown[] | undefined)?.length) add('FromEvery', modelBound ? '@fromEvery' : '.fromEvery');
|
|
46
|
+
if (definition.IsActive === false) add('IsActive', modelBound ? '@passive' : '.passive');
|
|
47
|
+
if (definition.EventSequenceId) add('EventSequenceId', modelBound ? '@eventSequence' : '.fromEventSequence');
|
|
48
|
+
if (definition.FromEventProperty) add('FromEventProperty', modelBound ? '@fromEvent' : '.from');
|
|
49
|
+
return result;
|
|
50
|
+
}
|