@cratis/chronicle 6.29.0 → 6.31.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 (33) hide show
  1. package/compliance/for_PIIManager/against_a_kernel/when_erasing_a_subject.integration.spec.ts +132 -0
  2. package/dist/testing/ReactorScenario.d.ts +3 -0
  3. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  4. package/dist/testing/ReactorScenario.js +2 -0
  5. package/dist/testing/ReactorScenario.js.map +1 -1
  6. package/dist/testing/ReadModelScenario.d.ts +11 -0
  7. package/dist/testing/ReadModelScenario.d.ts.map +1 -1
  8. package/dist/testing/ReadModelScenario.js +37 -8
  9. package/dist/testing/ReadModelScenario.js.map +1 -1
  10. package/dist/testing/projections/ProjectionCapabilities.d.ts.map +1 -1
  11. package/dist/testing/projections/ProjectionCapabilities.js +8 -2
  12. package/dist/testing/projections/ProjectionCapabilities.js.map +1 -1
  13. package/dist/testing/projections/ProjectionChildrenCapabilities.d.ts +33 -0
  14. package/dist/testing/projections/ProjectionChildrenCapabilities.d.ts.map +1 -0
  15. package/dist/testing/projections/ProjectionChildrenCapabilities.js +108 -0
  16. package/dist/testing/projections/ProjectionChildrenCapabilities.js.map +1 -0
  17. package/dist/testing/projections/ProjectionReadModelProcessor.d.ts +6 -0
  18. package/dist/testing/projections/ProjectionReadModelProcessor.d.ts.map +1 -1
  19. package/dist/testing/projections/ProjectionReadModelProcessor.js +94 -36
  20. package/dist/testing/projections/ProjectionReadModelProcessor.js.map +1 -1
  21. package/dist/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +2 -1
  23. package/testing/ReactorScenario.ts +2 -0
  24. package/testing/ReadModelScenario.childrenFrom.spec.ts +31 -5
  25. package/testing/ReadModelScenario.composition.spec.ts +111 -0
  26. package/testing/ReadModelScenario.ts +41 -7
  27. package/testing/projections/ProjectionCapabilities.ts +10 -3
  28. package/testing/projections/ProjectionChildrenCapabilities.ts +119 -0
  29. package/testing/projections/ProjectionReadModelProcessor.ts +105 -36
  30. package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_children_operations.spec.ts +73 -0
  31. package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +0 -3
  32. package/vitest.config.ts +2 -1
  33. package/vitest.integration.config.ts +19 -0
@@ -6,6 +6,7 @@ import type { Constructor } from '@cratis/fundamentals';
6
6
  import type { CompiledProjectionDefinitions } from '../../projections/CompiledProjectionDefinitions.js';
7
7
  import { eventContractPath } from '../../projections/eventContractPath.js';
8
8
  import type { FromRecord, RemovedWithRecord } from '../../projections/declarative/ProjectionBuilderCore.js';
9
+ import type { ChildrenDefinitionLike } from '../../projections/modelBound/childrenAndNestedBuilder.js';
9
10
  import { deserializeReadModel } from '../../readModels/deserializeReadModel.js';
10
11
  import type { JsonSchema } from '../../schemas/JsonSchema.js';
11
12
  import type { IReadModelProcessor } from '../IReadModelProcessor.js';
@@ -16,12 +17,21 @@ import { ProjectionExpressionEvaluator } from './ProjectionExpressionEvaluator.j
16
17
  import { ProjectionValueConverter } from './ProjectionValueConverter.js';
17
18
  import { UnsupportedProjectionOperation } from './UnsupportedProjectionOperation.js';
18
19
 
20
+ interface ChildBinding {
21
+ readonly property: string;
22
+ readonly identifiedBy: string;
23
+ readonly key: string;
24
+ readonly from: FromRecord | undefined;
25
+ readonly items: JsonSchema;
26
+ }
27
+
19
28
  /** In-process interpreter of the validated, registered projection contract subset. */
20
29
  export class ProjectionReadModelProcessor<TReadModel extends object> implements IReadModelProcessor<TReadModel> {
21
30
  private readonly _schema: JsonSchema;
22
31
  private readonly _initial: Record<string, unknown>;
23
32
  private readonly _from: ReadonlyMap<string, FromRecord>;
24
33
  private readonly _removed: ReadonlySet<string>;
34
+ private readonly _children: ReadonlyMap<string, ChildBinding>;
25
35
  private readonly _eventSchemas: ReadonlyMap<string, JsonSchema>;
26
36
  private readonly _bindings: ReadonlyMap<string, { generation: number; path: string; declaration: string }>;
27
37
  private _engineState: Record<string, Record<string, unknown>> = {};
@@ -32,15 +42,29 @@ export class ProjectionReadModelProcessor<TReadModel extends object> implements
32
42
  const registration = compiled.readModels.find(entry => entry.Type.Identifier === _definition.ReadModel)!;
33
43
  this._schema = JSON.parse(registration.Schema) as JsonSchema;
34
44
  this._initial = JSON.parse(_definition.InitialModelState || '{}') as Record<string, unknown>;
35
- const wire = _definition as unknown as { From: FromRecord[]; RemovedWith: RemovedWithRecord[] };
45
+ const wire = _definition as unknown as { From: FromRecord[]; RemovedWith: RemovedWithRecord[]; Children?: Record<string, ChildrenDefinitionLike> };
36
46
  this._from = new Map((wire.From ?? []).map(entry => [entry.Key.Id, entry]));
37
47
  this._removed = new Set((wire.RemovedWith ?? []).map(entry => entry.Key.Id));
38
- this._bindings = new Map([...(wire.From ?? []), ...(wire.RemovedWith ?? [])].map(entry => {
48
+ const declarationFor = (path: string) => compiled.provenance.get(_definition)?.find(provenance => provenance.contractPath === path)?.declaration;
49
+ const bindings = [...(wire.From ?? []), ...(wire.RemovedWith ?? [])].map(entry => {
39
50
  const section = (wire.From ?? []).includes(entry as FromRecord) ? 'From' : 'RemovedWith';
40
51
  const path = eventContractPath(section, entry.Key);
41
- const declaration = compiled.provenance.get(_definition)?.find(provenance => provenance.contractPath === path)?.declaration ?? 'contract';
42
- return [entry.Key.Id, { generation: entry.Key.Generation, path, declaration }];
43
- }));
52
+ return [entry.Key.Id, { generation: entry.Key.Generation, path, declaration: declarationFor(path) ?? 'contract' }] as const;
53
+ });
54
+ const children = new Map<string, ChildBinding>();
55
+ for (const [property, child] of Object.entries(wire.Children ?? {})) {
56
+ const items = this._schema.properties![property].items!;
57
+ for (const entry of [...child.From, ...child.RemovedWith]) {
58
+ const from = child.From.includes(entry as FromRecord) ? entry as FromRecord : undefined;
59
+ const path = `Children.${property}.${eventContractPath(from ? 'From' : 'RemovedWith', entry.Key)}`;
60
+ children.set(entry.Key.Id, { property, identifiedBy: child.IdentifiedBy, key: entry.Value.Key, from, items });
61
+ if (!bindings.some(([id]) => id === entry.Key.Id)) {
62
+ bindings.push([entry.Key.Id, { generation: entry.Key.Generation, path, declaration: declarationFor(`Children.${property}`) ?? 'contract' }]);
63
+ }
64
+ }
65
+ }
66
+ this._children = children;
67
+ this._bindings = new Map(bindings);
44
68
  this._eventSchemas = new Map([...(compiled.eventSchemas.get(_definition) ?? new Map()).values()]
45
69
  .map(entry => [entry.eventType.Id, entry.schema]));
46
70
  }
@@ -57,41 +81,36 @@ export class ProjectionReadModelProcessor<TReadModel extends object> implements
57
81
  const typeId = event.context.eventType.id.value;
58
82
  const from = this._from.get(typeId);
59
83
  const removed = this._removed.has(typeId);
60
- if (!from && !removed) continue;
84
+ const child = this._children.get(typeId);
85
+ if (!from && !removed && !child) continue;
61
86
  const binding = this._bindings.get(typeId)!;
62
87
  if (event.context.eventType.generation.value !== binding.generation) {
63
88
  throw new UnsupportedProjectionOperation(String(this._definition.ReadModel), binding.path, binding.declaration,
64
89
  `seeded event generation ${event.context.eventType.generation.value} differs from subscribed generation ${binding.generation}; multi-generation history requires a kernel-backed test`);
65
90
  }
66
- // A root removal has the final say when an event is registered for both operations.
67
91
  const key = this.keyFor(event.sourceId);
68
92
  if (removed) {
69
93
  delete engine[key];
70
94
  states.set(event.sourceId, { instance: null, deleted: true });
71
95
  continue;
72
96
  }
73
- if (!from) continue;
74
- const state = engine[key] ?? this.initialState(key, event);
75
- const properties = { ...from.Value.Properties };
76
- const schema = this._eventSchemas.get(typeId);
77
- const content = ProjectionValueConverter.eventContent(event.content, schema!);
78
- if (this._definition.AutoMap !== AutoMap.Disabled && schema) {
79
- const explicit = new Set(Object.keys(properties).map(name => name.toLowerCase()));
80
- const excluded = new Set((this._definition.NoAutoMapProperties ?? []).map(name => name.toLowerCase()));
81
- for (const source of Object.keys(schema.properties ?? {})) {
82
- const destination = Object.keys(this._schema.properties ?? {}).find(name => name.toLowerCase() === source.toLowerCase());
83
- if (destination && !explicit.has(destination.toLowerCase()) && !excluded.has(destination.toLowerCase())) {
84
- properties[destination] = source;
85
- explicit.add(destination.toLowerCase());
86
- }
87
- }
97
+ const schema = this._eventSchemas.get(typeId)!;
98
+ const content = ProjectionValueConverter.eventContent(event.content, schema);
99
+ let state = engine[key];
100
+ if (!state) {
101
+ // SetInitialState: an event only a child subscribes to creates an uninitialized parent.
102
+ state = from ? this.initialState(key, event) : this.identityState(key, event, false);
103
+ } else if (state.__initialized === false) {
104
+ for (const [name, value] of Object.entries(this.initialValues())) if (!(name in state)) state[name] = value;
105
+ state.__initialized = true;
88
106
  }
89
- for (const [destination, expression] of Object.entries(properties)) {
90
- const target = this._schema.properties![destination];
91
- const value = ProjectionExpressionEvaluator.value(expression, content, event.context, target);
92
- // The kernel compares old and new null values; null on an absent member is no change.
93
- if (value !== null || state[destination] !== undefined) state[destination] = value;
107
+ if (from) {
108
+ const explicit = { ...from.Value.Properties };
109
+ const excluded = (this._definition.NoAutoMapProperties ?? []);
110
+ const properties = this._definition.AutoMap !== AutoMap.Disabled ? this.withAutoMap(explicit, schema, this._schema, excluded) : explicit;
111
+ this.apply(properties, content, event, this._schema, state);
94
112
  }
113
+ if (child) this.applyChild(child, content, event, schema, state, binding);
95
114
  // InMemorySink.ApplyChanges always restores lowercase id after applying mappings.
96
115
  state.id = ProjectionValueConverter.convert(key, this._schema.properties!.id);
97
116
  engine[key] = state;
@@ -112,22 +131,72 @@ export class ProjectionReadModelProcessor<TReadModel extends object> implements
112
131
  return canonical;
113
132
  }
114
133
 
115
- private initialState(key: string, event: ScenarioEvent): Record<string, unknown> {
134
+ private applyChild(child: ChildBinding, content: unknown, event: ScenarioEvent, eventSchema: JsonSchema, state: Record<string, unknown>,
135
+ binding: { path: string; declaration: string }): void {
136
+ const identity = ProjectionExpressionEvaluator.pathValue(content, child.key);
137
+ if (typeof identity !== 'string') {
138
+ throw new UnsupportedProjectionOperation(String(this._definition.ReadModel), binding.path, binding.declaration,
139
+ `child key '${child.key}' has no string value in the seeded event; missing child keys require a kernel-backed test`);
140
+ }
141
+ const collection = (state[child.property] ??= []) as Record<string, unknown>[];
142
+ const index = collection.findIndex(item => item[child.identifiedBy] === identity);
143
+ if (!child.from) {
144
+ if (index >= 0) collection.splice(index, 1);
145
+ return;
146
+ }
147
+ const item = index >= 0 ? collection[index] : { [child.identifiedBy]: identity };
148
+ if (index < 0) collection.push(item);
149
+ // Validation admits only the identifier mapping explicitly; other fields come from same-name AutoMap.
150
+ // Kernel AutoMap is schema-driven: an untyped child item schema maps nothing (children-untyped-items).
151
+ const properties = child.items.properties ? this.withAutoMap({}, eventSchema, child.items, [child.identifiedBy]) : {};
152
+ this.apply(properties, content, event, child.items, item);
153
+ }
154
+
155
+ private withAutoMap(explicit: Record<string, string>, eventSchema: JsonSchema, target: JsonSchema, exclusions: readonly string[]): Record<string, string> {
156
+ const properties = { ...explicit };
157
+ const mapped = new Set(Object.keys(properties).map(name => name.toLowerCase()));
158
+ const excluded = new Set(exclusions.map(name => name.toLowerCase()));
159
+ for (const source of Object.keys(eventSchema.properties ?? {})) {
160
+ const destination = Object.keys(target.properties ?? {}).find(name => name.toLowerCase() === source.toLowerCase());
161
+ if (destination && !mapped.has(destination.toLowerCase()) && !excluded.has(destination.toLowerCase())) {
162
+ properties[destination] = source;
163
+ mapped.add(destination.toLowerCase());
164
+ }
165
+ }
166
+ return properties;
167
+ }
168
+
169
+ private apply(properties: Record<string, string>, content: unknown, event: ScenarioEvent, target: JsonSchema, state: Record<string, unknown>): void {
170
+ for (const [destination, expression] of Object.entries(properties)) {
171
+ const value = ProjectionExpressionEvaluator.value(expression, content, event.context, target.properties![destination]);
172
+ // The kernel compares old and new null values; null on an absent member is no change.
173
+ if (value !== null || state[destination] !== undefined) state[destination] = value;
174
+ }
175
+ }
176
+
177
+ private initialValues(): Record<string, unknown> {
116
178
  const initial: Record<string, unknown> = Object.create(null);
117
179
  if (Object.keys(this._initial).length) {
118
- Object.assign(initial, ProjectionValueConverter.convert(structuredClone(this._initial), this._schema, false, false));
180
+ return Object.assign(initial, ProjectionValueConverter.convert(structuredClone(this._initial), this._schema, false, false));
119
181
  }
120
- if (!Object.keys(this._initial).length) {
121
- for (const [name, property] of Object.entries(this._schema.properties ?? {})) {
122
- if (property.type === 'array') initial[name] = [];
123
- }
182
+ for (const [name, property] of Object.entries(this._schema.properties ?? {})) {
183
+ if (property.type === 'array') initial[name] = [];
124
184
  }
125
- initial.id = ProjectionValueConverter.convert(key, this._schema.properties!.id);
126
- initial.__subject = event.context.subject ?? event.sourceId;
127
- initial.__initialized = true;
128
185
  return initial;
129
186
  }
130
187
 
188
+ private initialState(key: string, event: ScenarioEvent): Record<string, unknown> {
189
+ return Object.assign(this.initialValues(), this.identityState(key, event, true));
190
+ }
191
+
192
+ private identityState(key: string, event: ScenarioEvent, initialized: boolean): Record<string, unknown> {
193
+ const state: Record<string, unknown> = Object.create(null);
194
+ state.id = ProjectionValueConverter.convert(key, this._schema.properties!.id);
195
+ state.__subject = event.context.subject ?? event.sourceId;
196
+ state.__initialized = initialized;
197
+ return state;
198
+ }
199
+
131
200
  private normalize(state: Record<string, unknown>): Record<string, unknown> {
132
201
  const result: Record<string, unknown> = Object.create(null);
133
202
  for (const [name, schema] of Object.entries(this._schema.properties ?? {})) {
@@ -0,0 +1,73 @@
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 { AutoMap } from '@cratis/chronicle.contracts';
6
+ import { field } from '@cratis/fundamentals';
7
+ import { chai, describe, it } from 'vitest';
8
+ import { eventType } from '../../../events/eventTypeDecorator.js';
9
+ import type { ChildrenDefinitionLike } from '../../../projections/modelBound/childrenAndNestedBuilder.js';
10
+ import { childrenFrom } from '../../../projections/modelBound/childrenFrom.js';
11
+ import { ProjectionCapabilities } from '../ProjectionCapabilities.js';
12
+ import { UnsupportedProjectionOperation } from '../UnsupportedProjectionOperation.js';
13
+ import { Changed } from './given/Changed.js';
14
+ import { compileDeclarative, compileModelBound } from './given/compile.js';
15
+
16
+ chai.should();
17
+
18
+ class ChildAdded { itemId!: string; name!: string; }
19
+ field(String)(ChildAdded.prototype, 'itemId');
20
+ field(String)(ChildAdded.prototype, 'name');
21
+ eventType('capability-child-added')(ChildAdded);
22
+
23
+ type Wire = { Children: Record<string, ChildrenDefinitionLike>; RemovedWith: unknown[] };
24
+
25
+ function compileChildren() {
26
+ const result = compileDeclarative(builder => {
27
+ builder.from(Changed).children<{ lineId: string }>(model => model.labels as never, child => child
28
+ .identifiedBy(line => line.lineId)
29
+ .from(ChildAdded, from => from.usingKey(event => event.itemId)));
30
+ }, [ChildAdded]);
31
+ const wire = result.definition as unknown as Wire;
32
+ return { ...result, wire, child: wire.Children.labels };
33
+ }
34
+
35
+ describe('when rejecting unsupported children operations before any event is seeded', () => {
36
+ it('should accept the fixture-backed keyed children shape', () => {
37
+ const { compiled, definition } = compileChildren();
38
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
39
+ });
40
+
41
+ const cases: Array<{ name: string; change: (child: ChildrenDefinitionLike, wire: Wire) => void; path: string; reason: string }> = [
42
+ { name: 'a join inside children', change: child => { child.Join = [{}]; }, path: 'Children.labels.Join', reason: 'joins require a kernel-backed test' },
43
+ { name: 'nested children collections', change: child => { child.Children = { inner: child }; }, path: 'Children.labels.Children', reason: 'nested children collections' },
44
+ { name: 'nested projections inside children', change: child => { child.Nested = { inner: child }; }, path: 'Children.labels.Nested', reason: 'nested projections inside children' },
45
+ { name: 'removedWithJoin inside children', change: child => { child.RemovedWithJoin = [child.RemovedWithJoin[0] ?? { Key: child.From[0].Key, Value: { Key: '' } }]; }, path: 'Children.labels.RemovedWithJoin', reason: 'removedWithJoin inside children' },
46
+ { name: 'value children from an event property', change: child => { (child as unknown as Record<string, unknown>).FromEventProperty = { PropertyExpression: 'name' }; }, path: 'Children.labels.FromEventProperty', reason: 'value children from an event property' },
47
+ { name: 'disabled child AutoMap', change: child => { child.AutoMap = AutoMap.Disabled; }, path: 'Children.labels.AutoMap', reason: 'AutoMap disabled' },
48
+ { name: 'a child without an identifying property', change: child => { child.IdentifiedBy = '*NotSet*'; }, path: 'Children.labels.IdentifiedBy', reason: 'without a direct identifying property' },
49
+ { name: 'an event-source-id child key', change: child => { child.From[0].Value.Key = '$eventSourceId'; }, path: 'Children.labels.From[capability-child-added:1].Key', reason: 'only a child key read from a string event property' },
50
+ { name: 'an event-property parent key', change: child => { child.From[0].Value.ParentKey = 'itemId'; }, path: 'Children.labels.From[capability-child-added:1].ParentKey', reason: 'parent keys other than the event source id' },
51
+ { name: 'an explicit mapping into a child', change: child => { child.From[0].Value.Properties = { name: 'name' }; }, path: 'Children.labels.From[capability-child-added:1].Properties.name', reason: 'explicit child property mappings' },
52
+ { name: 'a renamed child mapping', change: child => { child.From[0].Value.Properties = { title: 'name' }; }, path: 'Children.labels.From[capability-child-added:1].Properties.title', reason: 'only same-name AutoMap into typed children' },
53
+ { name: 'a child mapping from the event context', change: child => { child.From[0].Value.Properties = { name: '$eventContext(occurred)' }; }, path: 'Children.labels.From[capability-child-added:1].Properties.name', reason: 'explicit child property mappings' },
54
+ { name: 'children combined with initial model state', change: (_child, wire) => { (wire as unknown as Record<string, unknown>).InitialModelState = '{"name":"initial"}'; }, path: 'InitialModelState', reason: 'children with initial model state' },
55
+ { name: 'a child identifier mapped from another value', change: child => { child.From[0].Value.Properties = { lineId: 'name' }; }, path: 'Children.labels.From[capability-child-added:1].Properties.lineId', reason: 'mapping the child identifier from anything but the child key' },
56
+ { name: 'a child event that also removes the parent', change: (child, wire) => { wire.RemovedWith = [{ Key: child.From[0].Key, Value: { Key: '$eventSourceId', ParentKey: '' } }]; }, path: 'Children.labels.From[capability-child-added:1]', reason: 'both removes and changes' },
57
+ { name: 'one event that both adds and removes a child', change: child => { child.RemovedWith = [{ Key: child.From[0].Key, Value: { Key: 'itemId', ParentKey: '' } }]; }, path: 'Children.labels.RemovedWith[capability-child-added:1]', reason: 'more than one children operation' }
58
+ ];
59
+ for (const testCase of cases) {
60
+ it(`should reject ${testCase.name} with its contract path and reason`, () => {
61
+ const { compiled, definition, wire, child } = compileChildren();
62
+ testCase.change(child, wire);
63
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
64
+ .with.property('message').that.includes(testCase.path).and.includes(testCase.reason);
65
+ });
66
+ }
67
+
68
+ it('should reject a children collection that does not target an array of objects', () => {
69
+ const { compiled, definition } = compileModelBound(model => childrenFrom(ChildAdded, { key: 'itemId' })(model.prototype, 'name'), [ChildAdded]);
70
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
71
+ .with.property('message').that.includes('Children.name (@childrenFrom)').and.includes('must target an array of objects');
72
+ });
73
+ });
@@ -6,7 +6,6 @@ import { field } from '@cratis/fundamentals';
6
6
  import { chai, describe, it } from 'vitest';
7
7
  import { eventType } from '../../../events/eventTypeDecorator.js';
8
8
  import type { IProjectionBuilderFor } from '../../../projections/declarative/IProjectionBuilderFor.js';
9
- import { childrenFrom } from '../../../projections/modelBound/childrenFrom.js';
10
9
  import { entersOn } from '../../../projections/modelBound/entersOn.js';
11
10
  import { eventSequence } from '../../../projections/modelBound/eventSequence.js';
12
11
  import { fromEvent } from '../../../projections/modelBound/fromEvent.js';
@@ -30,7 +29,6 @@ chai.should();
30
29
 
31
30
  describe('when rejecting unsupported operations before any event is seeded', () => {
32
31
  const modelBound = [
33
- { name: 'children', configure: (model: Function) => childrenFrom(Removed)(model.prototype, 'name'), path: 'Children.name (@childrenFrom)', reason: 'children projections' },
34
32
  { name: 'nested', configure: (model: Function) => nested(model.prototype, 'name'), path: 'Nested.name (@nested)', reason: 'nested projections' },
35
33
  { name: 'join', configure: (model: Function) => join(Removed, 'name')(model.prototype, 'name'), path: 'Join[capability-removed:1] (@join)', reason: 'joins' },
36
34
  { name: 'removedWithJoin', configure: (model: Function) => removedWithJoin(Removed)(model), path: 'RemovedWithJoin[capability-removed:1] (@removedWithJoin)', reason: 'removedWithJoin' },
@@ -77,7 +75,6 @@ describe('when rejecting unsupported operations before any event is seeded', ()
77
75
  { name: 'composite key', define: builder => { builder.from(Changed, from => from.usingCompositeKey<{ name: string }>(key => key.set(target => target.name, event => event.name))); }, path: 'From[capability-changed:1].Key (.from().usingCompositeKey)' },
78
76
  { name: 'parent key', define: builder => { builder.from(Changed, from => from.usingParentKey(event => event.name)); }, path: 'From[capability-changed:1].ParentKey (.from().usingParentKey)' },
79
77
  { name: 'join', define: builder => { builder.from(Changed).join(Removed, join => join.on(model => model.id)); }, path: 'Join[capability-removed:1] (.join)' },
80
- { name: 'children', define: builder => { builder.from(Changed).children(model => model.labels, child => child.from(Removed)); }, path: 'Children.labels (.children)' },
81
78
  { name: 'nested', define: builder => { builder.from(Changed).nested(model => model.details, child => child.clearWith(Removed)); }, path: 'Nested.details (.nested)' },
82
79
  { name: 'removedWithJoin', define: builder => { builder.from(Changed).removedWithJoin(Removed); }, path: 'RemovedWithJoin[capability-removed:1] (.removedWithJoin)' },
83
80
  { name: 'removal key', define: builder => { builder.from(Removed).removedWith(Changed, removal => removal.usingKey(event => event.name)); }, path: 'RemovedWith[capability-changed:1].Key (.removedWith)' },
package/vitest.config.ts CHANGED
@@ -23,7 +23,8 @@ export default defineConfig({
23
23
  }],
24
24
  test: {
25
25
  include: ['**/*.spec.ts', '**/*.spec.js'],
26
- exclude: ['node_modules', 'dist'],
26
+ // Kernel-backed specifications run through vitest.integration.config.ts.
27
+ exclude: ['node_modules', 'dist', '**/*.integration.spec.ts'],
27
28
  environment: 'node'
28
29
  }
29
30
  });
@@ -0,0 +1,19 @@
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 { defineConfig } from 'vitest/config';
5
+ import baseConfig from './vitest.config.js';
6
+
7
+ // Kernel-backed specifications. They need a running Chronicle kernel and are skipped unless
8
+ // CHRONICLE_INTEGRATION_CONNECTION_STRING points at one, for example
9
+ // chronicle://chronicle-dev-client:chronicle-dev-secret@localhost:35000
10
+ export default defineConfig({
11
+ ...baseConfig,
12
+ test: {
13
+ include: ['**/*.integration.spec.ts'],
14
+ exclude: ['node_modules', 'dist'],
15
+ environment: 'node',
16
+ testTimeout: 60_000,
17
+ hookTimeout: 60_000
18
+ }
19
+ });