@aventara/core 0.1.0-pilot.4 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +338 -0
  2. package/README.md +100 -3
  3. package/dist/adapters/adapter-model.d.ts +8 -0
  4. package/dist/adapters/adapter.interface.d.ts +6 -2
  5. package/dist/compiler/contract-compiler-diagnostic.d.ts +6 -0
  6. package/dist/compiler/contract.compiler.js +67 -0
  7. package/dist/config/config.resolver.d.ts +3 -0
  8. package/dist/config/config.resolver.js +1 -0
  9. package/dist/config/config.validator.js +6 -0
  10. package/dist/config/define-fields.d.ts +10 -3
  11. package/dist/config/framework-config.d.ts +4 -1
  12. package/dist/config/framework-fields-config.d.ts +6 -0
  13. package/dist/contracts/client-contract-structure.validator.d.ts +21 -1
  14. package/dist/contracts/client-contract-structure.validator.js +99 -2
  15. package/dist/contracts/contract-canonicalizer.js +32 -0
  16. package/dist/contracts/contract.d.ts +7 -0
  17. package/dist/contracts/field.d.ts +10 -0
  18. package/dist/contracts/json-shape.d.ts +65 -0
  19. package/dist/contracts/json-shape.js +1 -0
  20. package/dist/contracts/reference-write-limit.d.ts +7 -0
  21. package/dist/contracts/reference-write-limit.js +1 -0
  22. package/dist/contracts/scalar-value-type.d.ts +12 -1
  23. package/dist/framework/contract-view.d.ts +126 -21
  24. package/dist/framework/framework.d.ts +5 -3
  25. package/dist/framework/framework.factory.js +56 -13
  26. package/dist/index.d.ts +8 -40
  27. package/dist/index.js +0 -20
  28. package/dist/internal.d.ts +35 -0
  29. package/dist/internal.js +16 -0
  30. package/dist/json/avz.d.ts +96 -0
  31. package/dist/json/avz.js +212 -0
  32. package/dist/json/index.d.ts +6 -0
  33. package/dist/json/index.js +1 -0
  34. package/dist/json/json-shape.registry.d.ts +22 -0
  35. package/dist/json/json-shape.registry.js +16 -0
  36. package/dist/json/zod-issue.translator.d.ts +51 -0
  37. package/dist/json/zod-issue.translator.js +142 -0
  38. package/dist/operations/operation-arguments.d.ts +151 -101
  39. package/dist/operations/operation-identity.d.ts +2 -0
  40. package/dist/operations/operation-identity.js +3 -0
  41. package/dist/operations/operation-validator-core.d.ts +1 -1
  42. package/dist/operations/operation-validator-core.js +28 -10
  43. package/dist/operations/operation-validator.d.ts +20 -5
  44. package/dist/operations/operation-validator.js +26 -10
  45. package/dist/operations/projection-scope.resolver.d.ts +1 -1
  46. package/dist/operations/structural-limits.walker.d.ts +19 -0
  47. package/dist/operations/structural-limits.walker.js +344 -0
  48. package/dist/protocol/absent-route.responder.d.ts +2 -1
  49. package/dist/protocol/absent-route.responder.js +32 -6
  50. package/dist/protocol/http-request.decoder.d.ts +6 -0
  51. package/dist/protocol/http-request.decoder.js +11 -3
  52. package/dist/protocol/protocol.binder.d.ts +4 -2
  53. package/dist/protocol/protocol.binder.js +3 -1
  54. package/dist/protocol/protocol.handler.d.ts +6 -2
  55. package/dist/protocol/protocol.handler.js +14 -4
  56. package/dist/protocol/unmounted-route.error.d.ts +19 -0
  57. package/dist/protocol/unmounted-route.error.js +10 -0
  58. package/dist/runtime/caller-data.copier.d.ts +59 -0
  59. package/dist/runtime/caller-data.copier.js +470 -0
  60. package/dist/runtime/caller-data.door.d.ts +56 -0
  61. package/dist/runtime/caller-data.door.js +65 -0
  62. package/dist/runtime/computed-read-runtime.js +3 -10
  63. package/dist/runtime/computed-value.error.d.ts +13 -0
  64. package/dist/runtime/computed-value.error.js +9 -0
  65. package/dist/runtime/create-requirement.d.ts +43 -7
  66. package/dist/runtime/create-requirement.js +89 -9
  67. package/dist/runtime/diagnostics-reporter.d.ts +20 -4
  68. package/dist/runtime/diagnostics-reporter.js +11 -4
  69. package/dist/runtime/error-boundary.d.ts +8 -0
  70. package/dist/runtime/error-boundary.js +65 -35
  71. package/dist/runtime/field-behavior.d.ts +2 -2
  72. package/dist/runtime/framework-error.d.ts +12 -0
  73. package/dist/runtime/misuse-reporter.d.ts +32 -0
  74. package/dist/runtime/misuse-reporter.js +19 -0
  75. package/dist/runtime/mutation-planner.js +13 -11
  76. package/dist/runtime/operation-request.d.ts +4 -11
  77. package/dist/runtime/operation-runtime.d.ts +14 -7
  78. package/dist/runtime/operation-runtime.js +37 -18
  79. package/dist/runtime/outcome-mapper.js +12 -2
  80. package/dist/runtime/output-validator.d.ts +10 -0
  81. package/dist/runtime/output-validator.js +11 -0
  82. package/dist/runtime/owned-execution.registry.d.ts +13 -0
  83. package/dist/runtime/owned-execution.registry.js +7 -0
  84. package/dist/runtime/pipeline.d.ts +19 -7
  85. package/dist/runtime/referential-action.d.ts +7 -0
  86. package/dist/runtime/request-envelope.validator.d.ts +5 -0
  87. package/dist/runtime/request-envelope.validator.js +118 -0
  88. package/dist/runtime/scalar-runtime.d.ts +30 -2
  89. package/dist/runtime/scalar-runtime.js +162 -79
  90. package/dist/transactions/transaction-plan-assembler.d.ts +5 -1
  91. package/dist/transactions/transaction-plan-assembler.js +74 -75
  92. package/dist/transactions/transaction-plan-validator.d.ts +30 -7
  93. package/dist/transactions/transaction-plan-validator.js +204 -153
  94. package/dist/transactions/transaction-reference-collector.d.ts +30 -0
  95. package/dist/transactions/transaction-reference-collector.js +204 -0
  96. package/dist/transactions/transaction-reference-resolver.js +10 -5
  97. package/dist/transactions/transaction-reference-validator.js +24 -17
  98. package/dist/transactions/transaction-reference.d.ts +23 -51
  99. package/dist/transactions/transaction-request.d.ts +3 -3
  100. package/dist/transactions/transaction-response.d.ts +6 -1
  101. package/dist/transactions/transaction-response.js +24 -1
  102. package/dist/transactions/transaction-runtime.d.ts +4 -2
  103. package/dist/transactions/transaction-runtime.js +4 -3
  104. package/package.json +17 -3
  105. package/dist/operations/operation-boolean-nodes.d.ts +0 -11
  106. package/dist/operations/operation-boolean-nodes.js +0 -283
  107. package/dist/operations/operation-nesting.d.ts +0 -8
  108. package/dist/operations/operation-nesting.js +0 -307
@@ -1,19 +1,34 @@
1
1
  import type { Contract } from "../contracts/contract.js";
2
- import type { OperationIdentity } from "./operation-identity.js";
2
+ import { type OperationIdentity } from "./operation-identity.js";
3
3
  import { type OperationValidationOptions, type OperationValidationResult } from "./operation-validator-core.js";
4
4
  import type { ValidationIssue } from "./validation-code.js";
5
5
  export type { OperationValidationOptions, OperationValidationResult, SemanticOperationArguments, } from "./operation-validator-core.js";
6
+ /** The key of a request's operation identity that makes it unavailable. */
7
+ export declare function unavailableOperationKey(contract: Contract, request: {
8
+ readonly resource: string;
9
+ readonly family: string;
10
+ }): "family" | "variant";
6
11
  /**
7
12
  * The Framework Validation Layer's issue for a `(resource, family, variant)` the
8
- * active Contract does not advertise on a Resource it has (`V1006` at `["operation"]`,
9
- * rolling up to `A2002`). The one spelling — the protocol's absence answers build
10
- * theirs with it, so a caller cannot tell which layer answered.
13
+ * active Contract does not advertise on a Resource it has: `V1006`, rolling up to
14
+ * `A2002`, at the key that is wrong (`["family"]` or `["variant"]`, see
15
+ * {@link unavailableOperationKey}). The one spelling — the protocol's absence
16
+ * answers build theirs with it, so a caller cannot tell which layer answered.
11
17
  */
12
- export declare function operationUnavailableIssue(request: {
18
+ export declare function operationUnavailableIssue(contract: Contract, request: {
13
19
  readonly resource: string;
14
20
  readonly family: string;
15
21
  readonly variant: string;
16
22
  }): ValidationIssue;
23
+ /**
24
+ * The availability answer alone — `V1006` when the active Contract does not advertise
25
+ * the operation on a Resource it has — read from the request's identity, never its
26
+ * arguments. A door that refuses unreadable arguments asks this first, so an
27
+ * unadvertised operation answers `A2002` whatever its arguments hold.
28
+ */
29
+ export declare function findOperationAvailabilityIssue(contract: Contract, request: OperationIdentity & {
30
+ readonly resource: string;
31
+ }): ValidationIssue | undefined;
17
32
  /**
18
33
  * Validates one standard operation's arguments against a Contract. Operation
19
34
  * availability and request limits are checked before the arguments' grammar.
@@ -1,8 +1,8 @@
1
1
  import { lookupOwn, lookupResource } from "../contracts/contract-lookup.js";
2
2
  import { isOperationVariantAvailable, } from "../contracts/resource-operations.js";
3
- import { findOperationBooleanNodeIssue } from "./operation-boolean-nodes.js";
4
- import { findOperationNestingIssue } from "./operation-nesting.js";
3
+ import { isOperationFamily, } from "./operation-identity.js";
5
4
  import { validateOperationArguments as validateSemanticOperationArguments, } from "./operation-validator-core.js";
5
+ import { findStructuralLimitIssues } from "./structural-limits.walker.js";
6
6
  function isArgumentObject(value) {
7
7
  return typeof value === "object" && value !== null && !Array.isArray(value);
8
8
  }
@@ -15,13 +15,28 @@ function isOperationAvailable(contract, request) {
15
15
  const family = lookupOwn(advertised, request.family);
16
16
  return isOperationVariantAvailable(lookupOwn(family, request.variant));
17
17
  }
18
- export function operationUnavailableIssue(request) {
18
+ export function unavailableOperationKey(contract, request) {
19
+ if (!isOperationFamily(request.family)) {
20
+ return "family";
21
+ }
22
+ const variants = lookupOwn(lookupResource(contract, request.resource)?.operations, request.family);
23
+ return variants !== undefined &&
24
+ Object.keys(variants).some((variant) => isOperationVariantAvailable(lookupOwn(variants, variant)))
25
+ ? "variant"
26
+ : "family";
27
+ }
28
+ export function operationUnavailableIssue(contract, request) {
19
29
  return {
20
- path: ["operation"],
30
+ path: [unavailableOperationKey(contract, request)],
21
31
  code: "V1006",
22
32
  message: `Operation "${request.family}.${request.variant}" is not available on Resource "${request.resource}".`,
23
33
  };
24
34
  }
35
+ export function findOperationAvailabilityIssue(contract, request) {
36
+ return isOperationAvailable(contract, request)
37
+ ? undefined
38
+ : operationUnavailableIssue(contract, request);
39
+ }
25
40
  function hasGeneralRootWhere(request) {
26
41
  if (!("where" in request.arguments)) {
27
42
  return false;
@@ -33,15 +48,16 @@ function hasGeneralRootWhere(request) {
33
48
  request.family === "upsert"));
34
49
  }
35
50
  export function validateOperationArguments(contract, request, options) {
36
- if (!isOperationAvailable(contract, request)) {
37
- return { ok: false, issues: [operationUnavailableIssue(request)] };
51
+ const unavailable = findOperationAvailabilityIssue(contract, request);
52
+ if (unavailable !== undefined) {
53
+ return { ok: false, issues: [unavailable] };
38
54
  }
39
55
  if (isArgumentObject(request.arguments)) {
40
- const nestingIssue = findOperationNestingIssue(contract, request.resource, request.arguments);
41
- if (nestingIssue !== undefined) {
42
- return { ok: false, issues: [nestingIssue] };
56
+ const finding = findStructuralLimitIssues(contract, request, request.arguments);
57
+ if (finding.nesting !== undefined) {
58
+ return { ok: false, issues: [finding.nesting] };
43
59
  }
44
- const booleanNodeIssue = findOperationBooleanNodeIssue(contract, request, request.arguments);
60
+ const booleanNodeIssue = finding.booleanNodes;
45
61
  if (booleanNodeIssue !== undefined) {
46
62
  return {
47
63
  ok: false,
@@ -7,7 +7,7 @@ import type { OperationIdentity } from "./operation-identity.js";
7
7
  * in, read off the variant the Resource advertises.
8
8
  *
9
9
  * Two readers need the answer and they run one after the other over the same
10
- * request — the `maxBooleanNodes` walk in `operation-boolean-nodes.ts`, then the
10
+ * request — the `maxBooleanNodes` tally in `structural-limits.walker.ts`, then the
11
11
  * grammar walk in `operation-validator-core.ts`. They must agree: a walk that
12
12
  * resolved a wider scope than the validator would count nodes inside a projection
13
13
  * the validator then refuses (harmless), and a walk that resolved a narrower one
@@ -0,0 +1,19 @@
1
+ import type { Contract } from "../contracts/contract.js";
2
+ import type { OperationIdentity } from "./operation-identity.js";
3
+ import type { ValidationIssue } from "./validation-code.js";
4
+ type Readable = Readonly<Record<string, unknown>>;
5
+ /** What one walk of an operation's arguments found. */
6
+ export type StructuralLimitsFinding = {
7
+ /** The nesting issue, which wins over any boolean-node issue. */
8
+ readonly nesting: ValidationIssue | undefined;
9
+ /** The boolean-node issue, when there is no nesting issue. */
10
+ readonly booleanNodes: ValidationIssue | undefined;
11
+ };
12
+ /**
13
+ * Enforces the structural limits on one operation's argument object: the nesting
14
+ * issue if there is one, otherwise the boolean-node issue if there is one.
15
+ */
16
+ export declare function findStructuralLimitIssues(contract: Contract, request: OperationIdentity & {
17
+ readonly resource: string;
18
+ }, args: Readable): StructuralLimitsFinding;
19
+ export {};
@@ -0,0 +1,344 @@
1
+ import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
2
+ import { advertisedDirectives, holdsAt, } from "../contracts/field.js";
3
+ import { NESTED_PROJECTION_SCOPE, resolveProjectionScope, } from "./projection-scope.resolver.js";
4
+ const UNTRACKED = [];
5
+ const RELATION_FILTER_OPERATORS = ["some", "every", "none"];
6
+ const DIRECTIVE_PAYLOADS = new Map([
7
+ ["$connectOrCreate", [["create", "create"]]],
8
+ ["$update", [["data", "update"]]],
9
+ [
10
+ "$upsert",
11
+ [
12
+ ["create", "create"],
13
+ ["update", "update"],
14
+ ],
15
+ ],
16
+ ]);
17
+ const NO_PAYLOADS = [];
18
+ function isRecord(value) {
19
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20
+ }
21
+ function extend(state, path, segment) {
22
+ return state.tracking ? [...path, segment] : UNTRACKED;
23
+ }
24
+ function nestingExceeded(state) {
25
+ state.nesting = {
26
+ path: ["arguments", state.rootKey],
27
+ code: "V1013",
28
+ message: `Request nesting exceeds maxNestingDepth (${state.nestingLimit}).`,
29
+ };
30
+ }
31
+ function countBooleanNode(state, path, key) {
32
+ if (state.booleanExceeded) {
33
+ return;
34
+ }
35
+ state.booleanCount += 1;
36
+ if (state.booleanCount > state.booleanLimit) {
37
+ state.booleanExceeded = true;
38
+ if (state.tracking) {
39
+ state.booleanNodes = {
40
+ path: [...path, key],
41
+ code: "V1015",
42
+ message: `Boolean/filter node count exceeds maxBooleanNodes (${state.booleanLimit}).`,
43
+ };
44
+ }
45
+ }
46
+ }
47
+ function walkWhere(state, resourceName, value, depth, path, counting) {
48
+ if (depth > state.nestingLimit) {
49
+ nestingExceeded(state);
50
+ return;
51
+ }
52
+ const resource = lookupResource(state.contract, resourceName);
53
+ if (!isRecord(value) || resource === undefined) {
54
+ return;
55
+ }
56
+ for (const key of Object.keys(value)) {
57
+ if (counting) {
58
+ countBooleanNode(state, path, key);
59
+ }
60
+ const child = value[key];
61
+ if (key === "AND" || key === "OR") {
62
+ if (Array.isArray(child)) {
63
+ for (let index = 0; index < child.length; index += 1) {
64
+ walkWhere(state, resourceName, child[index], depth + 1, state.tracking ? [...path, key, index] : UNTRACKED, counting);
65
+ if (state.nesting !== undefined) {
66
+ return;
67
+ }
68
+ }
69
+ }
70
+ continue;
71
+ }
72
+ if (key === "NOT") {
73
+ walkWhere(state, resourceName, child, depth + 1, extend(state, path, key), counting);
74
+ }
75
+ else {
76
+ const field = lookupField(resource, key);
77
+ if (field?.kind !== "relation") {
78
+ continue;
79
+ }
80
+ if (field.cardinality === "one") {
81
+ walkWhere(state, field.target, child, depth + 1, extend(state, path, key), counting);
82
+ }
83
+ else if (isRecord(child)) {
84
+ for (const operator of RELATION_FILTER_OPERATORS) {
85
+ if (!(operator in child)) {
86
+ continue;
87
+ }
88
+ walkWhere(state, field.target, child[operator], depth + 1, state.tracking ? [...path, key, operator] : UNTRACKED, counting);
89
+ if (state.nesting !== undefined) {
90
+ return;
91
+ }
92
+ }
93
+ }
94
+ }
95
+ if (state.nesting !== undefined) {
96
+ return;
97
+ }
98
+ }
99
+ }
100
+ function walkOrderBy(state, resourceName, value, depth) {
101
+ const resource = lookupResource(state.contract, resourceName);
102
+ if (resource === undefined) {
103
+ return;
104
+ }
105
+ const entries = Array.isArray(value) ? value : [value];
106
+ for (let index = 0; index < entries.length; index += 1) {
107
+ const entry = entries[index];
108
+ if (!isRecord(entry)) {
109
+ continue;
110
+ }
111
+ for (const fieldName of Object.keys(entry)) {
112
+ const field = lookupField(resource, fieldName);
113
+ if (field?.kind !== "relation" || field.cardinality === "many") {
114
+ continue;
115
+ }
116
+ const relationDepth = depth + 1;
117
+ if (relationDepth > state.nestingLimit) {
118
+ nestingExceeded(state);
119
+ return;
120
+ }
121
+ walkOrderBy(state, field.target, entry[fieldName], relationDepth);
122
+ if (state.nesting !== undefined) {
123
+ return;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ function soleKey(record) {
129
+ const keys = Object.keys(record);
130
+ return keys.length === 1 ? keys[0] : undefined;
131
+ }
132
+ function walkProjection(state, resourceName, value, depth, path, mode, scope) {
133
+ const resource = lookupResource(state.contract, resourceName);
134
+ if (!Array.isArray(value) || resource === undefined) {
135
+ return;
136
+ }
137
+ for (let index = 0; index < value.length; index += 1) {
138
+ const entry = value[index];
139
+ const relationName = isRecord(entry) ? soleKey(entry) : undefined;
140
+ const edge = relationName === undefined
141
+ ? undefined
142
+ : lookupField(resource, relationName);
143
+ if (relationName === undefined || edge?.kind !== "relation") {
144
+ continue;
145
+ }
146
+ const relationDepth = depth + 1;
147
+ if (relationDepth > state.nestingLimit) {
148
+ nestingExceeded(state);
149
+ return;
150
+ }
151
+ const projection = entry[relationName];
152
+ if (!isRecord(projection)) {
153
+ continue;
154
+ }
155
+ const relationPath = state.tracking
156
+ ? [...path, index, relationName]
157
+ : UNTRACKED;
158
+ const counted = scope !== undefined &&
159
+ holdsAt(mode === "select"
160
+ ? edge.capabilities.select
161
+ : edge.capabilities.include, scope);
162
+ if ("where" in projection) {
163
+ walkWhere(state, edge.target, projection.where, relationDepth, extend(state, relationPath, "where"), counted && edge.capabilities.filter?.includes("nested") === true);
164
+ if (state.nesting !== undefined) {
165
+ return;
166
+ }
167
+ }
168
+ if ("orderBy" in projection) {
169
+ walkOrderBy(state, edge.target, projection.orderBy, relationDepth);
170
+ if (state.nesting !== undefined) {
171
+ return;
172
+ }
173
+ }
174
+ for (const nestedMode of ["select", "include"]) {
175
+ if (!(nestedMode in projection)) {
176
+ continue;
177
+ }
178
+ walkProjection(state, edge.target, projection[nestedMode], relationDepth, extend(state, relationPath, nestedMode), nestedMode, counted ? NESTED_PROJECTION_SCOPE : undefined);
179
+ if (state.nesting !== undefined) {
180
+ return;
181
+ }
182
+ }
183
+ }
184
+ }
185
+ function walkMutationData(state, resourceName, value, depth, path, stage, counting) {
186
+ if (depth > state.nestingLimit) {
187
+ nestingExceeded(state);
188
+ return;
189
+ }
190
+ const resource = lookupResource(state.contract, resourceName);
191
+ if (!isRecord(value) || resource === undefined) {
192
+ return;
193
+ }
194
+ for (const fieldName of Object.keys(value)) {
195
+ const edge = lookupField(resource, fieldName);
196
+ const directives = value[fieldName];
197
+ if (edge?.kind !== "relation" || !isRecord(directives)) {
198
+ continue;
199
+ }
200
+ walkRelationDirectives(state, edge, directives, depth + 1, extend(state, path, fieldName), stage, counting);
201
+ if (state.nesting !== undefined) {
202
+ return;
203
+ }
204
+ }
205
+ }
206
+ function walkRelationDirectives(state, edge, directives, relationDepth, path, stage, counting) {
207
+ const supported = advertisedDirectives(edge.capabilities[stage]);
208
+ for (const directive of Object.keys(directives)) {
209
+ if (relationDepth > state.nestingLimit) {
210
+ nestingExceeded(state);
211
+ return;
212
+ }
213
+ const directivePath = extend(state, path, directive);
214
+ const directiveCounting = counting && supported?.includes(directive) === true;
215
+ const directiveValue = directives[directive];
216
+ if (Array.isArray(directiveValue)) {
217
+ for (let index = 0; index < directiveValue.length; index += 1) {
218
+ walkDirectiveEntry(state, edge.target, directive, directiveValue[index], relationDepth, extend(state, directivePath, index), directiveCounting);
219
+ if (state.nesting !== undefined) {
220
+ return;
221
+ }
222
+ }
223
+ }
224
+ else {
225
+ walkDirectiveEntry(state, edge.target, directive, directiveValue, relationDepth, directivePath, directiveCounting);
226
+ if (state.nesting !== undefined) {
227
+ return;
228
+ }
229
+ }
230
+ }
231
+ }
232
+ function walkDirectiveEntry(state, target, directive, entry, relationDepth, path, counting) {
233
+ if (directive === "$create") {
234
+ walkMutationData(state, target, entry, relationDepth, path, "create", counting);
235
+ return;
236
+ }
237
+ if (!isRecord(entry)) {
238
+ return;
239
+ }
240
+ if ((directive === "$update" || directive === "$delete") &&
241
+ "where" in entry) {
242
+ walkWhere(state, target, entry.where, relationDepth, extend(state, path, "where"), counting);
243
+ if (state.nesting !== undefined) {
244
+ return;
245
+ }
246
+ }
247
+ for (const [key, stage] of DIRECTIVE_PAYLOADS.get(directive) ?? NO_PAYLOADS) {
248
+ if (!(key in entry)) {
249
+ continue;
250
+ }
251
+ walkMutationData(state, target, entry[key], relationDepth, extend(state, path, key), stage, counting);
252
+ if (state.nesting !== undefined) {
253
+ return;
254
+ }
255
+ }
256
+ }
257
+ function isUniqueOperation(request) {
258
+ return (request.variant === "unique" &&
259
+ (request.family === "find" ||
260
+ request.family === "update" ||
261
+ request.family === "delete" ||
262
+ request.family === "upsert"));
263
+ }
264
+ function walkArguments(state, request, args) {
265
+ const resource = request.resource;
266
+ const rootPath = (key) => {
267
+ state.rootKey = key;
268
+ return state.tracking ? ["arguments", key] : UNTRACKED;
269
+ };
270
+ if ("where" in args) {
271
+ walkWhere(state, resource, args.where, 1, rootPath("where"), !isUniqueOperation(request));
272
+ if (state.nesting !== undefined) {
273
+ return;
274
+ }
275
+ }
276
+ if ("orderBy" in args) {
277
+ rootPath("orderBy");
278
+ walkOrderBy(state, resource, args.orderBy, 0);
279
+ if (state.nesting !== undefined) {
280
+ return;
281
+ }
282
+ }
283
+ for (const mode of ["select", "include"]) {
284
+ if (!(mode in args)) {
285
+ continue;
286
+ }
287
+ walkProjection(state, resource, args[mode], 0, rootPath(mode), mode, resolveProjectionScope(lookupResource(state.contract, resource), request));
288
+ if (state.nesting !== undefined) {
289
+ return;
290
+ }
291
+ }
292
+ if ("data" in args) {
293
+ const path = rootPath("data");
294
+ const data = args.data;
295
+ if (Array.isArray(data)) {
296
+ for (let index = 0; index < data.length; index += 1) {
297
+ walkMutationData(state, resource, data[index], 0, extend(state, path, index), "create", request.family === "create");
298
+ if (state.nesting !== undefined) {
299
+ return;
300
+ }
301
+ }
302
+ }
303
+ else {
304
+ walkMutationData(state, resource, data, 0, path, request.family === "update" ? "update" : "create", request.family === "create" || request.family === "update");
305
+ if (state.nesting !== undefined) {
306
+ return;
307
+ }
308
+ }
309
+ }
310
+ for (const branch of ["create", "update"]) {
311
+ if (!(branch in args)) {
312
+ continue;
313
+ }
314
+ walkMutationData(state, resource, args[branch], 0, rootPath(branch), branch, request.family === "upsert");
315
+ if (state.nesting !== undefined) {
316
+ return;
317
+ }
318
+ }
319
+ }
320
+ function walk(contract, request, args, tracking) {
321
+ const state = {
322
+ contract,
323
+ nestingLimit: contract.limits.maxNestingDepth,
324
+ booleanLimit: contract.limits.maxBooleanNodes,
325
+ tracking,
326
+ rootKey: "",
327
+ booleanCount: 0,
328
+ nesting: undefined,
329
+ booleanNodes: undefined,
330
+ booleanExceeded: false,
331
+ };
332
+ walkArguments(state, request, args);
333
+ return state;
334
+ }
335
+ export function findStructuralLimitIssues(contract, request, args) {
336
+ const untracked = walk(contract, request, args, false);
337
+ if (untracked.nesting !== undefined || !untracked.booleanExceeded) {
338
+ return { nesting: untracked.nesting, booleanNodes: undefined };
339
+ }
340
+ return {
341
+ nesting: undefined,
342
+ booleanNodes: walk(contract, request, args, true).booleanNodes,
343
+ };
344
+ }
@@ -1,5 +1,6 @@
1
1
  import type { FrameworkContracts } from "../compiler/contract.compiler.js";
2
2
  import { type OperationIdentity } from "../operations/operation-identity.js";
3
+ import { type FrameworkDiagnosticsSink } from "../runtime/diagnostics-reporter.js";
3
4
  import type { ClientProtocolIdentity } from "../runtime/operation-request.js";
4
5
  import { type OperationFailureResponse } from "../runtime/operation-response.js";
5
6
  import type { AvProtocolRequest } from "./http-request.decoder.js";
@@ -79,5 +80,5 @@ export declare function checkTransactionsRoute(contracts: FrameworkContracts, re
79
80
  * framework handling: it reads no body, runs no pipeline, calls no Adapter, and takes
80
81
  * only the compiled Contracts. In order:
81
82
  */
82
- export declare function answerAbsentRequest(contracts: FrameworkContracts, received: AvProtocolRequest): AvProtocolResponse | undefined;
83
+ export declare function answerAbsentRequest(contracts: FrameworkContracts, received: AvProtocolRequest, sink?: FrameworkDiagnosticsSink): AvProtocolResponse | undefined;
83
84
  export {};
@@ -3,6 +3,7 @@ import { isOperationVariantAvailable, } from "../contracts/resource-operations.j
3
3
  import { parseOperationIdentity, } from "../operations/operation-identity.js";
4
4
  import { operationUnavailableIssue } from "../operations/operation-validator.js";
5
5
  import { resourceUnavailableIssue } from "../operations/operation-validator-core.js";
6
+ import { createReporter, } from "../runtime/diagnostics-reporter.js";
6
7
  import { INTERNAL_FAILURE_MESSAGE, operationFailure, } from "../runtime/operation-response.js";
7
8
  import { protocolIdentityFailure } from "../runtime/operation-runtime.js";
8
9
  import { mapValidationOutcome } from "../runtime/outcome-mapper.js";
@@ -11,6 +12,7 @@ import { CONTRACT_ROUTE, GET_ONLY, isProtocolPath, operationRouteSegments, POST_
11
12
  import { encodeOperationResponse, } from "./http-response.encoder.js";
12
13
  import { identityHeadersMissing, transportFailure, } from "./protocol-failure.js";
13
14
  import { missingIdentityHeaders } from "./protocol-headers.js";
15
+ import { UnmountedRouteError } from "./unmounted-route.error.js";
14
16
  export function resolveOperationRoute(contracts, protocol, route) {
15
17
  const identityFailure = protocolIdentityFailure(contracts, protocol);
16
18
  if (identityFailure !== undefined) {
@@ -29,7 +31,9 @@ export function resolveOperationRoute(contracts, protocol, route) {
29
31
  !isOperationVariantAvailable(lookupOwn(lookupOwn(advertised, route.family), route.variant))) {
30
32
  return {
31
33
  ok: false,
32
- failure: mapValidationOutcome([operationUnavailableIssue(route)]),
34
+ failure: mapValidationOutcome([
35
+ operationUnavailableIssue(contracts.client, route),
36
+ ]),
33
37
  };
34
38
  }
35
39
  return { ok: true, operation };
@@ -82,7 +86,7 @@ function identityFailureOf(read) {
82
86
  function refusedWith(response) {
83
87
  return { ok: false, response };
84
88
  }
85
- export function answerAbsentRequest(contracts, received) {
89
+ export function answerAbsentRequest(contracts, received, sink) {
86
90
  const read = readRequest(received);
87
91
  if (!read.ok) {
88
92
  return read.response;
@@ -96,10 +100,32 @@ export function answerAbsentRequest(contracts, received) {
96
100
  ? unmounted(read.requestId)
97
101
  : transportFailure("A2012", read.requestId, GET_ONLY);
98
102
  }
99
- const route = path === TRANSACTIONS_ROUTE
100
- ? checkTransactionsRoute(contracts, read)
101
- : checkOperationRoute(contracts, read);
102
- return route.ok ? unmounted(read.requestId) : route.response;
103
+ if (path === TRANSACTIONS_ROUTE) {
104
+ const route = checkTransactionsRoute(contracts, read);
105
+ if (!route.ok) {
106
+ return route.response;
107
+ }
108
+ createReporter(sink, {
109
+ kind: "transaction",
110
+ origin: "client",
111
+ requestId: read.requestId,
112
+ })(new UnmountedRouteError("transactions", `POST ${TRANSACTIONS_ROUTE}`), "A3000");
113
+ return unmounted(read.requestId);
114
+ }
115
+ const route = checkOperationRoute(contracts, read);
116
+ if (!route.ok) {
117
+ return route.response;
118
+ }
119
+ const { family, variant } = route.operation;
120
+ createReporter(sink, {
121
+ kind: "operation",
122
+ origin: "client",
123
+ resource: route.resource,
124
+ family,
125
+ variant,
126
+ requestId: read.requestId,
127
+ })(new UnmountedRouteError(`${route.resource}.${family}.${variant}`, `POST ${path}`), "A3000");
128
+ return unmounted(read.requestId);
103
129
  }
104
130
  function unmounted(requestId) {
105
131
  return encodeOperationResponse(operationFailure("A3000", { message: INTERNAL_FAILURE_MESSAGE }), requestId);
@@ -67,4 +67,10 @@ export declare function decodeOperationRequest(contracts: FrameworkContracts, re
67
67
  * say otherwise. The body is the plan verbatim.
68
68
  */
69
69
  export declare function decodeTransactionRequest(contracts: FrameworkContracts, received: AvProtocolRequest): DecodedTransaction;
70
+ /**
71
+ * Whether `value` is a body this decoder parsed from raw content. Only the protocol's
72
+ * own shortcuts ask, about a request they have just decoded, before any host code
73
+ * could hold it; it is exported from no entry point.
74
+ */
75
+ export declare function isParsedByDecoder(value: unknown): boolean;
70
76
  export {};
@@ -16,7 +16,7 @@ export function decodeOperationRequest(contracts, received) {
16
16
  ? {
17
17
  ok: true,
18
18
  request: {
19
- scope: "client",
19
+ origin: "client",
20
20
  requestId: read.requestId,
21
21
  resource: route.resource,
22
22
  ...route.operation,
@@ -42,7 +42,7 @@ export function decodeTransactionRequest(contracts, received) {
42
42
  ? {
43
43
  ok: true,
44
44
  request: {
45
- scope: "client",
45
+ origin: "client",
46
46
  requestId: read.requestId,
47
47
  protocol: route.protocol,
48
48
  transport: transportOf(read),
@@ -85,7 +85,15 @@ function bodyValue(body, contentType, maxRequestBytes) {
85
85
  catch {
86
86
  return "A2000";
87
87
  }
88
- return isJsonObject(value) ? { value } : "A2000";
88
+ if (!isJsonObject(value)) {
89
+ return "A2000";
90
+ }
91
+ PARSED_HERE.add(value);
92
+ return { value };
93
+ }
94
+ const PARSED_HERE = new WeakSet();
95
+ export function isParsedByDecoder(value) {
96
+ return typeof value === "object" && value !== null && PARSED_HERE.has(value);
89
97
  }
90
98
  function isJsonMediaType(contentType) {
91
99
  return (contentType
@@ -48,7 +48,7 @@ export type AvProtocolBinding = {
48
48
  /**
49
49
  * The same for `/_transactions` — what `framework.executeTransaction` answered. An
50
50
  * `A3003` is reported to the Framework's diagnostics sink as a `"transaction"`
51
- * diagnostic — client scope, this `requestId`, the step it blames as `operation` —
51
+ * diagnostic — client origin, this `requestId`, the step it blames as `operation` —
52
52
  * with the value that had no wire form (or what its getter threw) as `error`.
53
53
  */
54
54
  readonly encodeTransaction: (response: TransactionResponse, requestId: string) => AvProtocolResponse;
@@ -89,7 +89,9 @@ export type AvProtocolBinding = {
89
89
  * `A2005`, `A2001`, `A2002` or `A2012` (with `Allow`) as the operation routes
90
90
  * would, `500 A3000` for a mounted route reached with its own method, and
91
91
  * `undefined` for a path outside the protocol's namespace, which the host
92
- * answers itself.
92
+ * answers itself. An operation or `/_transactions` route answered `500 A3000`
93
+ * is also reported once to the Framework's diagnostics sink, as an
94
+ * `UnmountedRouteError` naming the route the host did not mount.
93
95
  */
94
96
  readonly answerAbsent: (request: AvProtocolRequest) => AvProtocolResponse | undefined;
95
97
  };
@@ -1,4 +1,5 @@
1
1
  import { diagnosticsSinkOf } from "../runtime/diagnostics-reporter.js";
2
+ import { ownedExecutionOf } from "../runtime/owned-execution.registry.js";
2
3
  import { answerAbsentRequest } from "./absent-route.responder.js";
3
4
  import { contractDocumentOf, encodeContractRequest, } from "./contract-document.responder.js";
4
5
  import { decodeOperationRequest, decodeTransactionRequest, } from "./http-request.decoder.js";
@@ -15,6 +16,7 @@ export function bind(framework) {
15
16
  contracts,
16
17
  execute: (request) => framework.execute(request),
17
18
  executeTransaction: (request) => framework.executeTransaction(request),
19
+ owned: ownedExecutionOf(framework),
18
20
  sink,
19
21
  };
20
22
  return Object.freeze({
@@ -29,6 +31,6 @@ export function bind(framework) {
29
31
  handleOperation: (request) => handleOperationRequest(handler, request),
30
32
  handleTransaction: (request) => handleTransactionRequest(handler, request),
31
33
  encodeContract: (request) => encodeContractRequest(document, request),
32
- answerAbsent: (request) => answerAbsentRequest(contracts, request),
34
+ answerAbsent: (request) => answerAbsentRequest(contracts, request, sink),
33
35
  });
34
36
  }