@cratis/chronicle 6.35.0 → 6.37.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/EventStore.ts +3 -0
- package/IEventStore.ts +4 -0
- package/connection/ChronicleConnection.ts +6 -0
- package/connection/ChronicleServices.ts +2 -0
- package/dist/EventStore.d.ts +2 -0
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +3 -0
- package/dist/EventStore.js.map +1 -1
- package/dist/IEventStore.d.ts +3 -0
- package/dist/IEventStore.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.d.ts +1 -0
- package/dist/connection/ChronicleConnection.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.js +5 -1
- package/dist/connection/ChronicleConnection.js.map +1 -1
- package/dist/connection/ChronicleServices.d.ts +2 -1
- package/dist/connection/ChronicleServices.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/patterns/BehaviorPattern.d.ts +25 -0
- package/dist/patterns/BehaviorPattern.d.ts.map +1 -0
- package/dist/patterns/BehaviorPattern.js +4 -0
- package/dist/patterns/BehaviorPattern.js.map +1 -0
- package/dist/patterns/DayOfWeek.d.ts +11 -0
- package/dist/patterns/DayOfWeek.d.ts.map +1 -0
- package/dist/patterns/DayOfWeek.js +14 -0
- package/dist/patterns/DayOfWeek.js.map +1 -0
- package/dist/patterns/FacetName.d.ts +15 -0
- package/dist/patterns/FacetName.d.ts.map +1 -0
- package/dist/patterns/FacetName.js +18 -0
- package/dist/patterns/FacetName.js.map +1 -0
- package/dist/patterns/FacetSet.d.ts +7 -0
- package/dist/patterns/FacetSet.d.ts.map +1 -0
- package/dist/patterns/FacetSet.js +4 -0
- package/dist/patterns/FacetSet.js.map +1 -0
- package/dist/patterns/IPatterns.d.ts +45 -0
- package/dist/patterns/IPatterns.d.ts.map +1 -0
- package/dist/patterns/IPatterns.js +4 -0
- package/dist/patterns/IPatterns.js.map +1 -0
- package/dist/patterns/PatternMoment.d.ts +11 -0
- package/dist/patterns/PatternMoment.d.ts.map +1 -0
- package/dist/patterns/PatternMoment.js +4 -0
- package/dist/patterns/PatternMoment.js.map +1 -0
- package/dist/patterns/PatternQueryOptions.d.ts +8 -0
- package/dist/patterns/PatternQueryOptions.d.ts.map +1 -0
- package/dist/patterns/PatternQueryOptions.js +4 -0
- package/dist/patterns/PatternQueryOptions.js.map +1 -0
- package/dist/patterns/Patterns.d.ts +32 -0
- package/dist/patterns/Patterns.d.ts.map +1 -0
- package/dist/patterns/Patterns.js +76 -0
- package/dist/patterns/Patterns.js.map +1 -0
- package/dist/patterns/PatternsAtOptions.d.ts +8 -0
- package/dist/patterns/PatternsAtOptions.d.ts.map +1 -0
- package/dist/patterns/PatternsAtOptions.js +4 -0
- package/dist/patterns/PatternsAtOptions.js.map +1 -0
- package/dist/patterns/TimeBucket.d.ts +16 -0
- package/dist/patterns/TimeBucket.d.ts.map +1 -0
- package/dist/patterns/TimeBucket.js +19 -0
- package/dist/patterns/TimeBucket.js.map +1 -0
- package/dist/patterns/index.d.ts +13 -0
- package/dist/patterns/index.d.ts.map +1 -0
- package/dist/patterns/index.js +9 -0
- package/dist/patterns/index.js.map +1 -0
- package/dist/patterns/toBehaviorPattern.d.ts +9 -0
- package/dist/patterns/toBehaviorPattern.d.ts.map +1 -0
- package/dist/patterns/toBehaviorPattern.js +22 -0
- package/dist/patterns/toBehaviorPattern.js.map +1 -0
- package/dist/patterns/toDayOfWeek.d.ts +10 -0
- package/dist/patterns/toDayOfWeek.d.ts.map +1 -0
- package/dist/patterns/toDayOfWeek.js +16 -0
- package/dist/patterns/toDayOfWeek.js.map +1 -0
- package/dist/patterns/toOffsetDate.d.ts +9 -0
- package/dist/patterns/toOffsetDate.d.ts.map +1 -0
- package/dist/patterns/toOffsetDate.js +20 -0
- package/dist/patterns/toOffsetDate.js.map +1 -0
- package/dist/patterns/toTimeBucket.d.ts +10 -0
- package/dist/patterns/toTimeBucket.d.ts.map +1 -0
- package/dist/patterns/toTimeBucket.js +25 -0
- package/dist/patterns/toTimeBucket.js.map +1 -0
- package/dist/testing/ReactorScenario.js +1 -1
- package/dist/testing/ReactorScenario.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/index.ts +3 -0
- package/package.json +7 -2
- package/patterns/BehaviorPattern.ts +28 -0
- package/patterns/DayOfWeek.ts +13 -0
- package/patterns/FacetName.ts +17 -0
- package/patterns/FacetSet.ts +9 -0
- package/patterns/IPatterns.ts +52 -0
- package/patterns/PatternMoment.ts +13 -0
- package/patterns/PatternQueryOptions.ts +10 -0
- package/patterns/Patterns.ts +88 -0
- package/patterns/PatternsAtOptions.ts +11 -0
- package/patterns/TimeBucket.ts +18 -0
- package/patterns/for_PatternMoment/when_deriving_facets/with_invalid_moments.spec.ts +31 -0
- package/patterns/for_Patterns/given/a_patterns_service.fixture.ts +21 -0
- package/patterns/for_Patterns/when_browsing/with_scopes.spec.ts +21 -0
- package/patterns/for_Patterns/when_browsing/within_a_scope.spec.ts +25 -0
- package/patterns/for_Patterns/when_querying/with_query_outcomes.spec.ts +47 -0
- package/patterns/for_Patterns/when_querying_a_context/with_established_patterns.spec.ts +34 -0
- package/patterns/for_Patterns/when_querying_a_context/with_facets.spec.ts +32 -0
- package/patterns/for_Patterns/when_querying_at_a_moment/with_additional_facets.spec.ts +34 -0
- package/patterns/for_Patterns/when_querying_at_a_moment/with_an_invalid_moment.spec.ts +20 -0
- package/patterns/for_Patterns/when_querying_at_a_moment/without_a_moment.spec.ts +37 -0
- package/patterns/for_Patterns/when_querying_through_the_connection/after_resetting_the_channel.spec.ts +58 -0
- package/patterns/for_toDayOfWeek/when_reading_the_day/at_an_offset.spec.ts +31 -0
- package/patterns/for_toTimeBucket/when_bucketing/at_boundaries.spec.ts +39 -0
- package/patterns/index.ts +15 -0
- package/patterns/toBehaviorPattern.ts +25 -0
- package/patterns/toDayOfWeek.ts +19 -0
- package/patterns/toOffsetDate.ts +22 -0
- package/patterns/toTimeBucket.ts +22 -0
- package/testing/ReactorScenario.spec.ts +13 -0
- package/testing/ReactorScenario.ts +1 -1
- 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/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
|
@@ -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
|
});
|