@aventara/core 1.0.0-rc.1 → 1.0.0-rc.3
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 +341 -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 +50 -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 +145 -36
- 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 +18 -1
- package/dist/runtime/field-validator.runtime.js +47 -8
- 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 +82 -36
- 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,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 };
|
|
@@ -34,6 +34,22 @@ export type PlanArguments = {
|
|
|
34
34
|
readonly refused?: ReadonlyMap<number, AvCallerDataError>;
|
|
35
35
|
readonly statements?: ReadonlyMap<number, PlannedSqlStatement>;
|
|
36
36
|
};
|
|
37
|
+
/** A plan taken at a call, and the steps whose arguments could not be copied. */
|
|
38
|
+
export type CapturedPlan = {
|
|
39
|
+
readonly plan: unknown;
|
|
40
|
+
readonly refused: ReadonlyMap<number, AvCallerDataError>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* A host's parsed plan taken at `protocol.handleTransaction`'s call, before its first
|
|
44
|
+
* await (rc.2, e2e batch 2): its structure mirrored by descriptors, which is how this
|
|
45
|
+
* validator inspects it — so no getter it refuses runs, and every accessor stays one
|
|
46
|
+
* for it to refuse — and each step's `args` copied as {@link captureSteps} copies a
|
|
47
|
+
* caller's, its `$ref` control objects mirrored. Run as an `"owned"` plan with
|
|
48
|
+
* `refused`, it is answered exactly as the plan the host held at the call. A plan
|
|
49
|
+
* that is no object, or cannot be reflected, is returned as it is: nothing in it can
|
|
50
|
+
* change.
|
|
51
|
+
*/
|
|
52
|
+
export declare function capturePlanAtCall(plan: unknown): CapturedPlan;
|
|
37
53
|
export type { TransactionReferenceCandidateSlot } from "./transaction-reference-collector.js";
|
|
38
54
|
/** One validated Transaction Slot: where it is, what it holds, and its reference. */
|
|
39
55
|
export type TransactionReferenceSlot = {
|
|
@@ -4,7 +4,7 @@ import { parseOperationIdentity } from "../operations/operation-identity.js";
|
|
|
4
4
|
import { findOperationAvailabilityIssue, operationUnavailableIssue, unavailableOperationKey, } from "../operations/operation-validator.js";
|
|
5
5
|
import { resourceUnavailableIssue } from "../operations/operation-validator-core.js";
|
|
6
6
|
import { VALIDATION_CODE_MESSAGES, validationIssue, } from "../operations/validation-code.js";
|
|
7
|
-
import { AvCallerDataError, locateCallerDataError, } from "../runtime/caller-data.copier.js";
|
|
7
|
+
import { AvCallerDataError, locateCallerDataError, mirrorCallerData, } from "../runtime/caller-data.copier.js";
|
|
8
8
|
import { copyAtDoor, misuseIdentityOf, refuseCallerData, } from "../runtime/caller-data.door.js";
|
|
9
9
|
import { planReadOperation, } from "../runtime/computed-read-runtime.js";
|
|
10
10
|
import { planOperationArgumentsPath, planStepPath, } from "../runtime/error-boundary.js";
|
|
@@ -24,6 +24,32 @@ const CALLER_PLAN = { source: "caller" };
|
|
|
24
24
|
const STEP_ARGUMENTS_POLICY = {
|
|
25
25
|
mirror: (value) => Object.hasOwn(value, "$ref"),
|
|
26
26
|
};
|
|
27
|
+
export function capturePlanAtCall(plan) {
|
|
28
|
+
const refused = new Map();
|
|
29
|
+
if (typeof plan !== "object" || plan === null) {
|
|
30
|
+
return { plan, refused };
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const mirrored = mirrorCallerData(plan, {
|
|
34
|
+
copyAt: (path) => path.length === 3 &&
|
|
35
|
+
path[0] === "operations" &&
|
|
36
|
+
typeof path[1] === "number" &&
|
|
37
|
+
path[2] === "args"
|
|
38
|
+
? STEP_ARGUMENTS_POLICY
|
|
39
|
+
: undefined,
|
|
40
|
+
refused: (path, error) => {
|
|
41
|
+
refused.set(path[1], error);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return { plan: mirrored, refused };
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error instanceof AvCallerDataError) {
|
|
48
|
+
return { plan, refused };
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
27
53
|
function captureSteps(operations, operationCount, planArguments) {
|
|
28
54
|
const steps = [];
|
|
29
55
|
const statements = planArguments.source === "owned" ? planArguments.statements : undefined;
|