@aventara/core 1.0.0-rc.10 → 1.0.0-rc.11
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 +79 -0
- package/ERRORS.md +82 -82
- package/README.md +57 -1
- package/dist/compiler/contract-compiler-diagnostic.guidance.js +1 -1
- package/dist/compiler/contract.compiler.js +56 -9
- package/dist/compiler/create-requirements.projector.js +1 -3
- package/dist/compiler/field-validator.compiler.js +1 -3
- package/dist/compiler/managed-elsewhere.projector.js +5 -5
- package/dist/config/cache-stage.factory.d.ts +24 -0
- package/dist/config/cache-stage.factory.js +3 -0
- package/dist/config/config.resolver.js +1 -7
- package/dist/config/config.validator.js +61 -13
- package/dist/config/exact-framework-config.d.ts +6 -0
- package/dist/config/field-compute.resolver.js +1 -3
- package/dist/config/pipeline-config.d.ts +16 -0
- package/dist/config/pipeline-tree.resolver.js +2 -3
- package/dist/contracts/canonical-json.writer.d.ts +7 -0
- package/dist/contracts/canonical-json.writer.js +55 -0
- package/dist/contracts/client-contract-structure.validator.d.ts +64 -0
- package/dist/contracts/client-contract-structure.validator.js +12 -0
- package/dist/contracts/contract-canonicalizer.js +5 -0
- package/dist/contracts/contract-limits.d.ts +6 -0
- package/dist/contracts/contract-limits.js +2 -0
- package/dist/contracts/contract.d.ts +15 -0
- package/dist/contracts/json-schema.projector.js +1 -3
- package/dist/framework/access-facade.js +1 -3
- package/dist/framework/framework.factory.js +42 -13
- package/dist/framework/operation-facade.d.ts +67 -6
- package/dist/framework/operation-facade.js +7 -0
- package/dist/framework/operation-set.runner.d.ts +47 -0
- package/dist/framework/operation-set.runner.js +168 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +2 -0
- package/dist/operations/operation-validator-core.js +2 -4
- package/dist/operations/structural-limits.walker.js +1 -3
- package/dist/policy/access-question.runtime.js +1 -3
- package/dist/policy/policy-admission.js +1 -3
- package/dist/policy/policy-after-update.runtime.js +1 -3
- package/dist/policy/policy-nested-write.runtime.js +3 -12
- package/dist/policy/policy-read.runtime.js +1 -3
- package/dist/policy/policy-reference-write.runtime.js +2 -11
- package/dist/policy/policy-request.d.ts +31 -0
- package/dist/policy/policy-request.js +69 -17
- package/dist/policy/policy-session.js +1 -3
- package/dist/policy/policy-write.runtime.js +1 -3
- package/dist/prebuilt/core-version.generated.d.ts +1 -1
- package/dist/prebuilt/core-version.generated.js +1 -1
- package/dist/prebuilt/prebuilt-changes.js +1 -3
- package/dist/prebuilt/prebuilt-contracts.loader.js +1 -3
- package/dist/prebuilt/prebuilt-inputs.fingerprint.js +1 -3
- package/dist/protocol/absent-route.responder.d.ts +69 -16
- package/dist/protocol/absent-route.responder.js +75 -31
- package/dist/protocol/contract-document.responder.d.ts +7 -0
- package/dist/protocol/contract-document.responder.js +3 -2
- package/dist/protocol/http-request.decoder.d.ts +33 -0
- package/dist/protocol/http-request.decoder.js +23 -7
- package/dist/protocol/http-request.reader.d.ts +4 -0
- package/dist/protocol/http-request.reader.js +11 -2
- package/dist/protocol/http-response.encoder.d.ts +12 -0
- package/dist/protocol/http-response.encoder.js +15 -1
- package/dist/protocol/operation-response.guard.js +1 -3
- package/dist/protocol/operation-set.decoder.d.ts +48 -0
- package/dist/protocol/operation-set.decoder.js +185 -0
- package/dist/protocol/operation-set.handler.d.ts +27 -0
- package/dist/protocol/operation-set.handler.js +179 -0
- package/dist/protocol/protocol-headers.d.ts +9 -0
- package/dist/protocol/protocol-headers.js +12 -5
- package/dist/protocol/protocol-surface.d.ts +12 -4
- package/dist/protocol/protocol-surface.js +34 -1
- package/dist/protocol/protocol.binder.d.ts +37 -1
- package/dist/protocol/protocol.binder.js +2 -0
- package/dist/protocol/protocol.handler.d.ts +35 -2
- package/dist/protocol/protocol.handler.js +84 -8
- package/dist/protocol/read-cache.responder.d.ts +26 -0
- package/dist/protocol/read-cache.responder.js +47 -0
- package/dist/protocol/read-query.codec.d.ts +34 -0
- package/dist/protocol/read-query.codec.js +125 -0
- package/dist/runtime/computed-read-runtime.js +2 -8
- package/dist/runtime/create-requirement.js +1 -3
- package/dist/runtime/diagnostics-reporter.d.ts +6 -1
- package/dist/runtime/diagnostics-reporter.js +3 -8
- package/dist/runtime/error.catalog.d.ts +2 -2
- package/dist/runtime/error.catalog.js +1 -1
- package/dist/runtime/field-stage.runtime.d.ts +0 -2
- package/dist/runtime/field-stage.runtime.js +1 -7
- package/dist/runtime/field-validator.runtime.js +2 -1
- package/dist/runtime/mutation-planner.js +1 -3
- package/dist/runtime/nested-write.walker.js +2 -1
- package/dist/runtime/output-validator.js +1 -3
- package/dist/runtime/owned-execution.registry.d.ts +22 -2
- package/dist/runtime/pipeline.d.ts +5 -1
- package/dist/runtime/record.guard.d.ts +14 -0
- package/dist/runtime/record.guard.js +10 -0
- package/dist/runtime/request-envelope.validator.js +13 -10
- package/dist/runtime/scalar-runtime.d.ts +0 -10
- package/dist/runtime/scalar-runtime.js +1 -10
- package/dist/runtime/thenable.guard.d.ts +7 -0
- package/dist/runtime/thenable.guard.js +5 -0
- package/dist/runtime/unsent-value.normalizer.js +2 -1
- package/dist/transactions/operation-arguments-encoder.js +1 -7
- package/dist/transactions/sql-statement.validator.js +2 -1
- package/dist/transactions/transaction-plan-assembler.js +1 -7
- package/dist/transactions/transaction-plan-validator.js +4 -6
- package/dist/transactions/transaction-reference-collector.js +8 -10
- package/dist/transactions/transaction-reference-resolver.js +1 -3
- package/dist/transactions/transaction-runtime.js +31 -2
- package/dist/transactions/transaction-stage.runtime.js +1 -3
- package/package.json +1 -1
|
@@ -3,29 +3,40 @@ import { createDiagnosticsReporter, createReporter, } from "../runtime/diagnosti
|
|
|
3
3
|
import { sanitizeOperationFailure } from "../runtime/error-boundary.js";
|
|
4
4
|
import { internalFailure, } from "../runtime/operation-response.js";
|
|
5
5
|
import { capturePlanAtCall } from "../transactions/transaction-plan-validator.js";
|
|
6
|
-
import {
|
|
6
|
+
import { checkOperationRoute } from "./absent-route.responder.js";
|
|
7
|
+
import { adoptParsedBody, decodeCanRequest, decodeOperationRead, decodeTransactionRequest, isParsedByDecoder, transportOf, } from "./http-request.decoder.js";
|
|
8
|
+
import { readRequest } from "./http-request.reader.js";
|
|
7
9
|
import { encodeOperationResponse, encodeTransactionResponse, } from "./http-response.encoder.js";
|
|
8
|
-
|
|
10
|
+
import { transportFailure } from "./protocol-failure.js";
|
|
11
|
+
import { headerValue } from "./protocol-headers.js";
|
|
12
|
+
import { cachedReadAnswer, uncachedAnswer } from "./read-cache.responder.js";
|
|
13
|
+
import { readArgumentsOf } from "./read-query.codec.js";
|
|
14
|
+
export function admitOperation(context, request) {
|
|
9
15
|
const owned = context.owned;
|
|
10
16
|
if (isParsedByDecoder(request.arguments)) {
|
|
11
|
-
return {
|
|
17
|
+
return {
|
|
18
|
+
request,
|
|
19
|
+
run: owned === undefined
|
|
20
|
+
? (admitted) => context.execute(admitted)
|
|
21
|
+
: (admitted, member) => owned.execute(admitted, undefined, member),
|
|
22
|
+
};
|
|
12
23
|
}
|
|
13
24
|
const copy = copyAtDoor(request.arguments);
|
|
14
25
|
if (owned === undefined) {
|
|
15
26
|
return {
|
|
16
27
|
request: copy.ok ? { ...request, arguments: copy.value } : request,
|
|
17
|
-
run: context.execute,
|
|
28
|
+
run: (admitted) => context.execute(admitted),
|
|
18
29
|
};
|
|
19
30
|
}
|
|
20
31
|
if (copy.ok) {
|
|
21
32
|
return {
|
|
22
33
|
request: { ...request, arguments: copy.value },
|
|
23
|
-
run: (admitted) => owned.execute(admitted),
|
|
34
|
+
run: (admitted, member) => owned.execute(admitted, undefined, member),
|
|
24
35
|
};
|
|
25
36
|
}
|
|
26
37
|
return {
|
|
27
38
|
request: { ...request, arguments: {} },
|
|
28
|
-
run: (admitted) => owned.execute(admitted, copy.error),
|
|
39
|
+
run: (admitted, member) => owned.execute(admitted, copy.error, member),
|
|
29
40
|
};
|
|
30
41
|
}
|
|
31
42
|
function admitTransaction(context, request) {
|
|
@@ -50,7 +61,7 @@ function admitTransaction(context, request) {
|
|
|
50
61
|
run: (admitted) => owned.executeTransaction(admitted, captured.refused.size === 0 ? undefined : captured.refused),
|
|
51
62
|
};
|
|
52
63
|
}
|
|
53
|
-
async function seededState(options, report, view) {
|
|
64
|
+
export async function seededState(options, report, view) {
|
|
54
65
|
if (options?.state === undefined) {
|
|
55
66
|
return { ok: true, state: undefined };
|
|
56
67
|
}
|
|
@@ -82,10 +93,20 @@ export function operationUnencodableReport(sink, request) {
|
|
|
82
93
|
return (error) => createDiagnosticsReporter(sink, request)(error, "A3003");
|
|
83
94
|
}
|
|
84
95
|
export async function handleOperationRequest(context, request, options) {
|
|
85
|
-
const
|
|
96
|
+
const read = readRequest(request);
|
|
97
|
+
if (!read.ok) {
|
|
98
|
+
return read.response;
|
|
99
|
+
}
|
|
100
|
+
if (read.request.method === "GET") {
|
|
101
|
+
return handleCachedRead(context, read, options);
|
|
102
|
+
}
|
|
103
|
+
const decoded = decodeOperationRead(context.contracts, read, context.origin);
|
|
86
104
|
if (!decoded.ok) {
|
|
87
105
|
return decoded.response;
|
|
88
106
|
}
|
|
107
|
+
return runOperation(context, decoded, options);
|
|
108
|
+
}
|
|
109
|
+
async function runOperation(context, decoded, options) {
|
|
89
110
|
const admitted = admitOperation(context, decoded.request);
|
|
90
111
|
const seeded = await seededState(options, createDiagnosticsReporter(context.sink, admitted.request), { path: ["arguments"], arguments: admitted.request.arguments });
|
|
91
112
|
const response = seeded.ok
|
|
@@ -95,6 +116,61 @@ export async function handleOperationRequest(context, request, options) {
|
|
|
95
116
|
: seeded.response;
|
|
96
117
|
return encodeOperationResponse(response, decoded.requestId, operationUnencodableReport(context.sink, decoded.request));
|
|
97
118
|
}
|
|
119
|
+
async function handleCachedRead(context, read, options) {
|
|
120
|
+
const route = checkOperationRoute(context.contracts, read, context.origin);
|
|
121
|
+
if (!route.ok) {
|
|
122
|
+
return uncachedAnswer(route.response);
|
|
123
|
+
}
|
|
124
|
+
const { cache, query } = route;
|
|
125
|
+
if (cache === undefined || query === undefined) {
|
|
126
|
+
return uncachedAnswer(transportFailure("A2000", read.requestId));
|
|
127
|
+
}
|
|
128
|
+
const args = await readArgumentsOf(query.q, context.contracts[context.origin].limits.maxRequestBytes);
|
|
129
|
+
if (!args.ok) {
|
|
130
|
+
return uncachedAnswer(transportFailure(args.code, read.requestId));
|
|
131
|
+
}
|
|
132
|
+
adoptParsedBody(args.value);
|
|
133
|
+
const request = {
|
|
134
|
+
origin: context.origin,
|
|
135
|
+
requestId: read.requestId,
|
|
136
|
+
resource: route.resource,
|
|
137
|
+
...route.operation,
|
|
138
|
+
protocol: route.protocol,
|
|
139
|
+
transport: transportOf(read),
|
|
140
|
+
arguments: args.value,
|
|
141
|
+
};
|
|
142
|
+
const ifNoneMatch = headerValue(read.headers, "if-none-match");
|
|
143
|
+
const owned = context.owned;
|
|
144
|
+
if (cache.for !== "anonymous" || owned === undefined) {
|
|
145
|
+
const answer = await runOperation(context, { ok: true, request, requestId: read.requestId }, options);
|
|
146
|
+
return cachedReadAnswer(answer, cache, ifNoneMatch, false);
|
|
147
|
+
}
|
|
148
|
+
const report = createDiagnosticsReporter(context.sink, request);
|
|
149
|
+
const seeded = await seededState(options, report, {
|
|
150
|
+
path: ["arguments"],
|
|
151
|
+
arguments: request.arguments,
|
|
152
|
+
});
|
|
153
|
+
if (!seeded.ok) {
|
|
154
|
+
return uncachedAnswer(encodeOperationResponse(seeded.response, read.requestId));
|
|
155
|
+
}
|
|
156
|
+
const state = seeded.state ?? {};
|
|
157
|
+
let caller;
|
|
158
|
+
try {
|
|
159
|
+
caller = await owned.caller({ origin: context.origin, state });
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
report(error, "A3000");
|
|
163
|
+
return uncachedAnswer(encodeOperationResponse(internalFailure(), read.requestId));
|
|
164
|
+
}
|
|
165
|
+
const response = await owned.execute({ ...request, state }, undefined, caller === undefined ? undefined : { caller });
|
|
166
|
+
return cachedReadAnswer(encodeOperationResponse(response, read.requestId, operationUnencodableReport(context.sink, request)), cache, ifNoneMatch, isAnonymous(caller, read.headers));
|
|
167
|
+
}
|
|
168
|
+
function isAnonymous(caller, headers) {
|
|
169
|
+
return caller === undefined
|
|
170
|
+
? headerValue(headers, "cookie") === undefined &&
|
|
171
|
+
headerValue(headers, "authorization") === undefined
|
|
172
|
+
: caller.principal === null || caller.principal === undefined;
|
|
173
|
+
}
|
|
98
174
|
export async function handleTransactionRequest(context, request, options) {
|
|
99
175
|
const decoded = decodeTransactionRequest(context.contracts, request, context.origin);
|
|
100
176
|
if (!decoded.ok) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AvContractClient } from "../contracts/contract.js";
|
|
2
|
+
import type { AvProtocolResponse } from "./http-response.encoder.js";
|
|
3
|
+
/** A cached read's mark, as the Client Contract states it. */
|
|
4
|
+
type ReadCacheMark = NonNullable<NonNullable<AvContractClient["resources"][string]["cache"]>["first" | "unique" | "many" | "count"]>;
|
|
5
|
+
/**
|
|
6
|
+
* A `GET`'s answer that must not be kept: `Cache-Control: no-store` (with
|
|
7
|
+
* `Referrer-Policy: no-referrer` and `X-Content-Type-Options: nosniff`). Every
|
|
8
|
+
* refusal, a stale client's `A2005` included, and every answer but `200`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function uncachedAnswer(response: AvProtocolResponse): AvProtocolResponse;
|
|
11
|
+
/**
|
|
12
|
+
* A cached read's answer. A `200` gets, by the mark's `for`:
|
|
13
|
+
*
|
|
14
|
+
* - `"everyone"`: `Cache-Control: public, max-age=<n>`;
|
|
15
|
+
* - `"anonymous"`: `public` to an `anonymous` caller, `private` otherwise, both
|
|
16
|
+
* with `Vary: Authorization, Cookie`;
|
|
17
|
+
* - `"user"`: `Cache-Control: private, max-age=<n>`, `Vary: Authorization,
|
|
18
|
+
* Cookie`;
|
|
19
|
+
*
|
|
20
|
+
* and `Referrer-Policy: no-referrer`, `X-Content-Type-Options: nosniff` and an
|
|
21
|
+
* `ETag` over its body; it is `304` with no body when `ifNoneMatch` names that
|
|
22
|
+
* tag. A public answer echoes no `Aventara-Request-Id`. Any other status is
|
|
23
|
+
* {@link uncachedAnswer}.
|
|
24
|
+
*/
|
|
25
|
+
export declare function cachedReadAnswer(response: AvProtocolResponse, cache: ReadCacheMark, ifNoneMatch: string | undefined, anonymous: boolean): Promise<AvProtocolResponse>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { noneMatchHolds } from "./contract-document.responder.js";
|
|
2
|
+
import { headers, NO_SNIFF } from "./protocol-headers.js";
|
|
3
|
+
const CALLER_VARY = "Authorization, Cookie";
|
|
4
|
+
export function uncachedAnswer(response) {
|
|
5
|
+
return {
|
|
6
|
+
...response,
|
|
7
|
+
headers: {
|
|
8
|
+
...response.headers,
|
|
9
|
+
"Cache-Control": "no-store",
|
|
10
|
+
"Referrer-Policy": "no-referrer",
|
|
11
|
+
...NO_SNIFF,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export async function cachedReadAnswer(response, cache, ifNoneMatch, anonymous) {
|
|
16
|
+
if (response.status !== 200) {
|
|
17
|
+
return uncachedAnswer(response);
|
|
18
|
+
}
|
|
19
|
+
const shared = cache.for === "everyone" || (cache.for === "anonymous" && anonymous);
|
|
20
|
+
const validators = {
|
|
21
|
+
"Cache-Control": `${shared ? "public" : "private"}, max-age=${cache.maxAge}`,
|
|
22
|
+
...(cache.for === "everyone" ? {} : { Vary: CALLER_VARY }),
|
|
23
|
+
"Referrer-Policy": "no-referrer",
|
|
24
|
+
...NO_SNIFF,
|
|
25
|
+
ETag: `"${await bodyDigest(response.body)}"`,
|
|
26
|
+
};
|
|
27
|
+
const kept = { ...response.headers };
|
|
28
|
+
if (shared) {
|
|
29
|
+
delete kept[headers.requestId];
|
|
30
|
+
}
|
|
31
|
+
if (noneMatchHolds(ifNoneMatch, validators.ETag)) {
|
|
32
|
+
delete kept["Content-Type"];
|
|
33
|
+
return { status: 304, headers: { ...kept, ...validators }, body: "" };
|
|
34
|
+
}
|
|
35
|
+
return { ...response, headers: { ...kept, ...validators } };
|
|
36
|
+
}
|
|
37
|
+
async function bodyDigest(body) {
|
|
38
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(body)));
|
|
39
|
+
let binary = "";
|
|
40
|
+
for (const byte of digest) {
|
|
41
|
+
binary += String.fromCharCode(byte);
|
|
42
|
+
}
|
|
43
|
+
return btoa(binary)
|
|
44
|
+
.replace(/\+/g, "-")
|
|
45
|
+
.replace(/\//g, "_")
|
|
46
|
+
.replace(/=+$/, "");
|
|
47
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** A cached read's query: its two parameters, each percent-decoded once. */
|
|
2
|
+
export type ReadQuery = {
|
|
3
|
+
/** The arguments, in either form. */
|
|
4
|
+
readonly q: string;
|
|
5
|
+
/** The Client Contract hash the client was generated against. */
|
|
6
|
+
readonly v: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The query's `q` and `v`, or `undefined` when it is anything else: absent,
|
|
10
|
+
* either parameter missing or repeated, another parameter, or a percent-escape
|
|
11
|
+
* that does not decode.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readQueryOf(query: string | undefined): ReadQuery | undefined;
|
|
14
|
+
/** What reading `q` answers: the arguments object, or the transport code refusing it. */
|
|
15
|
+
type ReadArguments = {
|
|
16
|
+
readonly ok: true;
|
|
17
|
+
readonly value: object;
|
|
18
|
+
} | {
|
|
19
|
+
readonly ok: false;
|
|
20
|
+
readonly code: "A2000" | "A2010";
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* `q` as the arguments object: plain JSON when it starts with `{`, otherwise
|
|
24
|
+
* base64url(deflate-raw(JSON)). More than `maxBytes` bytes of JSON, or of
|
|
25
|
+
* compressed input, is `A2010`; anything that is not base64url, deflate-raw,
|
|
26
|
+
* UTF-8 or a JSON object is `A2000`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function readArgumentsOf(q: string, maxBytes: number): Promise<ReadArguments>;
|
|
29
|
+
/**
|
|
30
|
+
* A read's `q`: its arguments' canonical JSON, percent-encoded, or that JSON
|
|
31
|
+
* deflate-raw'd and base64url'd when that is shorter.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readQueryArgumentOf(args: object): Promise<string>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { canonicalJson } from "../contracts/canonical-json.writer.js";
|
|
2
|
+
const BASE64URL = /^[A-Za-z0-9_-]*$/;
|
|
3
|
+
export function readQueryOf(query) {
|
|
4
|
+
if (query === undefined || query === "") {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const found = new Map();
|
|
8
|
+
for (const pair of query.split("&")) {
|
|
9
|
+
const equals = pair.indexOf("=");
|
|
10
|
+
const name = equals === -1 ? pair : pair.slice(0, equals);
|
|
11
|
+
if ((name !== "q" && name !== "v") || found.has(name) || equals === -1) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const value = percentDecoded(pair.slice(equals + 1));
|
|
15
|
+
if (value === undefined) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
found.set(name, value);
|
|
19
|
+
}
|
|
20
|
+
const q = found.get("q");
|
|
21
|
+
const v = found.get("v");
|
|
22
|
+
return q === undefined || v === undefined ? undefined : { q, v };
|
|
23
|
+
}
|
|
24
|
+
function percentDecoded(text) {
|
|
25
|
+
try {
|
|
26
|
+
return decodeURIComponent(text);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function readArgumentsOf(q, maxBytes) {
|
|
33
|
+
if (q.startsWith("{")) {
|
|
34
|
+
const bytes = new TextEncoder().encode(q);
|
|
35
|
+
return bytes.byteLength > maxBytes
|
|
36
|
+
? { ok: false, code: "A2010" }
|
|
37
|
+
: parsedObject(q);
|
|
38
|
+
}
|
|
39
|
+
if (!BASE64URL.test(q) || q.length % 4 === 1) {
|
|
40
|
+
return { ok: false, code: "A2000" };
|
|
41
|
+
}
|
|
42
|
+
if (Math.floor((q.length * 3) / 4) > maxBytes) {
|
|
43
|
+
return { ok: false, code: "A2010" };
|
|
44
|
+
}
|
|
45
|
+
const compressed = base64urlBytes(q);
|
|
46
|
+
const inflated = await inflatedWithin(compressed, maxBytes);
|
|
47
|
+
if (inflated.ok === false) {
|
|
48
|
+
return inflated;
|
|
49
|
+
}
|
|
50
|
+
let text;
|
|
51
|
+
try {
|
|
52
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(inflated.bytes);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return { ok: false, code: "A2000" };
|
|
56
|
+
}
|
|
57
|
+
return parsedObject(text);
|
|
58
|
+
}
|
|
59
|
+
function parsedObject(text) {
|
|
60
|
+
let value;
|
|
61
|
+
try {
|
|
62
|
+
value = JSON.parse(text);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { ok: false, code: "A2000" };
|
|
66
|
+
}
|
|
67
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
68
|
+
? { ok: true, value }
|
|
69
|
+
: { ok: false, code: "A2000" };
|
|
70
|
+
}
|
|
71
|
+
function base64urlBytes(text) {
|
|
72
|
+
const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/") +
|
|
73
|
+
"=".repeat((4 - (text.length % 4)) % 4));
|
|
74
|
+
const bytes = new Uint8Array(binary.length);
|
|
75
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
76
|
+
bytes[index] = binary.charCodeAt(index);
|
|
77
|
+
}
|
|
78
|
+
return bytes;
|
|
79
|
+
}
|
|
80
|
+
async function inflatedWithin(compressed, maxBytes) {
|
|
81
|
+
const stream = new Blob([compressed])
|
|
82
|
+
.stream()
|
|
83
|
+
.pipeThrough(new DecompressionStream("deflate-raw"));
|
|
84
|
+
const reader = stream.getReader();
|
|
85
|
+
const chunks = [];
|
|
86
|
+
let total = 0;
|
|
87
|
+
try {
|
|
88
|
+
for (;;) {
|
|
89
|
+
const { done, value } = await reader.read();
|
|
90
|
+
if (done) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
total += value.byteLength;
|
|
94
|
+
if (total > maxBytes) {
|
|
95
|
+
await reader.cancel().catch(() => undefined);
|
|
96
|
+
return { ok: false, code: "A2010" };
|
|
97
|
+
}
|
|
98
|
+
chunks.push(value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return { ok: false, code: "A2000" };
|
|
103
|
+
}
|
|
104
|
+
const bytes = new Uint8Array(total);
|
|
105
|
+
let offset = 0;
|
|
106
|
+
for (const chunk of chunks) {
|
|
107
|
+
bytes.set(chunk, offset);
|
|
108
|
+
offset += chunk.byteLength;
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, bytes };
|
|
111
|
+
}
|
|
112
|
+
export async function readQueryArgumentOf(args) {
|
|
113
|
+
const json = canonicalJson(args);
|
|
114
|
+
const plain = encodeURIComponent(json);
|
|
115
|
+
const deflated = await new Response(new Blob([json]).stream().pipeThrough(new CompressionStream("deflate-raw"))).arrayBuffer();
|
|
116
|
+
let binary = "";
|
|
117
|
+
for (const byte of new Uint8Array(deflated)) {
|
|
118
|
+
binary += String.fromCharCode(byte);
|
|
119
|
+
}
|
|
120
|
+
const compressed = btoa(binary)
|
|
121
|
+
.replace(/\+/g, "-")
|
|
122
|
+
.replace(/\//g, "_")
|
|
123
|
+
.replace(/=+$/, "");
|
|
124
|
+
return compressed.length < plain.length ? compressed : plain;
|
|
125
|
+
}
|
|
@@ -2,10 +2,9 @@ import { adapterPrimaryIdentifier } from "../adapters/adapter-identifier.js";
|
|
|
2
2
|
import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
3
3
|
import { AvComputedValueError } from "./computed-value.error.js";
|
|
4
4
|
import { frozenCopy } from "./readonly-value.js";
|
|
5
|
+
import { isRecord } from "./record.guard.js";
|
|
5
6
|
import { normalizeScalarFieldValue } from "./scalar-runtime.js";
|
|
6
|
-
|
|
7
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
|
-
}
|
|
7
|
+
import { isThenable } from "./thenable.guard.js";
|
|
9
8
|
function readCompute(value) {
|
|
10
9
|
if (typeof value === "function") {
|
|
11
10
|
return { compute: value, dependsOn: [] };
|
|
@@ -342,11 +341,6 @@ function isNestedContainer(value) {
|
|
|
342
341
|
!(value instanceof Date) &&
|
|
343
342
|
!(value instanceof Uint8Array));
|
|
344
343
|
}
|
|
345
|
-
function isThenable(value) {
|
|
346
|
-
return (value !== null &&
|
|
347
|
-
(typeof value === "object" || typeof value === "function") &&
|
|
348
|
-
typeof value.then === "function");
|
|
349
|
-
}
|
|
350
344
|
function setOwn(target, key, value) {
|
|
351
345
|
if (key in target) {
|
|
352
346
|
Object.defineProperty(target, key, {
|
|
@@ -2,9 +2,7 @@ import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
|
2
2
|
import { advertisedDirectivesAt } from "../contracts/field.js";
|
|
3
3
|
import { LOOPBACK_WRITE_SCOPE, NESTED_WRITE_SCOPE, resolveWriteScope, } from "../operations/write-scope.resolver.js";
|
|
4
4
|
import { isRelationMutationDirective, RELATION_MUTATION_RECORD_SITES, } from "./mutation-planner.js";
|
|
5
|
-
|
|
6
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
|
-
}
|
|
5
|
+
import { isRecord } from "./record.guard.js";
|
|
8
6
|
function written(record, key) {
|
|
9
7
|
return Object.hasOwn(record, key) && record[key] !== undefined;
|
|
10
8
|
}
|
|
@@ -19,6 +19,11 @@ export type AvOperationDiagnostic = {
|
|
|
19
19
|
readonly variant: string;
|
|
20
20
|
readonly requestId: string;
|
|
21
21
|
readonly operation?: number;
|
|
22
|
+
/**
|
|
23
|
+
* The key of the Operation Set entry (`POST /_all`) the failure met; absent
|
|
24
|
+
* for an operation that ran alone or as a transaction step.
|
|
25
|
+
*/
|
|
26
|
+
readonly key?: string;
|
|
22
27
|
/**
|
|
23
28
|
* `true` when `error` is an `AvError` its thrower (a pipeline
|
|
24
29
|
* callback, a compute stage, a filter or an Adapter) raised deliberately with an
|
|
@@ -134,7 +139,7 @@ export declare function diagnosticsSinkOf(framework: object): AvDiagnosticsSink
|
|
|
134
139
|
* Binds the configured sink to one operation request. The sink is not awaited,
|
|
135
140
|
* and nothing it throws or rejects with reaches the response.
|
|
136
141
|
*/
|
|
137
|
-
export declare function createDiagnosticsReporter(sink: AvDiagnosticsSink | undefined, request: AvOperationRequest, operation?: number): DiagnosticsReporter;
|
|
142
|
+
export declare function createDiagnosticsReporter(sink: AvDiagnosticsSink | undefined, request: AvOperationRequest, operation?: number, key?: string): DiagnosticsReporter;
|
|
138
143
|
/** The diagnostics channel of one transaction request. */
|
|
139
144
|
export type TransactionDiagnosticsChannel = {
|
|
140
145
|
/** A plan-level report; `operation` attributes it to a recorded failing index. */
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { coreBrandOf } from "./core-class.brand.js";
|
|
2
2
|
import { internalFailure, } from "./operation-response.js";
|
|
3
3
|
import { requestIdOf } from "./request-id.js";
|
|
4
|
+
import { isThenable } from "./thenable.guard.js";
|
|
4
5
|
const IGNORE_DIAGNOSTIC = () => undefined;
|
|
5
|
-
function isThenable(value) {
|
|
6
|
-
if ((typeof value !== "object" || value === null) &&
|
|
7
|
-
typeof value !== "function") {
|
|
8
|
-
return false;
|
|
9
|
-
}
|
|
10
|
-
return typeof value.then === "function";
|
|
11
|
-
}
|
|
12
6
|
export function createReporter(sink, identity) {
|
|
13
7
|
if (sink === undefined) {
|
|
14
8
|
return IGNORE_DIAGNOSTIC;
|
|
@@ -74,7 +68,7 @@ export function recordDiagnosticsSink(framework, sink) {
|
|
|
74
68
|
export function diagnosticsSinkOf(framework) {
|
|
75
69
|
return CONFIGURED_SINKS.get(framework);
|
|
76
70
|
}
|
|
77
|
-
export function createDiagnosticsReporter(sink, request, operation) {
|
|
71
|
+
export function createDiagnosticsReporter(sink, request, operation, key) {
|
|
78
72
|
let identity;
|
|
79
73
|
try {
|
|
80
74
|
identity = {
|
|
@@ -85,6 +79,7 @@ export function createDiagnosticsReporter(sink, request, operation) {
|
|
|
85
79
|
variant: request.variant,
|
|
86
80
|
requestId: request.requestId,
|
|
87
81
|
...(operation === undefined ? {} : { operation }),
|
|
82
|
+
...(key === undefined ? {} : { key }),
|
|
88
83
|
};
|
|
89
84
|
}
|
|
90
85
|
catch {
|
|
@@ -8,7 +8,7 @@ import { type AvOperationCode } from "./operation-response.js";
|
|
|
8
8
|
* The documentation page every catalog entry links into. Each code has its own
|
|
9
9
|
* anchor there: the code in lower case (`#a2004`, `#config_required`).
|
|
10
10
|
*/
|
|
11
|
-
export declare const ERROR_CODES_DOCS_URL = "https://aventara
|
|
11
|
+
export declare const ERROR_CODES_DOCS_URL = "https://aventara.razor34.dev/framework/docs/error-codes";
|
|
12
12
|
/**
|
|
13
13
|
* What kind of code an entry is: an outcome code's family (`success` `A1xxx`,
|
|
14
14
|
* `request` `A2xxx`, `internal` `A3xxx`, `auth` `A4xxx`), a `validation` issue
|
|
@@ -43,7 +43,7 @@ export declare function errorDocsUrl(code: AvErrorCatalogCode): string;
|
|
|
43
43
|
*
|
|
44
44
|
* ```ts
|
|
45
45
|
* AvErrorCatalog[error.code].fix;
|
|
46
|
-
* AvErrorCatalog.V1000.docs; // https://aventara
|
|
46
|
+
* AvErrorCatalog.V1000.docs; // https://aventara.razor34.dev/framework/docs/error-codes#v1000
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
49
|
export declare const AvErrorCatalog: AvErrorCatalog;
|
|
@@ -5,7 +5,7 @@ import { VALIDATION_CODE_GUIDANCE } from "../operations/validation-code.guidance
|
|
|
5
5
|
import { VALIDATION_CODE_MESSAGES, VALIDATION_CODES, } from "../operations/validation-code.js";
|
|
6
6
|
import { OPERATION_CODE_GUIDANCE } from "./operation-code.guidance.js";
|
|
7
7
|
import { OPERATION_CODES } from "./operation-response.js";
|
|
8
|
-
export const ERROR_CODES_DOCS_URL = "https://aventara
|
|
8
|
+
export const ERROR_CODES_DOCS_URL = "https://aventara.razor34.dev/framework/docs/error-codes";
|
|
9
9
|
export function errorDocsUrl(code) {
|
|
10
10
|
return `${ERROR_CODES_DOCS_URL}#${code.toLowerCase()}`;
|
|
11
11
|
}
|
|
@@ -4,8 +4,6 @@ import type { AvOperationFamily } from "../operations/operation-identity.js";
|
|
|
4
4
|
import type { AvContext } from "./pipeline.js";
|
|
5
5
|
export type Path = readonly (string | number)[];
|
|
6
6
|
type UnknownRecord = Readonly<Record<string, unknown>>;
|
|
7
|
-
/** A plain object: what a write record, or a directive's operand, is. */
|
|
8
|
-
export declare function isPlainRecord(value: unknown): value is UnknownRecord;
|
|
9
7
|
/** The containers a family writes, by argument key. */
|
|
10
8
|
export declare const WRITE_KEYS: Readonly<Partial<Record<AvOperationFamily, readonly string[]>>>;
|
|
11
9
|
/** The fields of `resource` with stages, in the Resource's field order. */
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { frozenCopy } from "./readonly-value.js";
|
|
2
|
-
|
|
3
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4
|
-
return false;
|
|
5
|
-
}
|
|
6
|
-
const prototype = Object.getPrototypeOf(value);
|
|
7
|
-
return prototype === Object.prototype || prototype === null;
|
|
8
|
-
}
|
|
2
|
+
import { isPlainRecord } from "./record.guard.js";
|
|
9
3
|
export const WRITE_KEYS = {
|
|
10
4
|
create: ["data"],
|
|
11
5
|
update: ["data"],
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { hasFieldValidators, validatedFieldsOf, } from "../compiler/field-validator.compiler.js";
|
|
2
2
|
import { AvComputedValueError } from "./computed-value.error.js";
|
|
3
3
|
import { entryCheckIssues, fieldCheckIssues } from "./field-check.evaluator.js";
|
|
4
|
-
import {
|
|
4
|
+
import { valueAt, writeRecordPathsOf, } from "./field-stage.runtime.js";
|
|
5
5
|
import { nestedWriteTargetsOf } from "./nested-write.walker.js";
|
|
6
|
+
import { isPlainRecord } from "./record.guard.js";
|
|
6
7
|
import { jsonShapeIssues } from "./scalar-runtime.js";
|
|
7
8
|
function isDirective(value) {
|
|
8
9
|
return (isPlainRecord(value) &&
|
|
@@ -2,6 +2,7 @@ import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
|
2
2
|
import { AvComputedValueError } from "./computed-value.error.js";
|
|
3
3
|
import { replaceAt, valueAt } from "./field-stage.runtime.js";
|
|
4
4
|
import { frozenCopy } from "./readonly-value.js";
|
|
5
|
+
import { isRecord } from "./record.guard.js";
|
|
5
6
|
import { normalizeScalarFieldValue } from "./scalar-runtime.js";
|
|
6
7
|
function writeScalarField(contracts, resourceName, fieldName) {
|
|
7
8
|
const field = lookupField(lookupResource(contracts.core, resourceName), fieldName) ??
|
|
@@ -26,9 +27,6 @@ export const RELATION_MUTATION_RECORD_SITES = {
|
|
|
26
27
|
export function isRelationMutationDirective(key) {
|
|
27
28
|
return Object.hasOwn(RELATION_MUTATION_RECORD_SITES, key);
|
|
28
29
|
}
|
|
29
|
-
function isRecord(value) {
|
|
30
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
31
|
-
}
|
|
32
30
|
function cloneRecord(value) {
|
|
33
31
|
return { ...value };
|
|
34
32
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
2
|
-
import {
|
|
2
|
+
import { valueAt, writeRecordPathsOf, } from "./field-stage.runtime.js";
|
|
3
3
|
import { isRelationMutationDirective, RELATION_MUTATION_RECORD_SITES, } from "./mutation-planner.js";
|
|
4
|
+
import { isPlainRecord } from "./record.guard.js";
|
|
4
5
|
const DIRECTIVE_FAMILY = {
|
|
5
6
|
$create: "create",
|
|
6
7
|
$connectOrCreate: "create",
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
2
2
|
import { advertisedReducers, } from "../contracts/field.js";
|
|
3
|
+
import { isRecord } from "./record.guard.js";
|
|
3
4
|
import { normalizeScalarFieldValue } from "./scalar-runtime.js";
|
|
4
5
|
function issue(message, path = ["data"]) {
|
|
5
6
|
return { ok: false, issues: [{ path, code: "V1001", message }] };
|
|
6
7
|
}
|
|
7
|
-
function isRecord(value) {
|
|
8
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
-
}
|
|
10
8
|
function validatesRecordOrNull(value) {
|
|
11
9
|
return value === null || isRecord(value);
|
|
12
10
|
}
|
|
@@ -1,15 +1,35 @@
|
|
|
1
|
+
import type { RemoteRequestOrigin } from "../config/config-layer.js";
|
|
2
|
+
import type { PolicyCaller } from "../policy/policy-request.js";
|
|
1
3
|
import type { AvAgentTransactionRequest, AvClientTransactionRequest } from "../transactions/transaction-request.js";
|
|
2
4
|
import type { AvTransactionResponse } from "../transactions/transaction-response.js";
|
|
3
5
|
import type { AvCallerDataError } from "./caller-data.copier.js";
|
|
4
6
|
import type { RemoteOperationRequest } from "./operation-request.js";
|
|
5
7
|
import type { AvOperationResponse } from "./operation-response.js";
|
|
6
|
-
|
|
8
|
+
import type { AvState } from "./request-state.js";
|
|
9
|
+
/**
|
|
10
|
+
* The entries that run a request the protocol decoded itself, uncopied, and
|
|
11
|
+
* what an Operation Set reads off the Framework besides.
|
|
12
|
+
*/
|
|
7
13
|
export type OwnedExecution = {
|
|
8
14
|
/**
|
|
9
15
|
* Runs `request` uncopied; with `refused`, its arguments are a host's parsed body
|
|
10
16
|
* whose copy at the call failed, answered as the door answers that failure.
|
|
17
|
+
* `member` is an Operation Set entry's: the principal derived once for the
|
|
18
|
+
* set, and the entry's key, which its diagnostics name.
|
|
11
19
|
*/
|
|
12
|
-
readonly execute: (request: RemoteOperationRequest, refused?: AvCallerDataError
|
|
20
|
+
readonly execute: (request: RemoteOperationRequest, refused?: AvCallerDataError, member?: {
|
|
21
|
+
readonly caller?: PolicyCaller;
|
|
22
|
+
readonly key?: string;
|
|
23
|
+
}) => Promise<AvOperationResponse>;
|
|
24
|
+
/**
|
|
25
|
+
* The principal a remote request with this state runs as, derived once, or
|
|
26
|
+
* `undefined` when no policy checks it. Throws or rejects as
|
|
27
|
+
* `policy.principal` does.
|
|
28
|
+
*/
|
|
29
|
+
readonly caller: (request: {
|
|
30
|
+
readonly origin: RemoteRequestOrigin;
|
|
31
|
+
readonly state: AvState;
|
|
32
|
+
}) => PolicyCaller | undefined | Promise<PolicyCaller>;
|
|
13
33
|
/**
|
|
14
34
|
* Runs `request` uncopied; with `refused`, its plan is a host's parsed plan taken
|
|
15
35
|
* at the call, and these steps' arguments could not be copied there: each is
|
|
@@ -17,7 +17,11 @@ import type { AvState } from "./request-state.js";
|
|
|
17
17
|
* What a pipeline is told about the HTTP request a client-origin operation arrived on.
|
|
18
18
|
*/
|
|
19
19
|
export type AvTransportContext = {
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* `"POST"`, or `"GET"` for a read of a Resource with `cache` (a browser's
|
|
22
|
+
* prefetch or a CDN's refetch is a GET too).
|
|
23
|
+
*/
|
|
24
|
+
readonly method: "POST" | "GET";
|
|
21
25
|
readonly headers: Readonly<Record<string, string>>;
|
|
22
26
|
};
|
|
23
27
|
/** What a pipeline ctx states about the request, whatever the stage. */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** An object that is not a list: what core reads as a record of named values. */
|
|
2
|
+
export declare function isRecord<T extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>>(value: unknown): value is T;
|
|
3
|
+
/**
|
|
4
|
+
* A **plain** record: an object literal (prototype `Object.prototype` or
|
|
5
|
+
* `null`), not a `Date`, a core `AvDecimal`, a `Uint8Array` or another class
|
|
6
|
+
* instance. The one discriminant between an argument OBJECT and a VALUE wherever
|
|
7
|
+
* a position admits both: a write record, a directive's operand, a filter
|
|
8
|
+
* position above all, where a plain record is an operator object and anything
|
|
9
|
+
* else is a value that equality shorthand holds to the field's scalar. The
|
|
10
|
+
* validator (`operation-validator-core.ts`) reads a filter position through this
|
|
11
|
+
* same answer, and an adapter's filter translation is expected to, so a position
|
|
12
|
+
* cannot be operators to one reader and a value to another.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isPlainRecord<T extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>>(value: unknown): value is T;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function isRecord(value) {
|
|
2
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function isPlainRecord(value) {
|
|
5
|
+
if (!isRecord(value)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const prototype = Object.getPrototypeOf(value);
|
|
9
|
+
return prototype === Object.prototype || prototype === null;
|
|
10
|
+
}
|