@cratis/chronicle 6.18.0 → 6.20.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 (120) hide show
  1. package/dist/eventSequences/EventSequence.d.ts.map +1 -1
  2. package/dist/eventSequences/EventSequence.js +2 -9
  3. package/dist/eventSequences/EventSequence.js.map +1 -1
  4. package/dist/eventSequences/prepareSingleAppend.d.ts +12 -0
  5. package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -0
  6. package/dist/eventSequences/prepareSingleAppend.js +22 -0
  7. package/dist/eventSequences/prepareSingleAppend.js.map +1 -0
  8. package/dist/testing/EventScenario.d.ts +29 -0
  9. package/dist/testing/EventScenario.d.ts.map +1 -0
  10. package/dist/testing/EventScenario.js +60 -0
  11. package/dist/testing/EventScenario.js.map +1 -0
  12. package/dist/testing/EventScenarioGiven.d.ts +9 -0
  13. package/dist/testing/EventScenarioGiven.d.ts.map +1 -0
  14. package/dist/testing/EventScenarioGiven.js +19 -0
  15. package/dist/testing/EventScenarioGiven.js.map +1 -0
  16. package/dist/testing/EventScenarioGivenBuilder.d.ts +9 -0
  17. package/dist/testing/EventScenarioGivenBuilder.d.ts.map +1 -0
  18. package/dist/testing/EventScenarioGivenBuilder.js +12 -0
  19. package/dist/testing/EventScenarioGivenBuilder.js.map +1 -0
  20. package/dist/testing/EventScenarioOptions.d.ts +21 -0
  21. package/dist/testing/EventScenarioOptions.d.ts.map +1 -0
  22. package/dist/testing/EventScenarioOptions.js +4 -0
  23. package/dist/testing/EventScenarioOptions.js.map +1 -0
  24. package/dist/testing/EventScenarioSeed.d.ts +5 -0
  25. package/dist/testing/EventScenarioSeed.d.ts.map +1 -0
  26. package/dist/testing/EventScenarioSeed.js +14 -0
  27. package/dist/testing/EventScenarioSeed.js.map +1 -0
  28. package/dist/testing/EventScenarioWhen.d.ts +11 -0
  29. package/dist/testing/EventScenarioWhen.d.ts.map +1 -0
  30. package/dist/testing/EventScenarioWhen.js +17 -0
  31. package/dist/testing/EventScenarioWhen.js.map +1 -0
  32. package/dist/testing/EventScenarioWhenBuilder.d.ts +9 -0
  33. package/dist/testing/EventScenarioWhenBuilder.d.ts.map +1 -0
  34. package/dist/testing/EventScenarioWhenBuilder.js +12 -0
  35. package/dist/testing/EventScenarioWhenBuilder.js.map +1 -0
  36. package/dist/testing/IReadModelProcessor.d.ts +7 -0
  37. package/dist/testing/IReadModelProcessor.d.ts.map +1 -0
  38. package/dist/testing/IReadModelProcessor.js +4 -0
  39. package/dist/testing/IReadModelProcessor.js.map +1 -0
  40. package/dist/testing/InProcessEventSequence.d.ts +49 -0
  41. package/dist/testing/InProcessEventSequence.d.ts.map +1 -0
  42. package/dist/testing/InProcessEventSequence.js +265 -0
  43. package/dist/testing/InProcessEventSequence.js.map +1 -0
  44. package/dist/testing/ReadModelScenario.d.ts +6 -8
  45. package/dist/testing/ReadModelScenario.d.ts.map +1 -1
  46. package/dist/testing/ReadModelScenario.js +50 -50
  47. package/dist/testing/ReadModelScenario.js.map +1 -1
  48. package/dist/testing/ReadModelState.d.ts +6 -0
  49. package/dist/testing/ReadModelState.d.ts.map +1 -0
  50. package/dist/testing/ReadModelState.js +4 -0
  51. package/dist/testing/ReadModelState.js.map +1 -0
  52. package/dist/testing/ReducerReadModelProcessor.d.ts +13 -0
  53. package/dist/testing/ReducerReadModelProcessor.d.ts.map +1 -0
  54. package/dist/testing/ReducerReadModelProcessor.js +38 -0
  55. package/dist/testing/ReducerReadModelProcessor.js.map +1 -0
  56. package/dist/testing/ScenarioEvent.d.ts +8 -0
  57. package/dist/testing/ScenarioEvent.d.ts.map +1 -0
  58. package/dist/testing/ScenarioEvent.js +4 -0
  59. package/dist/testing/ScenarioEvent.js.map +1 -0
  60. package/dist/testing/UnsupportedEventSequenceOperation.d.ts +5 -0
  61. package/dist/testing/UnsupportedEventSequenceOperation.d.ts.map +1 -0
  62. package/dist/testing/UnsupportedEventSequenceOperation.js +10 -0
  63. package/dist/testing/UnsupportedEventSequenceOperation.js.map +1 -0
  64. package/dist/testing/index.d.ts +8 -0
  65. package/dist/testing/index.d.ts.map +1 -1
  66. package/dist/testing/index.js +7 -0
  67. package/dist/testing/index.js.map +1 -1
  68. package/dist/testing/projections/ProjectionCapabilities.d.ts +3 -0
  69. package/dist/testing/projections/ProjectionCapabilities.d.ts.map +1 -1
  70. package/dist/testing/projections/ProjectionCapabilities.js +117 -43
  71. package/dist/testing/projections/ProjectionCapabilities.js.map +1 -1
  72. package/dist/testing/projections/ProjectionExpressionEvaluator.d.ts +9 -0
  73. package/dist/testing/projections/ProjectionExpressionEvaluator.d.ts.map +1 -0
  74. package/dist/testing/projections/ProjectionExpressionEvaluator.js +68 -0
  75. package/dist/testing/projections/ProjectionExpressionEvaluator.js.map +1 -0
  76. package/dist/testing/projections/ProjectionReadModelProcessor.d.ts +30 -0
  77. package/dist/testing/projections/ProjectionReadModelProcessor.d.ts.map +1 -0
  78. package/dist/testing/projections/ProjectionReadModelProcessor.js +142 -0
  79. package/dist/testing/projections/ProjectionReadModelProcessor.js.map +1 -0
  80. package/dist/testing/projections/ProjectionValueConverter.d.ts +13 -0
  81. package/dist/testing/projections/ProjectionValueConverter.d.ts.map +1 -0
  82. package/dist/testing/projections/ProjectionValueConverter.js +122 -0
  83. package/dist/testing/projections/ProjectionValueConverter.js.map +1 -0
  84. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.d.ts +0 -2
  85. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.d.ts.map +1 -1
  86. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.js +0 -4
  87. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.js.map +1 -1
  88. package/dist/tsconfig.tsbuildinfo +1 -1
  89. package/eventSequences/EventSequence.ts +2 -12
  90. package/eventSequences/prepareSingleAppend.ts +25 -0
  91. package/package.json +1 -1
  92. package/testing/EventScenario.fixtures.spec.ts +271 -0
  93. package/testing/EventScenario.rejections.spec.ts +307 -0
  94. package/testing/EventScenario.ts +77 -0
  95. package/testing/EventScenarioGiven.ts +16 -0
  96. package/testing/EventScenarioGivenBuilder.ts +11 -0
  97. package/testing/EventScenarioOptions.ts +20 -0
  98. package/testing/EventScenarioSeed.ts +17 -0
  99. package/testing/EventScenarioWhen.ts +16 -0
  100. package/testing/EventScenarioWhenBuilder.ts +11 -0
  101. package/testing/IReadModelProcessor.ts +10 -0
  102. package/testing/InProcessEventSequence.ts +275 -0
  103. package/testing/ReadModelScenario.spec.ts +156 -10
  104. package/testing/ReadModelScenario.ts +57 -57
  105. package/testing/ReadModelState.ts +5 -0
  106. package/testing/ReducerReadModelProcessor.ts +39 -0
  107. package/testing/ScenarioEvent.ts +7 -0
  108. package/testing/UnsupportedEventSequenceOperation.ts +10 -0
  109. package/testing/index.ts +8 -0
  110. package/testing/projections/ProjectionCapabilities.ts +100 -39
  111. package/testing/projections/ProjectionExpressionEvaluator.ts +57 -0
  112. package/testing/projections/ProjectionReadModelProcessor.fixtures.spec.ts +135 -0
  113. package/testing/projections/ProjectionReadModelProcessor.ts +147 -0
  114. package/testing/projections/ProjectionValueConverter.spec.ts +81 -0
  115. package/testing/projections/ProjectionValueConverter.ts +113 -0
  116. package/testing/projections/for_ProjectionCapabilities/given/Changed.ts +1 -3
  117. package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +81 -1
  118. package/testing/projections/for_ProjectionCapabilities/when_validating_numeric_schemas.spec.ts +40 -12
  119. package/testing/projections/for_ProjectionCapabilities/when_validating_schema_boundaries.spec.ts +76 -29
  120. package/testing/projections/for_ProjectionCapabilities/when_validating_supported_operations.spec.ts +8 -35
@@ -12,22 +12,31 @@ chai.should();
12
12
 
13
13
  describe('when validating the numeric capability matrix', () => {
14
14
  for (const format of ['int32', 'uint32']) {
15
- it(`should accept ${format} arithmetic with a matching event operand and literal`, () => {
15
+ it(`should accept a finite ${format} literal without admitting arithmetic`, () => {
16
16
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from
17
- .add(model => model.total).with(event => event.quantity)
18
17
  .set(model => model.quantity).toValue(12)));
19
- const modelSchema = JSON.parse(compiled.readModels[0].Schema) as { properties: { total: { type: string; format?: string }; quantity: { type: string; format?: string } } };
20
- modelSchema.properties.total = { type: 'integer', format };
18
+ const modelSchema = JSON.parse(compiled.readModels[0].Schema) as { properties: { quantity: { type: string; format?: string } } };
21
19
  modelSchema.properties.quantity = { type: 'integer', format };
22
20
  compiled.readModels[0].Schema = JSON.stringify(modelSchema);
23
- const eventSchema = compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema;
24
- eventSchema.properties!.quantity = { type: 'integer', format };
25
21
  (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
26
22
  });
27
23
  }
28
24
 
29
- it('should accept GUID identifiers and finite double arithmetic', () => {
30
- const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
25
+ it('should admit mappings into unformatted Number fields generated by TypeScript', () => {
26
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from
27
+ .set(model => model.total).to(event => event.name)
28
+ .set(model => model.quantity).to(event => event.quantity)));
29
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { total: { type: string; format?: string }; quantity: { type: string; format?: string } } };
30
+ ('format' in schema.properties.total).should.be.false;
31
+ ('format' in schema.properties.quantity).should.be.false;
32
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
33
+ // The oracle's compatible-mappings fixture also tests formatted int32/uint32 sources into unformatted Number targets.
34
+ compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.quantity = { type: 'integer', format: 'int32' };
35
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
36
+ });
37
+
38
+ it('should accept GUID identifiers and a finite double literal', () => {
39
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.set(model => model.total).toValue(2.5)));
31
40
  const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { id: { type: string; format?: string }; total: { format?: string } } };
32
41
  schema.properties.id.format = 'guid';
33
42
  schema.properties.total.format = 'double';
@@ -35,10 +44,10 @@ describe('when validating the numeric capability matrix', () => {
35
44
  (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
36
45
  });
37
46
 
38
- it('should accept numeric identifiers with a declared number schema', () => {
47
+ it('should accept numeric identifiers with the fixture-backed double schema', () => {
39
48
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
40
- const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { id: { type: string } } };
41
- schema.properties.id.type = 'number';
49
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { id: { type: string; format?: string } } };
50
+ schema.properties.id = { type: 'number', format: 'double' };
42
51
  compiled.readModels[0].Schema = JSON.stringify(schema);
43
52
  (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
44
53
  });
@@ -54,6 +63,25 @@ describe('when validating the numeric capability matrix', () => {
54
63
  });
55
64
  }
56
65
 
66
+ it('should reject a double event operand into an integer accumulator before replay', () => {
67
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
68
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { total: { type: string; format?: string } } };
69
+ schema.properties.total = { type: 'integer', format: 'int32' };
70
+ compiled.readModels[0].Schema = JSON.stringify(schema);
71
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
72
+ .with.property('message').that.includes('arithmetic requires a kernel-backed test');
73
+ });
74
+
75
+ it('should reject an int32 $value literal with a fractional spelling even if its value is integral', () => {
76
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.set(model => model.total).toValue(1)));
77
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { total: { type: string; format?: string } } };
78
+ schema.properties.total = { type: 'integer', format: 'int32' };
79
+ compiled.readModels[0].Schema = JSON.stringify(schema);
80
+ definition.From[0].Value.Properties.total = '$value(1.0)';
81
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
82
+ .with.property('message').that.includes('$value numeric literal is outside');
83
+ });
84
+
57
85
  it('should reject an overflowing initial integer value', () => {
58
86
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed)
59
87
  .withInitialValues(() => ({ id: '', name: '', quantity: 0, total: 2147483648, state: '', labels: [], details: {} })));
@@ -68,6 +96,6 @@ describe('when validating the numeric capability matrix', () => {
68
96
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
69
97
  compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.quantity.format = 'int64';
70
98
  (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
71
- .with.property('message').that.includes('arithmetic operand requires a supported numeric event schema');
99
+ .with.property('message').that.includes('arithmetic requires a kernel-backed test');
72
100
  });
73
101
  });
@@ -29,22 +29,16 @@ function setModelSchema(compiled: ReturnType<typeof compileDeclarative>['compile
29
29
  }
30
30
 
31
31
  describe('when validating schema-bound projection operations', () => {
32
- it('should accept generated date, date concept, and nested date formats for pass-through and initial values', () => {
32
+ it('should accept generated scalar date and date-concept formats for pass-through', () => {
33
33
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from
34
34
  .set(model => model.name).to(event => event.name)
35
- .set(model => model.state).to(event => event.name)
36
- .set(model => model.details).to(event => event.details))
37
- .withInitialValues(() => ({ id: '', name: '', quantity: 0, total: 0, state: new Date('2026-01-01') as unknown as string,
38
- labels: [], details: { created: new Date('2026-01-01') } })));
35
+ .set(model => model.state).to(event => event.name)));
39
36
  const dates = JsonSchemaGenerator.generate(Dated).properties!;
40
37
  setModelSchema(compiled, properties => {
41
38
  properties.name = dates.created;
42
39
  properties.state = dates.timestamp;
43
- properties.details = dates.details;
44
40
  });
45
- const event = compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema;
46
- event.properties!.name = dates.created;
47
- event.properties!.details = dates.details;
41
+ compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.name = dates.created;
48
42
  (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
49
43
  });
50
44
 
@@ -56,6 +50,70 @@ describe('when validating schema-bound projection operations', () => {
56
50
  (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
57
51
  });
58
52
 
53
+ const mismatches: Array<{ name: string; source: JsonSchema; target: JsonSchema }> = [
54
+ { name: 'date-time to string', source: { type: 'string', format: 'date-time' }, target: { type: 'string' } },
55
+ { name: 'boolean to string', source: { type: 'boolean' }, target: { type: 'string' } },
56
+ { name: 'number to integer', source: { type: 'number', format: 'double' }, target: { type: 'integer', format: 'int32' } },
57
+ { name: 'object to scalar', source: { type: 'object', properties: { other: { type: 'string' } } }, target: { type: 'string' } },
58
+ { name: 'concept-shaped object to object', source: { type: 'object', properties: { value: { type: 'integer', format: 'int32' } } },
59
+ target: { type: 'object', properties: { value: { type: 'integer', format: 'int32' } } } }
60
+ ];
61
+ for (const mapping of mismatches) {
62
+ for (const autoMap of [false, true]) {
63
+ it(`should reject ${mapping.name} for ${autoMap ? 'AutoMap' : 'setFrom'} with the declaration and path`, () => {
64
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => {
65
+ if (!autoMap) from.set(model => model.name).to(event => event.name);
66
+ }));
67
+ setModelSchema(compiled, properties => { properties.name = mapping.target; });
68
+ compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.name = mapping.source;
69
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
70
+ .with.property('message').that.includes(`From[capability-changed:1].${autoMap ? 'AutoMap' : 'Properties'}.name`)
71
+ .and.includes(autoMap ? '.from (AutoMap)' : '.from().set')
72
+ .and.includes('kernel-backed test');
73
+ });
74
+ }
75
+ }
76
+
77
+ for (const [property, target, value, reason] of [
78
+ ['state', { type: 'boolean' }, 'yes', 'initial boolean value must be a JSON boolean'],
79
+ ['total', { type: 'integer', format: 'int32' }, '5', 'initial numeric value must be a JSON number'],
80
+ ['state', { type: 'string', format: 'guid' }, 'not-a-guid', 'initial GUID value must be canonical lowercase text']
81
+ ] as const) {
82
+ it(`should reject incompatible initial value for ${property}`, () => {
83
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
84
+ setModelSchema(compiled, properties => { properties[property] = target; });
85
+ definition.InitialModelState = JSON.stringify({ [property]: value });
86
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
87
+ .with.property('message').that.includes(`InitialModelState.${property} (.withInitialValues)`)
88
+ .and.includes(reason);
89
+ });
90
+ }
91
+
92
+ it('should reject unproven initial date-time, object and array values', () => {
93
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
94
+ for (const [property, value] of [['state', '2026-01-01T00:00:00.000Z'], ['details', {}], ['labels', []]] as const) {
95
+ if (property === 'state') setModelSchema(compiled, properties => { properties.state = { type: 'string', format: 'date-time' }; });
96
+ definition.InitialModelState = JSON.stringify({ [property]: value });
97
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
98
+ .with.property('message').that.includes(`InitialModelState.${property}`);
99
+ }
100
+ });
101
+
102
+ for (const [name, schema] of [
103
+ ['unmapped', { type: 'string', security: [{ metadataType: 'Secret', details: '' }] }],
104
+ ['nested', { type: 'object', properties: { secret: { type: 'string', compliance: [{ metadataType: 'PII', details: '' }] } } }],
105
+ ['array item', { type: 'array', items: { type: 'string', compliance: [{ metadataType: 'PII', details: '' }] } }],
106
+ ['additional property', { type: 'object', additionalProperties: { type: 'string', security: [{ metadataType: 'Encrypted', details: '' }] } }]
107
+ ] as const) {
108
+ it(`should reject protected ${name} schema members even when unmapped`, () => {
109
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
110
+ setModelSchema(compiled, properties => { properties.unmapped = schema as JsonSchema; });
111
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
112
+ .with.property('message').that.includes('ReadModel.Schema.unmapped')
113
+ .and.includes('protected fields require a kernel-backed test');
114
+ });
115
+ }
116
+
59
117
  it('should accept $null for a date destination', () => {
60
118
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.set(model => model.state).toValue(null)));
61
119
  setModelSchema(compiled, properties => { properties.state = { type: 'string', format: 'date-time' }; });
@@ -72,37 +130,26 @@ describe('when validating schema-bound projection operations', () => {
72
130
  });
73
131
  }
74
132
 
75
- it('should use a string key when neither id nor Id exists', () => {
133
+ it('should reject missing or differently cased id schemas until their key behavior is fixture-backed', () => {
76
134
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
77
135
  setModelSchema(compiled, properties => { delete properties.id; });
78
- (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
79
- });
80
-
81
- it('should use Id when id is absent, but prefer id when both exist', () => {
82
- const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
83
- setModelSchema(compiled, properties => { properties.Id = { type: 'string' }; delete properties.id; });
84
- (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
85
- setModelSchema(compiled, properties => { properties.id = { type: 'boolean' }; });
136
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation, 'ReadModel.Schema.id');
137
+ setModelSchema(compiled, properties => { properties.Id = { type: 'string' }; });
86
138
  (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation, 'ReadModel.Schema.id');
87
139
  });
88
140
 
89
- it('should not treat ID as the kernel identifier', () => {
141
+ it('should reject schemas whose properties collide with the lowercase id', () => {
90
142
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
91
- setModelSchema(compiled, properties => { delete properties.id; properties.ID = { type: 'boolean' }; });
92
- (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
93
- });
94
-
95
- it('should skip AutoMap validation for arithmetic-only events', () => {
96
- const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
97
- setModelSchema(compiled, properties => { properties.name.format = 'float'; });
98
- (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
143
+ setModelSchema(compiled, properties => { properties.Id = { type: 'string' }; });
144
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation,
145
+ 'case-insensitively colliding read-model properties require a kernel-backed test');
99
146
  });
100
147
 
101
148
  for (const [expression, destination, reason] of [
102
149
  ['$value(maybe)', 'state', '$value boolean literal'],
103
150
  ['$value(not-a-guid)', 'state', '$value GUID literal'],
104
- ['$value(text)', 'details', '$value object/array literals'],
105
- ['$value(text)', 'labels', '$value object/array literals']
151
+ ['$value(text)', 'details', 'object/array target mappings require a kernel-backed test'],
152
+ ['$value(text)', 'labels', 'object/array target mappings require a kernel-backed test']
106
153
  ]) {
107
154
  it(`should reject invalid ${expression} for ${destination} (${reason})`, () => {
108
155
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
@@ -3,17 +3,12 @@
3
3
 
4
4
  import 'reflect-metadata';
5
5
  import { chai, describe, it } from 'vitest';
6
- import { addFrom } from '../../../projections/modelBound/addFrom.js';
7
6
  import { clearWith } from '../../../projections/modelBound/clearWith.js';
8
- import { count } from '../../../projections/modelBound/count.js';
9
- import { decrement } from '../../../projections/modelBound/decrement.js';
10
- import { increment } from '../../../projections/modelBound/increment.js';
11
7
  import { noAutoMap } from '../../../projections/modelBound/noAutoMap.js';
12
8
  import { removedWith } from '../../../projections/modelBound/removedWith.js';
13
9
  import { setFrom } from '../../../projections/modelBound/setFrom.js';
14
10
  import { setFromContext } from '../../../projections/modelBound/setFromContext.js';
15
11
  import { setValue } from '../../../projections/modelBound/setValue.js';
16
- import { subtractFrom } from '../../../projections/modelBound/subtractFrom.js';
17
12
  import { ProjectionCapabilities } from '../ProjectionCapabilities.js';
18
13
  import { Changed } from './given/Changed.js';
19
14
  import { Removed } from './given/Removed.js';
@@ -25,16 +20,11 @@ describe('when validating the phase-one projection subset', () => {
25
20
  const modelBound = [
26
21
  { name: 'root From and schema-inferred AutoMap', configure: () => {} },
27
22
  { name: 'setFrom and event source identity', configure: (model: Function) => setFrom(Changed, 'name')(model.prototype, 'name') },
28
- { name: 'setFromContext maps eventType without needing it in the event schema', configure: (model: Function) => setFromContext(Changed, 'eventType')(model.prototype, 'state') },
23
+ { name: 'setFromContext maps eventSourceId without needing it in the event schema', configure: (model: Function) => setFromContext(Changed, 'eventSourceId')(model.prototype, 'state') },
29
24
  { name: 'setFromContext maps the event source identity', configure: (model: Function) => setFromContext(Changed, 'eventSourceId')(model.prototype, 'state') },
30
25
  { name: 'setFromContext maps a nested identity member', configure: (model: Function) => setFromContext(Changed, 'causedBy.subject')(model.prototype, 'state') },
31
26
  { name: 'constant text', configure: (model: Function) => setValue(Changed, 'ready')(model.prototype, 'state') },
32
27
  { name: 'scalar clearing', configure: (model: Function) => clearWith(Removed)(model.prototype, 'state') },
33
- { name: 'addition', configure: (model: Function) => addFrom(Changed, 'quantity')(model.prototype, 'total') },
34
- { name: 'subtraction', configure: (model: Function) => subtractFrom(Changed, 'quantity')(model.prototype, 'total') },
35
- { name: 'count', configure: (model: Function) => count(Changed)(model.prototype, 'total') },
36
- { name: 'increment', configure: (model: Function) => increment(Changed)(model.prototype, 'total') },
37
- { name: 'decrement', configure: (model: Function) => decrement(Changed)(model.prototype, 'total') },
38
28
  { name: 'root removal', configure: (model: Function) => removedWith(Removed)(model) },
39
29
  { name: 'AutoMap exclusions', configure: (model: Function) => noAutoMap(model.prototype, 'quantity') }
40
30
  ];
@@ -45,15 +35,11 @@ describe('when validating the phase-one projection subset', () => {
45
35
  });
46
36
  }
47
37
 
48
- it('should accept declarative multiple root sources, explicit mappings, constants, arithmetic, and removal', () => {
38
+ it('should accept declarative explicit mappings, constants, and a separate removal event', () => {
49
39
  const { compiled, definition } = compileDeclarative(builder => builder
50
40
  .from(Changed, from => from.set(model => model.name).to(event => event.name)
51
- .set(model => model.id).toEventSourceId()
52
- .set(model => model.state).toValue('ready')
53
- .add(model => model.total).with(event => event.quantity)
54
- .subtract(model => model.total).with(event => event.quantity)
55
- .increment(model => model.total).decrement(model => model.total).count(model => model.total))
56
- .from(Removed).removedWith(Removed).withInitialValues(() => ({ id: '', name: '', quantity: 0, total: 0, state: 'new', labels: [], details: {} })));
41
+ .set(model => model.state).toValue('ready'))
42
+ .removedWith(Removed).withInitialValues(() => ({ id: '', name: '', quantity: 0, total: 0, state: 'new' })));
57
43
  (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
58
44
  });
59
45
 
@@ -67,17 +53,10 @@ describe('when validating the phase-one projection subset', () => {
67
53
  ['eventStreamId', '$eventContext(EventStreamId)'],
68
54
  ['subject', '$eventContext(Subject)'],
69
55
  ['hash', '$eventContext(Hash)'],
70
- ['causedBy', '$eventContext(CausedBy)'],
71
- ['observationState', '$eventContext(ObservationState)'],
72
- ['eventType', '$eventContext(EventType)'],
73
- ['occurred', '$eventContext(Occurred)'],
74
56
  ['correlationId', '$eventContext(CorrelationId)'],
75
- ['causation', '$eventContext(Causation)'],
76
- ['tags', '$eventContext(Tags)'],
77
57
  ['causedBy.subject', '$eventContext(CausedBy.Subject)'],
78
58
  ['causedBy.name', '$eventContext(CausedBy.Name)'],
79
- ['causedBy.userName', '$eventContext(CausedBy.UserName)'],
80
- ['causedBy.onBehalfOf', '$eventContext(CausedBy.OnBehalfOf)']
59
+ ['causedBy.userName', '$eventContext(CausedBy.UserName)']
81
60
  ]) {
82
61
  it(`should accept the client's emitted context expression ${expression}`, () => {
83
62
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from =>
@@ -87,17 +66,11 @@ describe('when validating the phase-one projection subset', () => {
87
66
  });
88
67
  }
89
68
 
90
- it('should accept declarative null clearing and an explicit null initial accumulator', () => {
69
+ it('should accept declarative null clearing with numeric initial state', () => {
91
70
  const { compiled, definition } = compileDeclarative(builder => builder
92
- .from(Changed, from => from.set(model => model.state).toValue(null).count(model => model.total))
93
- .withInitialValues(() => ({ id: '', name: '', quantity: 0, total: null as unknown as number, state: '', labels: [], details: {} })));
71
+ .from(Changed, from => from.set(model => model.state).toValue(null))
72
+ .withInitialValues(() => ({ id: '', name: '', quantity: 0, total: 0, state: '' })));
94
73
  (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
95
74
  });
96
75
 
97
- it('should accept an ordinary object or array value without mistaking it for a child projection', () => {
98
- const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from
99
- .set(model => model.labels).to(event => event.labels)
100
- .set(model => model.details).to(event => event.details)));
101
- (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
102
- });
103
76
  });