@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
|
@@ -79,8 +79,11 @@ function projectPath(value) {
|
|
|
79
79
|
export const OPERATION_ARGUMENTS_PATH = [
|
|
80
80
|
"arguments",
|
|
81
81
|
];
|
|
82
|
+
export function planStepPath(index) {
|
|
83
|
+
return ["operations", index];
|
|
84
|
+
}
|
|
82
85
|
export function planOperationArgumentsPath(index) {
|
|
83
|
-
return [
|
|
86
|
+
return [...planStepPath(index), "args"];
|
|
84
87
|
}
|
|
85
88
|
const UNRESOLVED = Symbol("unresolved-path-segment");
|
|
86
89
|
function readOwnMember(container, key) {
|
|
@@ -100,38 +103,43 @@ function readSegment(container, segment) {
|
|
|
100
103
|
}
|
|
101
104
|
return readOwnMember(container, segment);
|
|
102
105
|
}
|
|
103
|
-
function
|
|
106
|
+
function vouchPath(path, view) {
|
|
104
107
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
for (let index = 0; index < view.path.length; index += 1) {
|
|
109
|
-
if (path[index] !== view.path[index]) {
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
108
|
+
const root = [view.path, OPERATION_ARGUMENTS_PATH].find((prefix) => startsWith(path, prefix));
|
|
109
|
+
if (root === undefined) {
|
|
110
|
+
return undefined;
|
|
112
111
|
}
|
|
113
112
|
let current = view.arguments;
|
|
114
|
-
for (let index =
|
|
113
|
+
for (let index = root.length; index < path.length; index += 1) {
|
|
115
114
|
const segment = path[index];
|
|
116
115
|
if (segment === undefined) {
|
|
117
|
-
return
|
|
116
|
+
return undefined;
|
|
118
117
|
}
|
|
119
118
|
current = readSegment(current, segment);
|
|
120
119
|
if (current === UNRESOLVED) {
|
|
121
|
-
return
|
|
120
|
+
return undefined;
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
|
-
return
|
|
123
|
+
return [...view.path, ...path.slice(root.length)];
|
|
125
124
|
}
|
|
126
125
|
catch {
|
|
127
|
-
return
|
|
126
|
+
return undefined;
|
|
128
127
|
}
|
|
129
128
|
}
|
|
129
|
+
function startsWith(path, prefix) {
|
|
130
|
+
return (path.length >= prefix.length &&
|
|
131
|
+
prefix.every((segment, index) => path[index] === segment));
|
|
132
|
+
}
|
|
130
133
|
function isValidationCode(value) {
|
|
131
134
|
return (typeof value === "string" &&
|
|
132
135
|
VALIDATION_CODES.includes(value));
|
|
133
136
|
}
|
|
134
|
-
|
|
137
|
+
export const MAX_DEVELOPER_ISSUE_MESSAGE_LENGTH = 1_000;
|
|
138
|
+
function isDeveloperMessage(message) {
|
|
139
|
+
return (message.length <= MAX_DEVELOPER_ISSUE_MESSAGE_LENGTH &&
|
|
140
|
+
message.trim().length > 0);
|
|
141
|
+
}
|
|
142
|
+
function projectIssue(value, unvouched, view, author) {
|
|
135
143
|
if (typeof value !== "object" || value === null) {
|
|
136
144
|
return undefined;
|
|
137
145
|
}
|
|
@@ -142,23 +150,32 @@ function projectIssue(value, unvouched, view) {
|
|
|
142
150
|
}
|
|
143
151
|
return undefined;
|
|
144
152
|
}
|
|
145
|
-
const
|
|
153
|
+
const supplied = readProperty(value, "message");
|
|
146
154
|
const rawPath = readProperty(value, "path");
|
|
147
|
-
if (typeof
|
|
155
|
+
if (typeof supplied !== "string") {
|
|
148
156
|
return undefined;
|
|
149
157
|
}
|
|
150
|
-
|
|
158
|
+
let message = VALIDATION_CODE_MESSAGES[code];
|
|
159
|
+
if (author === "developer") {
|
|
160
|
+
if (isDeveloperMessage(supplied)) {
|
|
161
|
+
message = supplied;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
unvouched.push(supplied);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
151
167
|
if (rawPath === undefined) {
|
|
152
|
-
return { code, message
|
|
168
|
+
return { code, message };
|
|
153
169
|
}
|
|
154
|
-
const
|
|
155
|
-
|
|
170
|
+
const projected = projectPath(rawPath);
|
|
171
|
+
const path = projected === undefined ? undefined : vouchPath(projected, view);
|
|
172
|
+
if (path === undefined) {
|
|
156
173
|
unvouched.push(rawPath);
|
|
157
|
-
return { code, message
|
|
174
|
+
return { code, message };
|
|
158
175
|
}
|
|
159
|
-
return { path, code, message
|
|
176
|
+
return { path, code, message };
|
|
160
177
|
}
|
|
161
|
-
function projectCause(value, unvouched, view) {
|
|
178
|
+
function projectCause(value, unvouched, view, author) {
|
|
162
179
|
const message = readProperty(value, "message");
|
|
163
180
|
if (typeof message !== "string") {
|
|
164
181
|
return undefined;
|
|
@@ -166,7 +183,7 @@ function projectCause(value, unvouched, view) {
|
|
|
166
183
|
const rawIssues = readList(readProperty(value, "issues"));
|
|
167
184
|
const rawOperation = readProperty(value, "operation");
|
|
168
185
|
const issues = rawIssues
|
|
169
|
-
?.map((issue) => projectIssue(issue, unvouched, view))
|
|
186
|
+
?.map((issue) => projectIssue(issue, unvouched, view, author))
|
|
170
187
|
.filter((issue) => issue !== undefined);
|
|
171
188
|
return {
|
|
172
189
|
message,
|
|
@@ -206,7 +223,7 @@ function isOperationErrorCode(value) {
|
|
|
206
223
|
OPERATION_CODES.includes(value) &&
|
|
207
224
|
!value.startsWith("A1"));
|
|
208
225
|
}
|
|
209
|
-
function vetDeliberateOutcome(error, view) {
|
|
226
|
+
function vetDeliberateOutcome(error, view, author) {
|
|
210
227
|
if (!isFrameworkErrorInstance(error)) {
|
|
211
228
|
return undefined;
|
|
212
229
|
}
|
|
@@ -218,7 +235,7 @@ function vetDeliberateOutcome(error, view) {
|
|
|
218
235
|
return MALFORMED_OUTCOME;
|
|
219
236
|
}
|
|
220
237
|
const unvouched = [];
|
|
221
|
-
const cause = projectCause(rawCause, unvouched, view);
|
|
238
|
+
const cause = projectCause(rawCause, unvouched, view, author);
|
|
222
239
|
return cause === undefined ? MALFORMED_OUTCOME : { code, cause, unvouched };
|
|
223
240
|
}
|
|
224
241
|
function reportUnvouched(outcome, report) {
|
|
@@ -226,13 +243,20 @@ function reportUnvouched(outcome, report) {
|
|
|
226
243
|
report(value, outcome.code);
|
|
227
244
|
}
|
|
228
245
|
}
|
|
246
|
+
function reportDeliberate(thrown, outcome, report) {
|
|
247
|
+
if (isInternalCode(outcome.code)) {
|
|
248
|
+
report(thrown, outcome.code, { deliberate: true });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
reportUnvouched(outcome, report);
|
|
252
|
+
}
|
|
229
253
|
function deliberateFailure(outcome) {
|
|
230
254
|
return operationFailure(outcome.code, isInternalCode(outcome.code)
|
|
231
255
|
? internalCause(outcome.code, outcome.cause)
|
|
232
256
|
: outcome.cause);
|
|
233
257
|
}
|
|
234
258
|
export function sanitizeOperationFailure(error, origin, report, view) {
|
|
235
|
-
const outcome = vetDeliberateOutcome(error, view);
|
|
259
|
+
const outcome = vetDeliberateOutcome(error, view, origin === "adapter" ? "adapter" : "developer");
|
|
236
260
|
if (outcome === undefined) {
|
|
237
261
|
report(error, unexpectedCode(origin));
|
|
238
262
|
return unexpectedFailure(origin);
|
|
@@ -240,7 +264,9 @@ export function sanitizeOperationFailure(error, origin, report, view) {
|
|
|
240
264
|
if (outcome === MALFORMED_OUTCOME) {
|
|
241
265
|
report(error, "A3000");
|
|
242
266
|
}
|
|
243
|
-
|
|
267
|
+
else {
|
|
268
|
+
reportDeliberate(error, outcome, report);
|
|
269
|
+
}
|
|
244
270
|
return deliberateFailure(outcome);
|
|
245
271
|
}
|
|
246
272
|
export async function resolveOperationFailure(error, filters, context, boundary, report, view) {
|
|
@@ -249,20 +275,24 @@ export async function resolveOperationFailure(error, filters, context, boundary,
|
|
|
249
275
|
if (filter === undefined) {
|
|
250
276
|
continue;
|
|
251
277
|
}
|
|
252
|
-
const handled = await attemptFilter(filter, error, context, view);
|
|
278
|
+
const handled = await attemptFilter(filter, error, context, view, boundary);
|
|
253
279
|
if (handled !== undefined) {
|
|
254
|
-
if (handled === MALFORMED_OUTCOME) {
|
|
280
|
+
if (handled.outcome === MALFORMED_OUTCOME) {
|
|
255
281
|
report(error, "A3000");
|
|
256
282
|
}
|
|
257
|
-
|
|
258
|
-
|
|
283
|
+
else {
|
|
284
|
+
reportDeliberate(handled.returned, handled.outcome, report);
|
|
285
|
+
}
|
|
286
|
+
return deliberateFailure(handled.outcome);
|
|
259
287
|
}
|
|
260
288
|
}
|
|
261
289
|
return sanitizeOperationFailure(error, boundary.originOf(error), report, view);
|
|
262
290
|
}
|
|
263
|
-
async function attemptFilter(filter, error, context, view) {
|
|
291
|
+
async function attemptFilter(filter, error, context, view, boundary) {
|
|
264
292
|
try {
|
|
265
|
-
|
|
293
|
+
const returned = await filter(error, context);
|
|
294
|
+
const outcome = vetDeliberateOutcome(returned, view, boundary.originOf(returned) === "adapter" ? "adapter" : "developer");
|
|
295
|
+
return outcome === undefined ? undefined : { returned, outcome };
|
|
266
296
|
}
|
|
267
297
|
catch {
|
|
268
298
|
return undefined;
|
|
@@ -8,7 +8,7 @@ import type { PipelineContext } from "./pipeline.js";
|
|
|
8
8
|
* What a write-stage compute callback is told about the field it is computing.
|
|
9
9
|
*/
|
|
10
10
|
type ComputedWriteContext<M extends AdapterModel, Cfg, R extends FrameworkResourceKey<M>> = {
|
|
11
|
-
readonly
|
|
11
|
+
readonly origin: RequestOrigin;
|
|
12
12
|
readonly resource: R;
|
|
13
13
|
readonly field: EffectiveFieldKey<M, Cfg, R>;
|
|
14
14
|
readonly value: unknown;
|
|
@@ -30,7 +30,7 @@ export type ComputeOnUpdate<M extends AdapterModel = AdapterModel, Cfg = AnyFram
|
|
|
30
30
|
* and returns the value to return.
|
|
31
31
|
*/
|
|
32
32
|
export type ComputeOnRead<M extends AdapterModel = AdapterModel, Cfg = AnyFrameworkConfig, R extends FrameworkResourceKey<M> = FrameworkResourceKey<M>> = (context: {
|
|
33
|
-
readonly
|
|
33
|
+
readonly origin: RequestOrigin;
|
|
34
34
|
readonly resource: R;
|
|
35
35
|
readonly field: EffectiveFieldKey<M, Cfg, R>;
|
|
36
36
|
readonly value: unknown;
|
|
@@ -2,6 +2,18 @@ import type { Cause, OperationErrorCode } from "./operation-response.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Throw from a pipeline, behavior or Adapter to answer the request with one
|
|
4
4
|
* framework error code and cause.
|
|
5
|
+
*
|
|
6
|
+
* For any code outside `A3xxx`, `cause.message` is sent to the caller exactly as
|
|
7
|
+
* written: put nothing in it you would not show the caller (no database text,
|
|
8
|
+
* SQL, stack, path or secret). So is each issue's `message`, when your own code
|
|
9
|
+
* (a pipe, guard, hook, interceptor, filter or behavior) throws it and it is not
|
|
10
|
+
* blank and at most 1,000 characters; otherwise, and for an issue an Adapter
|
|
11
|
+
* throws, the caller receives the framework's own text for the issue's code. An
|
|
12
|
+
* issue's `code` must be a validation code (`V1000` and up) and its `path` must
|
|
13
|
+
* name a member of the request's own arguments, or the issue, or the path, is
|
|
14
|
+
* not sent. For an `A3xxx` code the caller receives the framework's generic
|
|
15
|
+
* message instead, and nothing of the cause; the error itself reaches the root
|
|
16
|
+
* `diagnostics` sink once, marked `deliberate: true`.
|
|
5
17
|
*/
|
|
6
18
|
export declare class FrameworkError extends Error {
|
|
7
19
|
readonly code: OperationErrorCode;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RequestOrigin } from "../config/config-layer.js";
|
|
2
|
+
import type { OperationFamily } from "../operations/operation-identity.js";
|
|
3
|
+
/**
|
|
4
|
+
* One caller value the framework refused because it could not be read
|
|
5
|
+
* (`"unreadable-value"`: a revoked Proxy, a throwing getter or trap) or is not plain
|
|
6
|
+
* data (`"non-plain-value"`: a class instance other than `Date`, `Decimal` or
|
|
7
|
+
* `Uint8Array`). The caller received `A2004` with one `V1001` at `path`.
|
|
8
|
+
*
|
|
9
|
+
* The operation's identity is present when the framework could read it — absent for
|
|
10
|
+
* a request whose own envelope could not be read — and `operation` is the step's
|
|
11
|
+
* index in a transaction plan.
|
|
12
|
+
*/
|
|
13
|
+
export type FrameworkMisuseEvent = {
|
|
14
|
+
readonly kind: "unreadable-value" | "non-plain-value";
|
|
15
|
+
/** The issue path the caller received. */
|
|
16
|
+
readonly path: readonly (string | number)[];
|
|
17
|
+
readonly origin?: RequestOrigin;
|
|
18
|
+
readonly resource?: string;
|
|
19
|
+
readonly family?: OperationFamily;
|
|
20
|
+
readonly variant?: string;
|
|
21
|
+
readonly operation?: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Receives a {@link FrameworkMisuseEvent} for every caller value the framework
|
|
25
|
+
* refused as unreadable or not plain data. Set once, as the root configuration's
|
|
26
|
+
* `misuse`. What it returns, throws or rejects with never reaches the response.
|
|
27
|
+
*/
|
|
28
|
+
export type FrameworkMisuseSink = (event: FrameworkMisuseEvent) => void | Promise<void>;
|
|
29
|
+
/** The channel as the runtime holds it: total, never awaited. */
|
|
30
|
+
export type MisuseReporter = (event: FrameworkMisuseEvent) => void;
|
|
31
|
+
/** Binds the configured sink; without one, nothing is observed. */
|
|
32
|
+
export declare function createMisuseReporter(sink: FrameworkMisuseSink | undefined): MisuseReporter;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const IGNORE_MISUSE = () => undefined;
|
|
2
|
+
export function createMisuseReporter(sink) {
|
|
3
|
+
if (sink === undefined) {
|
|
4
|
+
return IGNORE_MISUSE;
|
|
5
|
+
}
|
|
6
|
+
return (event) => {
|
|
7
|
+
try {
|
|
8
|
+
const settled = sink(event);
|
|
9
|
+
if (typeof settled === "object" &&
|
|
10
|
+
settled !== null &&
|
|
11
|
+
typeof settled.then === "function") {
|
|
12
|
+
settled.then(undefined, () => undefined);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { normalizeScalarFieldValue } from "./scalar-runtime.js";
|
|
2
|
+
import { ComputedValueError } from "./computed-value.error.js";
|
|
3
|
+
import { jsonShapeIssues, normalizeScalarFieldValue, } from "./scalar-runtime.js";
|
|
5
4
|
function writeScalarField(contracts, resourceName, fieldName) {
|
|
6
5
|
const field = lookupField(lookupResource(contracts.core, resourceName), fieldName) ??
|
|
7
6
|
lookupField(lookupResource(contracts.active, resourceName), fieldName);
|
|
@@ -34,12 +33,6 @@ function cloneRecord(value) {
|
|
|
34
33
|
function relationEntries(value) {
|
|
35
34
|
return Array.isArray(value) ? value : [value];
|
|
36
35
|
}
|
|
37
|
-
function behaviorFailure(issues) {
|
|
38
|
-
throw new FrameworkError("A3000", {
|
|
39
|
-
message: INTERNAL_FAILURE_MESSAGE,
|
|
40
|
-
issues,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
36
|
async function computeRecord(contracts, behaviors, request, resourceName, stage, data, contextFactory) {
|
|
44
37
|
if (!isRecord(data)) {
|
|
45
38
|
return data;
|
|
@@ -64,7 +57,7 @@ async function computeRecord(contracts, behaviors, request, resourceName, stage,
|
|
|
64
57
|
continue;
|
|
65
58
|
}
|
|
66
59
|
const computed = await callback({
|
|
67
|
-
|
|
60
|
+
origin: request.origin,
|
|
68
61
|
resource: resourceName,
|
|
69
62
|
field: fieldName,
|
|
70
63
|
value: logicalData[fieldName],
|
|
@@ -76,7 +69,16 @@ async function computeRecord(contracts, behaviors, request, resourceName, stage,
|
|
|
76
69
|
}
|
|
77
70
|
const validation = normalizeScalarFieldValue(contracts.core, field, computed, "runtime", ["behaviors", resourceName, fieldName, stage]);
|
|
78
71
|
if (!validation.ok) {
|
|
79
|
-
|
|
72
|
+
throw new ComputedValueError(resourceName, fieldName, stage, validation.issues);
|
|
73
|
+
}
|
|
74
|
+
const shapeIssues = jsonShapeIssues(field, validation.value, [
|
|
75
|
+
"behaviors",
|
|
76
|
+
resourceName,
|
|
77
|
+
fieldName,
|
|
78
|
+
stage,
|
|
79
|
+
]);
|
|
80
|
+
if (shapeIssues.length > 0) {
|
|
81
|
+
throw new ComputedValueError(resourceName, fieldName, stage, shapeIssues);
|
|
80
82
|
}
|
|
81
83
|
logicalData[fieldName] = validation.value;
|
|
82
84
|
}
|
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import type { RequestOrigin } from "../config/config-layer.js";
|
|
2
1
|
import type { OperationIdentity } from "../operations/operation-identity.js";
|
|
3
2
|
import type { PipelineTransportContext } from "./pipeline.js";
|
|
4
|
-
/**
|
|
5
|
-
* @deprecated Use {@link RequestOrigin}. The set it names is not a property of
|
|
6
|
-
* an operation but the origin layer a request belongs to, derived from
|
|
7
|
-
* `ConfigLayer`; kept as an alias so existing consumers keep compiling.
|
|
8
|
-
*/
|
|
9
|
-
export type OperationScope = RequestOrigin;
|
|
10
3
|
/** The protocol version and ClientContract hash a client says it was built for. */
|
|
11
4
|
export type ClientProtocolIdentity = {
|
|
12
5
|
readonly version: number;
|
|
@@ -19,7 +12,7 @@ type RuntimeOperationBase = OperationIdentity & {
|
|
|
19
12
|
};
|
|
20
13
|
/** An in-process request, run under the Application Contract. */
|
|
21
14
|
export type ApplicationOperationRequest = RuntimeOperationBase & {
|
|
22
|
-
readonly
|
|
15
|
+
readonly origin: "application";
|
|
23
16
|
readonly protocol?: never;
|
|
24
17
|
readonly transport?: never;
|
|
25
18
|
};
|
|
@@ -28,7 +21,7 @@ export type ApplicationOperationRequest = RuntimeOperationBase & {
|
|
|
28
21
|
* the client sent.
|
|
29
22
|
*/
|
|
30
23
|
export type ClientOperationRequest = RuntimeOperationBase & {
|
|
31
|
-
readonly
|
|
24
|
+
readonly origin: "client";
|
|
32
25
|
readonly protocol: ClientProtocolIdentity;
|
|
33
26
|
readonly transport?: PipelineTransportContext;
|
|
34
27
|
};
|
|
@@ -37,10 +30,10 @@ export type FrameworkOperationRequest = ApplicationOperationRequest | ClientOper
|
|
|
37
30
|
type WithoutArguments<Request> = Request extends unknown ? Omit<Request, "arguments"> : never;
|
|
38
31
|
/**
|
|
39
32
|
* A request as every stage after the Framework Validation Layer sees it:
|
|
40
|
-
* identity,
|
|
33
|
+
* identity, origin, request id and transport, but never `arguments`. Those
|
|
41
34
|
* stages act on the validated arguments passed beside it; the unvalidated
|
|
42
35
|
* view — inside a transaction plan still carrying `$ref` placeholders — is
|
|
43
|
-
* unreachable by type. Distributive, so the
|
|
36
|
+
* unreachable by type. Distributive, so the origin and family/variant
|
|
44
37
|
* correlations survive the omission.
|
|
45
38
|
*/
|
|
46
39
|
export type OperationExecutionRequest = WithoutArguments<FrameworkOperationRequest>;
|
|
@@ -6,10 +6,12 @@ import type { ResolvedFrameworkConfig } from "../config/config.resolver.js";
|
|
|
6
6
|
import type { Contract } from "../contracts/contract.js";
|
|
7
7
|
import type { OperationValidationOptions, SemanticOperationArguments } from "../operations/operation-validator.js";
|
|
8
8
|
import type { FrameworkTransactionRequest } from "../transactions/transaction-request.js";
|
|
9
|
-
import { type
|
|
9
|
+
import { type ArgumentsSource } from "./caller-data.door.js";
|
|
10
|
+
import { type RecordWriteFacts } from "./create-requirement.js";
|
|
10
11
|
import type { DiagnosticsReporter } from "./diagnostics-reporter.js";
|
|
11
12
|
import type { ClientProtocolIdentity, FrameworkOperationRequest, OperationExecutionRequest } from "./operation-request.js";
|
|
12
13
|
import type { OperationFailureResponse, OperationResponse } from "./operation-response.js";
|
|
14
|
+
import { type NormalizationWrite } from "./scalar-runtime.js";
|
|
13
15
|
export type OperationRuntimeDependencies<M extends AdapterModel> = {
|
|
14
16
|
readonly adapter: Adapter<M>;
|
|
15
17
|
readonly contracts: FrameworkContracts;
|
|
@@ -21,17 +23,22 @@ export type OperationRuntimeDependencies<M extends AdapterModel> = {
|
|
|
21
23
|
* transaction plan the root Adapter is unreachable by type.
|
|
22
24
|
*/
|
|
23
25
|
export type OperationPipelineDependencies<M extends AdapterModel> = Omit<OperationRuntimeDependencies<M>, "adapter"> & {
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The Adapter's create requirements and referential edges, read once at
|
|
28
|
+
* construction.
|
|
29
|
+
*/
|
|
30
|
+
readonly recordWrites: RecordWriteFacts;
|
|
26
31
|
};
|
|
27
32
|
/**
|
|
28
33
|
* The executor core owns no diagnostics channel: the framework factory owns
|
|
29
34
|
* it and hands in one reporter already bound to this request, so every
|
|
30
35
|
* boundary — including the outermost totality guard, which runs outside this
|
|
31
|
-
* function — reports through the same channel.
|
|
36
|
+
* function — reports through the same channel. `source` says whether the
|
|
37
|
+
* request's arguments are the caller's own objects, copied at the door, or data
|
|
38
|
+
* no caller holds (see {@link ArgumentsSource}).
|
|
32
39
|
*/
|
|
33
|
-
export type FrameworkOperationExecutor = (request: FrameworkOperationRequest, report: DiagnosticsReporter) => Promise<OperationResponse>;
|
|
34
|
-
/** Selects the Contract of the request's
|
|
40
|
+
export type FrameworkOperationExecutor = (request: FrameworkOperationRequest, report: DiagnosticsReporter, source: ArgumentsSource) => Promise<OperationResponse>;
|
|
41
|
+
/** Selects the Contract of the request's origin. */
|
|
35
42
|
export declare function activeContract(contracts: FrameworkContracts, request: FrameworkOperationRequest | FrameworkTransactionRequest): Contract;
|
|
36
43
|
/** Creates the transport-independent operation executor owned by core. */
|
|
37
44
|
export declare function createOperationRuntime<M extends AdapterModel>(dependencies: OperationRuntimeDependencies<M>): FrameworkOperationExecutor;
|
|
@@ -53,7 +60,7 @@ export type ValidatedOperationArguments = {
|
|
|
53
60
|
readonly response: OperationFailureResponse;
|
|
54
61
|
};
|
|
55
62
|
/** Validates one operation's arguments through the Framework Validation Layer. */
|
|
56
|
-
export declare function validateOperationRequest(contract: Contract, request: FrameworkOperationRequest, options?: OperationValidationOptions): ValidatedOperationArguments;
|
|
63
|
+
export declare function validateOperationRequest(contract: Contract, request: FrameworkOperationRequest, options?: OperationValidationOptions, write?: NormalizationWrite): ValidatedOperationArguments;
|
|
57
64
|
/**
|
|
58
65
|
* Everything after the Framework Validation Layer: the developer/runtime path
|
|
59
66
|
* wrapped in the post-validation error boundary. Validation failures never
|
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
|
|
2
|
-
import { validateOperationArguments } from "../operations/operation-validator.js";
|
|
2
|
+
import { findOperationAvailabilityIssue, validateOperationArguments, } from "../operations/operation-validator.js";
|
|
3
|
+
import { copyAtDoor, misuseIdentityOf, refuseCallerData, } from "./caller-data.door.js";
|
|
3
4
|
import { materializeReadOperation, planReadOperation, } from "./computed-read-runtime.js";
|
|
4
|
-
import {
|
|
5
|
+
import { createRequirementIssues, recordWriteFactsOf, } from "./create-requirement.js";
|
|
5
6
|
import { AdapterBoundary, OPERATION_ARGUMENTS_PATH, resolveOperationFailure, sanitizeOperationFailure, } from "./error-boundary.js";
|
|
7
|
+
import { createMisuseReporter, } from "./misuse-reporter.js";
|
|
6
8
|
import { applyWriteFieldBehaviors, stripVirtualMutationInput, } from "./mutation-planner.js";
|
|
7
9
|
import { internalFailure, operationFailure } from "./operation-response.js";
|
|
8
10
|
import { mapOperationOutcome, mapValidationOutcome } from "./outcome-mapper.js";
|
|
9
|
-
import { validateOperationOutput } from "./output-validator.js";
|
|
11
|
+
import { OutputValidationError, validateOperationOutput, } from "./output-validator.js";
|
|
10
12
|
import { refusePipeOutput } from "./pipe-output.validator.js";
|
|
11
|
-
import { normalizeOperationScalarValues } from "./scalar-runtime.js";
|
|
13
|
+
import { normalizeOperationScalarValues, } from "./scalar-runtime.js";
|
|
12
14
|
export function activeContract(contracts, request) {
|
|
13
|
-
return request.
|
|
15
|
+
return request.origin === "application"
|
|
14
16
|
? contracts.application
|
|
15
17
|
: contracts.client;
|
|
16
18
|
}
|
|
17
19
|
function activeScopeConfig(config, request) {
|
|
18
|
-
return request.
|
|
20
|
+
return request.origin === "application" ? config.application : config.client;
|
|
19
21
|
}
|
|
20
22
|
function pipelineContext(contract, request, args) {
|
|
21
23
|
return {
|
|
22
24
|
contract,
|
|
23
|
-
|
|
25
|
+
origin: request.origin,
|
|
24
26
|
resource: request.resource,
|
|
25
27
|
family: request.family,
|
|
26
28
|
variant: request.variant,
|
|
27
29
|
args,
|
|
28
30
|
requestId: request.requestId,
|
|
29
|
-
...(request.
|
|
31
|
+
...(request.origin === "client" && request.transport !== undefined
|
|
30
32
|
? { transport: request.transport }
|
|
31
33
|
: {}),
|
|
32
34
|
};
|
|
@@ -37,24 +39,30 @@ function forbidden() {
|
|
|
37
39
|
});
|
|
38
40
|
}
|
|
39
41
|
function outputAccepted(contract, request, projection, data, report) {
|
|
42
|
+
let validation;
|
|
40
43
|
try {
|
|
41
|
-
|
|
44
|
+
validation = validateOperationOutput(contract, request, projection, data);
|
|
42
45
|
}
|
|
43
46
|
catch (error) {
|
|
44
47
|
report(error, "A3000");
|
|
45
48
|
return false;
|
|
46
49
|
}
|
|
50
|
+
if (!validation.ok) {
|
|
51
|
+
report(new OutputValidationError(request, validation.issues), "A3000");
|
|
52
|
+
}
|
|
53
|
+
return validation.ok;
|
|
47
54
|
}
|
|
48
55
|
export function createOperationRuntime(dependencies) {
|
|
49
56
|
const pipelineDependencies = {
|
|
50
57
|
contracts: dependencies.contracts,
|
|
51
58
|
config: dependencies.config,
|
|
52
|
-
|
|
59
|
+
recordWrites: recordWriteFactsOf(dependencies.adapter),
|
|
53
60
|
};
|
|
54
|
-
|
|
61
|
+
const misuse = createMisuseReporter(dependencies.config.misuse);
|
|
62
|
+
return async (request, report, source) => {
|
|
55
63
|
let validated;
|
|
56
64
|
try {
|
|
57
|
-
validated = validateOperation(dependencies, request);
|
|
65
|
+
validated = validateOperation(dependencies, request, source, misuse);
|
|
58
66
|
}
|
|
59
67
|
catch (error) {
|
|
60
68
|
return sanitizeOperationFailure(error, "runtime", report, {
|
|
@@ -68,20 +76,31 @@ export function createOperationRuntime(dependencies) {
|
|
|
68
76
|
return executeValidatedOperation(pipelineDependencies, request, validated.contract, validated.arguments, OPERATION_ARGUMENTS_PATH, dependencies.adapter, report);
|
|
69
77
|
};
|
|
70
78
|
}
|
|
71
|
-
function validateOperation(dependencies, request) {
|
|
79
|
+
function validateOperation(dependencies, request, source, misuse) {
|
|
72
80
|
const protocolFailure = validateProtocolIdentity(dependencies.contracts, request);
|
|
73
81
|
if (protocolFailure !== undefined) {
|
|
74
82
|
return rejected(protocolFailure);
|
|
75
83
|
}
|
|
76
84
|
const contract = activeContract(dependencies.contracts, request);
|
|
77
|
-
|
|
85
|
+
let owned = request;
|
|
86
|
+
if (source === "caller" &&
|
|
87
|
+
findOperationAvailabilityIssue(contract, request) === undefined) {
|
|
88
|
+
const copy = copyAtDoor(request.arguments);
|
|
89
|
+
if (!copy.ok) {
|
|
90
|
+
return rejected(mapValidationOutcome([
|
|
91
|
+
refuseCallerData(copy.error, OPERATION_ARGUMENTS_PATH, misuseIdentityOf(request), misuse),
|
|
92
|
+
]));
|
|
93
|
+
}
|
|
94
|
+
owned = { ...request, arguments: copy.value };
|
|
95
|
+
}
|
|
96
|
+
const validation = validateOperationRequest(contract, owned, undefined, "in-place");
|
|
78
97
|
if (!validation.ok) {
|
|
79
98
|
return rejected(validation.response);
|
|
80
99
|
}
|
|
81
100
|
return { ok: true, contract, arguments: validation.arguments };
|
|
82
101
|
}
|
|
83
102
|
export function validateProtocolIdentity(contracts, request) {
|
|
84
|
-
return request.
|
|
103
|
+
return request.origin === "client"
|
|
85
104
|
? protocolIdentityFailure(contracts, request.protocol)
|
|
86
105
|
: undefined;
|
|
87
106
|
}
|
|
@@ -98,12 +117,12 @@ export function protocolIdentityFailure(contracts, protocol) {
|
|
|
98
117
|
}
|
|
99
118
|
return undefined;
|
|
100
119
|
}
|
|
101
|
-
export function validateOperationRequest(contract, request, options) {
|
|
120
|
+
export function validateOperationRequest(contract, request, options, write = "copy") {
|
|
102
121
|
const validation = validateOperationArguments(contract, request, options);
|
|
103
122
|
if (!validation.ok) {
|
|
104
123
|
return { ok: false, response: mapValidationOutcome(validation.issues) };
|
|
105
124
|
}
|
|
106
|
-
const scalarNormalization = normalizeOperationScalarValues(contract, { ...request, arguments: validation.arguments }, request.
|
|
125
|
+
const scalarNormalization = normalizeOperationScalarValues(contract, { ...request, arguments: validation.arguments }, request.origin, options, write);
|
|
107
126
|
return scalarNormalization.ok
|
|
108
127
|
? { ok: true, arguments: scalarNormalization.arguments }
|
|
109
128
|
: {
|
|
@@ -144,7 +163,7 @@ export async function executeValidatedOperation(dependencies, request, contract,
|
|
|
144
163
|
}
|
|
145
164
|
}
|
|
146
165
|
args = await applyWriteFieldBehaviors({ core: dependencies.contracts.core, active: contract }, scopeConfig.behaviors, request, args, () => pipelineContext(contract, request, args));
|
|
147
|
-
const missing =
|
|
166
|
+
const missing = createRequirementIssues(contract, dependencies.recordWrites, request, args, argumentsPath);
|
|
148
167
|
if (missing.length > 0) {
|
|
149
168
|
return mapValidationOutcome(missing);
|
|
150
169
|
}
|
|
@@ -42,11 +42,21 @@ export function mapOperationOutcome(operation, data) {
|
|
|
42
42
|
? absenceOutcome(rule.absence)
|
|
43
43
|
: operationSuccess(data, rule.success);
|
|
44
44
|
}
|
|
45
|
+
function stepRelativeHead(issue) {
|
|
46
|
+
const path = issue.path;
|
|
47
|
+
return path?.[0] === "operations" && typeof path[1] === "number"
|
|
48
|
+
? path[2]
|
|
49
|
+
: path?.[0];
|
|
50
|
+
}
|
|
51
|
+
function namesUnavailableOperation(issue) {
|
|
52
|
+
const head = stepRelativeHead(issue);
|
|
53
|
+
return issue.code === "V1006" && (head === "family" || head === "variant");
|
|
54
|
+
}
|
|
45
55
|
function validationCode(issues) {
|
|
46
|
-
if (issues.some((issue) => issue.
|
|
56
|
+
if (issues.some((issue) => issue.code !== "V1019" && stepRelativeHead(issue) === "resource")) {
|
|
47
57
|
return "A2001";
|
|
48
58
|
}
|
|
49
|
-
if (issues.some(
|
|
59
|
+
if (issues.some(namesUnavailableOperation)) {
|
|
50
60
|
return "A2002";
|
|
51
61
|
}
|
|
52
62
|
if (issues.some((issue) => issue.code === "V1010" ||
|
|
@@ -11,6 +11,16 @@ export type OutputValidationResult = {
|
|
|
11
11
|
type OutputOperation = OperationIdentity & {
|
|
12
12
|
readonly resource: string;
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* The error the diagnostics channel receives when a result fails output
|
|
16
|
+
* validation: whose result it was and what the validator found. The caller
|
|
17
|
+
* receives the generic `A3000` and never this.
|
|
18
|
+
*/
|
|
19
|
+
export declare class OutputValidationError extends Error {
|
|
20
|
+
readonly issues: readonly ValidationIssue[];
|
|
21
|
+
readonly name = "OutputValidationError";
|
|
22
|
+
constructor(operation: OutputOperation, issues: readonly ValidationIssue[]);
|
|
23
|
+
}
|
|
14
24
|
/** Validate final operation output against fixed cardinality and executed projection. */
|
|
15
25
|
export declare function validateOperationOutput(contract: Contract, operation: OutputOperation, projection: ReadProjectionPlan | undefined, value: unknown): OutputValidationResult;
|
|
16
26
|
export {};
|
|
@@ -168,6 +168,17 @@ function validateProjection(contract, operation, projection, value) {
|
|
|
168
168
|
? validateExactRecord(contract, value, projection, ["data"])
|
|
169
169
|
: { ok: true };
|
|
170
170
|
}
|
|
171
|
+
export class OutputValidationError extends Error {
|
|
172
|
+
issues;
|
|
173
|
+
name = "OutputValidationError";
|
|
174
|
+
constructor(operation, issues) {
|
|
175
|
+
const [first] = issues;
|
|
176
|
+
super(`The result of ${operation.resource}.${operation.family}.${operation.variant} does not satisfy its Contract${first === undefined
|
|
177
|
+
? "."
|
|
178
|
+
: ` at "${(first.path ?? []).join(".")}" (${first.code}: ${first.message})`}`);
|
|
179
|
+
this.issues = issues;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
171
182
|
export function validateOperationOutput(contract, operation, projection, value) {
|
|
172
183
|
const cardinality = validateOperationOutputCardinality(operation, value);
|
|
173
184
|
return cardinality.ok
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ClientTransactionRequest } from "../transactions/transaction-request.js";
|
|
2
|
+
import type { TransactionResponse } from "../transactions/transaction-response.js";
|
|
3
|
+
import type { ClientOperationRequest } from "./operation-request.js";
|
|
4
|
+
import type { OperationResponse } from "./operation-response.js";
|
|
5
|
+
/** The two entries that run a request the protocol decoded itself, uncopied. */
|
|
6
|
+
export type OwnedExecution = {
|
|
7
|
+
readonly execute: (request: ClientOperationRequest) => Promise<OperationResponse>;
|
|
8
|
+
readonly executeTransaction: (request: ClientTransactionRequest) => Promise<TransactionResponse>;
|
|
9
|
+
};
|
|
10
|
+
/** Records `framework`'s owned entries; called once, at construction. */
|
|
11
|
+
export declare function recordOwnedExecution(framework: object, execution: OwnedExecution): void;
|
|
12
|
+
/** `framework`'s owned entries, or `undefined` for anything `createFramework` did not return. */
|
|
13
|
+
export declare function ownedExecutionOf(framework: object): OwnedExecution | undefined;
|