@cratis/chronicle 6.42.2 → 6.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/ChronicleClient.ts +7 -6
  2. package/ChronicleOptions.ts +1 -1
  3. package/EventStore.ts +1 -1
  4. package/Metrics.ts +3 -3
  5. package/WellKnownTelemetryNames.ts +9 -8
  6. package/dist/ChronicleClient.d.ts.map +1 -1
  7. package/dist/ChronicleClient.js +7 -6
  8. package/dist/ChronicleClient.js.map +1 -1
  9. package/dist/ChronicleOptions.d.ts +1 -1
  10. package/dist/ChronicleOptions.d.ts.map +1 -1
  11. package/dist/ChronicleOptions.js +1 -1
  12. package/dist/ChronicleOptions.js.map +1 -1
  13. package/dist/EventStore.js +1 -1
  14. package/dist/EventStore.js.map +1 -1
  15. package/dist/Metrics.d.ts +3 -3
  16. package/dist/Metrics.js +3 -3
  17. package/dist/WellKnownTelemetryNames.d.ts +9 -8
  18. package/dist/WellKnownTelemetryNames.d.ts.map +1 -1
  19. package/dist/WellKnownTelemetryNames.js +9 -8
  20. package/dist/WellKnownTelemetryNames.js.map +1 -1
  21. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  22. package/dist/eventSequences/EventSequence.js +31 -56
  23. package/dist/eventSequences/EventSequence.js.map +1 -1
  24. package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -1
  25. package/dist/projections/ProjectionDefinitionCompiler.js +30 -21
  26. package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -1
  27. package/dist/projections/modelBound/FromAllEventsMetadata.d.ts +8 -0
  28. package/dist/projections/modelBound/FromAllEventsMetadata.d.ts.map +1 -0
  29. package/dist/projections/modelBound/FromAllEventsMetadata.js +4 -0
  30. package/dist/projections/modelBound/FromAllEventsMetadata.js.map +1 -0
  31. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts +4 -2
  32. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
  33. package/dist/projections/modelBound/childrenAndNestedBuilder.js +15 -8
  34. package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
  35. package/dist/projections/modelBound/fromAll.d.ts +5 -2
  36. package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
  37. package/dist/projections/modelBound/fromAll.js +8 -3
  38. package/dist/projections/modelBound/fromAll.js.map +1 -1
  39. package/dist/projections/modelBound/fromAllEvents.d.ts +32 -0
  40. package/dist/projections/modelBound/fromAllEvents.d.ts.map +1 -0
  41. package/dist/projections/modelBound/fromAllEvents.js +44 -0
  42. package/dist/projections/modelBound/fromAllEvents.js.map +1 -0
  43. package/dist/projections/modelBound/index.d.ts +2 -0
  44. package/dist/projections/modelBound/index.d.ts.map +1 -1
  45. package/dist/projections/modelBound/index.js +1 -0
  46. package/dist/projections/modelBound/index.js.map +1 -1
  47. package/dist/telemetry/ChronicleTelemetryOptions.d.ts +5 -4
  48. package/dist/telemetry/ChronicleTelemetryOptions.d.ts.map +1 -1
  49. package/dist/telemetry/observeOperation.d.ts +1 -2
  50. package/dist/telemetry/observeOperation.d.ts.map +1 -1
  51. package/dist/telemetry/observeOperation.js +2 -3
  52. package/dist/telemetry/observeOperation.js.map +1 -1
  53. package/dist/telemetry/spanAttributes.d.ts +4 -4
  54. package/dist/telemetry/spanAttributes.d.ts.map +1 -1
  55. package/dist/telemetry/spanAttributes.js +4 -7
  56. package/dist/telemetry/spanAttributes.js.map +1 -1
  57. package/dist/tsconfig.tsbuildinfo +1 -1
  58. package/dist/types/modelBoundPropertyMetadata.js +1 -1
  59. package/dist/types/modelBoundPropertyMetadata.js.map +1 -1
  60. package/eventSequences/EventSequence.ts +31 -56
  61. package/for_ChronicleOptions/when_configuring_telemetry/with_span_names.spec.ts +21 -21
  62. package/package.json +2 -2
  63. package/projections/ProjectionDefinitionCompiler.ts +28 -21
  64. package/projections/for_ProjectionDefinitionCompiler/against_a_kernel/when_subscribing_with_from_all_events.integration.spec.ts +202 -0
  65. package/projections/for_ProjectionDefinitionCompiler/when_compiling_from_all_events.spec.ts +235 -0
  66. package/projections/for_ProjectionDefinitionCompiler/when_compiling_from_all_events.standard.spec.ts +107 -0
  67. package/projections/modelBound/FromAllEventsMetadata.ts +10 -0
  68. package/projections/modelBound/childrenAndNestedBuilder.ts +27 -8
  69. package/projections/modelBound/fromAll.ts +8 -3
  70. package/projections/modelBound/fromAllEvents.ts +48 -0
  71. package/projections/modelBound/index.ts +2 -0
  72. package/telemetry/ChronicleTelemetryOptions.ts +5 -4
  73. package/telemetry/for_ChronicleTelemetry/given/a_span_name_registry.fixture.ts +2 -2
  74. package/telemetry/for_ChronicleTelemetry/when_appending/{with_shared_and_legacy_attributes.spec.ts → with_canonical_attributes.spec.ts} +8 -9
  75. package/telemetry/for_ChronicleTelemetry/when_exposing_names/with_convention_names.spec.ts +65 -0
  76. package/telemetry/for_ChronicleTelemetry/when_naming_spans/{with_per_client_modes.spec.ts → with_convention_names.spec.ts} +45 -40
  77. package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts +4 -3
  78. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_client_operations.spec.ts +102 -0
  79. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_failed_appends.spec.ts +47 -0
  80. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_millisecond_adapters.spec.ts +99 -0
  81. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_rejected_appends.spec.ts +88 -0
  82. package/telemetry/for_ChronicleTelemetry/when_recording_product_attributes/with_convention_names.spec.ts +51 -0
  83. package/telemetry/for_ChronicleTelemetry/when_recording_sequence_numbers/with_integer_precision.spec.ts +1 -1
  84. package/telemetry/observeOperation.ts +1 -4
  85. package/telemetry/spanAttributes.ts +5 -8
  86. package/types/modelBoundPropertyMetadata.ts +1 -1
  87. package/telemetry/for_ChronicleTelemetry/when_exposing_names/with_legacy_compatibility.spec.ts +0 -44
  88. package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_legacy_overlap.spec.ts +0 -40
  89. package/telemetry/for_ChronicleTelemetry/when_recording_product_attributes/with_both_naming_modes.spec.ts +0 -52
@@ -37,6 +37,7 @@ import { getChildrenFromMetadata } from './modelBound/childrenFrom.js';
37
37
  import { getClearWithPropertyMetadata } from './modelBound/clearWith.js';
38
38
  import { getEventSequenceMetadata } from './modelBound/eventSequence.js';
39
39
  import { getFromAllMetadata } from './modelBound/fromAll.js';
40
+ import { getFromAllEventsMetadata } from './modelBound/fromAllEvents.js';
40
41
  import { getFromEveryMetadata } from './modelBound/fromEvery.js';
41
42
  import { getFromEventMetadata } from './modelBound/fromEvent.js';
42
43
  import { isModelBoundProjection } from './modelBound/isModelBoundProjection.js';
@@ -290,6 +291,27 @@ export class ProjectionDefinitionCompiler {
290
291
  const removedWithJoinByEventType = new Map<string, { Key: ContractEventType; Value: { Key: string } }>();
291
292
  const childrenByProperty: Record<string, ChildrenDefinitionLike> = {};
292
293
  const nestedByProperty: Record<string, ChildrenDefinitionLike> = {};
294
+ const allProperties: Record<string, string> = {};
295
+ const subscribesToAllEvents = properties.some(property => getFromAllEventsMetadata(prototype, property) !== undefined);
296
+ if (subscribesToAllEvents) overrides.set('SubscribesToAllEvents', '@fromAllEvents');
297
+ let includeChildren = false;
298
+ const collectSharedMapping = (memberPrototype: object, property: string, isRoot = false) => {
299
+ const fromAllEvents = getFromAllEventsMetadata(memberPrototype, property);
300
+ // Preserve legacy child behavior: only the new decorator contributes child mappings.
301
+ const fromEvery = isRoot ? getFromEveryMetadata(memberPrototype, property) : undefined;
302
+ const mapping = fromAllEvents ?? fromEvery ?? (isRoot ? getFromAllMetadata(memberPrototype, property) : undefined);
303
+ if (!mapping) return;
304
+ if (fromAllEvents) {
305
+ includeChildren = true;
306
+ overrides.set('All', '@fromAllEvents');
307
+ }
308
+ // .NET collects child and nested members by bare name into the root All block,
309
+ // not by a path prefixed with the containing collection or nested property.
310
+ allProperties[property] = mapping.contextProperty
311
+ ? eventContextPropertyExpression(mapping.contextProperty)
312
+ : (mapping.property ?? property);
313
+ overrides.set(`All.Properties.${property}`, fromAllEvents ? '@fromAllEvents' : fromEvery ? '@fromEvery' : '@fromAll');
314
+ };
293
315
 
294
316
  const fromEvents = getFromEventMetadata(type);
295
317
  for (const fromEvent of fromEvents) {
@@ -363,11 +385,11 @@ export class ProjectionDefinitionCompiler {
363
385
  }
364
386
  const childrenFromList = getChildrenFromMetadata(prototype, property);
365
387
  if (childrenFromList.length > 0) {
366
- childrenByProperty[property] = buildChildrenEntry(type, property, childrenFromList);
388
+ childrenByProperty[property] = buildChildrenEntry(type, property, childrenFromList, collectSharedMapping);
367
389
  }
368
390
  const propertyIsNested = isNested(prototype, property);
369
391
  if (propertyIsNested) {
370
- nestedByProperty[property] = buildNestedEntry(type, property);
392
+ nestedByProperty[property] = buildNestedEntry(type, property, collectSharedMapping);
371
393
  }
372
394
  // A scalar root property clears back to no value when the given event is observed.
373
395
  // Nested single-object clearWith is handled by buildNestedEntry instead.
@@ -378,16 +400,7 @@ export class ProjectionDefinitionCompiler {
378
400
  overrides.set(`${eventContractPath('From', toContractEventType(clearWith.eventType))}.Properties.${property}`, '@clearWith');
379
401
  }
380
402
  }
381
- }
382
-
383
- const allProperties: Record<string, string> = {};
384
- for (const property of properties) {
385
- const fromEvery = getFromEveryMetadata(prototype, property) ?? getFromAllMetadata(prototype, property);
386
- if (fromEvery) {
387
- allProperties[property] = fromEvery.contextProperty
388
- ? eventContextPropertyExpression(fromEvery.contextProperty)
389
- : (fromEvery.property ?? property);
390
- }
403
+ collectSharedMapping(prototype, property, true);
391
404
  }
392
405
 
393
406
  let from = Array.from(fromByEventType.values());
@@ -446,9 +459,10 @@ export class ProjectionDefinitionCompiler {
446
459
  FromEvery: [],
447
460
  All: {
448
461
  Properties: allProperties,
449
- IncludeChildren: false,
462
+ IncludeChildren: includeChildren,
450
463
  AutoMap: AutoMap.Inherit
451
464
  },
465
+ ...(subscribesToAllEvents ? { SubscribesToAllEvents: true } : {}),
452
466
  RemovedWith: Array.from(removedWithByEventType.values()),
453
467
  RemovedWithJoin: Array.from(removedWithJoinByEventType.values()),
454
468
  LastUpdated: { Value: '' },
@@ -458,18 +472,11 @@ export class ProjectionDefinitionCompiler {
458
472
  Nested: nestedByProperty
459
473
  };
460
474
  const provenance = captureProjectionProvenance({ ...definition, From: preLoweringFrom, Join: preLoweringJoin }, true, overrides);
461
- if (Object.keys(allProperties).length && Object.keys(allProperties).every(property =>
475
+ if (!includeChildren && Object.keys(allProperties).length && Object.keys(allProperties).every(property =>
462
476
  getFromAllMetadata(prototype, property) && !getFromEveryMetadata(prototype, property))) {
463
477
  const index = provenance.findIndex(entry => entry.contractPath === 'All');
464
478
  if (index >= 0) provenance[index] = { contractPath: 'All', declaration: '@fromAll' };
465
479
  }
466
- for (const property of Object.keys(allProperties)) {
467
- if (getFromAllMetadata(prototype, property) && !getFromEveryMetadata(prototype, property)) {
468
- const path = `All.Properties.${property}`;
469
- const index = provenance.findIndex(entry => entry.contractPath === path);
470
- if (index >= 0) provenance[index] = { contractPath: path, declaration: '@fromAll' };
471
- }
472
- }
473
480
  if (variant) {
474
481
  provenance.push({ contractPath: 'Variant', declaration: '@variantOf' });
475
482
  provenance.push({ contractPath: 'Variant.Key', declaration: '@variantOf' });
@@ -0,0 +1,202 @@
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 { randomUUID } from 'node:crypto';
5
+ import { field } from '@cratis/fundamentals';
6
+ import { ObserverRunningState } from '@cratis/chronicle.contracts';
7
+ import { afterAll, beforeAll, chai, describe, it } from 'vitest';
8
+ import { ChronicleClient } from '../../../ChronicleClient.js';
9
+ import { ChronicleOptions } from '../../../ChronicleOptions.js';
10
+ import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
11
+ import type { ChronicleConnection } from '../../../connection/index.js';
12
+ import { eventType } from '../../../events/index.js';
13
+ import type { IEventStore } from '../../../IEventStore.js';
14
+ import { childrenFrom, fromAll, fromAllEvents, fromEvery, fromEvent } from '../../index.js';
15
+
16
+ chai.should();
17
+
18
+ const connectionString = process.env.CHRONICLE_INTEGRATION_CONNECTION_STRING;
19
+
20
+ @eventType()
21
+ class Started {
22
+ @field(String) value = '';
23
+ }
24
+
25
+ @eventType()
26
+ class ChildAdded {
27
+ @field(String) childId = '';
28
+ @field(String) value = '';
29
+ }
30
+
31
+ @eventType()
32
+ class Unrelated {
33
+ @field(String) marker = '';
34
+ }
35
+
36
+ class Child {
37
+ @field(String) id = '';
38
+ @field(String) lastValue = '';
39
+ }
40
+
41
+ class AllOnly {
42
+ @field(String) @fromAllEvents(undefined, 'eventSourceId') source = '';
43
+ }
44
+
45
+ @fromEvent(Started)
46
+ class AllWithChildren {
47
+ @field(String) @fromEvery('ignored') @fromAllEvents('value') lastValue = '';
48
+ @field(Array, { genericArguments: [Child] }) @childrenFrom(ChildAdded, 'childId') children: Child[] = [];
49
+ }
50
+
51
+ @fromEvent(Started)
52
+ class RestrictedWithChildren {
53
+ @field(String) @fromAll('value') lastValue = '';
54
+ @field(Array, { genericArguments: [Child] }) @childrenFrom(ChildAdded, 'childId') children: Child[] = [];
55
+ }
56
+
57
+ class AllMappedChild {
58
+ @field(String) id = '';
59
+ @field(String) @fromAllEvents('value') lastValue = '';
60
+ }
61
+
62
+ class AllWithChildMappings {
63
+ @field(String) @fromAllEvents(undefined, 'eventSourceId') source = '';
64
+ @field(String) lastValue = '';
65
+ @field(Array, { genericArguments: [AllMappedChild] }) @childrenFrom(ChildAdded, 'childId') children: AllMappedChild[] = [];
66
+ }
67
+
68
+ // Without root all-event subscription, the shared All block needs a root From entry
69
+ // for ChildAdded as well as the child's creating entry.
70
+ @fromEvent(Started)
71
+ @fromEvent(ChildAdded)
72
+ class ChildOnlyMappings {
73
+ @field(String) @fromEvery('value') rootValue = '';
74
+ @field(Array, { genericArguments: [AllMappedChild] }) @childrenFrom(ChildAdded, 'childId') children: AllMappedChild[] = [];
75
+ }
76
+
77
+ const artifacts: IClientArtifactsProvider = {
78
+ eventTypes: [Started, ChildAdded, Unrelated],
79
+ readModels: [AllOnly, AllWithChildren, RestrictedWithChildren, AllWithChildMappings, ChildOnlyMappings],
80
+ projections: [], reactors: [], reducers: [], seeders: [], constraints: [], webhooks: [],
81
+ eventTypeMigrations: [], globalForHandlers: []
82
+ };
83
+
84
+ async function eventually<T>(read: () => Promise<T>, accept: (value: T) => boolean): Promise<T> {
85
+ const deadline = Date.now() + 20_000;
86
+ let value = await read();
87
+ while (!accept(value)) {
88
+ if (Date.now() > deadline) {
89
+ const state = JSON.stringify(value, (_key, member: unknown) => typeof member === 'bigint' ? member.toString() : member);
90
+ throw new Error(`Timed out waiting for the projected state: ${state}`);
91
+ }
92
+ await new Promise(resolve => setTimeout(resolve, 100));
93
+ value = await read();
94
+ }
95
+ return value;
96
+ }
97
+
98
+ describe.skipIf(!connectionString && !process.env.CI)('when opting into fromAllEvents against a kernel', () => {
99
+ const knownSource = randomUUID();
100
+ const unrelatedSource = randomUUID();
101
+ const sentinelSource = randomUUID();
102
+ const storeName = `AllEvents${randomUUID().replaceAll('-', '').slice(0, 12)}`;
103
+ let client: ChronicleClient;
104
+ let store: IEventStore;
105
+ let allOnly: AllOnly | null;
106
+ let unrelatedAll: AllWithChildren | null;
107
+ let unrelatedRestricted: RestrictedWithChildren | null;
108
+ let allAfterStarted: AllWithChildren | null;
109
+ let allAfterChild: AllWithChildren | null;
110
+ let restrictedAfterChild: RestrictedWithChildren | null;
111
+ let allAfterUnrelated: AllWithChildren | null;
112
+ let restrictedAfterUnrelated: RestrictedWithChildren | null;
113
+ let allChildMappingsAfterChild: AllWithChildMappings | null;
114
+ let childOnlyAfterChild: ChildOnlyMappings | null;
115
+ let childOnlyAfterUnrelated: ChildOnlyMappings | null;
116
+ let unrelatedChildOnly: ChildOnlyMappings | null;
117
+
118
+ async function append(source: string, event: object): Promise<bigint> {
119
+ const result = await store.eventLog.append(source, event);
120
+ result.isSuccess.should.be.true;
121
+ return result.sequenceNumber.value;
122
+ }
123
+
124
+ async function waitForObserver(observerId: string, sequenceNumber: bigint): Promise<void> {
125
+ const connection = (store as unknown as { _connection: ChronicleConnection })._connection;
126
+ await eventually(() => connection.observers.getObserverInformation({
127
+ EventStore: storeName,
128
+ Namespace: 'Default',
129
+ ObserverId: observerId,
130
+ EventSequenceId: 'event-log'
131
+ }), observer => observer.IsSubscribed && observer.RunningState === ObserverRunningState.Active &&
132
+ observer.LastHandledEventSequenceNumber >= sequenceNumber);
133
+ }
134
+
135
+ beforeAll(async () => {
136
+ client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, {
137
+ discoveryPatterns: [], clientArtifactsProvider: artifacts
138
+ }));
139
+ store = await client.getEventStore(storeName);
140
+
141
+ await append(knownSource, Object.assign(new Started(), { value: 'started' }));
142
+ allAfterStarted = await eventually(() => store.readModels.findInstanceById(AllWithChildren, knownSource), model => model?.lastValue === 'started');
143
+ await eventually(() => store.readModels.findInstanceById(RestrictedWithChildren, knownSource), model => model?.lastValue === 'started');
144
+ await append(knownSource, Object.assign(new ChildAdded(), { childId: 'child-one', value: 'child-value' }));
145
+ allAfterChild = await eventually(() => store.readModels.findInstanceById(AllWithChildren, knownSource), model => model?.children?.length === 1);
146
+ restrictedAfterChild = await eventually(() => store.readModels.findInstanceById(RestrictedWithChildren, knownSource), model => model?.children?.length === 1);
147
+ childOnlyAfterChild = await eventually(() => store.readModels.findInstanceById(ChildOnlyMappings, knownSource), model => model?.children?.length === 1);
148
+ allChildMappingsAfterChild = await eventually(() => store.readModels.findInstanceById(AllWithChildMappings, knownSource), model => model?.children?.length === 1);
149
+
150
+ await append(knownSource, Object.assign(new Unrelated(), { marker: 'no-value' }));
151
+ allAfterUnrelated = await eventually(() => store.readModels.findInstanceById(AllWithChildren, knownSource),
152
+ model => model !== null && (model.lastValue === null || model.lastValue === undefined));
153
+
154
+ await append(unrelatedSource, Object.assign(new Unrelated(), { marker: 'new-source' }));
155
+ allOnly = await eventually(() => store.readModels.findInstanceById(AllOnly, unrelatedSource), model => model !== null);
156
+ unrelatedAll = await eventually(() => store.readModels.findInstanceById(AllWithChildren, unrelatedSource), model => model !== null);
157
+ // A subscribed sentinel advances each restricted observer past both unrelated events.
158
+ // Progress on an all-event projection is not a barrier for another observer.
159
+ const sentinelSequenceNumber = await append(sentinelSource, Object.assign(new Started(), { value: 'sentinel' }));
160
+ await waitForObserver('RestrictedWithChildren', sentinelSequenceNumber);
161
+ await waitForObserver('ChildOnlyMappings', sentinelSequenceNumber);
162
+ restrictedAfterUnrelated = await store.readModels.findInstanceById(RestrictedWithChildren, knownSource);
163
+ unrelatedRestricted = await store.readModels.findInstanceById(RestrictedWithChildren, unrelatedSource);
164
+ childOnlyAfterUnrelated = await store.readModels.findInstanceById(ChildOnlyMappings, knownSource);
165
+ unrelatedChildOnly = await store.readModels.findInstanceById(ChildOnlyMappings, unrelatedSource);
166
+ });
167
+
168
+ afterAll(() => client?.dispose());
169
+
170
+ it('should subscribe a model with only fromAllEvents and map event context', () => allOnly!.source.should.equal(unrelatedSource));
171
+ it('should create a row for an unrelated event source only after opting in', () => {
172
+ (unrelatedAll !== null).should.be.true;
173
+ (unrelatedRestricted === null).should.be.true;
174
+ });
175
+ it('should give the explicit all-event mapping precedence over fromEvery', () => allAfterStarted!.lastValue.should.equal('started'));
176
+ it('should apply the root all-event mapping to a child', () => {
177
+ allAfterChild!.children.should.have.lengthOf(1);
178
+ allAfterChild!.children[0].id.should.equal('child-one');
179
+ allAfterChild!.children[0].lastValue.should.equal('child-value');
180
+ });
181
+ it('should not propagate the deprecated restricted alias mapping to children', () => {
182
+ restrictedAfterChild!.children.should.have.lengthOf(1);
183
+ (restrictedAfterChild!.children[0].lastValue !== 'child-value').should.be.true;
184
+ });
185
+ it('should clear the mapped root property on an unrelated event without that value', () => {
186
+ (allAfterUnrelated!.lastValue === null || allAfterUnrelated!.lastValue === undefined).should.be.true;
187
+ });
188
+ it('should leave the restricted alias unchanged on an unrelated event', () => restrictedAfterUnrelated!.lastValue.should.equal(restrictedAfterChild!.lastValue));
189
+ it('should apply a child-declared all-event mapping through the shared root All block', () => {
190
+ allChildMappingsAfterChild!.children[0].lastValue.should.equal('child-value');
191
+ // The child event's key routes the all-event fallback to the child, not the root.
192
+ allChildMappingsAfterChild!.lastValue.should.equal('started');
193
+ });
194
+ it('should keep the root subscription restricted when only the child declares fromAllEvents', () => {
195
+ childOnlyAfterChild!.rootValue.should.equal('child-value');
196
+ childOnlyAfterChild!.children[0].id.should.equal('child-one');
197
+ // The explicitly subscribed root From entry maps the root, not the child's shared values.
198
+ (childOnlyAfterChild!.children[0].lastValue === undefined).should.be.true;
199
+ childOnlyAfterUnrelated!.rootValue.should.equal('child-value');
200
+ (unrelatedChildOnly === null).should.be.true;
201
+ });
202
+ });
@@ -0,0 +1,235 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import 'reflect-metadata';
5
+ import { field } from '@cratis/fundamentals';
6
+ import { beforeEach, chai, describe, it } from 'vitest';
7
+ import { fromAllEvents, getFromAllEventsMetadata } from '../../index.js';
8
+ import { eventType } from '../../events/eventTypeDecorator.js';
9
+ import { childrenFrom } from '../modelBound/childrenFrom.js';
10
+ import { fromAll, getFromAllMetadata } from '../modelBound/fromAll.js';
11
+ import { fromEvery, getFromEveryMetadata } from '../modelBound/fromEvery.js';
12
+ import { fromEvent } from '../modelBound/fromEvent.js';
13
+ import { isModelBoundProjection } from '../modelBound/isModelBoundProjection.js';
14
+ import { nested } from '../modelBound/nested.js';
15
+ import { TypeIntrospector } from '../../types/index.js';
16
+ import { ReadModelScenario } from '../../testing/ReadModelScenario.js';
17
+ import { UnsupportedProjectionOperation } from '../../testing/projections/UnsupportedProjectionOperation.js';
18
+ import { compileModelBound } from './given/compile_model_bound.js';
19
+
20
+ chai.should();
21
+
22
+ class Model {
23
+ id = '';
24
+ title = '';
25
+ source = '';
26
+ sequence = '';
27
+ }
28
+ for (const property of ['id', 'title', 'source', 'sequence']) field(String)(Model.prototype, property);
29
+
30
+ function discoveredByExistingReaders(type: Function): boolean {
31
+ return TypeIntrospector.getTrackedProperties(type).some(property =>
32
+ !!getFromEveryMetadata(type.prototype, property) || !!getFromAllMetadata(type.prototype, property));
33
+ }
34
+
35
+ describe('when compiling legacy fromAllEvents without explicit event subscriptions', () => {
36
+ let result: ReturnType<typeof compileModelBound>;
37
+ beforeEach(() => {
38
+ class Candidate extends Model {}
39
+ fromAllEvents()(Candidate.prototype, 'title');
40
+ fromAllEvents(undefined, 'sequenceNumber')(Candidate.prototype, 'sequence');
41
+ fromEvery(undefined, 'eventSourceId')(Candidate.prototype, 'source');
42
+ result = compileModelBound(Candidate);
43
+ });
44
+ it('should subscribe to every event without expanding a known event list', () => {
45
+ result.definition.SubscribesToAllEvents.should.be.true;
46
+ result.definition.From.should.deep.equal([]);
47
+ result.definition.FromEvery.should.deep.equal([]);
48
+ });
49
+ it('should merge content and context mappings into All including children', () => {
50
+ result.definition.All!.should.deep.equal({ Properties: {
51
+ title: 'title', sequence: '$eventContext(SequenceNumber)', source: '$eventContext(EventSourceId)'
52
+ }, IncludeChildren: true, AutoMap: 0 });
53
+ });
54
+ it('should preserve the distinct decorator in subscription and mapping diagnostics', () => {
55
+ result.compiled.provenance.get(result.definition)!.should.include.deep.members([
56
+ { contractPath: 'SubscribesToAllEvents', declaration: '@fromAllEvents' },
57
+ { contractPath: 'All', declaration: '@fromAllEvents' },
58
+ { contractPath: 'All.Properties.title', declaration: '@fromAllEvents' },
59
+ { contractPath: 'All.Properties.source', declaration: '@fromEvery' }
60
+ ]);
61
+ });
62
+ });
63
+
64
+ describe('when using legacy fromAllEvents in a read-model scenario', () => {
65
+ it('should reject the subscription before events can be seeded', () => {
66
+ class Candidate extends Model {}
67
+ fromAllEvents('title')(Candidate.prototype, 'title');
68
+ (() => new ReadModelScenario(Candidate, { readModels: [Candidate], eventTypes: [], projections: [], reducers: [] }))
69
+ .should.throw(UnsupportedProjectionOperation, 'SubscribesToAllEvents (@fromAllEvents)');
70
+ });
71
+ });
72
+
73
+ describe('when discovering and inheriting legacy fromAllEvents metadata', () => {
74
+ class AllModel extends Model {}
75
+ fromAllEvents('name')(AllModel.prototype, 'title');
76
+ class Derived extends AllModel {}
77
+ class Sibling extends Model {}
78
+ fromAll('restrictedName')(Sibling.prototype, 'title');
79
+
80
+ it('should discover a model using only fromAllEvents with Arc style existing readers', () => {
81
+ discoveredByExistingReaders(AllModel).should.be.true;
82
+ isModelBoundProjection(AllModel).should.be.true;
83
+ getFromAllMetadata(AllModel.prototype, 'title')!.should.deep.equal({ property: 'name', contextProperty: undefined });
84
+ (getFromEveryMetadata(AllModel.prototype, 'title') === undefined).should.be.true;
85
+ });
86
+ it('should inherit discovery and the subscription without affecting a sibling', () => {
87
+ discoveredByExistingReaders(Derived).should.be.true;
88
+ compileModelBound(Derived).definition.SubscribesToAllEvents.should.be.true;
89
+ compileModelBound(Derived).definition.All!.should.deep.equal(compileModelBound(AllModel).definition.All);
90
+ (compileModelBound(Sibling).definition.SubscribesToAllEvents === undefined).should.be.true;
91
+ (getFromAllEventsMetadata(Sibling.prototype, 'title') === undefined).should.be.true;
92
+ });
93
+ it('should keep derived annotations independent from the base', () => {
94
+ class Override extends AllModel {}
95
+ fromAllEvents('derivedName')(Override.prototype, 'title');
96
+ compileModelBound(Override).definition.All!.Properties.title.should.equal('derivedName');
97
+ compileModelBound(AllModel).definition.All!.Properties.title.should.equal('name');
98
+ });
99
+ });
100
+
101
+ describe('when compiling fromAllEvents on a child model', () => {
102
+ class Child { id = ''; title = ''; }
103
+ field(String)(Child.prototype, 'id');
104
+ field(String)(Child.prototype, 'title');
105
+ fromAllEvents('name')(Child.prototype, 'title');
106
+ class Added { id = ''; name = ''; }
107
+ eventType('from-all-events-child-added')(Added);
108
+ field(String)(Added.prototype, 'id');
109
+ field(String)(Added.prototype, 'name');
110
+ class Parent extends Model { items: Child[] = []; }
111
+ field(Array, { enumerable: true, genericArguments: [Child] })(Parent.prototype, 'items');
112
+ childrenFrom(Added, 'id')(Parent.prototype, 'items');
113
+
114
+ it('should not widen the root subscription from child metadata', () => {
115
+ const { definition } = compileModelBound(Parent);
116
+ (definition.SubscribesToAllEvents === undefined).should.be.true;
117
+ ('SubscribesToAllEvents' in definition.Children.items).should.be.false;
118
+ });
119
+ it('should collect the child mapping by bare name in the root All without subscribing to all events', () => {
120
+ const { compiled, definition } = compileModelBound(Parent);
121
+ definition.All!.should.deep.equal({ Properties: { title: 'name' }, IncludeChildren: true, AutoMap: 0 });
122
+ definition.Children.items.All!.Properties.should.deep.equal({});
123
+ compiled.provenance.get(definition)!.should.include.deep.members([
124
+ { contractPath: 'All', declaration: '@fromAllEvents' },
125
+ { contractPath: 'All.Properties.title', declaration: '@fromAllEvents' }
126
+ ]);
127
+ });
128
+ it('should emit the subscription only at the root when the parent opts in', () => {
129
+ class AllParent extends Parent {}
130
+ fromAllEvents(undefined, 'eventSourceId')(AllParent.prototype, 'source');
131
+ const { definition } = compileModelBound(AllParent);
132
+ definition.SubscribesToAllEvents.should.be.true;
133
+ definition.All!.should.deep.equal({
134
+ Properties: { source: '$eventContext(EventSourceId)', title: 'name' }, IncludeChildren: true, AutoMap: 0
135
+ });
136
+ ('SubscribesToAllEvents' in definition.Children.items).should.be.false;
137
+ });
138
+ it('should share one bare-name mapping when a later child member shadows a root member', () => {
139
+ class AllParent extends Model { items: Child[] = []; }
140
+ fromAllEvents('rootName')(AllParent.prototype, 'title');
141
+ field(Array, { genericArguments: [Child] })(AllParent.prototype, 'items');
142
+ childrenFrom(Added, 'id')(AllParent.prototype, 'items');
143
+ compileModelBound(AllParent).definition.All!.Properties.should.deep.equal({ title: 'name' });
144
+ });
145
+ });
146
+
147
+ describe('when compiling fromAllEvents on nested and inherited child models', () => {
148
+ class Detail { label = ''; sequence = ''; }
149
+ field(String)(Detail.prototype, 'label');
150
+ field(String)(Detail.prototype, 'sequence');
151
+ fromAllEvents()(Detail.prototype, 'label');
152
+ fromAllEvents(undefined, 'sequenceNumber')(Detail.prototype, 'sequence');
153
+ class Added { id = ''; }
154
+ eventType('from-all-events-nested-child-added')(Added);
155
+ field(String)(Added.prototype, 'id');
156
+ class Child { id = ''; title = ''; ignored = ''; detail = new Detail(); }
157
+ field(String)(Child.prototype, 'id');
158
+ field(String)(Child.prototype, 'title');
159
+ field(String)(Child.prototype, 'ignored');
160
+ field(Detail)(Child.prototype, 'detail');
161
+ fromAllEvents('name')(Child.prototype, 'title');
162
+ fromEvery('ignoredEvery')(Child.prototype, 'ignored');
163
+ fromAll('ignoredAlias')(Child.prototype, 'ignored');
164
+ nested(Child.prototype, 'detail');
165
+ class InheritedChild extends Child {}
166
+ class Group { children: InheritedChild[] = []; }
167
+ field(Array, { genericArguments: [InheritedChild] })(Group.prototype, 'children');
168
+ childrenFrom(Added, 'id')(Group.prototype, 'children');
169
+ class Parent { groups: Group[] = []; detail = new Detail(); }
170
+ field(Array, { genericArguments: [Group] })(Parent.prototype, 'groups');
171
+ childrenFrom(Added, 'id')(Parent.prototype, 'groups');
172
+ field(Detail)(Parent.prototype, 'detail');
173
+ nested(Parent.prototype, 'detail');
174
+
175
+ it('should collect grandchild and nested mappings into the root without path prefixes or legacy child mappings', () => {
176
+ const { definition } = compileModelBound(Parent);
177
+ definition.All!.should.deep.equal({
178
+ Properties: { title: 'name', label: 'label', sequence: '$eventContext(SequenceNumber)' },
179
+ IncludeChildren: true, AutoMap: 0
180
+ });
181
+ (definition.SubscribesToAllEvents === undefined).should.be.true;
182
+ definition.Children.groups.Children.children.All!.Properties.should.deep.equal({});
183
+ definition.Children.groups.Children.children.Nested.detail.All!.Properties.should.deep.equal({});
184
+ definition.Nested.detail.All!.Properties.should.deep.equal({});
185
+ });
186
+ it('should collect a nested-only mapping without widening the root subscription', () => {
187
+ class NestedOnly { detail = new Detail(); }
188
+ fromEvent(Added)(NestedOnly);
189
+ field(Detail)(NestedOnly.prototype, 'detail');
190
+ nested(NestedOnly.prototype, 'detail');
191
+ const { definition } = compileModelBound(NestedOnly);
192
+ definition.All!.should.deep.equal({
193
+ Properties: { label: 'label', sequence: '$eventContext(SequenceNumber)' }, IncludeChildren: true, AutoMap: 0
194
+ });
195
+ (definition.SubscribesToAllEvents === undefined).should.be.true;
196
+ ('SubscribesToAllEvents' in definition.Nested.detail).should.be.false;
197
+ });
198
+ });
199
+
200
+ describe('when opting into fromAllEvents alongside inherited restricted mappings', () => {
201
+ it('should promote inherited fromEvery and fromAll mappings into the same shared All block', () => {
202
+ class Restricted extends Model {}
203
+ fromEvery(undefined, 'eventSourceId')(Restricted.prototype, 'source');
204
+ fromAll(undefined, 'sequenceNumber')(Restricted.prototype, 'sequence');
205
+ class AllModel extends Restricted {}
206
+ fromAllEvents('name')(AllModel.prototype, 'title');
207
+ const { definition } = compileModelBound(AllModel);
208
+ definition.SubscribesToAllEvents.should.be.true;
209
+ definition.All!.should.deep.equal({
210
+ Properties: { title: 'name', source: '$eventContext(EventSourceId)', sequence: '$eventContext(SequenceNumber)' },
211
+ IncludeChildren: true, AutoMap: 0
212
+ });
213
+ compileModelBound(Restricted).definition.All!.IncludeChildren.should.be.false;
214
+ });
215
+ });
216
+
217
+ for (const allEventsFirst of [true, false]) {
218
+ describe(`when all shared mapping decorators target one legacy property with fromAllEvents ${allEventsFirst ? 'first' : 'last'}`, () => {
219
+ it('should prefer the explicit subscription without overwriting compatibility metadata', () => {
220
+ class Candidate extends Model {}
221
+ if (allEventsFirst) fromAllEvents('newName')(Candidate.prototype, 'title');
222
+ fromEvery('everyName')(Candidate.prototype, 'title');
223
+ fromAll('aliasName')(Candidate.prototype, 'title');
224
+ if (!allEventsFirst) fromAllEvents('newName')(Candidate.prototype, 'title');
225
+ const { compiled, definition } = compileModelBound(Candidate);
226
+ definition.SubscribesToAllEvents.should.be.true;
227
+ definition.All!.Properties.title.should.equal('newName');
228
+ getFromAllMetadata(Candidate.prototype, 'title')!.property!.should.equal('aliasName');
229
+ getFromEveryMetadata(Candidate.prototype, 'title')!.property!.should.equal('everyName');
230
+ compiled.provenance.get(definition)!.should.include.deep.members([
231
+ { contractPath: 'All.Properties.title', declaration: '@fromAllEvents' }
232
+ ]);
233
+ });
234
+ });
235
+ }
@@ -0,0 +1,107 @@
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 { field } from '@cratis/fundamentals';
5
+ import { chai, describe, it, type Assertion } from 'vitest';
6
+ import { fromAllEvents, getFromAllEventsMetadata, fromAll, getFromAllMetadata, fromEvery, getFromEveryMetadata, childrenFrom, nested } from '../index.js';
7
+ import { eventType } from '../../events/eventTypeDecorator.js';
8
+ import { TypeIntrospector } from '../../types/index.js';
9
+ import { ReadModelScenario } from '../../testing/ReadModelScenario.js';
10
+ import { UnsupportedProjectionOperation } from '../../testing/projections/UnsupportedProjectionOperation.js';
11
+ import { compileModelBound } from './given/compile_model_bound.js';
12
+
13
+ chai.should();
14
+ function should(value: unknown): Assertion { return (value as { should: Assertion }).should; }
15
+
16
+ class AllModel {
17
+ @field(String) id = '';
18
+ @field(String) @fromAllEvents('name') title = '';
19
+ @field(String) @fromAllEvents(undefined, 'sequenceNumber') sequence = '';
20
+ @field(String) @fromEvery(undefined, 'eventSourceId') source = '';
21
+ constructor() { throw new Error('Model discovery must not construct the model'); }
22
+ }
23
+
24
+ describe('when compiling standard fromAllEvents without explicit event subscriptions', () => {
25
+ it('should emit the subscription and shared mappings without constructing the model', () => {
26
+ const { definition } = compileModelBound(AllModel);
27
+ should(definition.SubscribesToAllEvents).be.true;
28
+ should(definition.From).deep.equal([]);
29
+ should(definition.FromEvery).deep.equal([]);
30
+ should(definition.All).deep.equal({ Properties: {
31
+ title: 'name', sequence: '$eventContext(SequenceNumber)', source: '$eventContext(EventSourceId)'
32
+ }, IncludeChildren: true, AutoMap: 0 });
33
+ });
34
+
35
+ it('should discover a model using only fromAllEvents through existing Arc style readers', () => {
36
+ class OnlyAll {
37
+ @field(String) @fromAllEvents() title = '';
38
+ constructor() { throw new Error('Discovery must not construct the model'); }
39
+ }
40
+ class Derived extends OnlyAll {}
41
+ for (const type of [OnlyAll, Derived]) {
42
+ should(TypeIntrospector.getTrackedProperties(type).some(property =>
43
+ !!getFromEveryMetadata(type.prototype, property) || !!getFromAllMetadata(type.prototype, property))).be.true;
44
+ should(getFromAllMetadata(type.prototype, 'title')).deep.equal({ property: undefined, contextProperty: undefined });
45
+ should(getFromEveryMetadata(type.prototype, 'title') === undefined).be.true;
46
+ should(compileModelBound(type).definition.SubscribesToAllEvents).be.true;
47
+ }
48
+ });
49
+
50
+ it('should inherit and override mappings without leaking the subscription to a sibling', () => {
51
+ class Base { @field(String) id = ''; }
52
+ class All extends Base { @field(String) @fromAllEvents('name') title = ''; }
53
+ class Derived extends All {}
54
+ class Override extends All { @field(String) @fromAllEvents('derivedName') override title = ''; }
55
+ class Alias extends Base { @field(String) @fromAll('name') title = ''; }
56
+ should(compileModelBound(Derived).definition.SubscribesToAllEvents).be.true;
57
+ should(compileModelBound(Derived).definition.All).deep.equal(compileModelBound(All).definition.All);
58
+ should(compileModelBound(Override).definition.All!.Properties.title).equal('derivedName');
59
+ should(compileModelBound(All).definition.All!.Properties.title).equal('name');
60
+ should(compileModelBound(Alias).definition.SubscribesToAllEvents === undefined).be.true;
61
+ should(getFromAllEventsMetadata(Alias.prototype, 'title') === undefined).be.true;
62
+ });
63
+
64
+ it('should collect standard child and nested mappings in the root All without widening the subscription', () => {
65
+ @eventType('standard-from-all-events-child-added')
66
+ class Added { @field(String) id = ''; }
67
+ class Detail { @field(String) @fromAllEvents(undefined, 'sequenceNumber') sequence = ''; }
68
+ class Child {
69
+ @field(String) id = '';
70
+ @field(String) @fromAllEvents('name') title = '';
71
+ @field(Detail) @nested detail = new Detail();
72
+ }
73
+ class Derived extends Child {}
74
+ class Parent {
75
+ @field(Array, { genericArguments: [Derived] }) @childrenFrom(Added, 'id') items: Derived[] = [];
76
+ }
77
+ const { definition } = compileModelBound(Parent);
78
+ should(definition.All).deep.equal({
79
+ Properties: { title: 'name', sequence: '$eventContext(SequenceNumber)' }, IncludeChildren: true, AutoMap: 0
80
+ });
81
+ should(definition.SubscribesToAllEvents === undefined).be.true;
82
+ should(definition.Children.items.All!.Properties).deep.equal({});
83
+ should(definition.Children.items.Nested.detail.All!.Properties).deep.equal({});
84
+ });
85
+
86
+ it('should reject all-event scenarios before replay with the standard decorator declaration', () => {
87
+ should(() => new ReadModelScenario(AllModel, { readModels: [AllModel], eventTypes: [], projections: [], reducers: [] }))
88
+ .throw(UnsupportedProjectionOperation, 'SubscribesToAllEvents (@fromAllEvents)');
89
+ });
90
+
91
+ it('should prefer fromAllEvents in either decorator order without changing the alias metadata', () => {
92
+ class Both {
93
+ @field(String) id = '';
94
+ @field(String) @fromEvery('everyName') @fromAll('aliasName') @fromAllEvents('newName') title = '';
95
+ @field(String) @fromAllEvents('otherName') @fromAll('aliasName') @fromEvery('everyName') other = '';
96
+ }
97
+ const { compiled, definition } = compileModelBound(Both);
98
+ should(definition.SubscribesToAllEvents).be.true;
99
+ should(definition.All!.Properties).deep.equal({ title: 'newName', other: 'otherName' });
100
+ should(getFromAllMetadata(Both.prototype, 'title')!.property).equal('aliasName');
101
+ should(getFromEveryMetadata(Both.prototype, 'title')!.property).equal('everyName');
102
+ should(compiled.provenance.get(definition)).include.deep.members([
103
+ { contractPath: 'SubscribesToAllEvents', declaration: '@fromAllEvents' },
104
+ { contractPath: 'All.Properties.title', declaration: '@fromAllEvents' }
105
+ ]);
106
+ });
107
+ });
@@ -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
+ /** Metadata stored by the fromAllEvents property decorator. */
5
+ export interface FromAllEventsMetadata {
6
+ /** The event property name to read the value from. */
7
+ readonly property?: string;
8
+ /** The event context property name to read the value from. */
9
+ readonly contextProperty?: string;
10
+ }