@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.
- package/CHANGELOG.md +338 -0
- package/README.md +100 -3
- package/dist/adapters/adapter-model.d.ts +8 -0
- package/dist/adapters/adapter.interface.d.ts +6 -2
- package/dist/compiler/contract-compiler-diagnostic.d.ts +6 -0
- package/dist/compiler/contract.compiler.js +67 -0
- package/dist/config/config.resolver.d.ts +3 -0
- package/dist/config/config.resolver.js +1 -0
- package/dist/config/config.validator.js +6 -0
- package/dist/config/define-fields.d.ts +10 -3
- package/dist/config/framework-config.d.ts +4 -1
- package/dist/config/framework-fields-config.d.ts +6 -0
- package/dist/contracts/client-contract-structure.validator.d.ts +21 -1
- package/dist/contracts/client-contract-structure.validator.js +99 -2
- package/dist/contracts/contract-canonicalizer.js +32 -0
- package/dist/contracts/contract.d.ts +7 -0
- package/dist/contracts/field.d.ts +10 -0
- package/dist/contracts/json-shape.d.ts +65 -0
- package/dist/contracts/json-shape.js +1 -0
- package/dist/contracts/reference-write-limit.d.ts +7 -0
- package/dist/contracts/reference-write-limit.js +1 -0
- package/dist/contracts/scalar-value-type.d.ts +12 -1
- package/dist/framework/contract-view.d.ts +126 -21
- package/dist/framework/framework.d.ts +5 -3
- package/dist/framework/framework.factory.js +56 -13
- package/dist/index.d.ts +8 -40
- package/dist/index.js +0 -20
- package/dist/internal.d.ts +35 -0
- package/dist/internal.js +16 -0
- package/dist/json/avz.d.ts +96 -0
- package/dist/json/avz.js +212 -0
- package/dist/json/index.d.ts +6 -0
- package/dist/json/index.js +1 -0
- package/dist/json/json-shape.registry.d.ts +22 -0
- package/dist/json/json-shape.registry.js +16 -0
- package/dist/json/zod-issue.translator.d.ts +51 -0
- package/dist/json/zod-issue.translator.js +142 -0
- package/dist/operations/operation-arguments.d.ts +151 -101
- package/dist/operations/operation-identity.d.ts +2 -0
- package/dist/operations/operation-identity.js +3 -0
- package/dist/operations/operation-validator-core.d.ts +1 -1
- package/dist/operations/operation-validator-core.js +28 -10
- package/dist/operations/operation-validator.d.ts +20 -5
- package/dist/operations/operation-validator.js +26 -10
- package/dist/operations/projection-scope.resolver.d.ts +1 -1
- package/dist/operations/structural-limits.walker.d.ts +19 -0
- package/dist/operations/structural-limits.walker.js +344 -0
- package/dist/protocol/absent-route.responder.d.ts +2 -1
- package/dist/protocol/absent-route.responder.js +32 -6
- package/dist/protocol/http-request.decoder.d.ts +6 -0
- package/dist/protocol/http-request.decoder.js +11 -3
- package/dist/protocol/protocol.binder.d.ts +4 -2
- package/dist/protocol/protocol.binder.js +3 -1
- package/dist/protocol/protocol.handler.d.ts +6 -2
- package/dist/protocol/protocol.handler.js +14 -4
- package/dist/protocol/unmounted-route.error.d.ts +19 -0
- package/dist/protocol/unmounted-route.error.js +10 -0
- package/dist/runtime/caller-data.copier.d.ts +59 -0
- package/dist/runtime/caller-data.copier.js +470 -0
- package/dist/runtime/caller-data.door.d.ts +56 -0
- package/dist/runtime/caller-data.door.js +65 -0
- package/dist/runtime/computed-read-runtime.js +3 -10
- package/dist/runtime/computed-value.error.d.ts +13 -0
- package/dist/runtime/computed-value.error.js +9 -0
- package/dist/runtime/create-requirement.d.ts +43 -7
- package/dist/runtime/create-requirement.js +89 -9
- package/dist/runtime/diagnostics-reporter.d.ts +20 -4
- package/dist/runtime/diagnostics-reporter.js +11 -4
- package/dist/runtime/error-boundary.d.ts +8 -0
- package/dist/runtime/error-boundary.js +65 -35
- package/dist/runtime/field-behavior.d.ts +2 -2
- package/dist/runtime/framework-error.d.ts +12 -0
- package/dist/runtime/misuse-reporter.d.ts +32 -0
- package/dist/runtime/misuse-reporter.js +19 -0
- package/dist/runtime/mutation-planner.js +13 -11
- package/dist/runtime/operation-request.d.ts +4 -11
- package/dist/runtime/operation-runtime.d.ts +14 -7
- package/dist/runtime/operation-runtime.js +37 -18
- package/dist/runtime/outcome-mapper.js +12 -2
- package/dist/runtime/output-validator.d.ts +10 -0
- package/dist/runtime/output-validator.js +11 -0
- package/dist/runtime/owned-execution.registry.d.ts +13 -0
- package/dist/runtime/owned-execution.registry.js +7 -0
- package/dist/runtime/pipeline.d.ts +19 -7
- package/dist/runtime/referential-action.d.ts +7 -0
- package/dist/runtime/request-envelope.validator.d.ts +5 -0
- package/dist/runtime/request-envelope.validator.js +118 -0
- package/dist/runtime/scalar-runtime.d.ts +30 -2
- package/dist/runtime/scalar-runtime.js +162 -79
- package/dist/transactions/transaction-plan-assembler.d.ts +5 -1
- package/dist/transactions/transaction-plan-assembler.js +74 -75
- package/dist/transactions/transaction-plan-validator.d.ts +30 -7
- package/dist/transactions/transaction-plan-validator.js +204 -153
- package/dist/transactions/transaction-reference-collector.d.ts +30 -0
- package/dist/transactions/transaction-reference-collector.js +204 -0
- package/dist/transactions/transaction-reference-resolver.js +10 -5
- package/dist/transactions/transaction-reference-validator.js +24 -17
- package/dist/transactions/transaction-reference.d.ts +23 -51
- package/dist/transactions/transaction-request.d.ts +3 -3
- package/dist/transactions/transaction-response.d.ts +6 -1
- package/dist/transactions/transaction-response.js +24 -1
- package/dist/transactions/transaction-runtime.d.ts +4 -2
- package/dist/transactions/transaction-runtime.js +4 -3
- package/package.json +17 -3
- package/dist/operations/operation-boolean-nodes.d.ts +0 -11
- package/dist/operations/operation-boolean-nodes.js +0 -283
- package/dist/operations/operation-nesting.d.ts +0 -8
- package/dist/operations/operation-nesting.js +0 -307
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
import type { Contract } from "../contracts/contract.js";
|
|
2
|
-
import type
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 {
|
|
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
|
|
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: [
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
41
|
-
if (
|
|
42
|
-
return { ok: false, issues: [
|
|
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 =
|
|
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`
|
|
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([
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|