@aventara/core 1.0.0-rc.1 → 1.0.0-rc.2
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/CHANGELOG.md +295 -1
- package/ERRORS.md +142 -0
- package/README.md +24 -1
- package/dist/adapters/adapter-discovery-diagnostic.guidance.d.ts +6 -0
- package/dist/adapters/adapter-discovery-diagnostic.guidance.js +7 -0
- package/dist/adapters/adapter-field.d.ts +14 -0
- package/dist/adapters/adapter-model.d.ts +9 -0
- package/dist/adapters/adapter.interface.d.ts +10 -0
- package/dist/compiler/contract-compiler-diagnostic.guidance.d.ts +6 -0
- package/dist/compiler/contract-compiler-diagnostic.guidance.js +97 -0
- package/dist/compiler/contract.compiler.js +94 -6
- package/dist/config/computed-fields.d.ts +62 -0
- package/dist/config/computed-fields.js +80 -0
- package/dist/config/config-diagnostic.guidance.d.ts +6 -0
- package/dist/config/config-diagnostic.guidance.js +32 -0
- package/dist/config/define-resource.d.ts +1 -1
- package/dist/config/exact-framework-config.d.ts +48 -20
- package/dist/config/field-compute.resolver.d.ts +2 -0
- package/dist/config/field-compute.resolver.js +8 -0
- package/dist/config/framework-config.d.ts +7 -6
- package/dist/config/framework-resources-config.d.ts +18 -12
- package/dist/config/pipeline-config.d.ts +4 -4
- package/dist/contracts/capability.d.ts +10 -1
- package/dist/contracts/capability.js +2 -0
- package/dist/contracts/client-contract-structure.validator.d.ts +65 -35
- package/dist/contracts/client-contract-structure.validator.js +4 -2
- package/dist/contracts/contract-canonicalizer.d.ts +7 -0
- package/dist/contracts/contract-canonicalizer.js +18 -4
- package/dist/contracts/field.d.ts +30 -1
- package/dist/framework/contract-view.d.ts +4 -4
- package/dist/framework/framework-construction.error.d.ts +12 -3
- package/dist/framework/framework-construction.error.js +5 -1
- package/dist/framework/framework.factory.d.ts +1 -1
- package/dist/framework/framework.factory.js +9 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +1 -0
- package/dist/operations/database-refusal.d.ts +51 -0
- package/dist/operations/database-refusal.js +45 -0
- package/dist/operations/operation-arguments.d.ts +102 -31
- package/dist/operations/operation-validator-core.js +86 -4
- package/dist/operations/validation-code.guidance.d.ts +6 -0
- package/dist/operations/validation-code.guidance.js +90 -0
- package/dist/operations/write-scope.resolver.d.ts +4 -0
- package/dist/operations/write-scope.resolver.js +2 -0
- package/dist/protocol/protocol.handler.js +55 -16
- package/dist/runtime/caller-data.copier.d.ts +17 -0
- package/dist/runtime/caller-data.copier.js +18 -1
- package/dist/runtime/caller-data.door.d.ts +11 -5
- package/dist/runtime/computed-read-runtime.js +15 -2
- package/dist/runtime/computed-value.error.d.ts +3 -2
- package/dist/runtime/computed-value.error.js +3 -2
- package/dist/runtime/create-requirement.d.ts +41 -5
- package/dist/runtime/create-requirement.js +137 -20
- package/dist/runtime/diagnostics-reporter.d.ts +2 -1
- package/dist/runtime/error-boundary.js +4 -0
- package/dist/runtime/error-guidance.d.ts +12 -0
- package/dist/runtime/error-guidance.js +1 -0
- package/dist/runtime/error.catalog.d.ts +49 -0
- package/dist/runtime/error.catalog.js +49 -0
- package/dist/runtime/field-stage.runtime.d.ts +2 -0
- package/dist/runtime/field-stage.runtime.js +8 -4
- package/dist/runtime/field-validator.runtime.d.ts +9 -1
- package/dist/runtime/field-validator.runtime.js +11 -6
- package/dist/runtime/json-value.d.ts +10 -5
- package/dist/runtime/level-lifecycle.runtime.d.ts +2 -2
- package/dist/runtime/level-lifecycle.runtime.js +2 -5
- package/dist/runtime/mutation-planner.js +3 -14
- package/dist/runtime/operation-code.guidance.d.ts +6 -0
- package/dist/runtime/operation-code.guidance.js +167 -0
- package/dist/runtime/operation-runtime.d.ts +20 -6
- package/dist/runtime/operation-runtime.js +77 -35
- package/dist/runtime/owned-execution.registry.d.ts +12 -2
- package/dist/runtime/pipe-output.validator.d.ts +10 -0
- package/dist/runtime/pipe-output.validator.js +4 -2
- package/dist/runtime/pipeline.d.ts +35 -8
- package/dist/runtime/readonly-value.d.ts +14 -3
- package/dist/runtime/readonly-value.js +10 -1
- package/dist/runtime/scalar-runtime.d.ts +5 -1
- package/dist/runtime/scalar-runtime.js +86 -16
- package/dist/runtime/unsent-value.normalizer.d.ts +15 -0
- package/dist/runtime/unsent-value.normalizer.js +128 -0
- package/dist/transactions/operation-fingerprint.js +2 -2
- package/dist/transactions/sql-statement.d.ts +8 -0
- package/dist/transactions/sql-statement.validator.js +36 -3
- package/dist/transactions/transaction-plan-assembler.d.ts +1 -1
- package/dist/transactions/transaction-plan-assembler.js +33 -10
- package/dist/transactions/transaction-plan-validator.d.ts +16 -0
- package/dist/transactions/transaction-plan-validator.js +27 -1
- package/dist/transactions/transaction-reference-validator.d.ts +42 -0
- package/dist/transactions/transaction-reference-validator.js +82 -56
- package/dist/transactions/transaction-reference.js +5 -0
- package/dist/transactions/transaction-runtime.js +47 -22
- package/dist/transactions/transaction-stage.runtime.d.ts +2 -1
- package/dist/transactions/transaction-stage.runtime.js +7 -4
- package/package.json +4 -3
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { AvDecimal } from "./decimal.js";
|
|
2
2
|
/** A value `AvDeepReadonly` leaves as it is: a primitive, a function, or a scalar instance. */
|
|
3
3
|
export type AvDeepReadonlyLeaf = string | number | boolean | bigint | symbol | null | undefined | Date | Uint8Array | AvDecimal | ((...args: never[]) => unknown);
|
|
4
|
+
/**
|
|
5
|
+
* Whether array type `T` is an open array (every length: `"array"`) or a tuple
|
|
6
|
+
* (`"tuple"`). A tuple whose only fixed positions are optional ones before a rest
|
|
7
|
+
* element (`[a?, ...b[]]`) has every length too and reads as an array.
|
|
8
|
+
*/
|
|
9
|
+
export type ArrayForm<T extends readonly unknown[]> = T extends readonly [] | readonly [unknown, ...unknown[]] | readonly [...unknown[], unknown] ? "tuple" : T extends {
|
|
10
|
+
readonly length: infer Length;
|
|
11
|
+
} ? number extends Length ? "array" : "tuple" : "tuple";
|
|
4
12
|
/**
|
|
5
13
|
* `T` with every property, element and nested property read-only: what a stage
|
|
6
|
-
* that only reads is told. Arrays become read-only arrays
|
|
7
|
-
* `Uint8Array`, an `AvDecimal` and functions are left as
|
|
14
|
+
* that only reads is told. Arrays become read-only arrays, tuples read-only
|
|
15
|
+
* tuples; a `Date`, a `Uint8Array`, an `AvDecimal` and functions are left as
|
|
16
|
+
* they are.
|
|
8
17
|
*/
|
|
9
|
-
export type AvDeepReadonly<T> = T extends AvDeepReadonlyLeaf ? T : T extends
|
|
18
|
+
export type AvDeepReadonly<T> = T extends AvDeepReadonlyLeaf ? T : T extends readonly unknown[] ? ArrayForm<T> extends "array" ? readonly AvDeepReadonly<T[number]>[] : {
|
|
19
|
+
readonly [K in keyof T]: AvDeepReadonly<T[K]>;
|
|
20
|
+
} : T extends object ? {
|
|
10
21
|
readonly [K in keyof T]: AvDeepReadonly<T[K]>;
|
|
11
22
|
} : T;
|
|
12
23
|
/**
|
|
@@ -36,20 +36,25 @@ function scalarCopy(value) {
|
|
|
36
36
|
}
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
39
|
+
const SCALAR_FREE = new WeakSet();
|
|
39
40
|
export function frozenCopy(value) {
|
|
40
41
|
const copies = new Map();
|
|
42
|
+
const walking = new Set();
|
|
43
|
+
let cycled = false;
|
|
41
44
|
const copy = (current) => {
|
|
42
45
|
const scalar = scalarCopy(current);
|
|
43
46
|
if (scalar !== undefined) {
|
|
44
47
|
return scalar;
|
|
45
48
|
}
|
|
46
|
-
if (!isContainer(current)) {
|
|
49
|
+
if (!isContainer(current) || SCALAR_FREE.has(current)) {
|
|
47
50
|
return current;
|
|
48
51
|
}
|
|
49
52
|
if (copies.has(current)) {
|
|
53
|
+
cycled ||= walking.has(current);
|
|
50
54
|
return copies.get(current);
|
|
51
55
|
}
|
|
52
56
|
copies.set(current, current);
|
|
57
|
+
walking.add(current);
|
|
53
58
|
const entries = [];
|
|
54
59
|
let changed = !DEEP_FROZEN.has(current);
|
|
55
60
|
for (const key of Reflect.ownKeys(current)) {
|
|
@@ -63,8 +68,12 @@ export function frozenCopy(value) {
|
|
|
63
68
|
changed ||= next !== descriptor.value;
|
|
64
69
|
entries.push([key, descriptor, next]);
|
|
65
70
|
}
|
|
71
|
+
walking.delete(current);
|
|
66
72
|
if (!changed) {
|
|
67
73
|
copies.set(current, current);
|
|
74
|
+
if (!cycled) {
|
|
75
|
+
SCALAR_FREE.add(current);
|
|
76
|
+
}
|
|
68
77
|
return current;
|
|
69
78
|
}
|
|
70
79
|
const target = Array.isArray(current)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AvRequestOrigin } from "../config/config-layer.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type AvScalarFilterOperator } from "../contracts/capability.js";
|
|
3
3
|
import type { AvContract } from "../contracts/contract.js";
|
|
4
4
|
import type { AvScalarField } from "../contracts/field.js";
|
|
5
5
|
import type { AvOperationIdentity } from "../operations/operation-identity.js";
|
|
@@ -31,6 +31,10 @@ type Path = readonly (string | number)[];
|
|
|
31
31
|
* one reader and a value to another.
|
|
32
32
|
*/
|
|
33
33
|
export declare function isPlainRecord(value: unknown): value is Readonly<Record<string, unknown>>;
|
|
34
|
+
/** What a refused `undefined` the request reads with is told. */
|
|
35
|
+
export declare const UNSENT_READ_VALUE_MESSAGE = "Value must not be undefined: it would be left out, and the request would reach more rows.";
|
|
36
|
+
/** The `V1001` of an `undefined` the request reads with. */
|
|
37
|
+
export declare function unsentReadValueIssue(path: Path): AvValidationIssue;
|
|
34
38
|
/**
|
|
35
39
|
* An operation argument's JSON value is bounded by `maxJsonDepth`, which the
|
|
36
40
|
* structural walk enforces before this runs; a `$sql` step's parameter is not
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { shapeOfField } from "../avz/json-shape.registry.js";
|
|
2
|
+
import { AVZ_FORMATS } from "../config/framework-field-definition.js";
|
|
3
|
+
import { FILTER_MODES, } from "../contracts/capability.js";
|
|
2
4
|
import { lookupField, lookupOwn, lookupResource, } from "../contracts/contract-lookup.js";
|
|
3
5
|
import { decodeBase64, WIRE_SCALAR_PATTERNS, } from "../contracts/wire-scalar-format.js";
|
|
4
6
|
import { routeTransactionReferenceCandidate, } from "../transactions/transaction-reference.js";
|
|
5
7
|
import { AvDecimal } from "./decimal.js";
|
|
8
|
+
import { fieldCheckIssues } from "./field-check.evaluator.js";
|
|
6
9
|
const INT_MIN = -(2 ** 31);
|
|
7
10
|
const INT_MAX = 2 ** 31 - 1;
|
|
8
11
|
const BIGINT_MIN = -(2n ** 63n);
|
|
9
12
|
const BIGINT_MAX = 2n ** 63n - 1n;
|
|
10
13
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
14
|
+
const NAMED_FORMATS = new Set(AVZ_FORMATS);
|
|
15
|
+
function isNamedFormat(format) {
|
|
16
|
+
return NAMED_FORMATS.has(format);
|
|
17
|
+
}
|
|
11
18
|
function isRecord(value) {
|
|
12
19
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
20
|
}
|
|
@@ -24,6 +31,10 @@ function issue(code, path, message) {
|
|
|
24
31
|
function typeIssue(path, expected) {
|
|
25
32
|
return issue("V1001", path, `Value must be ${expected}.`);
|
|
26
33
|
}
|
|
34
|
+
export const UNSENT_READ_VALUE_MESSAGE = "Value must not be undefined: it would be left out, and the request would reach more rows.";
|
|
35
|
+
export function unsentReadValueIssue(path) {
|
|
36
|
+
return issue("V1001", path, UNSENT_READ_VALUE_MESSAGE);
|
|
37
|
+
}
|
|
27
38
|
function formatIssue(path, expected) {
|
|
28
39
|
return issue("V1003", path, `Value must use ${expected}.`);
|
|
29
40
|
}
|
|
@@ -83,18 +94,14 @@ export function normalizeJsonValue(value, path, seen) {
|
|
|
83
94
|
const normalized = [];
|
|
84
95
|
task.emit(normalized);
|
|
85
96
|
stack.push({ kind: "leave", container: current });
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
entries.push([index, entry]);
|
|
89
|
-
});
|
|
90
|
-
for (let cursor = entries.length - 1; cursor >= 0; cursor -= 1) {
|
|
91
|
-
const entry = entries[cursor];
|
|
97
|
+
for (let index = current.length - 1; index >= 0; index -= 1) {
|
|
98
|
+
const entry = current[index];
|
|
92
99
|
stack.push({
|
|
93
100
|
kind: "visit",
|
|
94
|
-
value: entry
|
|
95
|
-
link: { parent: task.link, segment:
|
|
101
|
+
value: entry === undefined ? null : entry,
|
|
102
|
+
link: { parent: task.link, segment: index },
|
|
96
103
|
emit: (child) => {
|
|
97
|
-
normalized
|
|
104
|
+
normalized[index] = child;
|
|
98
105
|
},
|
|
99
106
|
});
|
|
100
107
|
}
|
|
@@ -112,7 +119,7 @@ export function normalizeJsonValue(value, path, seen) {
|
|
|
112
119
|
const normalized = {};
|
|
113
120
|
task.emit(normalized);
|
|
114
121
|
stack.push({ kind: "leave", container: current });
|
|
115
|
-
const entries = Object.entries(current);
|
|
122
|
+
const entries = Object.entries(current).filter((entry) => entry[1] !== undefined);
|
|
116
123
|
for (let cursor = entries.length - 1; cursor >= 0; cursor -= 1) {
|
|
117
124
|
const entry = entries[cursor];
|
|
118
125
|
stack.push({
|
|
@@ -359,9 +366,21 @@ export function normalizeScalarFieldValue(contract, field, value, origin, path)
|
|
|
359
366
|
});
|
|
360
367
|
return issues.length > 0
|
|
361
368
|
? { ok: false, issues }
|
|
362
|
-
: { ok: true, value: normalized };
|
|
369
|
+
: heldToFormat(field, { ok: true, value: normalized }, origin, path);
|
|
370
|
+
}
|
|
371
|
+
return heldToFormat(field, normalizeScalarAtom(contract, field, value, origin === "runtime" ? "application" : origin, path, field.nullable), origin, path);
|
|
372
|
+
}
|
|
373
|
+
function heldToFormat(field, normalized, origin, path) {
|
|
374
|
+
const format = field.format;
|
|
375
|
+
if (origin !== "runtime" ||
|
|
376
|
+
!normalized.ok ||
|
|
377
|
+
format === undefined ||
|
|
378
|
+
format === "uuid" ||
|
|
379
|
+
!isNamedFormat(format)) {
|
|
380
|
+
return normalized;
|
|
363
381
|
}
|
|
364
|
-
|
|
382
|
+
const issues = fieldCheckIssues([{ check: "string_format", format }], normalized.value, "string", path, field.list);
|
|
383
|
+
return issues.length === 0 ? normalized : { ok: false, issues };
|
|
365
384
|
}
|
|
366
385
|
export function jsonShapeIssues(field, value, path, position = "value") {
|
|
367
386
|
if (field.json === undefined || value === null || value === undefined) {
|
|
@@ -427,6 +446,11 @@ export function copyScalarFieldValue(field, value) {
|
|
|
427
446
|
function normalizeListElement(contract, field, value, origin, path) {
|
|
428
447
|
return normalizeScalarAtom(contract, field, value, origin, path, false);
|
|
429
448
|
}
|
|
449
|
+
function normalizeFilterEntry(contract, field, value, origin, path) {
|
|
450
|
+
return value === undefined
|
|
451
|
+
? { ok: false, issues: [unsentReadValueIssue(path)] }
|
|
452
|
+
: normalizeListElement(contract, field, value, origin, path);
|
|
453
|
+
}
|
|
430
454
|
function mergeResult(result, issues) {
|
|
431
455
|
if (result.ok) {
|
|
432
456
|
return result.value;
|
|
@@ -463,10 +487,40 @@ const NULL_OPERAND_OPERATORS = new Set([
|
|
|
463
487
|
"arrayStartsWith",
|
|
464
488
|
"arrayEndsWith",
|
|
465
489
|
]);
|
|
490
|
+
const KEYWORD_OPERANDS = Object.freeze({
|
|
491
|
+
mode: (operand, path, issues) => {
|
|
492
|
+
if (!FILTER_MODES.includes(operand)) {
|
|
493
|
+
issues.push(typeIssue(path, '"default", "sensitive" or "insensitive"'));
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
isEmpty: (operand, path, issues) => {
|
|
497
|
+
if (typeof operand !== "boolean") {
|
|
498
|
+
issues.push(typeIssue(path, "a boolean"));
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
path: (operand, path, issues) => {
|
|
502
|
+
if (!Array.isArray(operand)) {
|
|
503
|
+
issues.push(typeIssue(path, "an array of strings"));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
for (let index = 0; index < operand.length; index += 1) {
|
|
507
|
+
if (operand[index] === undefined) {
|
|
508
|
+
issues.push(unsentReadValueIssue([...path, index]));
|
|
509
|
+
}
|
|
510
|
+
else if (typeof operand[index] !== "string") {
|
|
511
|
+
issues.push(typeIssue([...path, index], "a string"));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
});
|
|
466
516
|
function normalizeScalarFilter(contract, field, value, origin, path, issues, references = false) {
|
|
467
517
|
if (isKeptReference(references, value)) {
|
|
468
518
|
return value;
|
|
469
519
|
}
|
|
520
|
+
if (value === undefined) {
|
|
521
|
+
issues.push(unsentReadValueIssue(path));
|
|
522
|
+
return value;
|
|
523
|
+
}
|
|
470
524
|
if (!isPlainRecord(value)) {
|
|
471
525
|
return mergeResult(normalizeScalarFieldValue(contract, field, value, origin, path), issues);
|
|
472
526
|
}
|
|
@@ -477,11 +531,17 @@ function normalizeScalarFilter(contract, field, value, origin, path, issues, ref
|
|
|
477
531
|
const normalized = cloneRecord(value);
|
|
478
532
|
for (const [operator, operand] of Object.entries(value)) {
|
|
479
533
|
const operandPath = [...path, operator];
|
|
534
|
+
if (operand === undefined) {
|
|
535
|
+
issues.push(unsentReadValueIssue(operandPath));
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
480
538
|
if (operand === null && !acceptsNullOperand(operator)) {
|
|
481
539
|
issues.push(typeIssue(operandPath, "non-null"));
|
|
482
540
|
continue;
|
|
483
541
|
}
|
|
484
|
-
|
|
542
|
+
const keyword = lookupOwn(KEYWORD_OPERANDS, operator);
|
|
543
|
+
if (keyword !== undefined) {
|
|
544
|
+
keyword(operand, operandPath, issues);
|
|
485
545
|
continue;
|
|
486
546
|
}
|
|
487
547
|
if (referenceOperandShape(operator) !== undefined &&
|
|
@@ -497,14 +557,14 @@ function normalizeScalarFilter(contract, field, value, origin, path, issues, ref
|
|
|
497
557
|
issues.push(typeIssue(operandPath, "an array"));
|
|
498
558
|
continue;
|
|
499
559
|
}
|
|
500
|
-
normalized[operator] = operand.map((entry, index) => mergeResult(
|
|
560
|
+
normalized[operator] = operand.map((entry, index) => mergeResult(normalizeFilterEntry(contract, field, entry, origin, [
|
|
501
561
|
...operandPath,
|
|
502
562
|
index,
|
|
503
563
|
]), issues));
|
|
504
564
|
continue;
|
|
505
565
|
}
|
|
506
566
|
if (operator === "has") {
|
|
507
|
-
normalized[operator] = mergeResult(
|
|
567
|
+
normalized[operator] = mergeResult(normalizeFilterEntry(contract, field, operand, origin, operandPath), issues);
|
|
508
568
|
continue;
|
|
509
569
|
}
|
|
510
570
|
if (operator === "hasEvery" || operator === "hasSome") {
|
|
@@ -512,7 +572,7 @@ function normalizeScalarFilter(contract, field, value, origin, path, issues, ref
|
|
|
512
572
|
issues.push(typeIssue(operandPath, "an array"));
|
|
513
573
|
continue;
|
|
514
574
|
}
|
|
515
|
-
normalized[operator] = operand.map((entry, index) => mergeResult(
|
|
575
|
+
normalized[operator] = operand.map((entry, index) => mergeResult(normalizeFilterEntry(contract, field, entry, origin, [
|
|
516
576
|
...operandPath,
|
|
517
577
|
index,
|
|
518
578
|
]), issues));
|
|
@@ -521,6 +581,12 @@ function normalizeScalarFilter(contract, field, value, origin, path, issues, ref
|
|
|
521
581
|
if ("scalar" in field.type &&
|
|
522
582
|
field.type.scalar === "json" &&
|
|
523
583
|
(operator.startsWith("string") || operator.startsWith("array"))) {
|
|
584
|
+
if (operator.startsWith("string")) {
|
|
585
|
+
if (typeof operand !== "string") {
|
|
586
|
+
issues.push(typeIssue(operandPath, "a string"));
|
|
587
|
+
}
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
524
590
|
const jsonResult = normalizeJsonValue(operand, operandPath, new Set());
|
|
525
591
|
normalized[operator] = mergeResult(jsonResult, issues);
|
|
526
592
|
continue;
|
|
@@ -585,6 +651,10 @@ function normalizeUniqueSelector(contract, resourceName, selector, origin, path,
|
|
|
585
651
|
const normalized = cloneRecord(selector);
|
|
586
652
|
for (const [fieldName, value] of Object.entries(selector)) {
|
|
587
653
|
const field = lookupField(resource, fieldName);
|
|
654
|
+
if (field?.kind === "scalar" && value === undefined) {
|
|
655
|
+
issues.push(unsentReadValueIssue([...path, fieldName]));
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
588
658
|
if (field?.kind === "scalar" && !isKeptReference(references, value)) {
|
|
589
659
|
normalized[fieldName] = mergeResult(normalizeScalarFieldValue(contract, field, value, origin, [
|
|
590
660
|
...path,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AvContract } from "../contracts/contract.js";
|
|
2
|
+
import type { AvOperationFamily } from "../operations/operation-identity.js";
|
|
3
|
+
/**
|
|
4
|
+
* One operation's arguments with every write record's unsent fields removed — a
|
|
5
|
+
* field whose value is `undefined`, at any depth of nested write data the
|
|
6
|
+
* Contract's `maxNestingDepth` admits — and
|
|
7
|
+
* nothing else changed (see the module note): `where`, selectors and every other
|
|
8
|
+
* member are left as they are. The same object when nothing was unsent; otherwise
|
|
9
|
+
* a copy of each container on the way, never a write into `args`. Total over
|
|
10
|
+
* malformed input.
|
|
11
|
+
*/
|
|
12
|
+
export declare function withoutUnsentWriteValues(contract: AvContract, request: {
|
|
13
|
+
readonly resource: string;
|
|
14
|
+
readonly family: AvOperationFamily;
|
|
15
|
+
}, args: unknown): unknown;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
2
|
+
import { isPlainRecord, WRITE_KEYS } from "./field-stage.runtime.js";
|
|
3
|
+
import { isRelationMutationDirective, RELATION_MUTATION_RECORD_SITES, } from "./mutation-planner.js";
|
|
4
|
+
const REMOVE = Symbol("remove");
|
|
5
|
+
const DEPTH_CEILING = 256;
|
|
6
|
+
function rebuilt(record, changes) {
|
|
7
|
+
const copy = Object.getPrototypeOf(record) === null ? Object.create(null) : {};
|
|
8
|
+
for (const key of Object.keys(record)) {
|
|
9
|
+
const value = changes.has(key) ? changes.get(key) : record[key];
|
|
10
|
+
if (value === REMOVE) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(copy, key, {
|
|
14
|
+
value,
|
|
15
|
+
writable: true,
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return copy;
|
|
21
|
+
}
|
|
22
|
+
function eachEntry(value, strip) {
|
|
23
|
+
if (!Array.isArray(value)) {
|
|
24
|
+
return strip(value);
|
|
25
|
+
}
|
|
26
|
+
let copy;
|
|
27
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
28
|
+
if (!(index in value)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const entry = value[index];
|
|
32
|
+
const stripped = strip(entry);
|
|
33
|
+
if (stripped !== entry) {
|
|
34
|
+
copy ??= value.slice();
|
|
35
|
+
copy[index] = stripped;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return copy ?? value;
|
|
39
|
+
}
|
|
40
|
+
function stripRecord(walk, resourceName, record, depth) {
|
|
41
|
+
if (depth > walk.depth || !isPlainRecord(record)) {
|
|
42
|
+
return record;
|
|
43
|
+
}
|
|
44
|
+
const resource = lookupResource(walk.contract, resourceName);
|
|
45
|
+
let changes;
|
|
46
|
+
for (const key of Object.keys(record)) {
|
|
47
|
+
const value = record[key];
|
|
48
|
+
if (value === undefined) {
|
|
49
|
+
changes ??= new Map();
|
|
50
|
+
changes.set(key, REMOVE);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const field = lookupField(resource, key);
|
|
54
|
+
if (field?.kind !== "relation") {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const stripped = stripRelationWrite(walk, field.target, value, depth + 1);
|
|
58
|
+
if (stripped !== value) {
|
|
59
|
+
changes ??= new Map();
|
|
60
|
+
changes.set(key, stripped);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return changes === undefined ? record : rebuilt(record, changes);
|
|
64
|
+
}
|
|
65
|
+
function stripRelationWrite(walk, target, value, depth) {
|
|
66
|
+
if (!isPlainRecord(value)) {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
let changes;
|
|
70
|
+
for (const directive of Object.keys(value)) {
|
|
71
|
+
if (!isRelationMutationDirective(directive)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const sites = RELATION_MUTATION_RECORD_SITES[directive];
|
|
75
|
+
if (sites.length === 0) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const operand = value[directive];
|
|
79
|
+
const next = eachEntry(operand, (entry) => {
|
|
80
|
+
let stripped = entry;
|
|
81
|
+
for (const site of sites) {
|
|
82
|
+
if (site.key === undefined) {
|
|
83
|
+
stripped = stripRecord(walk, target, stripped, depth);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!isPlainRecord(stripped) || !Object.hasOwn(stripped, site.key)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const inner = stripped[site.key];
|
|
90
|
+
const record = stripRecord(walk, target, inner, depth);
|
|
91
|
+
if (record !== inner) {
|
|
92
|
+
stripped = rebuilt(stripped, new Map([[site.key, record]]));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return stripped;
|
|
96
|
+
});
|
|
97
|
+
if (next !== operand) {
|
|
98
|
+
changes ??= new Map();
|
|
99
|
+
changes.set(directive, next);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return changes === undefined ? value : rebuilt(value, changes);
|
|
103
|
+
}
|
|
104
|
+
export function withoutUnsentWriteValues(contract, request, args) {
|
|
105
|
+
const keys = Object.hasOwn(WRITE_KEYS, request.family)
|
|
106
|
+
? WRITE_KEYS[request.family]
|
|
107
|
+
: undefined;
|
|
108
|
+
if (keys === undefined || !isPlainRecord(args)) {
|
|
109
|
+
return args;
|
|
110
|
+
}
|
|
111
|
+
const walk = {
|
|
112
|
+
contract,
|
|
113
|
+
depth: Math.min(contract.limits.maxNestingDepth, DEPTH_CEILING),
|
|
114
|
+
};
|
|
115
|
+
let changes;
|
|
116
|
+
for (const key of keys) {
|
|
117
|
+
if (!Object.hasOwn(args, key)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const value = args[key];
|
|
121
|
+
const next = eachEntry(value, (entry) => stripRecord(walk, request.resource, entry, 0));
|
|
122
|
+
if (next !== value) {
|
|
123
|
+
changes ??= new Map();
|
|
124
|
+
changes.set(key, next);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return changes === undefined ? args : rebuilt(args, changes);
|
|
128
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { canonicalizeWireJson } from "../contracts/contract-canonicalizer.js";
|
|
2
2
|
const OPERATION_FINGERPRINT_PATTERN = /^fp1:[A-Za-z0-9_-]{22}$/;
|
|
3
3
|
function encodeBase64Url(bytes) {
|
|
4
4
|
let binary = "";
|
|
@@ -14,7 +14,7 @@ export function isOperationFingerprint(value) {
|
|
|
14
14
|
return typeof value === "string" && OPERATION_FINGERPRINT_PATTERN.test(value);
|
|
15
15
|
}
|
|
16
16
|
export async function computeOperationFingerprint(node) {
|
|
17
|
-
const canonical =
|
|
17
|
+
const canonical = canonicalizeWireJson({
|
|
18
18
|
resource: node.resource,
|
|
19
19
|
family: node.family,
|
|
20
20
|
variant: node.variant,
|
|
@@ -35,6 +35,14 @@ export type PlannedSqlValue = {
|
|
|
35
35
|
readonly operation: number;
|
|
36
36
|
readonly field: string;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* A whole value that is a `$ref` control object the caller wrote, mirrored at
|
|
40
|
+
* the builder: validated as an operation step's reference is.
|
|
41
|
+
*/
|
|
42
|
+
| {
|
|
43
|
+
readonly kind: "candidate";
|
|
44
|
+
readonly candidate: unknown;
|
|
45
|
+
}
|
|
38
46
|
/** A `$ref` placeholder found inside a value, at `path` within it: refused. */
|
|
39
47
|
| {
|
|
40
48
|
readonly kind: "misplaced-reference";
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { validationIssue } from "../operations/validation-code.js";
|
|
2
|
+
import { AvCallerDataError, locateCallerDataError, } from "../runtime/caller-data.copier.js";
|
|
2
3
|
import { refuseCallerData } from "../runtime/caller-data.door.js";
|
|
3
4
|
import { AvDecimal } from "../runtime/decimal.js";
|
|
4
5
|
import { planStepPath } from "../runtime/error-boundary.js";
|
|
5
6
|
import { operationFailure } from "../runtime/operation-response.js";
|
|
6
7
|
import { mapValidationOutcome } from "../runtime/outcome-mapper.js";
|
|
7
8
|
import { isPlainRecord, normalizeJsonValue, } from "../runtime/scalar-runtime.js";
|
|
8
|
-
import { invalidSourceIndexIssue, referenceSourceAt, referenceSourceField, } from "./transaction-reference-validator.js";
|
|
9
|
+
import { inspectReferenceCandidate, invalidSourceIndexIssue, referenceSourceAt, referenceSourceField, } from "./transaction-reference-validator.js";
|
|
9
10
|
import { transactionOperationFailure } from "./transaction-response.js";
|
|
10
11
|
function refused(issue, index) {
|
|
11
12
|
return {
|
|
@@ -56,7 +57,7 @@ export function normalizeSqlValue(value, path) {
|
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
export function validateSqlStatement(statement, context) {
|
|
59
|
-
const { index
|
|
60
|
+
const { index } = context;
|
|
60
61
|
if (!context.available) {
|
|
61
62
|
const message = "SQL statements are not available on this Adapter.";
|
|
62
63
|
return {
|
|
@@ -73,6 +74,22 @@ export function validateSqlStatement(statement, context) {
|
|
|
73
74
|
if (statement.refused !== undefined) {
|
|
74
75
|
return refused(refuseCallerData(statement.refused, [...planStepPath(index), "values"], { origin: "application", operation: index }, context.misuse), index);
|
|
75
76
|
}
|
|
77
|
+
try {
|
|
78
|
+
return validateSqlValues(statement, statement.strings, context);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!(error instanceof AvCallerDataError)) {
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
return refused(refuseCallerData(locateCallerDataError(error, statement.values.map((planned) => planned.kind === "value"
|
|
85
|
+
? planned.value
|
|
86
|
+
: planned.kind === "candidate"
|
|
87
|
+
? planned.candidate
|
|
88
|
+
: undefined)), [...planStepPath(index), "values"], { origin: "application", operation: index }, context.misuse), index);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function validateSqlValues(statement, strings, context) {
|
|
92
|
+
const { index, earlier } = context;
|
|
76
93
|
const values = [];
|
|
77
94
|
const issues = [];
|
|
78
95
|
for (const [position, planned] of statement.values.entries()) {
|
|
@@ -80,6 +97,22 @@ export function validateSqlStatement(statement, context) {
|
|
|
80
97
|
switch (planned.kind) {
|
|
81
98
|
case "misplaced-reference":
|
|
82
99
|
return refused(validationIssue("V1010", [...path, ...planned.path, "$ref"], "A transaction reference in a SQL step must be a whole value."), index);
|
|
100
|
+
case "candidate": {
|
|
101
|
+
const inspection = inspectReferenceCandidate(planned.candidate, path, earlier, index);
|
|
102
|
+
if (!inspection.ok) {
|
|
103
|
+
return refused(inspection.issue, index);
|
|
104
|
+
}
|
|
105
|
+
if (inspection.field.kind !== "scalar" || inspection.field.list) {
|
|
106
|
+
return refused(validationIssue("V1011", inspection.pathPath, "Transaction reference source is not a single scalar field."), index);
|
|
107
|
+
}
|
|
108
|
+
values.push({
|
|
109
|
+
kind: "reference",
|
|
110
|
+
operation: inspection.source.index,
|
|
111
|
+
field: inspection.segment,
|
|
112
|
+
source: inspection.field,
|
|
113
|
+
});
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
83
116
|
case "reference": {
|
|
84
117
|
const source = referenceSourceAt(earlier, planned.operation, index);
|
|
85
118
|
if (source === undefined) {
|
|
@@ -127,7 +160,7 @@ export function validateSqlStatement(statement, context) {
|
|
|
127
160
|
statement: {
|
|
128
161
|
kind: "sql",
|
|
129
162
|
index,
|
|
130
|
-
strings
|
|
163
|
+
strings,
|
|
131
164
|
values,
|
|
132
165
|
},
|
|
133
166
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AvContracts } from "../compiler/contract.compiler.js";
|
|
2
2
|
import type { AvOperationIdentity } from "../operations/operation-identity.js";
|
|
3
|
-
import
|
|
3
|
+
import { AvCallerDataError } from "../runtime/caller-data.copier.js";
|
|
4
4
|
import type { MisuseReporter } from "../runtime/misuse-reporter.js";
|
|
5
5
|
import type { ClientProtocolIdentity } from "../runtime/operation-request.js";
|
|
6
6
|
import type { AvOperationFailureResponse } from "../runtime/operation-response.js";
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { VALIDATION_CODE_MESSAGES, validationIssue, } from "../operations/validation-code.js";
|
|
2
|
+
import { AvCallerDataError, } from "../runtime/caller-data.copier.js";
|
|
2
3
|
import { copyAtDoor, refuseCallerData } from "../runtime/caller-data.door.js";
|
|
3
4
|
import { operationFailure } from "../runtime/operation-response.js";
|
|
4
5
|
import { mapValidationOutcome } from "../runtime/outcome-mapper.js";
|
|
6
|
+
import { withoutUnsentWriteValues } from "../runtime/unsent-value.normalizer.js";
|
|
5
7
|
import { encodeOperationArgumentsForWire } from "./operation-arguments-encoder.js";
|
|
6
8
|
import { computeOperationFingerprint } from "./operation-fingerprint.js";
|
|
7
9
|
import { readSqlTemplate } from "./sql-statement.js";
|
|
10
|
+
import { routeTransactionReferenceCandidate } from "./transaction-reference.js";
|
|
8
11
|
import { transactionOperationFailure } from "./transaction-response.js";
|
|
9
12
|
const SQL_STEP_NODE = Object.freeze({ fingerprint: null });
|
|
10
13
|
function isPlainRecord(value) {
|
|
@@ -46,6 +49,9 @@ function rebuildArguments(root, leaf) {
|
|
|
46
49
|
if (!Array.isArray(value) && !isPlainRecord(value)) {
|
|
47
50
|
return { value };
|
|
48
51
|
}
|
|
52
|
+
if (Object.hasOwn(value, "$ref")) {
|
|
53
|
+
return { value };
|
|
54
|
+
}
|
|
49
55
|
if (ancestors.has(value)) {
|
|
50
56
|
return { value };
|
|
51
57
|
}
|
|
@@ -92,15 +98,17 @@ function operationEntryFailure(operation, message) {
|
|
|
92
98
|
function referenceFailure(issue, operation) {
|
|
93
99
|
return transactionOperationFailure(mapValidationOutcome([issue]), operation);
|
|
94
100
|
}
|
|
101
|
+
const STEP_REFERENCE_MIRROR = (value) => Object.hasOwn(value, "$ref");
|
|
95
102
|
export function createTransactionPlanAssembler(contracts, protocol, misuse) {
|
|
96
103
|
const handles = new WeakMap();
|
|
97
104
|
const placeholders = new WeakMap();
|
|
98
105
|
const originOf = (origin) => origin === "application"
|
|
99
106
|
? { origin: "application" }
|
|
100
107
|
: { origin: "client", protocol };
|
|
101
|
-
const snapshot = (args) => {
|
|
108
|
+
const snapshot = (args, mirror) => {
|
|
102
109
|
const copy = copyAtDoor(args, {
|
|
103
110
|
keep: (value) => placeholders.has(value),
|
|
111
|
+
...(mirror === undefined ? {} : { mirror }),
|
|
104
112
|
});
|
|
105
113
|
return copy.ok ? { args: copy.value } : { args: {}, refused: copy.error };
|
|
106
114
|
};
|
|
@@ -131,15 +139,28 @@ export function createTransactionPlanAssembler(contracts, protocol, misuse) {
|
|
|
131
139
|
handles.set(handle, record);
|
|
132
140
|
return handle;
|
|
133
141
|
};
|
|
134
|
-
const defer = (origin, resource, identity, args) =>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
const defer = (origin, resource, identity, args) => {
|
|
143
|
+
const copy = snapshot(args, STEP_REFERENCE_MIRROR);
|
|
144
|
+
let sent = copy.args;
|
|
145
|
+
try {
|
|
146
|
+
sent = withoutUnsentWriteValues(origin === "application" ? contracts.application : contracts.client, { resource, family: identity.family }, copy.args);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (!(error instanceof AvCallerDataError)) {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return createHandle({
|
|
154
|
+
kind: "operation",
|
|
155
|
+
origin,
|
|
156
|
+
resource,
|
|
157
|
+
identity,
|
|
158
|
+
...copy,
|
|
159
|
+
args: sent,
|
|
160
|
+
});
|
|
161
|
+
};
|
|
141
162
|
const deferSql = (strings, values) => {
|
|
142
|
-
const copy = snapshot(values);
|
|
163
|
+
const copy = snapshot(values, STEP_REFERENCE_MIRROR);
|
|
143
164
|
return createHandle({
|
|
144
165
|
kind: "sql",
|
|
145
166
|
strings: readSqlTemplate(strings, values.length),
|
|
@@ -153,7 +174,9 @@ export function createTransactionPlanAssembler(contracts, protocol, misuse) {
|
|
|
153
174
|
const sites = placeholderSites(value);
|
|
154
175
|
const [site] = sites;
|
|
155
176
|
if (site === undefined) {
|
|
156
|
-
return
|
|
177
|
+
return routeTransactionReferenceCandidate(value).kind === "ordinary"
|
|
178
|
+
? { kind: "value", value }
|
|
179
|
+
: { kind: "candidate", candidate: value };
|
|
157
180
|
}
|
|
158
181
|
if (site.path.length > 0) {
|
|
159
182
|
return { kind: "misplaced-reference", path: site.path };
|