@cratis/chronicle 6.17.0 → 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/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 +1 -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.js +1 -1
- package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
- 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/package.json +2 -2
- package/projections/ProjectionDefinitionCompiler.registration.spec.ts +4 -2
- package/projections/Projections.childrenAndNested.spec.ts +250 -1
- package/projections/declarative/AddChildBuilder.ts +33 -2
- package/projections/declarative/FromBuilder.ts +9 -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 +1 -1
- package/projections/declarative/addChildCompatibility.standard.spec.ts +80 -0
- package/projections/eventContextExpressions.spec.ts +20 -0
- package/projections/modelBound/childrenAndNestedBuilder.ts +45 -11
|
@@ -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
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals';
|
|
5
|
+
import { eventContextPropertyExpression } from '../eventContextPropertyExpression.js';
|
|
5
6
|
import { IAddChildBuilder } from './IAddChildBuilder.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -14,8 +15,10 @@ export interface ChildAdditionEntry {
|
|
|
14
15
|
targetProperty: string;
|
|
15
16
|
/** The child model property used to identify instances, when set via the builder callback overload. */
|
|
16
17
|
identifiedBy?: string;
|
|
17
|
-
/** The event property used as the key, when set via the builder callback overload. */
|
|
18
|
+
/** The event property or context expression used as the key, when set via the builder callback overload. */
|
|
18
19
|
usingKey?: string;
|
|
20
|
+
/** The event context expression used as the parent key, when set via the builder callback overload. */
|
|
21
|
+
usingParentKey?: string;
|
|
19
22
|
/** The event property whose value becomes the child directly, when set via the plain accessor overload. */
|
|
20
23
|
fromEventProperty?: string;
|
|
21
24
|
}
|
|
@@ -36,8 +39,9 @@ export class AddChildBuilder<TChildModel, TEvent> implements IAddChildBuilder<TC
|
|
|
36
39
|
private _usedAsBuilder = false;
|
|
37
40
|
private _identifiedByProperty: string | undefined;
|
|
38
41
|
private _usingKeyProperty: string | undefined;
|
|
42
|
+
private _usingParentKeyProperty: string | undefined;
|
|
39
43
|
|
|
40
|
-
/** Whether any builder member (`identifiedBy
|
|
44
|
+
/** Whether any builder member (`identifiedBy`, `usingKey`, `usingKeyFromContext`, or `usingParentKeyFromContext`) was invoked on the probe. */
|
|
41
45
|
get usedAsBuilder(): boolean {
|
|
42
46
|
return this._usedAsBuilder;
|
|
43
47
|
}
|
|
@@ -52,6 +56,11 @@ export class AddChildBuilder<TChildModel, TEvent> implements IAddChildBuilder<TC
|
|
|
52
56
|
return this._usingKeyProperty;
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
/** The parent key expression captured via `usingParentKeyFromContext()`, if any. */
|
|
60
|
+
get usingParentKeyProperty(): string | undefined {
|
|
61
|
+
return this._usingParentKeyProperty;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
/** The event property path captured when the probe was used as a plain accessor. */
|
|
56
65
|
get capturedEventProperty(): string {
|
|
57
66
|
return this._pathHandler.property;
|
|
@@ -77,10 +86,26 @@ export class AddChildBuilder<TChildModel, TEvent> implements IAddChildBuilder<TC
|
|
|
77
86
|
return this;
|
|
78
87
|
}
|
|
79
88
|
|
|
89
|
+
/** @inheritdoc */
|
|
90
|
+
usingKeyFromContext(contextPropertyName: string): IAddChildBuilder<TChildModel, TEvent> {
|
|
91
|
+
this._usingKeyProperty = eventContextPropertyExpression(contextPropertyName);
|
|
92
|
+
this._usedAsBuilder = true;
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @inheritdoc */
|
|
97
|
+
usingParentKeyFromContext(contextPropertyName: string): IAddChildBuilder<TChildModel, TEvent> {
|
|
98
|
+
this._usingParentKeyProperty = eventContextPropertyExpression(contextPropertyName);
|
|
99
|
+
this._usedAsBuilder = true;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
80
103
|
/**
|
|
81
104
|
* Proxy `get` trap: known builder members dispatch to the real implementation above;
|
|
82
105
|
* anything else (a plain event property accessed by the accessor overload) falls through
|
|
83
106
|
* to path-capturing so the accessor overload keeps working against the same probe.
|
|
107
|
+
* The builder member names are therefore reserved: an event property with one of these names
|
|
108
|
+
* cannot be selected through the accessor overload.
|
|
84
109
|
* @param _target - The proxy's (unused) empty target object.
|
|
85
110
|
* @param prop - The accessed property name.
|
|
86
111
|
* @returns The resolved member.
|
|
@@ -92,6 +117,12 @@ export class AddChildBuilder<TChildModel, TEvent> implements IAddChildBuilder<TC
|
|
|
92
117
|
if (prop === 'usingKey') {
|
|
93
118
|
return this.usingKey.bind(this);
|
|
94
119
|
}
|
|
120
|
+
if (prop === 'usingKeyFromContext') {
|
|
121
|
+
return this.usingKeyFromContext.bind(this);
|
|
122
|
+
}
|
|
123
|
+
if (prop === 'usingParentKeyFromContext') {
|
|
124
|
+
return this.usingParentKeyFromContext.bind(this);
|
|
125
|
+
}
|
|
95
126
|
return Reflect.get(this._pathProxy as object, prop);
|
|
96
127
|
}
|
|
97
128
|
}
|
|
@@ -171,6 +171,14 @@ export class FromBuilder<TReadModel, TEvent> implements IFromBuilder<TReadModel,
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/** @inheritdoc */
|
|
174
|
+
addChild<TChildModel>(targetPropertyAccessor: (model: TReadModel) => readonly TChildModel[] | null | undefined, builderCallback: (builder: IAddChildBuilder<TChildModel, TEvent> & TEvent) => void): this;
|
|
175
|
+
/** @inheritdoc */
|
|
176
|
+
addChild<TChildModel>(targetPropertyAccessor: (model: TReadModel) => readonly TChildModel[] | null | undefined, eventPropertyAccessor: PropertyAccessor<TEvent>): this;
|
|
177
|
+
/** @inheritdoc */
|
|
178
|
+
addChild<TChildModel>(
|
|
179
|
+
targetPropertyAccessor: PropertyAccessor<TReadModel>,
|
|
180
|
+
eventPropertyAccessorOrBuilderCallback: PropertyAccessor<TEvent> | ((builder: IAddChildBuilder<TChildModel, TEvent>) => void)
|
|
181
|
+
): this;
|
|
174
182
|
addChild<TChildModel>(
|
|
175
183
|
targetPropertyAccessor: PropertyAccessor<TReadModel>,
|
|
176
184
|
eventPropertyAccessorOrBuilderCallback: PropertyAccessor<TEvent> | ((builder: IAddChildBuilder<TChildModel, TEvent>) => void)
|
|
@@ -184,7 +192,7 @@ export class FromBuilder<TReadModel, TEvent> implements IFromBuilder<TReadModel,
|
|
|
184
192
|
(eventPropertyAccessorOrBuilderCallback as (value: unknown) => void)(probeProxy);
|
|
185
193
|
|
|
186
194
|
this.entry.children.push(probe.usedAsBuilder
|
|
187
|
-
? { targetProperty: targetHandler.property, identifiedBy: probe.identifiedByProperty, usingKey: probe.usingKeyProperty }
|
|
195
|
+
? { targetProperty: targetHandler.property, identifiedBy: probe.identifiedByProperty, usingKey: probe.usingKeyProperty, usingParentKey: probe.usingParentKeyProperty }
|
|
188
196
|
: { targetProperty: targetHandler.property, fromEventProperty: probe.capturedEventProperty });
|
|
189
197
|
return this;
|
|
190
198
|
}
|
|
@@ -22,4 +22,18 @@ export interface IAddChildBuilder<TChildModel, TEvent> {
|
|
|
22
22
|
* @returns This builder for fluent chaining.
|
|
23
23
|
*/
|
|
24
24
|
usingKey(eventPropertyAccessor: PropertyAccessor<TEvent>): IAddChildBuilder<TChildModel, TEvent>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Specifies an event context property used as the key when adding a child.
|
|
28
|
+
* @param contextPropertyName - Property path on EventContext, using TypeScript casing.
|
|
29
|
+
* @returns This builder for fluent chaining.
|
|
30
|
+
*/
|
|
31
|
+
usingKeyFromContext(contextPropertyName: string): IAddChildBuilder<TChildModel, TEvent>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Specifies an event context property used as the parent key when adding a child.
|
|
35
|
+
* @param contextPropertyName - Property path on EventContext, using TypeScript casing.
|
|
36
|
+
* @returns This builder for fluent chaining.
|
|
37
|
+
*/
|
|
38
|
+
usingParentKeyFromContext(contextPropertyName: string): IAddChildBuilder<TChildModel, TEvent>;
|
|
25
39
|
}
|
|
@@ -107,7 +107,24 @@ export interface IReadModelPropertiesBuilder<TReadModel, TEvent, TBuilder> {
|
|
|
107
107
|
count(readModelPropertyAccessor: PropertyAccessor<TReadModel>): TBuilder;
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* Adds a child model
|
|
110
|
+
* Adds a child model configured through a child builder to a collection on the read model.
|
|
111
|
+
* Event members remain available while TypeScript resolves the two function-shaped overloads.
|
|
112
|
+
* @param targetPropertyAccessor - Accessor for the collection property on the read model.
|
|
113
|
+
* @param builderCallback - Callback configuring the child identifier and keys.
|
|
114
|
+
* @returns This builder for fluent chaining.
|
|
115
|
+
*/
|
|
116
|
+
addChild<TChildModel>(targetPropertyAccessor: (model: TReadModel) => readonly TChildModel[] | null | undefined, builderCallback: (builder: IAddChildBuilder<TChildModel, TEvent> & TEvent) => void): TBuilder;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Adds a child model directly from an event property to a collection on the read model.
|
|
120
|
+
* @param targetPropertyAccessor - Accessor for the collection property on the read model.
|
|
121
|
+
* @param eventPropertyAccessor - Accessor for the event property holding the child value.
|
|
122
|
+
* @returns This builder for fluent chaining.
|
|
123
|
+
*/
|
|
124
|
+
addChild<TChildModel>(targetPropertyAccessor: (model: TReadModel) => readonly TChildModel[] | null | undefined, eventPropertyAccessor: PropertyAccessor<TEvent>): TBuilder;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Retains support for arbitrary read model accessors and explicitly selected child types.
|
|
111
128
|
* @param targetPropertyAccessor - Accessor for the collection property on the read model.
|
|
112
129
|
* @param eventPropertyAccessorOrBuilderCallback - Either a direct event property accessor or a builder callback.
|
|
113
130
|
* @returns This builder for fluent chaining.
|
|
@@ -170,6 +170,14 @@ export class JoinBuilder<TReadModel, TEvent> implements IJoinBuilder<TReadModel,
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
/** @inheritdoc */
|
|
173
|
+
addChild<TChildModel>(targetPropertyAccessor: (model: TReadModel) => readonly TChildModel[] | null | undefined, builderCallback: (builder: IAddChildBuilder<TChildModel, TEvent> & TEvent) => void): this;
|
|
174
|
+
/** @inheritdoc */
|
|
175
|
+
addChild<TChildModel>(targetPropertyAccessor: (model: TReadModel) => readonly TChildModel[] | null | undefined, eventPropertyAccessor: PropertyAccessor<TEvent>): this;
|
|
176
|
+
/** @inheritdoc */
|
|
177
|
+
addChild<TChildModel>(
|
|
178
|
+
targetPropertyAccessor: PropertyAccessor<TReadModel>,
|
|
179
|
+
eventPropertyAccessorOrBuilderCallback: PropertyAccessor<TEvent> | ((builder: IAddChildBuilder<TChildModel, TEvent>) => void)
|
|
180
|
+
): this;
|
|
173
181
|
addChild<TChildModel>(
|
|
174
182
|
targetPropertyAccessor: PropertyAccessor<TReadModel>,
|
|
175
183
|
eventPropertyAccessorOrBuilderCallback: PropertyAccessor<TEvent> | ((builder: IAddChildBuilder<TChildModel, TEvent>) => void)
|
|
@@ -183,7 +191,7 @@ export class JoinBuilder<TReadModel, TEvent> implements IJoinBuilder<TReadModel,
|
|
|
183
191
|
(eventPropertyAccessorOrBuilderCallback as (value: unknown) => void)(probeProxy);
|
|
184
192
|
|
|
185
193
|
this.entry.children.push(probe.usedAsBuilder
|
|
186
|
-
? { targetProperty: targetHandler.property, identifiedBy: probe.identifiedByProperty, usingKey: probe.usingKeyProperty }
|
|
194
|
+
? { targetProperty: targetHandler.property, identifiedBy: probe.identifiedByProperty, usingKey: probe.usingKeyProperty, usingParentKey: probe.usingParentKeyProperty }
|
|
187
195
|
: { targetProperty: targetHandler.property, fromEventProperty: probe.capturedEventProperty });
|
|
188
196
|
return this;
|
|
189
197
|
}
|
|
@@ -256,7 +256,7 @@ export abstract class ProjectionBuilderCore<TReadModel, TBuilder> {
|
|
|
256
256
|
Key: eventType,
|
|
257
257
|
Value: {
|
|
258
258
|
Key: child.usingKey ?? '$eventSourceId',
|
|
259
|
-
ParentKey: '$eventSourceId',
|
|
259
|
+
ParentKey: child.usingParentKey ?? '$eventSourceId',
|
|
260
260
|
Properties: {}
|
|
261
261
|
}
|
|
262
262
|
});
|
|
@@ -0,0 +1,80 @@
|
|
|
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 { describe, expect, it } from 'vitest';
|
|
5
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
6
|
+
import { FromBuilder } from './FromBuilder.js';
|
|
7
|
+
import { IAddChildBuilder } from './IAddChildBuilder.js';
|
|
8
|
+
import { IReadModelPropertiesBuilder } from './IReadModelPropertiesBuilder.js';
|
|
9
|
+
import { JoinBuilder } from './JoinBuilder.js';
|
|
10
|
+
import { ProjectionBuilderFor } from './ProjectionBuilderFor.js';
|
|
11
|
+
|
|
12
|
+
class Line {
|
|
13
|
+
id = '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class Base {
|
|
17
|
+
id = '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class Derived extends Base {
|
|
21
|
+
derivedId = '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class LineAdded {
|
|
25
|
+
line = new Line();
|
|
26
|
+
}
|
|
27
|
+
eventType()(LineAdded);
|
|
28
|
+
|
|
29
|
+
class Order {
|
|
30
|
+
id = '';
|
|
31
|
+
lines: Line[] = [];
|
|
32
|
+
nullableLines: Line[] | null = null;
|
|
33
|
+
bases: Base[] = [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// This checks the shared public interface independently of either concrete builder.
|
|
37
|
+
function acceptOriginalCallSites(builder: IReadModelPropertiesBuilder<Order, LineAdded, unknown>): void {
|
|
38
|
+
builder.addChild<Line>(model => model.nullableLines, event => event.line);
|
|
39
|
+
builder.addChild(model => model.nullableLines, child => child.identifiedBy(item => item.id));
|
|
40
|
+
builder.addChild<Derived>(model => model.bases, (event: LineAdded) => event.line);
|
|
41
|
+
builder.addChild<Derived>(model => model.bases, (child: IAddChildBuilder<Derived, LineAdded>) => child
|
|
42
|
+
.identifiedBy(item => item.derivedId)
|
|
43
|
+
.usingKeyFromContext('sequenceNumber'));
|
|
44
|
+
builder.addChild<Line>(model => model.lines, (event: LineAdded) => event.line);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('addChild source compatibility', () => {
|
|
48
|
+
it('should accept nullable targets and explicitly narrower child types on the shared interface', () => {
|
|
49
|
+
const builder: IReadModelPropertiesBuilder<Order, LineAdded, unknown> = new FromBuilder<Order, LineAdded>();
|
|
50
|
+
acceptOriginalCallSites(builder);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should accept the original call shapes on the concrete from builder', () => {
|
|
54
|
+
const builder = new FromBuilder<Order, LineAdded>();
|
|
55
|
+
builder.addChild<Line>(model => model.nullableLines, event => event.line);
|
|
56
|
+
builder.addChild<Derived>(model => model.bases, (child: IAddChildBuilder<Derived, LineAdded>) => child.identifiedBy(item => item.derivedId));
|
|
57
|
+
builder.addChild<Derived>(model => model.bases, (event: LineAdded) => event.line);
|
|
58
|
+
builder.addChild<Line>(model => model.lines, (event: LineAdded) => event.line);
|
|
59
|
+
expect(builder.entry.children).toHaveLength(4);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should accept the original call shapes on the concrete join builder', () => {
|
|
63
|
+
const builder = new JoinBuilder<Order, LineAdded>();
|
|
64
|
+
builder.addChild<Line>(model => model.nullableLines, event => event.line);
|
|
65
|
+
builder.addChild<Derived>(model => model.bases, (child: IAddChildBuilder<Derived, LineAdded>) => child.identifiedBy(item => item.derivedId));
|
|
66
|
+
builder.addChild<Derived>(model => model.bases, (event: LineAdded) => event.line);
|
|
67
|
+
builder.addChild<Line>(model => model.lines, (event: LineAdded) => event.line);
|
|
68
|
+
expect(builder.entry.children).toHaveLength(4);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should contextually type the unannotated builder callback in the documentation example', () => {
|
|
72
|
+
const projection = new ProjectionBuilderFor<Order>();
|
|
73
|
+
projection.from(LineAdded, from => from
|
|
74
|
+
.addChild(model => model.lines, child => child
|
|
75
|
+
.identifiedBy(item => item.id)
|
|
76
|
+
.usingKeyFromContext('sequenceNumber')
|
|
77
|
+
.usingParentKeyFromContext('eventSourceId')));
|
|
78
|
+
expect(projection.build('order', 'Order').Children).toHaveProperty('lines');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -182,6 +182,24 @@ describe('event context expressions', () => {
|
|
|
182
182
|
.From[0].Value.Key.should.equal('$eventContext(EventSourceId)');
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
+
it('should use context properties as addChild keys in a from clause', () => {
|
|
186
|
+
const child = declarative(builder => builder.from(Recorded, from => from.addChild<Child>(model => model.children, added => added
|
|
187
|
+
.identifiedBy(model => model.happened)
|
|
188
|
+
.usingKeyFromContext('sequenceNumber')
|
|
189
|
+
.usingParentKeyFromContext('eventSourceId'))));
|
|
190
|
+
child.Children.children.From[0].Value.Key.should.equal('$eventContext(SequenceNumber)');
|
|
191
|
+
child.Children.children.From[0].Value.ParentKey.should.equal('$eventContext(EventSourceId)');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should use context properties as addChild keys in a join clause', () => {
|
|
195
|
+
const child = declarative(builder => builder.join(Recorded, join => join.on(model => model.occurred)
|
|
196
|
+
.addChild<Child>(model => model.children, added => added
|
|
197
|
+
.usingKeyFromContext('eventSourceId')
|
|
198
|
+
.usingParentKeyFromContext('sequenceNumber'))));
|
|
199
|
+
child.Children.children.From[0].Value.Key.should.equal('$eventContext(EventSourceId)');
|
|
200
|
+
child.Children.children.From[0].Value.ParentKey.should.equal('$eventContext(SequenceNumber)');
|
|
201
|
+
});
|
|
202
|
+
|
|
185
203
|
it('should use a context property as a from parent key', () => {
|
|
186
204
|
declarative(builder => builder.from(Recorded, from => from.usingParentKeyFromContext('eventSourceId')))
|
|
187
205
|
.From[0].Value.ParentKey.should.equal('$eventContext(EventSourceId)');
|
|
@@ -255,6 +273,8 @@ describe('event context expressions', () => {
|
|
|
255
273
|
const mappings = [
|
|
256
274
|
(builder: ProjectionBuilderFor<Model>) => builder.from(Recorded, from => from.usingKeyFromContext('missingKey')),
|
|
257
275
|
(builder: ProjectionBuilderFor<Model>) => builder.from(Recorded, from => from.usingParentKeyFromContext('missingParent')),
|
|
276
|
+
(builder: ProjectionBuilderFor<Model>) => builder.from(Recorded, from => from.addChild<Child>(model => model.children, child => child.usingKeyFromContext('missingChildKey'))),
|
|
277
|
+
(builder: ProjectionBuilderFor<Model>) => builder.join(Recorded, join => join.on(model => model.occurred).addChild<Child>(model => model.children, child => child.usingParentKeyFromContext('missingChildParent'))),
|
|
258
278
|
(builder: ProjectionBuilderFor<Model>) => builder.join(Recorded, join => join.on(model => model.occurred).usingKeyFromContext('missingJoinKey')),
|
|
259
279
|
(builder: ProjectionBuilderFor<Model>) => builder.removedWith(Removed, removed => removed.usingKeyFromContext('missingRemovalKey'))
|
|
260
280
|
];
|