@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +341 -1
  2. package/ERRORS.md +142 -0
  3. package/README.md +24 -1
  4. package/dist/adapters/adapter-discovery-diagnostic.guidance.d.ts +6 -0
  5. package/dist/adapters/adapter-discovery-diagnostic.guidance.js +7 -0
  6. package/dist/adapters/adapter-field.d.ts +14 -0
  7. package/dist/adapters/adapter-model.d.ts +9 -0
  8. package/dist/adapters/adapter.interface.d.ts +10 -0
  9. package/dist/compiler/contract-compiler-diagnostic.guidance.d.ts +6 -0
  10. package/dist/compiler/contract-compiler-diagnostic.guidance.js +97 -0
  11. package/dist/compiler/contract.compiler.js +94 -6
  12. package/dist/config/computed-fields.d.ts +62 -0
  13. package/dist/config/computed-fields.js +80 -0
  14. package/dist/config/config-diagnostic.guidance.d.ts +6 -0
  15. package/dist/config/config-diagnostic.guidance.js +32 -0
  16. package/dist/config/define-resource.d.ts +1 -1
  17. package/dist/config/exact-framework-config.d.ts +50 -20
  18. package/dist/config/field-compute.resolver.d.ts +2 -0
  19. package/dist/config/field-compute.resolver.js +8 -0
  20. package/dist/config/framework-config.d.ts +7 -6
  21. package/dist/config/framework-resources-config.d.ts +18 -12
  22. package/dist/config/pipeline-config.d.ts +4 -4
  23. package/dist/contracts/capability.d.ts +10 -1
  24. package/dist/contracts/capability.js +2 -0
  25. package/dist/contracts/client-contract-structure.validator.d.ts +65 -35
  26. package/dist/contracts/client-contract-structure.validator.js +4 -2
  27. package/dist/contracts/contract-canonicalizer.d.ts +7 -0
  28. package/dist/contracts/contract-canonicalizer.js +18 -4
  29. package/dist/contracts/field.d.ts +30 -1
  30. package/dist/framework/contract-view.d.ts +4 -4
  31. package/dist/framework/framework-construction.error.d.ts +12 -3
  32. package/dist/framework/framework-construction.error.js +5 -1
  33. package/dist/framework/framework.factory.d.ts +1 -1
  34. package/dist/framework/framework.factory.js +9 -2
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +1 -0
  37. package/dist/internal.d.ts +2 -0
  38. package/dist/internal.js +1 -0
  39. package/dist/operations/database-refusal.d.ts +51 -0
  40. package/dist/operations/database-refusal.js +45 -0
  41. package/dist/operations/operation-arguments.d.ts +102 -31
  42. package/dist/operations/operation-validator-core.js +145 -36
  43. package/dist/operations/validation-code.guidance.d.ts +6 -0
  44. package/dist/operations/validation-code.guidance.js +90 -0
  45. package/dist/operations/write-scope.resolver.d.ts +4 -0
  46. package/dist/operations/write-scope.resolver.js +2 -0
  47. package/dist/protocol/protocol.handler.js +55 -16
  48. package/dist/runtime/caller-data.copier.d.ts +17 -0
  49. package/dist/runtime/caller-data.copier.js +18 -1
  50. package/dist/runtime/caller-data.door.d.ts +11 -5
  51. package/dist/runtime/computed-read-runtime.js +15 -2
  52. package/dist/runtime/computed-value.error.d.ts +3 -2
  53. package/dist/runtime/computed-value.error.js +3 -2
  54. package/dist/runtime/create-requirement.d.ts +41 -5
  55. package/dist/runtime/create-requirement.js +137 -20
  56. package/dist/runtime/diagnostics-reporter.d.ts +2 -1
  57. package/dist/runtime/error-boundary.js +4 -0
  58. package/dist/runtime/error-guidance.d.ts +12 -0
  59. package/dist/runtime/error-guidance.js +1 -0
  60. package/dist/runtime/error.catalog.d.ts +49 -0
  61. package/dist/runtime/error.catalog.js +49 -0
  62. package/dist/runtime/field-stage.runtime.d.ts +2 -0
  63. package/dist/runtime/field-stage.runtime.js +8 -4
  64. package/dist/runtime/field-validator.runtime.d.ts +18 -1
  65. package/dist/runtime/field-validator.runtime.js +47 -8
  66. package/dist/runtime/json-value.d.ts +10 -5
  67. package/dist/runtime/level-lifecycle.runtime.d.ts +2 -2
  68. package/dist/runtime/level-lifecycle.runtime.js +2 -5
  69. package/dist/runtime/mutation-planner.js +3 -14
  70. package/dist/runtime/operation-code.guidance.d.ts +6 -0
  71. package/dist/runtime/operation-code.guidance.js +167 -0
  72. package/dist/runtime/operation-runtime.d.ts +20 -6
  73. package/dist/runtime/operation-runtime.js +82 -36
  74. package/dist/runtime/owned-execution.registry.d.ts +12 -2
  75. package/dist/runtime/pipe-output.validator.d.ts +10 -0
  76. package/dist/runtime/pipe-output.validator.js +4 -2
  77. package/dist/runtime/pipeline.d.ts +35 -8
  78. package/dist/runtime/readonly-value.d.ts +14 -3
  79. package/dist/runtime/readonly-value.js +10 -1
  80. package/dist/runtime/scalar-runtime.d.ts +5 -1
  81. package/dist/runtime/scalar-runtime.js +86 -16
  82. package/dist/runtime/unsent-value.normalizer.d.ts +15 -0
  83. package/dist/runtime/unsent-value.normalizer.js +128 -0
  84. package/dist/transactions/operation-fingerprint.js +2 -2
  85. package/dist/transactions/sql-statement.d.ts +8 -0
  86. package/dist/transactions/sql-statement.validator.js +36 -3
  87. package/dist/transactions/transaction-plan-assembler.d.ts +1 -1
  88. package/dist/transactions/transaction-plan-assembler.js +33 -10
  89. package/dist/transactions/transaction-plan-validator.d.ts +16 -0
  90. package/dist/transactions/transaction-plan-validator.js +27 -1
  91. package/dist/transactions/transaction-reference-validator.d.ts +42 -0
  92. package/dist/transactions/transaction-reference-validator.js +82 -56
  93. package/dist/transactions/transaction-reference.js +5 -0
  94. package/dist/transactions/transaction-runtime.js +47 -22
  95. package/dist/transactions/transaction-stage.runtime.d.ts +2 -1
  96. package/dist/transactions/transaction-stage.runtime.js +7 -4
  97. 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
- const entries = [];
87
- current.forEach((entry, index) => {
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[1],
95
- link: { parent: task.link, segment: entry[0] },
101
+ value: entry === undefined ? null : entry,
102
+ link: { parent: task.link, segment: index },
96
103
  emit: (child) => {
97
- normalized.push(child);
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
- return normalizeScalarAtom(contract, field, value, origin === "runtime" ? "application" : origin, path, field.nullable);
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
- if (operator === "mode" || operator === "isEmpty" || operator === "path") {
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(normalizeListElement(contract, field, entry, origin, [
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(normalizeListElement(contract, field, operand, origin, operandPath), issues);
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(normalizeListElement(contract, field, entry, origin, [
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 { canonicalizeJson } from "../contracts/contract-canonicalizer.js";
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 = canonicalizeJson({
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, earlier } = context;
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: statement.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 type { AvCallerDataError } from "../runtime/caller-data.copier.js";
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) => createHandle({
135
- kind: "operation",
136
- origin,
137
- resource,
138
- identity,
139
- ...snapshot(args),
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 { kind: "value", value };
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;