@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +295 -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 +48 -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 +86 -4
  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 +9 -1
  65. package/dist/runtime/field-validator.runtime.js +11 -6
  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 +77 -35
  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,10 +1,11 @@
1
+ import { FILTER_MODES, } from "../contracts/capability.js";
1
2
  import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
2
3
  import { advertisedDirectivesAt, advertisedReducers, advertisedReducersAt, holdsAt, } from "../contracts/field.js";
3
- import { isPlainRecord } from "../runtime/scalar-runtime.js";
4
+ import { isPlainRecord, unsentReadValueIssue, } from "../runtime/scalar-runtime.js";
4
5
  import { routeTransactionReferenceCandidate } from "../transactions/transaction-reference.js";
5
6
  import { isOperationFamily, } from "./operation-identity.js";
6
7
  import { NESTED_PROJECTION_SCOPE, resolveProjectionScope, } from "./projection-scope.resolver.js";
7
- import { NESTED_WRITE_SCOPE, resolveWriteScope, } from "./write-scope.resolver.js";
8
+ import { NESTED_WRITE_SCOPE, resolveWriteScope, TO_MANY_UPDATE_WRITE_SCOPE, } from "./write-scope.resolver.js";
8
9
  const OPERATION_ARGUMENT_RULES = {
9
10
  "find.first": {
10
11
  allowed: ["where", "orderBy", "select", "include"],
@@ -190,11 +191,16 @@ function validateScalarCondition(contract, fieldName, field, supported, value, p
190
191
  addIssue(state.issues, "V1001", path, `Filter object for field "${fieldName}" must contain an operator.`);
191
192
  return;
192
193
  }
194
+ refuseDanglingQualifiers(fieldName, value, path, state);
193
195
  for (const operator of operators) {
194
196
  if (!includes(supported, operator)) {
195
197
  addIssue(state.issues, "V1006", [...path, operator], `Filter operator "${operator}" is not available on field "${fieldName}".`);
196
198
  continue;
197
199
  }
200
+ if (operator === "mode") {
201
+ refuseUnavailableMode(field, fieldName, value, path, state);
202
+ continue;
203
+ }
198
204
  if (operator === "not") {
199
205
  const operand = value[operator];
200
206
  if (isPlainRecord(operand) && !checkDepth(contract, state, depth + 1)) {
@@ -204,12 +210,55 @@ function validateScalarCondition(contract, fieldName, field, supported, value, p
204
210
  }
205
211
  }
206
212
  }
213
+ function refuseDanglingQualifiers(fieldName, level, path, state) {
214
+ const qualifiers = DANGLING_QUALIFIERS.filter((key) => Object.hasOwn(level, key));
215
+ if (qualifiers.length === 0) {
216
+ return;
217
+ }
218
+ const compares = Object.entries(level).some(([operator, operand]) => !includes(DANGLING_QUALIFIERS, operator) &&
219
+ !(operator === "not" && isNegatedOperatorObject(operand, state)));
220
+ if (compares) {
221
+ return;
222
+ }
223
+ for (const qualifier of qualifiers) {
224
+ addIssue(state.issues, "V1001", [...path, qualifier], `${qualifier} on field "${fieldName}" qualifies no operator: write it next to the operator it applies to (inside "not" to apply to a negated one).`);
225
+ }
226
+ }
227
+ const DANGLING_QUALIFIERS = ["mode", "path"];
228
+ function isNegatedOperatorObject(operand, state) {
229
+ return (isPlainRecord(operand) &&
230
+ !(state.transactionReferences === true &&
231
+ routeTransactionReferenceCandidate(operand).kind !== "ordinary"));
232
+ }
233
+ function refuseUnavailableMode(field, fieldName, level, path, state) {
234
+ const mode = level.mode;
235
+ if (mode === "default" ||
236
+ typeof mode !== "string" ||
237
+ !includes(FILTER_MODES, mode)) {
238
+ return;
239
+ }
240
+ const { filterModes = [], pathFilterModes } = field.capabilities;
241
+ const atPath = Object.hasOwn(level, "path") && pathFilterModes !== undefined;
242
+ if (includes(atPath ? pathFilterModes : filterModes, mode)) {
243
+ return;
244
+ }
245
+ const where = atPath
246
+ ? " at a path"
247
+ : pathFilterModes !== undefined && includes(pathFilterModes, mode)
248
+ ? " without a path"
249
+ : "";
250
+ addIssue(state.issues, "V1006", [...path, "mode"], `Filter mode "${mode}" is not available on field "${fieldName}"${where}.`);
251
+ }
207
252
  function validateRelationFilter(contract, fieldName, field, value, path, state, depth) {
208
253
  const supported = field.capabilities.filter;
209
254
  if (supported === undefined) {
210
255
  addIssue(state.issues, "V1006", path, `Filtering is not available on relation "${fieldName}".`);
211
256
  return;
212
257
  }
258
+ if (value === undefined) {
259
+ state.issues.push(unsentReadValueIssue(path));
260
+ return;
261
+ }
213
262
  if (field.cardinality === "one") {
214
263
  if (value === null) {
215
264
  if (!field.nullable || !includes(supported, "null")) {
@@ -249,10 +298,20 @@ function validateRelationFilter(contract, fieldName, field, value, path, state,
249
298
  validateWhere(contract, field.target, value[operator], [...path, operator], state, depth + 1);
250
299
  }
251
300
  }
301
+ const UNDEFINED_ARGUMENT_MESSAGES = {
302
+ where: "where must not be undefined: leave the key out to mean no filter.",
303
+ limit: "limit must not be undefined: leave the key out to mean no limit.",
304
+ offset: "offset must not be undefined: leave the key out to mean no offset.",
305
+ cursor: "cursor must not be undefined: leave the key out to mean no cursor.",
306
+ };
252
307
  function validateWhere(contract, resourceName, where, path, state, depth) {
253
308
  if (!checkDepth(contract, state, depth)) {
254
309
  return;
255
310
  }
311
+ if (where === undefined) {
312
+ state.issues.push(unsentReadValueIssue(path));
313
+ return;
314
+ }
256
315
  if (!isRecord(where)) {
257
316
  addIssue(state.issues, "V1001", path, "where must be an object.");
258
317
  return;
@@ -366,7 +425,9 @@ function validateOrderBy(contract, resourceName, orderBy, path, issues) {
366
425
  valueKeys[1] !== "sort" ||
367
426
  (value.sort !== "asc" && value.sort !== "desc") ||
368
427
  (value.nulls !== "first" && value.nulls !== "last")) {
369
- addIssue(issues, "V1001", fieldPath, `Extended null ordering for "${fieldName}" must contain sort and nulls.`);
428
+ addIssue(issues, "V1001", fieldPath, Object.hasOwn(value, "$ref")
429
+ ? `A transaction reference cannot stand in orderBy: order "${fieldName}" by "asc", "desc" or { sort, nulls }.`
430
+ : `Null ordering for "${fieldName}" must be exactly { sort: "asc" | "desc", nulls: "first" | "last" }.`);
370
431
  continue;
371
432
  }
372
433
  if (!field.nullable || field.capabilities.orderNulls !== true) {
@@ -393,6 +454,10 @@ function validateOrderBy(contract, resourceName, orderBy, path, issues) {
393
454
  return analysis;
394
455
  }
395
456
  function validatePaginationValue(contract, name, value, path, issues) {
457
+ if (value === undefined) {
458
+ addIssue(issues, "V1001", path, UNDEFINED_ARGUMENT_MESSAGES[name]);
459
+ return;
460
+ }
396
461
  if (!Number.isInteger(value) || typeof value !== "number" || value < 0) {
397
462
  addIssue(issues, "V1001", path, `${name} must be a non-negative integer.`);
398
463
  return;
@@ -402,6 +467,10 @@ function validatePaginationValue(contract, name, value, path, issues) {
402
467
  }
403
468
  }
404
469
  function validateCursor(contract, resourceName, cursor, orderBy, path, issues) {
470
+ if (cursor === undefined) {
471
+ addIssue(issues, "V1009", path, UNDEFINED_ARGUMENT_MESSAGES.cursor);
472
+ return;
473
+ }
405
474
  if (!isRecord(cursor) || orderBy === undefined) {
406
475
  addIssue(issues, "V1009", path, "Cursor pagination requires an object cursor and explicit deterministic orderBy.");
407
476
  return;
@@ -513,6 +582,9 @@ function validateRelationProjection(contract, edge, scope, value, path, issues)
513
582
  if (!includes(edge.capabilities.filter, "nested")) {
514
583
  addIssue(issues, "V1006", [...path, "where"], "Nested returned-relation filtering is not available on this edge.");
515
584
  }
585
+ else if (value.where === undefined) {
586
+ addIssue(issues, "V1001", [...path, "where"], UNDEFINED_ARGUMENT_MESSAGES.where);
587
+ }
516
588
  else {
517
589
  const state = {
518
590
  issues,
@@ -586,11 +658,16 @@ function validateScalarMutation(fieldName, field, value, stage, path, issues, tr
586
658
  addIssue(issues, "V1001", path, "Scalar mutation action objects may contain only $-prefixed directive keys.");
587
659
  return;
588
660
  }
661
+ let unavailable = false;
589
662
  for (const directive of directiveKeys) {
590
663
  if (!includes(directives, directive)) {
664
+ unavailable = true;
591
665
  addIssue(issues, "V1006", [...path, directive], `Mutation directive "${directive}" is not available on field "${fieldName}".`);
592
666
  }
593
667
  }
668
+ if (!unavailable && directiveKeys.length > 1) {
669
+ addIssue(issues, "V1001", path, "Scalar mutation action objects may contain only one directive.");
670
+ }
594
671
  }
595
672
  function validateRelationMutationEntry(contract, edge, directive, value, path, issues, transactionReferences) {
596
673
  const target = edge.target;
@@ -680,7 +757,9 @@ function validateRelationMutationEntry(contract, edge, directive, value, path, i
680
757
  };
681
758
  validateWhere(contract, target, entry.where, [...path, "where"], state, 1);
682
759
  }
683
- validateMutationData(contract, target, entry.data, "update", NESTED_WRITE_SCOPE, [...path, "data"], issues, transactionReferences);
760
+ validateMutationData(contract, target, entry.data, "update", edge.cardinality === "many"
761
+ ? TO_MANY_UPDATE_WRITE_SCOPE
762
+ : NESTED_WRITE_SCOPE, [...path, "data"], issues, transactionReferences);
684
763
  }
685
764
  return;
686
765
  }
@@ -854,6 +933,9 @@ export function validateOperationArguments(contract, request, options) {
854
933
  if (isUniqueOperation(request)) {
855
934
  validateUniqueSelector(contract, request.resource, request.arguments.where, ["arguments", "where"], issues);
856
935
  }
936
+ else if (request.arguments.where === undefined) {
937
+ addIssue(issues, "V1001", ["arguments", "where"], UNDEFINED_ARGUMENT_MESSAGES.where);
938
+ }
857
939
  else {
858
940
  const state = {
859
941
  issues,
@@ -0,0 +1,6 @@
1
+ import type { ErrorCodeGuidance } from "../runtime/error-guidance.js";
2
+ import type { AvValidationCode } from "./validation-code.js";
3
+ /** Cause and fix of every validation code. */
4
+ export declare const VALIDATION_CODE_GUIDANCE: {
5
+ readonly [C in AvValidationCode]: ErrorCodeGuidance;
6
+ };
@@ -0,0 +1,90 @@
1
+ export const VALIDATION_CODE_GUIDANCE = {
2
+ V1000: {
3
+ cause: "A required value is absent: a create leaves out a required field that has no database default and no compute.create (the path names the field), a required relation and its reference field are both missing, a typed Json field lacks a required key, or a $ref has no fingerprint.",
4
+ fix: "Send the value at the issue's path. When the server must fill it, give the field a compute.create stage or a database default; a pipe cannot fill it.",
5
+ },
6
+ V1001: {
7
+ cause: "A value of the wrong type: a string where a number is expected, a negative limit, a non-integer id, null given to an operator with no null meaning (gt, contains, in), two directives in one scalar action object (`{ $increment: 1, $decrement: 2 }`), or a server-side value that is not plain data.",
8
+ fix: "Send the type the contract states at that path (the generated client's types say it).",
9
+ },
10
+ V1002: {
11
+ cause: "A value that is not a member of the enum, or outside a typed Json field's AvZ.enum or AvZ.literal.",
12
+ fix: "Send one of the enum's members.",
13
+ },
14
+ V1003: {
15
+ cause: 'A malformed datetime, decimal, base64 or UUID string, or a failed pattern or format validator (`check: "string_format"`).',
16
+ fix: "Send the canonical encoding (ISO-8601 UTC datetime, decimal string, base64 bytes) and satisfy the validator that check and params name.",
17
+ },
18
+ V1004: {
19
+ cause: "A number, length or size outside its range: a failed bound, length or size validator, a BigInt outside signed 64 bits, an AvZ.int() outside the safe integer range, or a transaction with no operations.",
20
+ fix: "Send a value inside the range; check and params name the validator that failed.",
21
+ },
22
+ V1005: {
23
+ cause: "A field that does not exist or is hidden in the caller's layer, an unknown Resource (under A2001), or a key a typed Json field's shape does not declare.",
24
+ fix: "Use a field the caller's contract exposes, expose it through exemptions, or make the call from server code.",
25
+ },
26
+ V1006: {
27
+ cause: "An operator, directive or nested write the field does not offer (at any depth, inside not too; a to-many $update's data takes only what a bulk write takes), a mode it does not offer, or an operation that is not available (under A2002).",
28
+ fix: "Use what the contract lists for that field or Resource, or expose it through exemptions.",
29
+ },
30
+ V1007: {
31
+ cause: "The where of a unique operation is not exactly one complete identifier.",
32
+ fix: "Pass exactly one identifier: the id, or every field of one @unique or @@unique.",
33
+ },
34
+ V1008: {
35
+ cause: "Both select and include, a write-only field selected, or a relation that is not selectable there.",
36
+ fix: "Use select or include, not both, and select only readable fields.",
37
+ },
38
+ V1009: {
39
+ cause: "A cursor without an explicit orderBy, or cursor keys that do not match the ordered fields.",
40
+ fix: "Give an orderBy and a cursor holding exactly its ordered fields.",
41
+ },
42
+ V1010: {
43
+ cause: "A $ref path the earlier step does not return, a $ref to a step that is not in the transaction, or to a step with no single row (a many or count step).",
44
+ fix: "Select the referenced field in the earlier step, and reference a single-row step.",
45
+ },
46
+ V1011: {
47
+ cause: "The referenced value's type does not fit its target, a nullable source where only a non-null value is accepted included.",
48
+ fix: "Reference a field of the target's type, non-null where the target needs it.",
49
+ },
50
+ V1012: {
51
+ cause: "A nested write that does not fit the relation (a list where one record is expected), a reference field set together with its own relation, one record mixing reference fields and relation writes where the adapter cannot run both, or a record written through a relation that writes that relation (or a field holding its reference) back, where the adapter admits no such write.",
52
+ fix: "Write either the relation or its reference field, in the relation's cardinality.",
53
+ },
54
+ V1013: {
55
+ cause: "Nested projections, filters or writes deeper than maxNestingDepth.",
56
+ fix: "Nest less, or raise limits.maxNestingDepth.",
57
+ },
58
+ V1014: {
59
+ cause: "A limit above maxListLimit, or more steps than maxTransactionOperations.",
60
+ fix: "Paginate or split the plan, or raise the limit.",
61
+ },
62
+ V1015: {
63
+ cause: "A where tree with more nodes than maxBooleanNodes.",
64
+ fix: "Simplify the filter (in instead of many OR clauses), or raise limits.maxBooleanNodes.",
65
+ },
66
+ V1016: {
67
+ cause: "A $ref to the referencing step itself or a later one, or to an index no step has.",
68
+ fix: "Reference an earlier step only.",
69
+ },
70
+ V1017: {
71
+ cause: "A hand-written plan whose fingerprint does not match its step.",
72
+ fix: "Let the generated client, framework.appTx or framework.clientTx build the plan.",
73
+ },
74
+ V1018: {
75
+ cause: "The referenced step returned no value at that path, such as a first step that matched no row.",
76
+ fix: "Make sure the earlier step returns a row, or use a unique step, which fails with A2003.",
77
+ },
78
+ V1019: {
79
+ cause: "A later step uses a row that an earlier step's delete removed through a database cascade.",
80
+ fix: "Reorder or drop the dependent step.",
81
+ },
82
+ V1020: {
83
+ cause: "One framework.transaction([...]) list mixes appTx and clientTx steps.",
84
+ fix: "Build every step of a plan from one origin.",
85
+ },
86
+ V1021: {
87
+ cause: "A Json field's value nests deeper than maxJsonDepth.",
88
+ fix: "Flatten the value, or raise limits.maxJsonDepth.",
89
+ },
90
+ };
@@ -13,3 +13,7 @@ export declare function resolveWriteScope(resource: AvResource<AvContract> | und
13
13
  * unrestricted one, at every depth, under every top-level scope.
14
14
  */
15
15
  export declare const NESTED_WRITE_SCOPE: AvWriteScope;
16
+ /** The position a to-many `$update`'s `data` sits in: the flat-bulk write position. */
17
+ export declare const TO_MANY_UPDATE_WRITE_SCOPE: AvWriteScope;
18
+ /** The position of a relation's other end inside a record written through it. */
19
+ export declare const LOOPBACK_WRITE_SCOPE: AvWriteScope;
@@ -5,3 +5,5 @@ export function resolveWriteScope(resource, request) {
5
5
  return writeScopeOf(lookupOwn(advertised, request.variant));
6
6
  }
7
7
  export const NESTED_WRITE_SCOPE = "nested";
8
+ export const TO_MANY_UPDATE_WRITE_SCOPE = "bulkWrite";
9
+ export const LOOPBACK_WRITE_SCOPE = "loopback";
@@ -1,16 +1,53 @@
1
+ import { copyAtDoor } from "../runtime/caller-data.door.js";
1
2
  import { createDiagnosticsReporter, createReporter, } from "../runtime/diagnostics-reporter.js";
2
3
  import { sanitizeOperationFailure } from "../runtime/error-boundary.js";
4
+ import { capturePlanAtCall } from "../transactions/transaction-plan-validator.js";
3
5
  import { decodeOperationRequest, decodeTransactionRequest, isParsedByDecoder, } from "./http-request.decoder.js";
4
6
  import { encodeOperationResponse, encodeTransactionResponse, } from "./http-response.encoder.js";
5
- function operationEntry(context, request) {
6
- return context.owned !== undefined && isParsedByDecoder(request.arguments)
7
- ? context.owned.execute
8
- : context.execute;
7
+ function admitOperation(context, request) {
8
+ const owned = context.owned;
9
+ if (isParsedByDecoder(request.arguments)) {
10
+ return { request, run: owned?.execute ?? context.execute };
11
+ }
12
+ const copy = copyAtDoor(request.arguments);
13
+ if (owned === undefined) {
14
+ return {
15
+ request: copy.ok ? { ...request, arguments: copy.value } : request,
16
+ run: context.execute,
17
+ };
18
+ }
19
+ if (copy.ok) {
20
+ return {
21
+ request: { ...request, arguments: copy.value },
22
+ run: (admitted) => owned.execute(admitted),
23
+ };
24
+ }
25
+ return {
26
+ request: { ...request, arguments: {} },
27
+ run: (admitted) => owned.execute(admitted, copy.error),
28
+ };
9
29
  }
10
- function transactionEntry(context, request) {
11
- return context.owned !== undefined && isParsedByDecoder(request.plan)
12
- ? context.owned.executeTransaction
13
- : context.executeTransaction;
30
+ function admitTransaction(context, request) {
31
+ const owned = context.owned;
32
+ if (isParsedByDecoder(request.plan)) {
33
+ return {
34
+ request,
35
+ run: owned?.executeTransaction ?? context.executeTransaction,
36
+ };
37
+ }
38
+ const captured = capturePlanAtCall(request.plan);
39
+ if (owned === undefined) {
40
+ return {
41
+ request: captured.refused.size === 0
42
+ ? { ...request, plan: captured.plan }
43
+ : request,
44
+ run: context.executeTransaction,
45
+ };
46
+ }
47
+ return {
48
+ request: { ...request, plan: captured.plan },
49
+ run: (admitted) => owned.executeTransaction(admitted, captured.refused.size === 0 ? undefined : captured.refused),
50
+ };
14
51
  }
15
52
  async function seededState(options, report, view) {
16
53
  if (options?.state === undefined) {
@@ -48,11 +85,12 @@ export async function handleOperationRequest(context, request, options) {
48
85
  if (!decoded.ok) {
49
86
  return decoded.response;
50
87
  }
51
- const seeded = await seededState(options, createDiagnosticsReporter(context.sink, decoded.request), { path: ["arguments"], arguments: decoded.request.arguments });
88
+ const admitted = admitOperation(context, decoded.request);
89
+ const seeded = await seededState(options, createDiagnosticsReporter(context.sink, admitted.request), { path: ["arguments"], arguments: admitted.request.arguments });
52
90
  const response = seeded.ok
53
- ? await operationEntry(context, decoded.request)(seeded.state === undefined
54
- ? decoded.request
55
- : { ...decoded.request, state: seeded.state })
91
+ ? await admitted.run(seeded.state === undefined
92
+ ? admitted.request
93
+ : { ...admitted.request, state: seeded.state })
56
94
  : seeded.response;
57
95
  return encodeOperationResponse(response, decoded.requestId, operationUnencodableReport(context.sink, decoded.request));
58
96
  }
@@ -61,15 +99,16 @@ export async function handleTransactionRequest(context, request, options) {
61
99
  if (!decoded.ok) {
62
100
  return decoded.response;
63
101
  }
102
+ const admitted = admitTransaction(context, decoded.request);
64
103
  const seeded = await seededState(options, createReporter(context.sink, {
65
104
  kind: "transaction",
66
105
  origin: context.origin,
67
106
  requestId: decoded.requestId,
68
- }), { path: ["plan"], arguments: decoded.request.plan });
107
+ }), { path: ["plan"], arguments: admitted.request.plan });
69
108
  const response = seeded.ok
70
- ? await transactionEntry(context, decoded.request)(seeded.state === undefined
71
- ? decoded.request
72
- : { ...decoded.request, state: seeded.state })
109
+ ? await admitted.run(seeded.state === undefined
110
+ ? admitted.request
111
+ : { ...admitted.request, state: seeded.state })
73
112
  : seeded.response;
74
113
  return encodeTransactionResponse(response, decoded.requestId, planUnencodableReport(context.sink, decoded.requestId, context.origin));
75
114
  }
@@ -41,6 +41,22 @@ export type CallerDataPolicy = {
41
41
  readonly keep?: CallerDataKeep;
42
42
  readonly mirror?: CallerDataMirror;
43
43
  };
44
+ /** The members a structural mirror copies by value rather than mirroring. */
45
+ type MirrorMembers = {
46
+ /** The policy a member at `path` (from the mirror's root) is copied by, or `undefined`. */
47
+ readonly copyAt: (path: CallerDataPath) => CallerDataPolicy | undefined;
48
+ /** Receives a member whose copy failed; the mirror holds `undefined` for it. */
49
+ readonly refused: (path: CallerDataPath, error: AvCallerDataError) => void;
50
+ };
51
+ /**
52
+ * Mirrors `value` by its descriptors, as a `$ref` control object is mirrored (no
53
+ * getter runs, an accessor stays a tripwire), except the members `members.copyAt`
54
+ * names by their path: each is copied by value under the policy it returns, and one
55
+ * whose copy fails is reported to `members.refused` and held as `undefined`.
56
+ *
57
+ * @throws {@link AvCallerDataError} at `[]` when `value` itself cannot be reflected.
58
+ */
59
+ export declare function mirrorCallerData(value: object, members: MirrorMembers): unknown;
44
60
  /**
45
61
  * Places a mirror tripwire's failure in the value the door copied: its path from the
46
62
  * copy's root, where `error.path` started from the mirror. A failure that is not a
@@ -57,3 +73,4 @@ export declare function locateCallerDataError(error: AvCallerDataError, copy: un
57
73
  export declare function copyCallerData(value: unknown, policy?: CallerDataPolicy): unknown;
58
74
  /** The `V1001` message a door answers a {@link AvCallerDataError} with. */
59
75
  export declare function callerDataFailureMessage(kind: CallerDataFailureKind): string;
76
+ export {};
@@ -268,7 +268,7 @@ function pathSegment(array, key) {
268
268
  }
269
269
  return key;
270
270
  }
271
- function mirrorControlData(value) {
271
+ function mirrorControlData(value, members) {
272
272
  const shape = reflectNode(value);
273
273
  if (shape === undefined) {
274
274
  throw new AvCallerDataError("unreadable-value", []);
@@ -375,6 +375,20 @@ function mirrorControlData(value) {
375
375
  continue;
376
376
  }
377
377
  const memberLink = childLink(link, pathSegment(shape.array, key));
378
+ const byValue = members?.copyAt(pathOf(memberLink));
379
+ if (byValue !== undefined) {
380
+ try {
381
+ store(copyCallerData(member, byValue));
382
+ }
383
+ catch (error) {
384
+ if (!(error instanceof AvCallerDataError)) {
385
+ throw error;
386
+ }
387
+ members?.refused(pathOf(memberLink), error);
388
+ store(undefined);
389
+ }
390
+ continue;
391
+ }
378
392
  const memberShape = reflectNode(member);
379
393
  if (memberShape === undefined) {
380
394
  store(UNREADABLE);
@@ -408,6 +422,9 @@ function mirrorControlData(value) {
408
422
  }
409
423
  return root;
410
424
  }
425
+ export function mirrorCallerData(value, members) {
426
+ return mirrorControlData(value, members);
427
+ }
411
428
  export function locateCallerDataError(error, copy) {
412
429
  const within = error.within;
413
430
  if (within === undefined) {
@@ -6,13 +6,19 @@ import type { AvMisuseEvent, MisuseReporter } from "./misuse-reporter.js";
6
6
  *
7
7
  * - `"caller"`: a server-side caller's own objects — copied at the door.
8
8
  * - `"owned"`: data no caller holds a reference to — a body the protocol decoder
9
- * parsed with `JSON.parse` for this request alone, or a plan the typed runner
10
- * assembled from copies its builders took. It is never copied again.
9
+ * parsed with `JSON.parse` for this request alone, a host's parsed body the
10
+ * protocol's shortcut copied at the call, or a plan the typed runner assembled
11
+ * from copies its builders took. It is never copied again.
11
12
  *
12
- * Only core's own modules choose `"owned"`: the public `framework.execute` and
13
- * `executeTransaction` always pass `"caller"`.
13
+ * - `{ refused }`: a body the protocol's shortcut copied at the call, whose copy
14
+ * failed — answered at the door as `"caller"` would answer it, never read again.
15
+ *
16
+ * Only core's own modules choose anything but `"caller"`: the public
17
+ * `framework.execute` and `executeTransaction` always pass `"caller"`.
14
18
  */
15
- export type ArgumentsSource = "caller" | "owned";
19
+ export type ArgumentsSource = "caller" | "owned" | {
20
+ readonly refused: AvCallerDataError;
21
+ };
16
22
  /** The operation identity a misuse event may name, as far as it could be read. */
17
23
  export type MisuseIdentity = Omit<AvMisuseEvent, "kind" | "path">;
18
24
  /**
@@ -1,5 +1,6 @@
1
1
  import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
2
2
  import { AvComputedValueError } from "./computed-value.error.js";
3
+ import { frozenCopy } from "./readonly-value.js";
3
4
  import { normalizeScalarFieldValue } from "./scalar-runtime.js";
4
5
  function isRecord(value) {
5
6
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -36,6 +37,12 @@ function appendUniqueProjection(values, value) {
36
37
  values.push(value);
37
38
  }
38
39
  }
40
+ function adapterIdentifier(model, resource) {
41
+ const resources = model.resources;
42
+ return ((Object.hasOwn(resources, resource)
43
+ ? resources[resource]?.identifiers?.[0]
44
+ : undefined) ?? []);
45
+ }
39
46
  function supportsProjection(request) {
40
47
  if (request.family === "upsert") {
41
48
  return true;
@@ -51,6 +58,11 @@ function addScalarProjection(core, model, active, compute, resourceName, fieldNa
51
58
  const stage = readCompute(compute?.[resourceName]?.[fieldName]?.read);
52
59
  if (stage !== undefined) {
53
60
  appendUnique(computed, fieldName);
61
+ if (stage.dependsOn.length === 0) {
62
+ for (const member of adapterIdentifier(model, resourceName)) {
63
+ appendUniqueProjection(adapterSelect, member);
64
+ }
65
+ }
54
66
  for (const dependency of stage.dependsOn) {
55
67
  if (storedScalar(model, resourceName, dependency)) {
56
68
  appendUniqueProjection(adapterSelect, dependency);
@@ -307,10 +319,11 @@ async function materializeRecord(active, compute, request, plan, value, contextF
307
319
  if (field?.kind !== "scalar" || stage === undefined) {
308
320
  continue;
309
321
  }
322
+ const view = frozenCopy(record);
310
323
  const computedValue = await stage.compute({
311
324
  field: fieldName,
312
- value: record[fieldName],
313
- record,
325
+ value: view[fieldName],
326
+ record: view,
314
327
  ctx: contextFactory(),
315
328
  });
316
329
  if (computedValue === undefined) {
@@ -1,7 +1,8 @@
1
1
  import type { AvValidationIssue } from "../operations/validation-code.js";
2
2
  /**
3
3
  * What the diagnostics channel receives when a computed value — a read or write
4
- * compute stage's return — is one its Contract refuses: the developer's broken code,
4
+ * compute stage's return — is one its Contract refuses, or a `compute.create`
5
+ * stage returned `undefined` for a field a create cannot do without (rc.2): the developer's broken code,
5
6
  * never the caller's. It is thrown, not a deliberate outcome, so the framework's
6
7
  * boundary answers the caller the generic `A3000` and reports this error, with the
7
8
  * issues, exactly once.
@@ -9,5 +10,5 @@ import type { AvValidationIssue } from "../operations/validation-code.js";
9
10
  export declare class AvComputedValueError extends Error {
10
11
  readonly issues: readonly AvValidationIssue[];
11
12
  readonly name = "AvComputedValueError";
12
- constructor(resource: string, field: string, stage: "read" | "create" | "update", issues: readonly AvValidationIssue[]);
13
+ constructor(resource: string, field: string, stage: "read" | "create" | "update", issues: readonly AvValidationIssue[], message?: string);
13
14
  }
@@ -1,9 +1,10 @@
1
1
  export class AvComputedValueError extends Error {
2
2
  issues;
3
3
  name = "AvComputedValueError";
4
- constructor(resource, field, stage, issues) {
4
+ constructor(resource, field, stage, issues, message) {
5
5
  const [first] = issues;
6
- super(`The compute.${stage} stage of ${resource}.${field} returned a value its Contract refuses${first === undefined ? "." : ` (${first.code}: ${first.message})`}`);
6
+ super(message ??
7
+ `The compute.${stage} stage of ${resource}.${field} returned a value its Contract refuses${first === undefined ? "." : ` (${first.code}: ${first.message})`}`);
7
8
  this.issues = issues;
8
9
  }
9
10
  }
@@ -1,20 +1,32 @@
1
1
  import type { AvContract } from "../contracts/contract.js";
2
+ import type { AvCreateRequirements } from "../contracts/create-requirements.js";
2
3
  import type { AvOperationIdentity } from "../operations/operation-identity.js";
3
4
  import type { AvValidationIssue } from "../operations/validation-code.js";
4
5
  import type { ReferentialActionTable } from "./referential-action.js";
5
6
  type Path = readonly (string | number)[];
7
+ /** One required field a `compute.create` stage was to fill and left missing. */
8
+ export type ComputedRequirementGap = {
9
+ readonly resource: string;
10
+ readonly field: string;
11
+ readonly issue: AvValidationIssue;
12
+ };
6
13
  /**
7
- * What the Adapter stated beside its model that the check reads: its
8
- * referential edges, for the fields that hold each reference. Read once when
9
- * the Framework is constructed. The requirements themselves are the active
10
- * Contract's.
14
+ * What the Adapter stated that the checks read: its referential edges, for the
15
+ * fields that hold each reference, and its own create requirements per
16
+ * Resource, before any layer projected them (what write compute must not leave
17
+ * missing). Read once when the Framework is constructed. The requirements the
18
+ * caller is held to are the active Contract's.
11
19
  */
12
20
  export type RecordWriteFacts = {
13
21
  readonly references: ReferentialActionTable;
22
+ readonly stated: (resource: string) => AvCreateRequirements | undefined;
14
23
  };
15
24
  /** The facts an Adapter stated, read off it once; an absent table states nothing. */
16
25
  export declare function recordWriteFactsOf(adapter: {
17
26
  readonly referentialActions?: ReferentialActionTable;
27
+ readonly model?: {
28
+ readonly resources: Readonly<Record<string, unknown>>;
29
+ };
18
30
  }): RecordWriteFacts;
19
31
  /**
20
32
  * The `V1000` issues for every create position in one operation's arguments
@@ -24,7 +36,31 @@ export declare function recordWriteFactsOf(adapter: {
24
36
  * reference both, in argument order. Empty when there are none. Total over
25
37
  * malformed input.
26
38
  */
27
- export declare function createRequirementIssues(contract: AvContract, facts: RecordWriteFacts, request: AvOperationIdentity & {
39
+ export declare function createRequirementIssues(contract: AvContract, facts: Pick<RecordWriteFacts, "references">, request: AvOperationIdentity & {
28
40
  readonly resource: string;
29
41
  }, args: unknown, argumentsPath: Path): readonly AvValidationIssue[];
42
+ /**
43
+ * The `V1000` issues for every create position of one operation's arguments
44
+ * that lacks a value the active Contract requires, and the `V1012` issues for
45
+ * every write record that sets a relation and its reference both, on the
46
+ * arguments as the caller sent them, before any pipeline stage runs. Empty when
47
+ * there are none.
48
+ */
49
+ export declare function preliminaryRecordIssues(contract: AvContract, facts: Pick<RecordWriteFacts, "references">, request: AvOperationIdentity & {
50
+ readonly resource: string;
51
+ }, args: unknown, argumentsPath: Path): readonly AvValidationIssue[];
52
+ /**
53
+ * Every required field a `compute.create` stage was to fill and that is still
54
+ * missing after write compute, at every create position of one operation's
55
+ * arguments (`create.*`'s `data`, `upsert`'s `create`, every nested create),
56
+ * in argument order, read against the Adapter's own statement of each
57
+ * Resource's create requirements; `computed` names the fields a Resource's
58
+ * `compute.create` stages fill.
59
+ * A field the parent supplies to a nested create, a group whose relation the
60
+ * record writes, and a value the caller sent are not missing. Empty when
61
+ * nothing is.
62
+ */
63
+ export declare function computedRequirementGapsOf(contract: AvContract, facts: RecordWriteFacts, request: AvOperationIdentity & {
64
+ readonly resource: string;
65
+ }, args: unknown, argumentsPath: Path, computed: (resource: string) => ReadonlySet<string>): readonly ComputedRequirementGap[];
30
66
  export {};