@cratis/chronicle 6.32.0 → 6.33.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/readModels/IReadModels.d.ts +14 -0
- package/dist/readModels/IReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.d.ts +6 -0
- package/dist/readModels/ReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.js +110 -0
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/readModels/for_ReadModels/given/a_stored_read_model.d.ts +20 -0
- package/dist/readModels/for_ReadModels/given/a_stored_read_model.d.ts.map +1 -0
- package/dist/readModels/for_ReadModels/given/a_stored_read_model.js +79 -0
- package/dist/readModels/for_ReadModels/given/a_stored_read_model.js.map +1 -0
- package/dist/schemas/JsonSchemaGenerator.d.ts +4 -4
- package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
- package/dist/schemas/JsonSchemaGenerator.js +23 -8
- package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
- package/dist/testing/projections/ProjectionChildrenCapabilities.d.ts +2 -1
- package/dist/testing/projections/ProjectionChildrenCapabilities.d.ts.map +1 -1
- package/dist/testing/projections/ProjectionChildrenCapabilities.js +9 -3
- package/dist/testing/projections/ProjectionChildrenCapabilities.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/readModels/IReadModels.ts +15 -0
- package/readModels/ReadModels.ts +118 -1
- package/readModels/for_ReadModels/given/a_stored_read_model.ts +62 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_a_default_subject.spec.ts +39 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_a_frozen_document.spec.ts +28 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_a_malformed_response.spec.ts +24 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_a_reported_failure.spec.ts +38 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_an_empty_released_value.spec.ts +19 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_an_unknown_subject_property.spec.ts +42 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_bookkeeping_fields.spec.ts +27 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_conflicting_or_missing_properties.spec.ts +32 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_empty_subject_metadata.spec.ts +56 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_invalid_subject_metadata.spec.ts +27 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_mixed_subjects.spec.ts +38 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_property_subjects.spec.ts +52 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_whitespace_subjects.spec.ts +24 -0
- package/readModels/for_ReadModels/when_releasing_a_document/without_subjects.spec.ts +24 -0
- package/schemas/JsonSchemaGenerator.ts +22 -7
- package/schemas/for_JsonSchemaGenerator/when_generating_children/with_legacy_decorators.legacy.spec.ts +143 -0
- package/schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts +66 -0
- package/testing/ReadModelScenario.childrenFrom.spec.ts +70 -3
- package/testing/projections/ProjectionChildrenCapabilities.ts +9 -2
- package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_children_operations.spec.ts +34 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { chai, describe, it } from 'vitest';
|
|
5
6
|
import { eventType } from '../events/eventTypeDecorator.js';
|
|
6
7
|
import { childrenFrom } from '../projections/modelBound/childrenFrom.js';
|
|
7
8
|
import { fromEvent } from '../projections/modelBound/fromEvent.js';
|
|
8
9
|
import { setFrom } from '../projections/modelBound/setFrom.js';
|
|
10
|
+
import { JsonSchemaGenerator } from '../schemas/JsonSchemaGenerator.js';
|
|
9
11
|
import { ReadModelScenario, UnsupportedProjectionOperation } from './index.js';
|
|
10
12
|
|
|
11
13
|
chai.should();
|
|
@@ -27,6 +29,18 @@ class ScenarioOrder {
|
|
|
27
29
|
childrenFrom(ScenarioItemAdded, { childType: ScenarioItem, key: 'itemId' })(ScenarioOrder.prototype, 'items');
|
|
28
30
|
fromEvent(ScenarioItemAdded)(ScenarioOrder);
|
|
29
31
|
|
|
32
|
+
class ScenarioKeyedItem {
|
|
33
|
+
itemId = '';
|
|
34
|
+
name = '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class ScenarioKeyedOrder {
|
|
38
|
+
id = '';
|
|
39
|
+
items: ScenarioKeyedItem[] = [];
|
|
40
|
+
}
|
|
41
|
+
childrenFrom(ScenarioItemAdded, { childType: ScenarioKeyedItem, key: 'itemId' })(ScenarioKeyedOrder.prototype, 'items');
|
|
42
|
+
fromEvent(ScenarioItemAdded)(ScenarioKeyedOrder);
|
|
43
|
+
|
|
30
44
|
class ScenarioNamedItem {
|
|
31
45
|
id = '';
|
|
32
46
|
name = '';
|
|
@@ -40,17 +54,70 @@ class ScenarioNamedOrder {
|
|
|
40
54
|
childrenFrom(ScenarioItemAdded, { childType: ScenarioNamedItem, key: 'itemId' })(ScenarioNamedOrder.prototype, 'items');
|
|
41
55
|
fromEvent(ScenarioItemAdded)(ScenarioNamedOrder);
|
|
42
56
|
|
|
57
|
+
class ScenarioUnknownOrder {
|
|
58
|
+
id = '';
|
|
59
|
+
items: ScenarioItem[] = [];
|
|
60
|
+
}
|
|
61
|
+
childrenFrom(ScenarioItemAdded, { key: 'itemId', identifiedBy: 'id' })(ScenarioUnknownOrder.prototype, 'items');
|
|
62
|
+
fromEvent(ScenarioItemAdded)(ScenarioUnknownOrder);
|
|
63
|
+
|
|
64
|
+
class ScenarioUnresolvedItem {
|
|
65
|
+
id!: string;
|
|
66
|
+
name!: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class ScenarioUnresolvedOrder {
|
|
70
|
+
id = '';
|
|
71
|
+
items: ScenarioUnresolvedItem[] = [];
|
|
72
|
+
}
|
|
73
|
+
childrenFrom(ScenarioItemAdded, { childType: ScenarioUnresolvedItem, key: 'itemId', identifiedBy: 'id' })(ScenarioUnresolvedOrder.prototype, 'items');
|
|
74
|
+
fromEvent(ScenarioItemAdded)(ScenarioUnresolvedOrder);
|
|
75
|
+
|
|
43
76
|
const artifacts = { eventTypes: [ScenarioItemAdded], reducers: [], projections: [] };
|
|
44
77
|
|
|
45
78
|
describe('when a read model scenario evaluates keyed children from the options form', () => {
|
|
46
|
-
it('should add one child per key and update an existing child in place (children-
|
|
79
|
+
it('should add one child per key and update an existing child in place (children-typed-items)', async () => {
|
|
47
80
|
const scenario = new ReadModelScenario(ScenarioOrder, artifacts);
|
|
48
81
|
scenario.given.forEventSource('order-1').events(
|
|
49
82
|
new ScenarioItemAdded('a', 'first'), new ScenarioItemAdded('b', 'second'), new ScenarioItemAdded('a', 'again'));
|
|
50
83
|
const order = await scenario.instanceForEventSourceId('order-1');
|
|
51
84
|
order!.id.should.equal('order-1');
|
|
52
|
-
// The
|
|
53
|
-
order!.items.map(item => ({ id: item.id, name: item.name })).should.deep.equal([{ id: 'a', name: '' }, { id: 'b', name: '' }]);
|
|
85
|
+
// The explicit child type registers a typed item schema, so AutoMap preserves the event's name.
|
|
86
|
+
order!.items.map(item => ({ id: item.id, name: item.name })).should.deep.equal([{ id: 'a', name: 'again' }, { id: 'b', name: 'second' }]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should register the same typed items schema as the oracle fixture', () => {
|
|
90
|
+
const fixture = JSON.parse(readFileSync(new URL('./projections/fixtures/children-typed-items.json', import.meta.url), 'utf8'));
|
|
91
|
+
JsonSchemaGenerator.generate(ScenarioOrder).properties!.items.should.deep.equal(fixture.readModel.schema.properties.items);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should add and update typed legacy children whose discovered identifier equals the event key (children-identifier-equals-key)', async () => {
|
|
95
|
+
const fixture = JSON.parse(readFileSync(new URL('./projections/fixtures/children-identifier-equals-key.json', import.meta.url), 'utf8'));
|
|
96
|
+
JsonSchemaGenerator.generate(ScenarioKeyedOrder).properties!.items.should.deep.equal(fixture.readModel.schema.properties.items);
|
|
97
|
+
const scenario = new ReadModelScenario(ScenarioKeyedOrder, artifacts);
|
|
98
|
+
for (let step = 0; step < 3; step++) {
|
|
99
|
+
const { itemId, name } = fixture.events[step].content;
|
|
100
|
+
scenario.given.forEventSource('order-1').events(new ScenarioItemAdded(itemId, name));
|
|
101
|
+
const order = await scenario.instanceForEventSourceId('order-1');
|
|
102
|
+
order!.should.deep.equal(fixture.expected[step].publicRead['order-1']);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should retain only the identifier when the child type is unknown (children-untyped-items)', async () => {
|
|
107
|
+
const scenario = new ReadModelScenario(ScenarioUnknownOrder, artifacts);
|
|
108
|
+
scenario.given.forEventSource('order-1').events(new ScenarioItemAdded('a', 'first'));
|
|
109
|
+
const order = await scenario.instanceForEventSourceId('order-1');
|
|
110
|
+
order!.items.should.deep.equal([{ id: 'a' }]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should retain only the identifier when the declared child members have no runtime types (children-untyped-items)', async () => {
|
|
114
|
+
const fixture = JSON.parse(readFileSync(new URL('./projections/fixtures/children-untyped-items.json', import.meta.url), 'utf8'));
|
|
115
|
+
JsonSchemaGenerator.generate(ScenarioUnresolvedOrder).properties!.items.should.deep.equal(fixture.readModel.schema.properties.items);
|
|
116
|
+
const scenario = new ReadModelScenario(ScenarioUnresolvedOrder, artifacts);
|
|
117
|
+
scenario.given.forEventSource('order-1').events(
|
|
118
|
+
new ScenarioItemAdded('a', 'first'), new ScenarioItemAdded('b', 'second'), new ScenarioItemAdded('a', 'again'));
|
|
119
|
+
const order = await scenario.instanceForEventSourceId('order-1');
|
|
120
|
+
order!.items.map(item => ({ id: item.id, name: item.name })).should.deep.equal([{ id: 'a', name: undefined }, { id: 'b', name: undefined }]);
|
|
54
121
|
});
|
|
55
122
|
|
|
56
123
|
it('should reject explicit child property mappings', () => {
|
|
@@ -30,7 +30,8 @@ const kernelBacked = 'require a kernel-backed test (ChronicleKernelScenario / li
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Validates one level of children collections against the packaged-kernel fixtures
|
|
33
|
-
* (`children-from-keyed`, `children-identified-removed`, `children-
|
|
33
|
+
* (`children-from-keyed`, `children-identified-removed`, `children-typed-items`,
|
|
34
|
+
* `children-untyped-items`, `children-identifier-equals-key`).
|
|
34
35
|
*/
|
|
35
36
|
export class ProjectionChildrenCapabilities {
|
|
36
37
|
/**
|
|
@@ -109,7 +110,13 @@ export class ProjectionChildrenCapabilities {
|
|
|
109
110
|
const candidates = Object.keys(eventSchema.properties ?? {}).filter(source => source.toLowerCase() === destination.toLowerCase());
|
|
110
111
|
if (!candidates.length) continue;
|
|
111
112
|
const mappingPath = `${path}.AutoMap.${destination}`;
|
|
112
|
-
if (destination === identifiedBy)
|
|
113
|
+
if (destination === identifiedBy) {
|
|
114
|
+
if (candidates.length !== 1 || candidates[0] !== entry.Value.Key) {
|
|
115
|
+
reject(mappingPath, `AutoMap into the child identifier ${kernelBacked}`);
|
|
116
|
+
}
|
|
117
|
+
// Changeset.AddChild initializes the identifier from this same key.
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
113
120
|
if (candidates.length > 1) reject(mappingPath, `inferred AutoMap source is ambiguous: ${candidates.join(', ')}`);
|
|
114
121
|
context.checkMapping(items!, eventSchema, destination, candidates[0], mappingPath);
|
|
115
122
|
}
|
|
@@ -8,6 +8,7 @@ import { chai, describe, it } from 'vitest';
|
|
|
8
8
|
import { eventType } from '../../../events/eventTypeDecorator.js';
|
|
9
9
|
import type { ChildrenDefinitionLike } from '../../../projections/modelBound/childrenAndNestedBuilder.js';
|
|
10
10
|
import { childrenFrom } from '../../../projections/modelBound/childrenFrom.js';
|
|
11
|
+
import type { JsonSchema } from '../../../schemas/JsonSchema.js';
|
|
11
12
|
import { ProjectionCapabilities } from '../ProjectionCapabilities.js';
|
|
12
13
|
import { UnsupportedProjectionOperation } from '../UnsupportedProjectionOperation.js';
|
|
13
14
|
import { Changed } from './given/Changed.js';
|
|
@@ -32,12 +33,45 @@ function compileChildren() {
|
|
|
32
33
|
return { ...result, wire, child: wire.Children.labels };
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
function compileTypedChildren() {
|
|
37
|
+
const result = compileChildren();
|
|
38
|
+
result.child.IdentifiedBy = 'itemId';
|
|
39
|
+
result.child.From[0].Value.Properties = {};
|
|
40
|
+
const schema = JSON.parse(result.compiled.readModels[0].Schema) as JsonSchema;
|
|
41
|
+
schema.properties!.labels.items = {
|
|
42
|
+
type: 'object', properties: { itemId: { type: 'string' }, name: { type: 'string' } }
|
|
43
|
+
};
|
|
44
|
+
result.compiled.readModels[0].Schema = JSON.stringify(schema);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
describe('when rejecting unsupported children operations before any event is seeded', () => {
|
|
36
49
|
it('should accept the fixture-backed keyed children shape', () => {
|
|
37
50
|
const { compiled, definition } = compileChildren();
|
|
38
51
|
(() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
|
|
39
52
|
});
|
|
40
53
|
|
|
54
|
+
it('should accept AutoMap into a typed child identifier from the entry key alone', () => {
|
|
55
|
+
const { compiled, definition } = compileTypedChildren();
|
|
56
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should reject AutoMap into a typed child identifier from a property other than the entry key', () => {
|
|
60
|
+
const { compiled, definition, child } = compileTypedChildren();
|
|
61
|
+
child.From[0].Value.Key = 'name';
|
|
62
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
63
|
+
.with.property('message').that.includes('Children.labels.From[capability-child-added:1].AutoMap.itemId')
|
|
64
|
+
.and.includes('AutoMap into the child identifier');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should reject ambiguous AutoMap into a typed child identifier even if one candidate is the entry key', () => {
|
|
68
|
+
const { compiled, definition } = compileTypedChildren();
|
|
69
|
+
compiled.eventSchemas.get(definition)!.get('capability-child-added:1:0')!.schema.properties!.ItemId = { type: 'string' };
|
|
70
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
71
|
+
.with.property('message').that.includes('Children.labels.From[capability-child-added:1].AutoMap.itemId')
|
|
72
|
+
.and.includes('AutoMap into the child identifier');
|
|
73
|
+
});
|
|
74
|
+
|
|
41
75
|
const cases: Array<{ name: string; change: (child: ChildrenDefinitionLike, wire: Wire) => void; path: string; reason: string }> = [
|
|
42
76
|
{ name: 'a join inside children', change: child => { child.Join = [{}]; }, path: 'Children.labels.Join', reason: 'joins require a kernel-backed test' },
|
|
43
77
|
{ name: 'nested children collections', change: child => { child.Children = { inner: child }; }, path: 'Children.labels.Children', reason: 'nested children collections' },
|