@cratis/chronicle 6.34.0 → 6.36.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/ChronicleClient.ts +8 -2
- package/EventStore.ts +8 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +10 -2
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +8 -0
- package/dist/EventStore.js.map +1 -1
- package/dist/events/handles.d.ts +12 -0
- package/dist/events/handles.d.ts.map +1 -0
- package/dist/events/handles.js +33 -0
- package/dist/events/handles.js.map +1 -0
- package/dist/events/index.d.ts +1 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +1 -0
- package/dist/events/index.js.map +1 -1
- package/dist/observation/EventHandler.d.ts +7 -0
- package/dist/observation/EventHandler.d.ts.map +1 -0
- package/dist/observation/EventHandler.js +4 -0
- package/dist/observation/EventHandler.js.map +1 -0
- package/dist/observation/createHandlerInstanceValidator.d.ts +4 -0
- package/dist/observation/createHandlerInstanceValidator.d.ts.map +1 -0
- package/dist/observation/createHandlerInstanceValidator.js +40 -0
- package/dist/observation/createHandlerInstanceValidator.js.map +1 -0
- package/dist/observation/getEventHandlers.d.ts +5 -0
- package/dist/observation/getEventHandlers.d.ts.map +1 -0
- package/dist/observation/getEventHandlers.js +40 -0
- package/dist/observation/getEventHandlers.js.map +1 -0
- package/dist/observation/getInstanceMethods.d.ts +4 -0
- package/dist/observation/getInstanceMethods.d.ts.map +1 -0
- package/dist/observation/getInstanceMethods.js +29 -0
- package/dist/observation/getInstanceMethods.js.map +1 -0
- package/dist/reactors/ReactorDispatcher.d.ts.map +1 -1
- package/dist/reactors/ReactorDispatcher.js +27 -32
- package/dist/reactors/ReactorDispatcher.js.map +1 -1
- package/dist/reactors/Reactors.d.ts +1 -0
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +27 -14
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/reactors/index.d.ts +1 -0
- package/dist/reactors/index.d.ts.map +1 -1
- package/dist/reactors/index.js +1 -0
- package/dist/reactors/index.js.map +1 -1
- package/dist/reducers/ReducerEventDispatcher.d.ts +1 -0
- package/dist/reducers/ReducerEventDispatcher.d.ts.map +1 -1
- package/dist/reducers/ReducerEventDispatcher.js +5 -17
- package/dist/reducers/ReducerEventDispatcher.js.map +1 -1
- package/dist/reducers/ReducerEventHandler.d.ts +3 -6
- package/dist/reducers/ReducerEventHandler.d.ts.map +1 -1
- package/dist/reducers/Reducers.d.ts +1 -0
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +22 -10
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/reducers/index.d.ts +1 -0
- package/dist/reducers/index.d.ts.map +1 -1
- package/dist/reducers/index.js +1 -0
- package/dist/reducers/index.js.map +1 -1
- package/dist/testing/ReactorScenario.d.ts +1 -0
- package/dist/testing/ReactorScenario.d.ts.map +1 -1
- package/dist/testing/ReactorScenario.js +9 -1
- package/dist/testing/ReactorScenario.js.map +1 -1
- package/dist/testing/ReducerReadModelProcessor.d.ts.map +1 -1
- package/dist/testing/ReducerReadModelProcessor.js +1 -0
- package/dist/testing/ReducerReadModelProcessor.js.map +1 -1
- package/dist/testing/projections/ProjectionArithmetic.d.ts +11 -0
- package/dist/testing/projections/ProjectionArithmetic.d.ts.map +1 -0
- package/dist/testing/projections/ProjectionArithmetic.js +70 -0
- package/dist/testing/projections/ProjectionArithmetic.js.map +1 -0
- package/dist/testing/projections/ProjectionCapabilities.d.ts.map +1 -1
- package/dist/testing/projections/ProjectionCapabilities.js +26 -10
- package/dist/testing/projections/ProjectionCapabilities.js.map +1 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.d.ts.map +1 -1
- package/dist/testing/projections/ProjectionReadModelProcessor.js +10 -3
- package/dist/testing/projections/ProjectionReadModelProcessor.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/events/for_handles/when_decorating_a_method.legacy.spec.ts +91 -0
- package/events/for_handles/when_decorating_a_method.standard.spec.ts +102 -0
- package/events/handles.ts +37 -0
- package/events/index.ts +1 -0
- package/for_ChronicleClient/when_registering_artifacts/with_invalid_handler_mappings.spec.ts +142 -0
- package/observation/EventHandler.ts +9 -0
- package/observation/createHandlerInstanceValidator.ts +42 -0
- package/observation/for_getEventHandlers/when_discovering_handlers.spec.ts +137 -0
- package/observation/for_getEventHandlers/when_reobserving_explicit_handlers.spec.ts +73 -0
- package/observation/getEventHandlers.ts +43 -0
- package/observation/getInstanceMethods.ts +29 -0
- package/package.json +2 -2
- package/reactors/ReactorDispatcher.ts +26 -30
- package/reactors/Reactors.ts +32 -16
- package/reactors/decorators.standard.spec.ts +8 -0
- package/reactors/for_ReactorDispatcher/when_selecting_explicit_handlers.spec.ts +64 -0
- package/reactors/for_Reactors/when_activating_a_field_override.spec.ts +62 -0
- package/reactors/for_Reactors/when_delivering_to_instance_handlers.spec.ts +108 -0
- package/reactors/for_Reactors/when_observing_explicit_handlers.legacy.spec.ts +77 -0
- package/reactors/for_Reactors/when_registering_explicit_handlers.spec.ts +53 -0
- package/reactors/index.ts +1 -0
- package/reducers/ReducerEventDispatcher.ts +5 -16
- package/reducers/ReducerEventHandler.ts +4 -6
- package/reducers/Reducers.ts +24 -12
- package/reducers/for_ReducerEventDispatcher/when_invoking_an_explicit_handler.spec.ts +51 -0
- package/reducers/for_ReducerEventDispatcher/when_invoking_instance_handlers.spec.ts +69 -0
- package/reducers/for_Reducers/when_activating_a_field_override.spec.ts +63 -0
- package/reducers/for_Reducers/when_observing_explicit_handlers.legacy.spec.ts +73 -0
- package/reducers/for_Reducers/when_registering_explicit_handlers.spec.ts +57 -0
- package/reducers/index.ts +1 -0
- package/testing/ReactorScenario.ts +9 -1
- package/testing/ReducerReadModelProcessor.ts +1 -0
- package/testing/for_ReactorScenario/when_delivering_to_an_explicit_handler.spec.ts +100 -0
- package/testing/for_ReadModelScenario/when_evaluating_arithmetic/with_a_fluent_projection.spec.ts +85 -0
- package/testing/for_ReadModelScenario/when_evaluating_arithmetic/with_legacy_decorators.legacy.spec.ts +91 -0
- package/testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts +92 -0
- package/testing/for_ReducerReadModelProcessor/when_an_instance_field_hides_a_handler.spec.ts +29 -0
- package/testing/projections/ProjectionArithmetic.ts +71 -0
- package/testing/projections/ProjectionCapabilities.ts +27 -9
- package/testing/projections/ProjectionReadModelProcessor.fixtures.spec.ts +9 -6
- package/testing/projections/ProjectionReadModelProcessor.ts +9 -3
- package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +3 -3
- package/testing/projections/for_ProjectionCapabilities/when_validating_arithmetic.spec.ts +127 -0
- package/testing/projections/for_ProjectionCapabilities/when_validating_numeric_schemas.spec.ts +4 -5
|
@@ -0,0 +1,92 @@
|
|
|
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 { eventType } from '../../../events/eventTypeDecorator.js';
|
|
7
|
+
import { addFrom } from '../../../projections/modelBound/addFrom.js';
|
|
8
|
+
import { subtractFrom } from '../../../projections/modelBound/subtractFrom.js';
|
|
9
|
+
import { count } from '../../../projections/modelBound/count.js';
|
|
10
|
+
import { increment } from '../../../projections/modelBound/increment.js';
|
|
11
|
+
import { decrement } from '../../../projections/modelBound/decrement.js';
|
|
12
|
+
import { setFrom } from '../../../projections/modelBound/setFrom.js';
|
|
13
|
+
import { ReadModelScenario } from '../../ReadModelScenario.js';
|
|
14
|
+
import { UnsupportedProjectionOperation } from '../../projections/UnsupportedProjectionOperation.js';
|
|
15
|
+
|
|
16
|
+
chai.should();
|
|
17
|
+
function should(value: unknown): Assertion { return (value as { should: Assertion }).should; }
|
|
18
|
+
|
|
19
|
+
@eventType('standard-arithmetic-added')
|
|
20
|
+
class Added {
|
|
21
|
+
@field(Number) amount: number;
|
|
22
|
+
@field(String) title = 'not mapped';
|
|
23
|
+
constructor(amount: number) { this.amount = amount; }
|
|
24
|
+
}
|
|
25
|
+
@eventType('standard-arithmetic-subtracted')
|
|
26
|
+
class Subtracted {
|
|
27
|
+
@field(Number) amount: number;
|
|
28
|
+
constructor(amount: number) { this.amount = amount; }
|
|
29
|
+
}
|
|
30
|
+
@eventType('standard-arithmetic-renamed')
|
|
31
|
+
class Renamed {
|
|
32
|
+
@field(Number) amount = 1;
|
|
33
|
+
@field(String) caption = 'explicit';
|
|
34
|
+
@field(String) title = 'automapped';
|
|
35
|
+
}
|
|
36
|
+
class Totals {
|
|
37
|
+
@field(String) id = '';
|
|
38
|
+
@field(Number) @addFrom(Added, 'amount') @subtractFrom(Subtracted, 'amount') @addFrom(Renamed, 'amount') total = 0;
|
|
39
|
+
@field(Number) @addFrom(Added) @subtractFrom(Subtracted) amount = 0;
|
|
40
|
+
@field(Number) @count(Added) counted = 0;
|
|
41
|
+
@field(Number) @increment(Added) incremented = 0;
|
|
42
|
+
@field(Number) @decrement(Added) decremented = 0;
|
|
43
|
+
@field(String) @setFrom(Renamed, 'caption') label = '';
|
|
44
|
+
@field(String) title = '';
|
|
45
|
+
}
|
|
46
|
+
const artifacts = { eventTypes: [Added, Subtracted, Renamed], readModels: [Totals], reducers: [], projections: [] };
|
|
47
|
+
|
|
48
|
+
// The same operations are captured in arithmetic.json, arithmetic-numeric-matrix.json and arithmetic-automap.json.
|
|
49
|
+
describe('when evaluating arithmetic with standard model-bound decorators', () => {
|
|
50
|
+
for (const [operation, decorate] of [['add', addFrom], ['subtract', subtractFrom]] as const) {
|
|
51
|
+
it(`should reject @${operation}From with a snake_case operand before any events are seeded`, () => {
|
|
52
|
+
@eventType(`standard-arithmetic-snake-case-${operation}`)
|
|
53
|
+
class PriceChanged {
|
|
54
|
+
@field(Number) unit_price = 2;
|
|
55
|
+
}
|
|
56
|
+
class PriceTotals {
|
|
57
|
+
@field(String) id = '';
|
|
58
|
+
@field(Number) @decorate(PriceChanged, 'unit_price') total = 0;
|
|
59
|
+
}
|
|
60
|
+
should(() => new ReadModelScenario(PriceTotals, { eventTypes: [PriceChanged], readModels: [PriceTotals], reducers: [], projections: [] }))
|
|
61
|
+
.throw(UnsupportedProjectionOperation).with.property('message')
|
|
62
|
+
.that.includes('kernel only permits an underscore as the first character')
|
|
63
|
+
.and.includes('https://github.com/Cratis/Chronicle/issues/4491');
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
it('should accumulate all five operations and default event property names', async () => {
|
|
68
|
+
const scenario = new ReadModelScenario(Totals, artifacts);
|
|
69
|
+
scenario.given.forEventSource('A').events(new Added(2.5), new Added(3), new Subtracted(1));
|
|
70
|
+
const result = (await scenario.instance)!;
|
|
71
|
+
should([result.total, result.amount, result.counted, result.incremented, result.decremented]).deep.equal([4.5, 4.5, 2, 2, -2]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should replay additional seeds without counting the previous replay twice and keep sources independent', async () => {
|
|
75
|
+
const scenario = new ReadModelScenario(Totals, artifacts);
|
|
76
|
+
scenario.given.forEventSource('A').events(new Added(2));
|
|
77
|
+
should((await scenario.instance)!.total).equal(2);
|
|
78
|
+
scenario.given.forEventSource('B').events(new Added(7));
|
|
79
|
+
scenario.given.forEventSource('A').events(new Added(3));
|
|
80
|
+
should((await scenario.instanceForEventSourceId('A'))!.total).equal(5);
|
|
81
|
+
should((await scenario.instanceForEventSourceId('B'))!.total).equal(7);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should suppress aggregate-only AutoMap and restore it for mixed mappings', async () => {
|
|
85
|
+
const scenario = new ReadModelScenario(Totals, artifacts);
|
|
86
|
+
scenario.given.forEventSource('A').events(new Added(2));
|
|
87
|
+
should((await scenario.instance)!.title === undefined).be.true;
|
|
88
|
+
scenario.given.forEventSource('A').events(new Renamed());
|
|
89
|
+
const result = (await scenario.instance)!;
|
|
90
|
+
should([result.total, result.title, result.label]).deep.equal([3, 'automapped', 'explicit']);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
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 { chai, describe, it } from 'vitest';
|
|
5
|
+
import { eventType } from '../../events/eventTypeDecorator.js';
|
|
6
|
+
import { handles } from '../../events/handles.js';
|
|
7
|
+
import { ReducerReadModelProcessor } from '../ReducerReadModelProcessor.js';
|
|
8
|
+
|
|
9
|
+
chai.should();
|
|
10
|
+
|
|
11
|
+
@eventType('scenario-field-reducer')
|
|
12
|
+
class AuthorRegistered {}
|
|
13
|
+
class Model {}
|
|
14
|
+
class Base {
|
|
15
|
+
@handles(AuthorRegistered) update() { return new Model(); }
|
|
16
|
+
}
|
|
17
|
+
class Derived extends Base {
|
|
18
|
+
override update = () => new Model();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('when a reducer scenario instance field hides an inherited handler', () => {
|
|
22
|
+
it('should reject the instance before processing events', async () => {
|
|
23
|
+
const processor = new ReducerReadModelProcessor(Model, Derived, [AuthorRegistered]);
|
|
24
|
+
const [result] = await Promise.allSettled([processor.process([])]);
|
|
25
|
+
result.status.should.equal('rejected');
|
|
26
|
+
if (result.status === 'rejected') (result.reason as Error).message.should.equal(
|
|
27
|
+
"Override 'update' on 'Derived' hides @handles(AuthorRegistered) declared on 'Base'; use a method with @handles instead of an instance field.");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
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 type { JsonSchema } from '../../schemas/JsonSchema.js';
|
|
5
|
+
import { ProjectionExpressionEvaluator } from './ProjectionExpressionEvaluator.js';
|
|
6
|
+
import { ProjectionValueConverter } from './ProjectionValueConverter.js';
|
|
7
|
+
|
|
8
|
+
/** The root arithmetic subset captured by the packaged-kernel arithmetic fixtures. */
|
|
9
|
+
export class ProjectionArithmetic {
|
|
10
|
+
static isArithmetic(expression: string): boolean {
|
|
11
|
+
return /^\$(?:add|subtract)\(/.test(expression) || ['$count', '$increment', '$decrement'].includes(expression);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The kernel suppresses AutoMap for a nonempty set of aggregate-only mappings. */
|
|
15
|
+
static suppressesAutoMap(properties: Record<string, string>): boolean {
|
|
16
|
+
const expressions = Object.values(properties);
|
|
17
|
+
return expressions.length > 0 && expressions.every(expression => this.isArithmetic(expression));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static validate(expression: string, target: JsonSchema, eventSchema: JsonSchema, fail: (reason: string) => never): void {
|
|
21
|
+
if (!((target.type === 'number' && (!target.format || target.format === 'double')) ||
|
|
22
|
+
(target.type === 'integer' && target.format === 'int32'))) {
|
|
23
|
+
fail('arithmetic targets must be number/double or integer/int32; other targets require a kernel-backed test');
|
|
24
|
+
}
|
|
25
|
+
if (['$count', '$increment', '$decrement'].includes(expression)) return;
|
|
26
|
+
const operand = /^\$(?:add|subtract)\(([^()]+)\)$/.exec(expression)?.[1];
|
|
27
|
+
if (operand?.slice(1).includes('_')) {
|
|
28
|
+
fail('the Chronicle 19.26.2 kernel only permits an underscore as the first character of an arithmetic operand; snake_case operands are unsupported (https://github.com/Cratis/Chronicle/issues/4491)');
|
|
29
|
+
}
|
|
30
|
+
// Direct-property subset of EventValueProviderRegularExpressions.Expression in Chronicle 19.26.2.
|
|
31
|
+
// AddExpressionResolver and SubtractExpressionResolver anchor that grammar; nested/$ operands remain unproven.
|
|
32
|
+
const source = /^\$(?:add|subtract)\(([A-Za-z_][A-Za-z0-9]*)\)$/.exec(expression)?.[1];
|
|
33
|
+
if (!source || ['true', 'True', 'false', 'False'].includes(source)) {
|
|
34
|
+
fail('arithmetic operands must be direct numeric event properties; nested paths, literals and context require a kernel-backed test');
|
|
35
|
+
}
|
|
36
|
+
const schema = eventSchema.properties?.[source!];
|
|
37
|
+
if (!schema) fail(`arithmetic event property '${source}' is absent from the participating event schema`);
|
|
38
|
+
if (schema!.compliance?.length || schema!.security?.length) fail('protected arithmetic operands require a kernel-backed test');
|
|
39
|
+
if (!((schema!.type === 'number' && (!schema!.format || schema!.format === 'double')) ||
|
|
40
|
+
(schema!.type === 'integer' && ['int32', 'uint32'].includes(schema!.format ?? '')))) {
|
|
41
|
+
fail('arithmetic operands must have number/double, integer/int32 or integer/uint32 schemas; non-numeric and other operands require a kernel-backed test');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static value(expression: string, content: unknown, target: JsonSchema, current: unknown): number {
|
|
46
|
+
const operation = /^\$(add|subtract)\(([^()]+)\)$/.exec(expression);
|
|
47
|
+
const operand = operation ? ProjectionExpressionEvaluator.pathValue(content, operation[2]) : 1;
|
|
48
|
+
// Absent accumulators start at zero, unlike explicitly cleared (null) accumulators.
|
|
49
|
+
if (current === null) throw new RangeError('Projection arithmetic cannot use a null accumulator.');
|
|
50
|
+
const previous = current === undefined ? 0 : this.number(current, target);
|
|
51
|
+
const amount = this.number(operand, target);
|
|
52
|
+
const result = operation?.[1] === 'subtract' || expression === '$decrement' ? previous - amount : previous + amount;
|
|
53
|
+
// Int32 arithmetic is unchecked in the kernel; operand conversion is checked and rounds ties to even.
|
|
54
|
+
if (target.format === 'int32') return result | 0;
|
|
55
|
+
ProjectionValueConverter.checkNumber(result, target);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private static number(value: unknown, target: JsonSchema): number {
|
|
60
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
61
|
+
throw new RangeError('Projection arithmetic requires finite numeric values.');
|
|
62
|
+
}
|
|
63
|
+
let number = value;
|
|
64
|
+
if (target.format === 'int32') {
|
|
65
|
+
const lower = Math.floor(value);
|
|
66
|
+
number = value - lower === 0.5 ? (lower % 2 === 0 ? lower : lower + 1) : Math.round(value);
|
|
67
|
+
}
|
|
68
|
+
ProjectionValueConverter.checkNumber(number, target);
|
|
69
|
+
return number;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -8,6 +8,7 @@ import { eventContractPath } from '../../projections/eventContractPath.js';
|
|
|
8
8
|
import type { ContractEventType, FromRecord, RemovedWithRecord } from '../../projections/declarative/ProjectionBuilderCore.js';
|
|
9
9
|
import { getEventTypeMapKey, type ChildrenDefinitionLike } from '../../projections/modelBound/childrenAndNestedBuilder.js';
|
|
10
10
|
import type { JsonSchema } from '../../schemas/JsonSchema.js';
|
|
11
|
+
import { ProjectionArithmetic } from './ProjectionArithmetic.js';
|
|
11
12
|
import { ProjectionChildrenCapabilities } from './ProjectionChildrenCapabilities.js';
|
|
12
13
|
import { UnsupportedProjectionOperation } from './UnsupportedProjectionOperation.js';
|
|
13
14
|
|
|
@@ -95,6 +96,19 @@ export class ProjectionCapabilities {
|
|
|
95
96
|
reject(eventContractPath('From', entry.Key), 'events subscribed through both From and RemovedWith require a kernel-backed test (ChronicleKernelScenario / live kernel)');
|
|
96
97
|
}
|
|
97
98
|
}
|
|
99
|
+
const accumulators = new Set(from.flatMap(entry => Object.entries(entry.Value.Properties ?? {})
|
|
100
|
+
.filter(([, expression]) => ProjectionArithmetic.isArithmetic(expression)).map(([property]) => property)));
|
|
101
|
+
if (accumulators.size && Object.keys(wire.Children as object ?? {}).length) {
|
|
102
|
+
reject('Children', 'root arithmetic combined with children requires a kernel-backed test');
|
|
103
|
+
}
|
|
104
|
+
for (const entry of from) {
|
|
105
|
+
for (const [property, expression] of Object.entries(entry.Value.Properties ?? {})) {
|
|
106
|
+
if (expression === '$null' && accumulators.has(property)) {
|
|
107
|
+
reject(`${eventContractPath('From', entry.Key)}.Properties.${property}`,
|
|
108
|
+
'clearing an arithmetic accumulator requires a kernel-backed test; the kernel rejects arithmetic on a present null');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
98
112
|
for (const entry of [...from, ...removedWith]) {
|
|
99
113
|
const section = from.includes(entry as FromRecord) ? 'From' : 'RemovedWith';
|
|
100
114
|
const path = eventContractPath(section, entry.Key);
|
|
@@ -105,10 +119,10 @@ export class ProjectionCapabilities {
|
|
|
105
119
|
const properties = (entry as FromRecord).Value.Properties ?? {};
|
|
106
120
|
for (const [property, expression] of Object.entries(properties)) {
|
|
107
121
|
const mappingPath = `${path}.Properties.${property}`;
|
|
108
|
-
this.checkMapping(schema!, eventSchema, property, expression, mappingPath, reject);
|
|
122
|
+
this.checkMapping(schema!, eventSchema, property, expression, mappingPath, reject, accumulators);
|
|
109
123
|
}
|
|
110
|
-
if (wire.AutoMap !== AutoMap.Disabled) {
|
|
111
|
-
this.checkAutoMap(schema!, eventSchema, properties, wire.NoAutoMapProperties as string[] ?? [], path, reject);
|
|
124
|
+
if (wire.AutoMap !== AutoMap.Disabled && !ProjectionArithmetic.suppressesAutoMap(properties)) {
|
|
125
|
+
this.checkAutoMap(schema!, eventSchema, properties, wire.NoAutoMapProperties as string[] ?? [], path, reject, accumulators);
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
128
|
ProjectionChildrenCapabilities.validate(wire.Children as Record<string, ChildrenDefinitionLike> ?? {}, {
|
|
@@ -121,20 +135,20 @@ export class ProjectionCapabilities {
|
|
|
121
135
|
|
|
122
136
|
private static checkAutoMap(
|
|
123
137
|
modelSchema: JsonSchema, eventSchema: JsonSchema, explicit: Record<string, string>, exclusions: string[],
|
|
124
|
-
path: string, reject: (path: string, reason: string) => never
|
|
138
|
+
path: string, reject: (path: string, reason: string) => never, accumulators: ReadonlySet<string>
|
|
125
139
|
): void {
|
|
126
140
|
for (const [destination] of Object.entries(modelSchema.properties ?? {})) {
|
|
127
141
|
if (Object.keys(explicit).some(name => name.toLowerCase() === destination.toLowerCase()) ||
|
|
128
142
|
exclusions.some(name => name.toLowerCase() === destination.toLowerCase())) continue;
|
|
129
143
|
const candidates = Object.keys(eventSchema.properties ?? {}).filter(source => source.toLowerCase() === destination.toLowerCase());
|
|
130
144
|
if (candidates.length > 1) reject(`${path}.AutoMap.${destination}`, `inferred AutoMap source is ambiguous: ${candidates.join(', ')}`);
|
|
131
|
-
if (candidates.length) this.checkMapping(modelSchema, eventSchema, destination, candidates[0], `${path}.AutoMap.${destination}`, reject);
|
|
145
|
+
if (candidates.length) this.checkMapping(modelSchema, eventSchema, destination, candidates[0], `${path}.AutoMap.${destination}`, reject, accumulators);
|
|
132
146
|
}
|
|
133
147
|
}
|
|
134
148
|
|
|
135
149
|
private static checkMapping(
|
|
136
150
|
modelSchema: JsonSchema, eventSchema: JsonSchema, destination: string, expression: string,
|
|
137
|
-
path: string, reject: (path: string, reason: string) => never
|
|
151
|
+
path: string, reject: (path: string, reason: string) => never, accumulators?: ReadonlySet<string>
|
|
138
152
|
): void {
|
|
139
153
|
const fail = (reason: string): never => reject(path, `expression '${expression}': ${reason}`);
|
|
140
154
|
const target = modelSchema.properties?.[destination]
|
|
@@ -184,9 +198,10 @@ export class ProjectionCapabilities {
|
|
|
184
198
|
}
|
|
185
199
|
return;
|
|
186
200
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
201
|
+
if (ProjectionArithmetic.isArithmetic(expression)) {
|
|
202
|
+
ProjectionArithmetic.validate(expression, target, eventSchema, fail);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
190
205
|
if (/^[A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)*$/.test(expression)) {
|
|
191
206
|
if (['true', 'True', 'false', 'False'].includes(expression)) {
|
|
192
207
|
fail('kernel resolves this expression as a boolean literal before event content');
|
|
@@ -195,6 +210,9 @@ export class ProjectionCapabilities {
|
|
|
195
210
|
const source = this.propertyAt(eventSchema, expression);
|
|
196
211
|
if (!source) return fail(`event property '${expression}' is absent from the participating event schema`);
|
|
197
212
|
this.checkSchema(source, path, (_path, reason) => fail(reason));
|
|
213
|
+
if (accumulators?.has(destination) && !this.numeric(source)) {
|
|
214
|
+
fail('assigning non-numeric event properties to an arithmetic accumulator requires a kernel-backed test; missing values can clear it');
|
|
215
|
+
}
|
|
198
216
|
if (!this.compatible(source, target)) {
|
|
199
217
|
fail(`source '${expression}' (${source.type}${source.format ? `/${source.format}` : ''}) to target '${destination}' (${target.type}${target.format ? `/${target.format}` : ''}) requires a kernel-backed test`);
|
|
200
218
|
}
|
|
@@ -38,11 +38,11 @@ const allFixtures = readdirSync(directory).filter(name => name.endsWith('.json')
|
|
|
38
38
|
.map(name => ({ name, fixture: JSON.parse(readFileSync(new URL(name, directory), 'utf8')) as Fixture }));
|
|
39
39
|
const fixtures = allFixtures.filter(({ fixture }) => fixture.kind === 'kernelSemantics');
|
|
40
40
|
const guardErrors: Record<string, { type: string; message: string }> = {
|
|
41
|
-
'
|
|
42
|
-
'arithmetic.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'null-accumulator.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic requires a kernel-backed test' },
|
|
41
|
+
'arithmetic-string-operand.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic operands must have number/double' },
|
|
42
|
+
'arithmetic-nonnumeric-operand.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic operands must have number/double' },
|
|
43
|
+
'arithmetic-uint32-overflow.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic targets must be number/double or integer/int32' },
|
|
44
|
+
'arithmetic-null-initial.json': { type: 'UnsupportedProjectionOperation', message: 'initial numeric value must be a JSON number' },
|
|
45
|
+
'null-accumulator.json': { type: 'UnsupportedProjectionOperation', message: 'clearing an arithmetic accumulator requires a kernel-backed test' },
|
|
46
46
|
'concept-shaped-objects.json': { type: 'UnsupportedProjectionOperation', message: 'nested event property paths require a kernel-backed test' },
|
|
47
47
|
'mapped-and-removed.json': { type: 'UnsupportedProjectionOperation', message: 'both From and RemovedWith require a kernel-backed test' },
|
|
48
48
|
'mapped-id-key-wins.json': { type: 'UnsupportedProjectionOperation', message: 'identifier or case-insensitively colliding target mappings require a kernel-backed test' },
|
|
@@ -97,9 +97,12 @@ describe('when replaying committed kernel semantics fixtures', () => {
|
|
|
97
97
|
() => { throw new Error('Evaluator accepted a kernel-rejected mapping'); },
|
|
98
98
|
error => {
|
|
99
99
|
(error instanceof RangeError).should.be.true;
|
|
100
|
-
(error as Error).message.should.include(
|
|
100
|
+
(error as Error).message.should.include(input.expectedError!.type === 'System.OverflowException'
|
|
101
|
+
? 'outside the supported integer/int32 range' : 'not supported by integer/int32');
|
|
101
102
|
}
|
|
102
103
|
);
|
|
104
|
+
processor.engineState.should.deep.equal(expected.engineState);
|
|
105
|
+
processor.publicRead.should.deep.equal(expected.publicRead);
|
|
103
106
|
} else {
|
|
104
107
|
await processor.process(events.slice(0, step + 1));
|
|
105
108
|
processor.engineState.should.deep.equal(expected.engineState);
|
|
@@ -12,6 +12,7 @@ import type { JsonSchema } from '../../schemas/JsonSchema.js';
|
|
|
12
12
|
import type { IReadModelProcessor } from '../IReadModelProcessor.js';
|
|
13
13
|
import type { ReadModelState } from '../ReadModelState.js';
|
|
14
14
|
import type { ScenarioEvent } from '../ScenarioEvent.js';
|
|
15
|
+
import { ProjectionArithmetic } from './ProjectionArithmetic.js';
|
|
15
16
|
import { ProjectionCapabilities } from './ProjectionCapabilities.js';
|
|
16
17
|
import { ProjectionExpressionEvaluator } from './ProjectionExpressionEvaluator.js';
|
|
17
18
|
import { ProjectionValueConverter } from './ProjectionValueConverter.js';
|
|
@@ -107,7 +108,8 @@ export class ProjectionReadModelProcessor<TReadModel extends object> implements
|
|
|
107
108
|
if (from) {
|
|
108
109
|
const explicit = { ...from.Value.Properties };
|
|
109
110
|
const excluded = (this._definition.NoAutoMapProperties ?? []);
|
|
110
|
-
const properties = this._definition.AutoMap !== AutoMap.Disabled
|
|
111
|
+
const properties = this._definition.AutoMap !== AutoMap.Disabled && !ProjectionArithmetic.suppressesAutoMap(explicit)
|
|
112
|
+
? this.withAutoMap(explicit, schema, this._schema, excluded) : explicit;
|
|
111
113
|
this.apply(properties, content, event, this._schema, state);
|
|
112
114
|
}
|
|
113
115
|
if (child) this.applyChild(child, content, event, schema, state, binding);
|
|
@@ -168,8 +170,12 @@ export class ProjectionReadModelProcessor<TReadModel extends object> implements
|
|
|
168
170
|
|
|
169
171
|
private apply(properties: Record<string, string>, content: unknown, event: ScenarioEvent, target: JsonSchema, state: Record<string, unknown>): void {
|
|
170
172
|
for (const [destination, expression] of Object.entries(properties)) {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
+
const arithmetic = ProjectionArithmetic.isArithmetic(expression);
|
|
174
|
+
const value = arithmetic
|
|
175
|
+
? ProjectionArithmetic.value(expression, content, target.properties![destination], state[destination])
|
|
176
|
+
: ProjectionExpressionEvaluator.value(expression, content, event.context, target.properties![destination]);
|
|
177
|
+
// Zero arithmetic on an absent accumulator is no change, just like null on an absent scalar.
|
|
178
|
+
if (arithmetic && value === 0 && state[destination] === undefined) continue;
|
|
173
179
|
if (value !== null || state[destination] !== undefined) state[destination] = value;
|
|
174
180
|
}
|
|
175
181
|
}
|
package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts
CHANGED
|
@@ -148,11 +148,11 @@ describe('when rejecting unsupported operations before any event is seeded', ()
|
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
for (const expression of ['$add(quantity)', '$subtract(quantity)', '$count', '$increment', '$decrement']) {
|
|
151
|
-
it(`should reject
|
|
151
|
+
it(`should reject arithmetic ${expression} on non-numeric targets before replay`, () => {
|
|
152
152
|
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.set(model => model.total).toValue(0)));
|
|
153
|
-
definition.From[0].Value.Properties
|
|
153
|
+
definition.From[0].Value.Properties = { name: expression };
|
|
154
154
|
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
155
|
-
.with.property('message').that.includes('arithmetic
|
|
155
|
+
.with.property('message').that.includes('arithmetic targets must be number/double or integer/int32');
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -0,0 +1,127 @@
|
|
|
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 { chai, describe, it } from 'vitest';
|
|
5
|
+
import { count } from '../../../projections/modelBound/count.js';
|
|
6
|
+
import { increment } from '../../../projections/modelBound/increment.js';
|
|
7
|
+
import { decrement } from '../../../projections/modelBound/decrement.js';
|
|
8
|
+
import type { JsonSchema } from '../../../schemas/JsonSchema.js';
|
|
9
|
+
import { ProjectionCapabilities } from '../ProjectionCapabilities.js';
|
|
10
|
+
import { UnsupportedProjectionOperation } from '../UnsupportedProjectionOperation.js';
|
|
11
|
+
import { Changed } from './given/Changed.js';
|
|
12
|
+
import { Removed } from './given/Removed.js';
|
|
13
|
+
import { compileDeclarative, compileModelBound } from './given/compile.js';
|
|
14
|
+
|
|
15
|
+
chai.should();
|
|
16
|
+
|
|
17
|
+
describe('when validating arithmetic before replay', () => {
|
|
18
|
+
for (const expression of ['$add(quantity)', '$subtract(quantity)', '$count', '$increment', '$decrement']) {
|
|
19
|
+
it(`should admit ${expression} on number fields without seeding events`, () => {
|
|
20
|
+
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
|
|
21
|
+
definition.From[0].Value.Properties.total = expression;
|
|
22
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const expression of ['$add(name)', '$subtract(name)', '$add(missing)', '$add(inner.quantity)', '$subtract($eventSourceId)', '$add($value(1))', '$add(true)', '$add()']) {
|
|
27
|
+
it(`should reject unproven arithmetic operands in ${expression} with the contract path`, () => {
|
|
28
|
+
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
|
|
29
|
+
definition.From[0].Value.Properties.total = expression;
|
|
30
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
31
|
+
.with.property('message').that.includes('From[capability-changed:1].Properties.total (.from().addFrom)').and.includes('arithmetic');
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const operation of ['add', 'subtract']) {
|
|
36
|
+
for (const property of ['unit_price', '_unit_price']) {
|
|
37
|
+
it(`should reject $${operation}(${property}) with the kernel operand limitation before replay`, () => {
|
|
38
|
+
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
|
|
39
|
+
compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties![property] = { type: 'number' };
|
|
40
|
+
definition.From[0].Value.Properties.total = `$${operation}(${property})`;
|
|
41
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
42
|
+
.with.property('message').that.includes('From[capability-changed:1].Properties.total')
|
|
43
|
+
.and.includes('kernel only permits an underscore as the first character')
|
|
44
|
+
.and.includes('https://github.com/Cratis/Chronicle/issues/4491');
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
it(`should admit $${operation}(_quantity) with a leading underscore`, () => {
|
|
48
|
+
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
|
|
49
|
+
compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!._quantity = { type: 'number' };
|
|
50
|
+
definition.From[0].Value.Properties.total = `$${operation}(_quantity)`;
|
|
51
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const target of [{ type: 'integer', format: 'uint32' }, { type: ['number', 'null'] }, { type: 'string' }, { type: 'boolean' }]) {
|
|
56
|
+
it(`should reject an unproven ${JSON.stringify(target)} accumulator schema`, () => {
|
|
57
|
+
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.increment(model => model.total)));
|
|
58
|
+
const schema = JSON.parse(compiled.readModels[0].Schema) as JsonSchema;
|
|
59
|
+
schema.properties!.total = target as JsonSchema;
|
|
60
|
+
compiled.readModels[0].Schema = JSON.stringify(schema);
|
|
61
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
62
|
+
.with.property('message').that.includes('From[capability-changed:1].Properties.total');
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const source of [{ type: 'boolean' }, { type: 'object' }, { type: ['number', 'null'] }, { type: 'number', format: 'decimal' }, { type: 'integer', format: 'int64' }]) {
|
|
67
|
+
it(`should reject an unproven ${JSON.stringify(source)} operand schema`, () => {
|
|
68
|
+
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
|
|
69
|
+
compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.quantity = source as JsonSchema;
|
|
70
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
71
|
+
.with.property('message').that.includes('arithmetic operands must have number/double');
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
it('should reject arithmetic on a protected operand', () => {
|
|
76
|
+
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
|
|
77
|
+
compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.quantity.security = [{ metadataType: 'EncryptedSubject', details: '' }];
|
|
78
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
79
|
+
.with.property('message').that.includes('protected arithmetic operands');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should reject any declaration that can clear an accumulator before arithmetic', () => {
|
|
83
|
+
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.increment(model => model.total))
|
|
84
|
+
.from(Removed, from => from.set(model => model.total).toValue(0)));
|
|
85
|
+
definition.From[1].Value.Properties.total = '$null';
|
|
86
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
87
|
+
.with.property('message').that.includes('From[capability-removed:1].Properties.total').and.includes('clearing an arithmetic accumulator');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should reject a text assignment that can clear an arithmetic accumulator', () => {
|
|
91
|
+
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.set(model => model.total).to(event => event.name))
|
|
92
|
+
.from(Removed, from => from.increment(model => model.total)));
|
|
93
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
94
|
+
.with.property('message').that.includes('assigning non-numeric event properties to an arithmetic accumulator');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should reject inferred text AutoMap that can clear an arithmetic accumulator', () => {
|
|
98
|
+
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed)
|
|
99
|
+
.from(Removed, from => from.increment(model => model.total)));
|
|
100
|
+
compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.total = { type: 'string' };
|
|
101
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
102
|
+
.with.property('message').that.includes('AutoMap.total').and.includes('assigning non-numeric event properties');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should reject root arithmetic combined with children without widening the child boundary', () => {
|
|
106
|
+
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.increment(model => model.total))
|
|
107
|
+
.children(model => model.labels, child => child.from(Removed)));
|
|
108
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
109
|
+
.with.property('message').that.includes('root arithmetic combined with children');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
for (const [name, decorate] of [['count', count], ['increment', increment], ['decrement', decrement]] as const) {
|
|
113
|
+
it(`should keep @${name} constant keys outside the supported source-id shape`, () => {
|
|
114
|
+
const { compiled, definition } = compileModelBound(model => decorate(Removed, 'all')(model.prototype, 'total'));
|
|
115
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
116
|
+
.with.property('message').that.includes(`(@${name})`).and.includes('only $eventSourceId keys');
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
it('should not validate suppressed AutoMap as an executed mapping', () => {
|
|
121
|
+
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.increment(model => model.total)));
|
|
122
|
+
const schema = JSON.parse(compiled.readModels[0].Schema) as JsonSchema;
|
|
123
|
+
schema.properties!.quantity = { type: 'number', format: 'decimal' };
|
|
124
|
+
compiled.readModels[0].Schema = JSON.stringify(schema);
|
|
125
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
|
|
126
|
+
});
|
|
127
|
+
});
|
package/testing/projections/for_ProjectionCapabilities/when_validating_numeric_schemas.spec.ts
CHANGED
|
@@ -12,7 +12,7 @@ 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 a finite ${format} literal
|
|
15
|
+
it(`should accept a finite ${format} literal`, () => {
|
|
16
16
|
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from
|
|
17
17
|
.set(model => model.quantity).toValue(12)));
|
|
18
18
|
const modelSchema = JSON.parse(compiled.readModels[0].Schema) as { properties: { quantity: { type: string; format?: string } } };
|
|
@@ -63,13 +63,12 @@ describe('when validating the numeric capability matrix', () => {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
it('should
|
|
66
|
+
it('should admit fixture-backed rounding of a double event operand into an int32 accumulator', () => {
|
|
67
67
|
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
|
|
68
68
|
const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { total: { type: string; format?: string } } };
|
|
69
69
|
schema.properties.total = { type: 'integer', format: 'int32' };
|
|
70
70
|
compiled.readModels[0].Schema = JSON.stringify(schema);
|
|
71
|
-
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(
|
|
72
|
-
.with.property('message').that.includes('arithmetic requires a kernel-backed test');
|
|
71
|
+
(() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
|
|
73
72
|
});
|
|
74
73
|
|
|
75
74
|
it('should reject an int32 $value literal with a fractional spelling even if its value is integral', () => {
|
|
@@ -96,6 +95,6 @@ describe('when validating the numeric capability matrix', () => {
|
|
|
96
95
|
const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
|
|
97
96
|
compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.quantity.format = 'int64';
|
|
98
97
|
(() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
|
|
99
|
-
.with.property('message').that.includes('arithmetic
|
|
98
|
+
.with.property('message').that.includes('arithmetic operands must have number/double');
|
|
100
99
|
});
|
|
101
100
|
});
|