@aventara/core 1.0.0-rc.10 → 1.0.0-rc.12
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 +93 -0
- package/ERRORS.md +82 -82
- package/README.md +57 -1
- package/dist/compiler/contract-compiler-diagnostic.d.ts +3 -2
- package/dist/compiler/contract-compiler-diagnostic.guidance.js +3 -3
- package/dist/compiler/contract.compiler.js +64 -12
- 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/contract-view.d.ts +13 -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
|
@@ -7,6 +7,7 @@ import { DEFAULT_CONTRACT_LIMITS, } from "../contracts/contract-limits.js";
|
|
|
7
7
|
import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
|
|
8
8
|
import { advertisedDirectives, advertisedReducers, holdsInAnyScope, } from "../contracts/field.js";
|
|
9
9
|
import { isOperationVariantAvailable, } from "../contracts/resource-operations.js";
|
|
10
|
+
import { isRecord } from "../runtime/record.guard.js";
|
|
10
11
|
import { checkCreateRequirements, checkHiddenRequirements, projectCreateRequirements, } from "./create-requirements.projector.js";
|
|
11
12
|
import { applyValidators, bindFieldValidators, checkLayerValidators, compileValidatorTable, } from "./field-validator.compiler.js";
|
|
12
13
|
import { explainManagedRefusals, hideManagedElsewhere, managedElsewhereOf, managedWriteReason, markCoreResources, pruneManagedEnums, withholdManagedWrites, } from "./managed-elsewhere.projector.js";
|
|
@@ -24,9 +25,6 @@ const COMPUTED_TRAITS = new Set([
|
|
|
24
25
|
function diagnostic(code, path, message) {
|
|
25
26
|
return { code, path, message };
|
|
26
27
|
}
|
|
27
|
-
function isRecord(value) {
|
|
28
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
29
|
-
}
|
|
30
28
|
function cloneValue(value) {
|
|
31
29
|
if (Array.isArray(value)) {
|
|
32
30
|
return value.map((entry) => cloneValue(entry));
|
|
@@ -438,7 +436,8 @@ function checkResourceTree(model, baseline, scope, tree, definitions, managed, d
|
|
|
438
436
|
diagnostics.push(diagnostic("COMPILER_UNKNOWN_FIELD", `${resourcePath}.fields.${fieldName}`, `field "${fieldName}" is neither discovered by the adapter nor defined at the root`));
|
|
439
437
|
continue;
|
|
440
438
|
}
|
|
441
|
-
if (!isRecord(fieldEntry) ||
|
|
439
|
+
if (!isRecord(fieldEntry) ||
|
|
440
|
+
!isRecord(fieldEntry.pipelines)) {
|
|
442
441
|
continue;
|
|
443
442
|
}
|
|
444
443
|
const pipelinesPath = `${resourcePath}.fields.${fieldName}.pipelines`;
|
|
@@ -764,7 +763,9 @@ function exemptOperations(walk, resourceName, universe, parent, entry, path) {
|
|
|
764
763
|
if (value === false || value === undefined) {
|
|
765
764
|
continue;
|
|
766
765
|
}
|
|
767
|
-
if (value !== true &&
|
|
766
|
+
if (value !== true &&
|
|
767
|
+
!Array.isArray(value) &&
|
|
768
|
+
!isRecord(value)) {
|
|
768
769
|
walk.diagnostics.push(diagnostic("COMPILER_UNAVAILABLE_OPERATION", familyPath, "an operation family takes true, false, a list of its variants or an object of them"));
|
|
769
770
|
continue;
|
|
770
771
|
}
|
|
@@ -811,7 +812,8 @@ function exemptResource(walk, resourceName, universe, parent, entry, path) {
|
|
|
811
812
|
}
|
|
812
813
|
function projectExposure(walk, universe, parent, exemptions) {
|
|
813
814
|
const path = `${walk.layer}.exemptions.resources`;
|
|
814
|
-
const entries = isRecord(exemptions) &&
|
|
815
|
+
const entries = isRecord(exemptions) &&
|
|
816
|
+
isRecord(exemptions.resources)
|
|
815
817
|
? exemptions.resources
|
|
816
818
|
: {};
|
|
817
819
|
const resources = {};
|
|
@@ -855,7 +857,8 @@ function projectExposure(walk, universe, parent, exemptions) {
|
|
|
855
857
|
}
|
|
856
858
|
function pruneEmptyOperationFamilies(operations) {
|
|
857
859
|
for (const [family, variants] of Object.entries(operations)) {
|
|
858
|
-
if (isRecord(variants) &&
|
|
860
|
+
if (isRecord(variants) &&
|
|
861
|
+
Object.keys(variants).length === 0) {
|
|
859
862
|
delete operations[family];
|
|
860
863
|
}
|
|
861
864
|
}
|
|
@@ -863,9 +866,14 @@ function pruneEmptyOperationFamilies(operations) {
|
|
|
863
866
|
function identifierLabel(identifier) {
|
|
864
867
|
return identifier.map((name) => `\`${name}\``).join(" and ");
|
|
865
868
|
}
|
|
869
|
+
function isAddressable(field, parentField) {
|
|
870
|
+
return (field?.kind === "scalar" &&
|
|
871
|
+
(capabilityIsEnabled(field.capabilities.filter) ||
|
|
872
|
+
!capabilityIsEnabled(parentField?.capabilities?.filter)));
|
|
873
|
+
}
|
|
866
874
|
function reconcileIdentifiers(walk, resource, parent, path) {
|
|
867
875
|
resource.identifiers = parent.identifiers
|
|
868
|
-
.filter((identifier) => identifier.every((name) => resource.fields[name]
|
|
876
|
+
.filter((identifier) => identifier.every((name) => isAddressable(resource.fields[name], parent.fields[name])))
|
|
869
877
|
.map((identifier) => [...identifier]);
|
|
870
878
|
const first = parent.identifiers[0];
|
|
871
879
|
if (resource.identifiers.length > 0 || first === undefined) {
|
|
@@ -879,8 +887,8 @@ function reconcileIdentifiers(walk, resource, parent, path) {
|
|
|
879
887
|
}
|
|
880
888
|
const variantPath = `${path}.operations.${family}.${variant}`;
|
|
881
889
|
walk.diagnostics.push(diagnostic("COMPILER_MISSING_IDENTIFIER", variantPath, walk.style === "pick"
|
|
882
|
-
? `${variantPath} needs an identifier field; pick ${identifierLabel(first)} (or another complete identifier) too`
|
|
883
|
-
: `${variantPath} needs an identifier field, and the exemptions hide every complete identifier; keep ${identifierLabel(first)} (or another complete identifier) exposed, or exempt the operation too`));
|
|
890
|
+
? `${variantPath} needs an identifier field; pick ${identifierLabel(first)} with its filter (or another complete identifier) too`
|
|
891
|
+
: `${variantPath} needs an identifier field, and the exemptions hide (or stop filtering on) every complete identifier; keep ${identifierLabel(first)} (or another complete identifier) exposed and filterable, or exempt the operation too`));
|
|
884
892
|
}
|
|
885
893
|
}
|
|
886
894
|
function validateScalarReference(projection, field, path, diagnostics) {
|
|
@@ -1151,9 +1159,10 @@ export async function compileContracts(model, config) {
|
|
|
1151
1159
|
}
|
|
1152
1160
|
const core = semanticObject(coreCompilation.projection);
|
|
1153
1161
|
const application = semanticObject(applicationProjection.projection);
|
|
1162
|
+
const clientMarked = withReadCache(clientProjection.projection, compilerConfig.client.pipelines);
|
|
1154
1163
|
const client = await identifiedContract(compilerConfig.access === undefined
|
|
1155
|
-
?
|
|
1156
|
-
: { ...
|
|
1164
|
+
? clientMarked
|
|
1165
|
+
: { ...clientMarked, access: compilerConfig.access });
|
|
1157
1166
|
const agent = await identifiedContract(agentProjection.projection);
|
|
1158
1167
|
for (const [layer, contract] of [
|
|
1159
1168
|
["client", client],
|
|
@@ -1180,6 +1189,49 @@ export async function compileContracts(model, config) {
|
|
|
1180
1189
|
});
|
|
1181
1190
|
return { ok: true, contracts };
|
|
1182
1191
|
}
|
|
1192
|
+
function lastCacheOf(levels) {
|
|
1193
|
+
for (let index = (levels?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
1194
|
+
const stage = levels?.[index]?.cache;
|
|
1195
|
+
if (stage !== undefined) {
|
|
1196
|
+
return { for: stage.for ?? "user", maxAge: stage.maxAge };
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return undefined;
|
|
1200
|
+
}
|
|
1201
|
+
function withReadCache(projection, tree) {
|
|
1202
|
+
const layer = lastCacheOf(tree.layer);
|
|
1203
|
+
const resources = {};
|
|
1204
|
+
for (const [name, resource] of Object.entries(projection.resources)) {
|
|
1205
|
+
const pipelines = Object.hasOwn(tree.resources, name)
|
|
1206
|
+
? tree.resources[name]
|
|
1207
|
+
: undefined;
|
|
1208
|
+
const find = Object.hasOwn(pipelines?.families ?? {}, "find")
|
|
1209
|
+
? pipelines?.families.find
|
|
1210
|
+
: undefined;
|
|
1211
|
+
const fallback = lastCacheOf(find?.pipelines) ??
|
|
1212
|
+
lastCacheOf(pipelines?.pipelines) ??
|
|
1213
|
+
layer;
|
|
1214
|
+
const advertised = (resource.operations.find ?? {});
|
|
1215
|
+
const marks = {};
|
|
1216
|
+
for (const variant of Object.keys(advertised)) {
|
|
1217
|
+
if (!isOperationVariantAvailable(advertised[variant])) {
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
const own = Object.hasOwn(find?.variants ?? {}, variant)
|
|
1221
|
+
? find?.variants[variant]?.pipelines
|
|
1222
|
+
: undefined;
|
|
1223
|
+
const mark = lastCacheOf(own) ?? fallback;
|
|
1224
|
+
if (mark !== undefined) {
|
|
1225
|
+
marks[variant] = mark;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
resources[name] =
|
|
1229
|
+
Object.keys(marks).length === 0
|
|
1230
|
+
? resource
|
|
1231
|
+
: { ...resource, cache: marks };
|
|
1232
|
+
}
|
|
1233
|
+
return { ...projection, resources };
|
|
1234
|
+
}
|
|
1183
1235
|
function bindRuntimePieces(contracts, definitions, validators) {
|
|
1184
1236
|
for (const origin of ["core", "application", "client", "agent"]) {
|
|
1185
1237
|
bindShapedFields(contracts[origin], definitions);
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { advertisedDirectives, advertisedDirectivesAt, } from "../contracts/field.js";
|
|
2
2
|
import { isOperationVariantAvailable, writeScopeOf, } from "../contracts/resource-operations.js";
|
|
3
|
-
|
|
4
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5
|
-
}
|
|
3
|
+
import { isRecord } from "../runtime/record.guard.js";
|
|
6
4
|
function own(record, key) {
|
|
7
5
|
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
8
6
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { avzCheckOf, } from "../avz/avz-check.registry.js";
|
|
2
2
|
import { FIELD_CHECK_SUBJECTS, } from "../runtime/field-check.evaluator.js";
|
|
3
|
-
|
|
4
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5
|
-
}
|
|
3
|
+
import { isRecord } from "../runtime/record.guard.js";
|
|
6
4
|
function own(record, key) {
|
|
7
5
|
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
8
6
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from "../runtime/record.guard.js";
|
|
1
2
|
const WRITE_FAMILIES = [
|
|
2
3
|
"create",
|
|
3
4
|
"update",
|
|
@@ -8,16 +9,14 @@ const SOURCE_ROW_DIRECTIVES = [
|
|
|
8
9
|
"$connect",
|
|
9
10
|
"$disconnect",
|
|
10
11
|
];
|
|
11
|
-
function isRecord(value) {
|
|
12
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
-
}
|
|
14
12
|
export function managedElsewhereOf(rootTree) {
|
|
15
13
|
const managed = new Set();
|
|
16
14
|
if (!isRecord(rootTree)) {
|
|
17
15
|
return managed;
|
|
18
16
|
}
|
|
19
17
|
for (const [name, entry] of Object.entries(rootTree)) {
|
|
20
|
-
if (isRecord(entry) &&
|
|
18
|
+
if (isRecord(entry) &&
|
|
19
|
+
entry.managedElsewhere === true) {
|
|
21
20
|
managed.add(name);
|
|
22
21
|
}
|
|
23
22
|
}
|
|
@@ -28,7 +27,8 @@ function keptDirectives(capability, kept) {
|
|
|
28
27
|
const left = capability.filter((directive) => kept.includes(directive));
|
|
29
28
|
return left.length > 0 ? left : undefined;
|
|
30
29
|
}
|
|
31
|
-
if (isRecord(capability) &&
|
|
30
|
+
if (isRecord(capability) &&
|
|
31
|
+
Array.isArray(capability.directives)) {
|
|
32
32
|
const left = capability.directives.filter((directive) => kept.includes(directive));
|
|
33
33
|
return left.length > 0 ? { ...capability, directives: left } : undefined;
|
|
34
34
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A read's cache stage, for `pipelines.cache` at the root or client layer, a
|
|
3
|
+
* Resource, the `find` family or one of its variants:
|
|
4
|
+
* `pipelines: { cache: cache({ for: "user", maxAge: 60 }) }`. The read is then
|
|
5
|
+
* also served as a GET a browser or a CDN may keep for `maxAge` seconds.
|
|
6
|
+
*
|
|
7
|
+
* Always write `for`, even for the default `"user"`: the audience is the
|
|
8
|
+
* promise this read makes, and it should be visible in the code.
|
|
9
|
+
*/
|
|
10
|
+
export declare function cache(stage: {
|
|
11
|
+
/**
|
|
12
|
+
* Who may share one answer: `"user"` (the default; each caller's own
|
|
13
|
+
* browser keeps its own), `"anonymous"` (anonymous callers share one answer a
|
|
14
|
+
* CDN may keep; a signed-in caller's stays private) or `"everyone"` (one
|
|
15
|
+
* answer for every caller, which a CDN may hand to anyone). Always write it,
|
|
16
|
+
* even for `"user"`: the audience is the promise this read makes.
|
|
17
|
+
*/
|
|
18
|
+
readonly for?: "user" | "anonymous" | "everyone";
|
|
19
|
+
/** How long an answer may be kept, in whole seconds (`max-age`). */
|
|
20
|
+
readonly maxAge: number;
|
|
21
|
+
}): {
|
|
22
|
+
readonly for: "user" | "anonymous" | "everyone";
|
|
23
|
+
readonly maxAge: number;
|
|
24
|
+
};
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import { AvConstructionError } from "../framework/framework-construction.error.js";
|
|
2
2
|
import { ACCESS_VERB_NAMES, providerVerb, } from "../policy/access-verb.table.js";
|
|
3
|
+
import { isPlainRecord } from "../runtime/record.guard.js";
|
|
3
4
|
import { mountPathRefusalDiagnostic } from "./config.validator.js";
|
|
4
5
|
import { normalizeEntrypoint } from "./entrypoint.normalizer.js";
|
|
5
6
|
import { chainCompute, computeOfLayer, } from "./field-compute.resolver.js";
|
|
6
7
|
import { resolvePipelineTree, } from "./pipeline-tree.resolver.js";
|
|
7
8
|
const hasOwn = (value, key) => Object.hasOwn(value, key);
|
|
8
|
-
function isPlainRecord(value) {
|
|
9
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
const prototype = Object.getPrototypeOf(value);
|
|
13
|
-
return prototype === Object.prototype || prototype === null;
|
|
14
|
-
}
|
|
15
9
|
function cloneConfigValue(value) {
|
|
16
10
|
if (Array.isArray(value)) {
|
|
17
11
|
return [...value];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAccessPath, isAccessUrl } from "../contracts/access-route.rules.js";
|
|
2
2
|
import { TRANSACTION_CAPABILITIES } from "../contracts/transaction-capability.js";
|
|
3
3
|
import { ACCESS_VERB_NAMES, isAccessVerbName, providerVerb, } from "../policy/access-verb.table.js";
|
|
4
|
+
import { isPlainRecord } from "../runtime/record.guard.js";
|
|
4
5
|
import { normalizeEntrypoint } from "./entrypoint.normalizer.js";
|
|
5
6
|
import { AVZ_FORMATS } from "./framework-field-definition.js";
|
|
6
7
|
const ROOT_ONLY_PROPERTIES = new Set([
|
|
@@ -77,6 +78,8 @@ const LIMIT_PROPERTIES = new Set([
|
|
|
77
78
|
"maxTransactionOperations",
|
|
78
79
|
"maxBooleanNodes",
|
|
79
80
|
"maxJsonDepth",
|
|
81
|
+
"maxAll",
|
|
82
|
+
"maxAllConcurrency",
|
|
80
83
|
]);
|
|
81
84
|
const PIPELINE_PROPERTIES = new Set([
|
|
82
85
|
"guards",
|
|
@@ -86,13 +89,6 @@ const PIPELINE_PROPERTIES = new Set([
|
|
|
86
89
|
"filters",
|
|
87
90
|
]);
|
|
88
91
|
const PIPELINE_HOOK_PROPERTIES = new Set(["before", "after"]);
|
|
89
|
-
function isPlainRecord(value) {
|
|
90
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
const prototype = Object.getPrototypeOf(value);
|
|
94
|
-
return prototype === Object.prototype || prototype === null;
|
|
95
|
-
}
|
|
96
92
|
function diagnostic(code, path, message) {
|
|
97
93
|
return { code, path, message };
|
|
98
94
|
}
|
|
@@ -119,6 +115,14 @@ function validateLimits(value, path, diagnostics) {
|
|
|
119
115
|
diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", limitPath, `unknown limit property: ${key}`));
|
|
120
116
|
continue;
|
|
121
117
|
}
|
|
118
|
+
if (key === "maxAllConcurrency") {
|
|
119
|
+
if (typeof limit !== "number" ||
|
|
120
|
+
!Number.isSafeInteger(limit) ||
|
|
121
|
+
limit < 1) {
|
|
122
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", limitPath, `${limitPath} must be a whole number of at least 1`));
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
122
126
|
if (typeof limit !== "number" ||
|
|
123
127
|
!Number.isFinite(limit) ||
|
|
124
128
|
!Number.isInteger(limit) ||
|
|
@@ -127,6 +131,33 @@ function validateLimits(value, path, diagnostics) {
|
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
}
|
|
134
|
+
const CACHE_PROPERTIES = new Set(["for", "maxAge"]);
|
|
135
|
+
const CACHE_AUDIENCES = new Set(["user", "anonymous", "everyone"]);
|
|
136
|
+
function validateCache(value, path, diagnostics) {
|
|
137
|
+
if (!requireRecord(value, path, diagnostics)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
for (const key of Object.keys(value)) {
|
|
141
|
+
if (!CACHE_PROPERTIES.has(key)) {
|
|
142
|
+
diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", `${path}.${key}`, `unknown cache property: ${key}; cache takes for and maxAge`));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const maxAgePath = `${path}.maxAge`;
|
|
146
|
+
if (!Object.hasOwn(value, "maxAge")) {
|
|
147
|
+
diagnostics.push(diagnostic("CONFIG_REQUIRED", maxAgePath, `${maxAgePath} is required`));
|
|
148
|
+
}
|
|
149
|
+
else if (typeof value.maxAge !== "number" ||
|
|
150
|
+
!Number.isInteger(value.maxAge) ||
|
|
151
|
+
value.maxAge < 0 ||
|
|
152
|
+
value.maxAge > MAX_CACHE_AGE) {
|
|
153
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", maxAgePath, `${maxAgePath} must be a whole number of seconds from 0 to ${MAX_CACHE_AGE}`));
|
|
154
|
+
}
|
|
155
|
+
if (Object.hasOwn(value, "for") && !CACHE_AUDIENCES.has(value.for)) {
|
|
156
|
+
const forPath = `${path}.for`;
|
|
157
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", forPath, `${forPath} must be "user", "anonymous" or "everyone"`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const MAX_CACHE_AGE = 2_147_483_647;
|
|
130
161
|
function validateArray(value, key, path, diagnostics) {
|
|
131
162
|
if (!Object.hasOwn(value, key)) {
|
|
132
163
|
return;
|
|
@@ -148,7 +179,7 @@ function validatePipelineHooks(value, path, diagnostics) {
|
|
|
148
179
|
validateArray(value, "before", path, diagnostics);
|
|
149
180
|
validateArray(value, "after", path, diagnostics);
|
|
150
181
|
}
|
|
151
|
-
function validatePipelines(value, path, diagnostics) {
|
|
182
|
+
function validatePipelines(value, path, diagnostics, cacheRefusal) {
|
|
152
183
|
if (!requireRecord(value, path, diagnostics)) {
|
|
153
184
|
return;
|
|
154
185
|
}
|
|
@@ -157,6 +188,16 @@ function validatePipelines(value, path, diagnostics) {
|
|
|
157
188
|
diagnostics.push(diagnostic("CONFIG_MOVED_PROPERTY", `${path}.${key}`, `${path}.${key} moved in rc.1: hooks are ${path}.hooks.${key}`));
|
|
158
189
|
continue;
|
|
159
190
|
}
|
|
191
|
+
if (key === "cache") {
|
|
192
|
+
const refusal = cacheRefusal ?? layerCacheRefusal(path);
|
|
193
|
+
if (refusal !== undefined) {
|
|
194
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", `${path}.cache`, `${path}.cache: ${refusal}`));
|
|
195
|
+
}
|
|
196
|
+
else if (value.cache !== undefined) {
|
|
197
|
+
validateCache(value.cache, `${path}.cache`, diagnostics);
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
160
201
|
if (!PIPELINE_PROPERTIES.has(key)) {
|
|
161
202
|
diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", `${path}.${key}`, `unknown pipeline property: ${key}`));
|
|
162
203
|
}
|
|
@@ -168,6 +209,13 @@ function validatePipelines(value, path, diagnostics) {
|
|
|
168
209
|
validatePipelineHooks(value.hooks, `${path}.hooks`, diagnostics);
|
|
169
210
|
}
|
|
170
211
|
}
|
|
212
|
+
function layerCacheRefusal(path) {
|
|
213
|
+
const layer = path.split(".")[0];
|
|
214
|
+
return layer === "application" || layer === "agent"
|
|
215
|
+
? `cache applies to a client's reads; the ${layer} layer is never served a GET (state it at the root or in client)`
|
|
216
|
+
: undefined;
|
|
217
|
+
}
|
|
218
|
+
const WRITE_CACHE_REFUSAL = "cache applies to reads (the find family) only; a write is never a GET";
|
|
171
219
|
function validateForceTransaction(value, path, diagnostics) {
|
|
172
220
|
if (!Object.hasOwn(value, "forceTransaction")) {
|
|
173
221
|
return;
|
|
@@ -177,21 +225,21 @@ function validateForceTransaction(value, path, diagnostics) {
|
|
|
177
225
|
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", propertyPath, `${propertyPath} must be a boolean`));
|
|
178
226
|
}
|
|
179
227
|
}
|
|
180
|
-
function validateOperationLevel(value, path, isFamily, diagnostics) {
|
|
228
|
+
function validateOperationLevel(value, path, isFamily, diagnostics, isRead) {
|
|
181
229
|
if (!requireRecord(value, path, diagnostics)) {
|
|
182
230
|
return;
|
|
183
231
|
}
|
|
184
232
|
for (const [key, child] of Object.entries(value)) {
|
|
185
233
|
const keyPath = `${path}.${key}`;
|
|
186
234
|
if (key === "pipelines") {
|
|
187
|
-
validatePipelines(child, keyPath, diagnostics);
|
|
235
|
+
validatePipelines(child, keyPath, diagnostics, isRead ? undefined : WRITE_CACHE_REFUSAL);
|
|
188
236
|
continue;
|
|
189
237
|
}
|
|
190
238
|
if (key === "forceTransaction") {
|
|
191
239
|
continue;
|
|
192
240
|
}
|
|
193
241
|
if (isFamily && OPERATION_VARIANTS.has(key)) {
|
|
194
|
-
validateOperationLevel(child, keyPath, false, diagnostics);
|
|
242
|
+
validateOperationLevel(child, keyPath, false, diagnostics, isRead);
|
|
195
243
|
continue;
|
|
196
244
|
}
|
|
197
245
|
diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", keyPath, isFamily
|
|
@@ -210,7 +258,7 @@ function validateOperations(value, path, diagnostics) {
|
|
|
210
258
|
diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", familyPath, `unknown operation family: ${family}`));
|
|
211
259
|
continue;
|
|
212
260
|
}
|
|
213
|
-
validateOperationLevel(level, familyPath, true, diagnostics);
|
|
261
|
+
validateOperationLevel(level, familyPath, true, diagnostics, family === "find");
|
|
214
262
|
}
|
|
215
263
|
}
|
|
216
264
|
function validateResourceTransactions(value, path, diagnostics) {
|
|
@@ -223,7 +271,7 @@ function validateResourceTransactions(value, path, diagnostics) {
|
|
|
223
271
|
diagnostics.push(diagnostic("CONFIG_UNKNOWN_PROPERTY", keyPath, `unknown Resource transactions property: ${key}`));
|
|
224
272
|
continue;
|
|
225
273
|
}
|
|
226
|
-
validatePipelines(child, keyPath, diagnostics);
|
|
274
|
+
validatePipelines(child, keyPath, diagnostics, "cache applies to reads; a transaction is never a GET");
|
|
227
275
|
}
|
|
228
276
|
}
|
|
229
277
|
function validateDefinition(value, path, diagnostics) {
|
|
@@ -16,6 +16,10 @@ type PipelinesSchema = {
|
|
|
16
16
|
};
|
|
17
17
|
readonly interceptors: Leaf;
|
|
18
18
|
readonly filters: Leaf;
|
|
19
|
+
readonly cache: {
|
|
20
|
+
readonly for: Leaf;
|
|
21
|
+
readonly maxAge: Leaf;
|
|
22
|
+
};
|
|
19
23
|
};
|
|
20
24
|
type LimitsSchema = {
|
|
21
25
|
readonly maxRequestBytes: Leaf;
|
|
@@ -24,6 +28,8 @@ type LimitsSchema = {
|
|
|
24
28
|
readonly maxTransactionOperations: Leaf;
|
|
25
29
|
readonly maxBooleanNodes: Leaf;
|
|
26
30
|
readonly maxJsonDepth: Leaf;
|
|
31
|
+
readonly maxAll: Leaf;
|
|
32
|
+
readonly maxAllConcurrency: Leaf;
|
|
27
33
|
};
|
|
28
34
|
type LevelSchema = {
|
|
29
35
|
readonly pipelines: PipelinesSchema;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from "../runtime/record.guard.js";
|
|
1
2
|
function chainWrite(root, overlay) {
|
|
2
3
|
if (root === undefined || overlay === undefined) {
|
|
3
4
|
return root ?? overlay;
|
|
@@ -46,9 +47,6 @@ function chainRead(root, overlay) {
|
|
|
46
47
|
? compute
|
|
47
48
|
: { dependsOn, compute };
|
|
48
49
|
}
|
|
49
|
-
function isRecord(value) {
|
|
50
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
51
|
-
}
|
|
52
50
|
export function computeOfLayer(resources) {
|
|
53
51
|
const result = {};
|
|
54
52
|
if (!isRecord(resources)) {
|
|
@@ -23,6 +23,17 @@ export type AvPipeline<M extends AvAdapterModel = AvAdapterModel, R extends stri
|
|
|
23
23
|
readonly interceptors?: readonly AvInterceptor<M, R, F, V, A>[];
|
|
24
24
|
/** Turn a thrown error into a {@link AvError} outcome. */
|
|
25
25
|
readonly filters?: readonly AvFilter<M, R, F, V, PreCompute>[];
|
|
26
|
+
/**
|
|
27
|
+
* A read's cache stage, `cache({ for: "user", maxAge: 60 })`: the reads this
|
|
28
|
+
* level covers (its `find` operations) are also served as a GET a browser or
|
|
29
|
+
* a CDN may keep. The most specific level's stage wins. Always write `for`,
|
|
30
|
+
* even for the default `"user"`: the audience is the promise this read
|
|
31
|
+
* makes, and it should be visible in the code.
|
|
32
|
+
*/
|
|
33
|
+
readonly cache?: {
|
|
34
|
+
readonly for?: "user" | "anonymous" | "everyone";
|
|
35
|
+
readonly maxAge: number;
|
|
36
|
+
};
|
|
26
37
|
};
|
|
27
38
|
/**
|
|
28
39
|
* A Resource's transaction stages: they run once per transaction with a
|
|
@@ -51,6 +62,11 @@ export type ResolvedPipelineConfig = {
|
|
|
51
62
|
};
|
|
52
63
|
readonly interceptors: readonly AvInterceptor[];
|
|
53
64
|
readonly filters: readonly AvFilter[];
|
|
65
|
+
/** The level's read cache stage, as authored (validated); rc.11. */
|
|
66
|
+
readonly cache?: {
|
|
67
|
+
readonly for?: "user" | "anonymous" | "everyone";
|
|
68
|
+
readonly maxAge: number;
|
|
69
|
+
};
|
|
54
70
|
};
|
|
55
71
|
/** One layer's registrations on one Resource's transactions. */
|
|
56
72
|
export type ResolvedTransactionPipelineConfig = {
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
-
}
|
|
1
|
+
import { isRecord } from "../runtime/record.guard.js";
|
|
4
2
|
function own(record, key) {
|
|
5
3
|
return record !== undefined && Object.hasOwn(record, key)
|
|
6
4
|
? record[key]
|
|
@@ -23,6 +21,7 @@ function levelOf(value) {
|
|
|
23
21
|
},
|
|
24
22
|
interceptors: list(pipeline.interceptors),
|
|
25
23
|
filters: list(pipeline.filters),
|
|
24
|
+
...(isRecord(pipeline.cache) ? { cache: pipeline.cache } : {}),
|
|
26
25
|
};
|
|
27
26
|
}
|
|
28
27
|
function transactionLevelOf(value) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical JSON text (RFC 8785) of a JSON value: an object's keys in
|
|
3
|
+
* UTF-16 code-unit order, a member holding `undefined` left out, nothing but
|
|
4
|
+
* `null`, booleans, finite numbers, strings, arrays and plain objects. Throws a
|
|
5
|
+
* `TypeError` for anything else, a lone surrogate in a string or key included.
|
|
6
|
+
*/
|
|
7
|
+
export declare function canonicalJson(value: unknown): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function canonicalJson(value) {
|
|
2
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
3
|
+
const text = (string) => {
|
|
4
|
+
if (LONE_SURROGATE.test(string)) {
|
|
5
|
+
throw new TypeError("Canonical JSON cannot write a lone surrogate.");
|
|
6
|
+
}
|
|
7
|
+
return JSON.stringify(string);
|
|
8
|
+
};
|
|
9
|
+
const write = (current, ancestors) => {
|
|
10
|
+
if (current === null) {
|
|
11
|
+
return "null";
|
|
12
|
+
}
|
|
13
|
+
switch (typeof current) {
|
|
14
|
+
case "boolean":
|
|
15
|
+
return current ? "true" : "false";
|
|
16
|
+
case "number":
|
|
17
|
+
if (!Number.isFinite(current)) {
|
|
18
|
+
throw new TypeError("Canonical JSON cannot write a non-finite number.");
|
|
19
|
+
}
|
|
20
|
+
return JSON.stringify(current);
|
|
21
|
+
case "string":
|
|
22
|
+
return text(current);
|
|
23
|
+
case "object":
|
|
24
|
+
break;
|
|
25
|
+
default:
|
|
26
|
+
throw new TypeError(`Canonical JSON cannot write a ${typeof current}.`);
|
|
27
|
+
}
|
|
28
|
+
const container = current;
|
|
29
|
+
if (ancestors.includes(container)) {
|
|
30
|
+
throw new TypeError("Canonical JSON cannot write a cycle.");
|
|
31
|
+
}
|
|
32
|
+
const path = [...ancestors, container];
|
|
33
|
+
if (Array.isArray(container)) {
|
|
34
|
+
const items = [];
|
|
35
|
+
for (let index = 0; index < container.length; index += 1) {
|
|
36
|
+
items.push(write(container[index] ?? null, path));
|
|
37
|
+
}
|
|
38
|
+
return `[${items.join(",")}]`;
|
|
39
|
+
}
|
|
40
|
+
const prototype = Object.getPrototypeOf(container);
|
|
41
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
42
|
+
throw new TypeError("Canonical JSON writes plain objects only.");
|
|
43
|
+
}
|
|
44
|
+
const record = container;
|
|
45
|
+
const members = [];
|
|
46
|
+
for (const key of Object.keys(record).sort((left, right) => left < right ? -1 : left > right ? 1 : 0)) {
|
|
47
|
+
const member = record[key];
|
|
48
|
+
if (member !== undefined) {
|
|
49
|
+
members.push(`${text(key)}:${write(member, path)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return `{${members.join(",")}}`;
|
|
53
|
+
};
|
|
54
|
+
return write(value, []);
|
|
55
|
+
}
|
|
@@ -1307,6 +1307,64 @@ export declare const CLIENT_CONTRACT_STRUCTURE: {
|
|
|
1307
1307
|
};
|
|
1308
1308
|
};
|
|
1309
1309
|
};
|
|
1310
|
+
readonly cache: {
|
|
1311
|
+
readonly is: "object";
|
|
1312
|
+
readonly required: {};
|
|
1313
|
+
readonly optional: {
|
|
1314
|
+
readonly first: {
|
|
1315
|
+
readonly is: "object";
|
|
1316
|
+
readonly required: {
|
|
1317
|
+
readonly for: {
|
|
1318
|
+
readonly is: "literal";
|
|
1319
|
+
readonly values: readonly ["user", "anonymous", "everyone"];
|
|
1320
|
+
};
|
|
1321
|
+
readonly maxAge: {
|
|
1322
|
+
readonly is: "number";
|
|
1323
|
+
};
|
|
1324
|
+
};
|
|
1325
|
+
readonly optional: {};
|
|
1326
|
+
};
|
|
1327
|
+
readonly unique: {
|
|
1328
|
+
readonly is: "object";
|
|
1329
|
+
readonly required: {
|
|
1330
|
+
readonly for: {
|
|
1331
|
+
readonly is: "literal";
|
|
1332
|
+
readonly values: readonly ["user", "anonymous", "everyone"];
|
|
1333
|
+
};
|
|
1334
|
+
readonly maxAge: {
|
|
1335
|
+
readonly is: "number";
|
|
1336
|
+
};
|
|
1337
|
+
};
|
|
1338
|
+
readonly optional: {};
|
|
1339
|
+
};
|
|
1340
|
+
readonly many: {
|
|
1341
|
+
readonly is: "object";
|
|
1342
|
+
readonly required: {
|
|
1343
|
+
readonly for: {
|
|
1344
|
+
readonly is: "literal";
|
|
1345
|
+
readonly values: readonly ["user", "anonymous", "everyone"];
|
|
1346
|
+
};
|
|
1347
|
+
readonly maxAge: {
|
|
1348
|
+
readonly is: "number";
|
|
1349
|
+
};
|
|
1350
|
+
};
|
|
1351
|
+
readonly optional: {};
|
|
1352
|
+
};
|
|
1353
|
+
readonly count: {
|
|
1354
|
+
readonly is: "object";
|
|
1355
|
+
readonly required: {
|
|
1356
|
+
readonly for: {
|
|
1357
|
+
readonly is: "literal";
|
|
1358
|
+
readonly values: readonly ["user", "anonymous", "everyone"];
|
|
1359
|
+
};
|
|
1360
|
+
readonly maxAge: {
|
|
1361
|
+
readonly is: "number";
|
|
1362
|
+
};
|
|
1363
|
+
};
|
|
1364
|
+
readonly optional: {};
|
|
1365
|
+
};
|
|
1366
|
+
};
|
|
1367
|
+
};
|
|
1310
1368
|
};
|
|
1311
1369
|
};
|
|
1312
1370
|
};
|
|
@@ -1331,6 +1389,12 @@ export declare const CLIENT_CONTRACT_STRUCTURE: {
|
|
|
1331
1389
|
readonly maxJsonDepth: {
|
|
1332
1390
|
readonly is: "number";
|
|
1333
1391
|
};
|
|
1392
|
+
readonly maxAll: {
|
|
1393
|
+
readonly is: "number";
|
|
1394
|
+
};
|
|
1395
|
+
readonly maxAllConcurrency: {
|
|
1396
|
+
readonly is: "number";
|
|
1397
|
+
};
|
|
1334
1398
|
};
|
|
1335
1399
|
readonly optional: Record<never, never>;
|
|
1336
1400
|
};
|