@aventara/core 1.0.0-rc.3 → 1.0.0-rc.4
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 +130 -1
- package/ERRORS.md +6 -6
- package/README.md +72 -12
- package/dist/adapters/adapter-operation.d.ts +7 -0
- package/dist/compiler/contract.compiler.d.ts +6 -0
- package/dist/compiler/contract.compiler.js +73 -12
- package/dist/config/computed-fields.js +2 -9
- package/dist/config/config-diagnostic.guidance.js +3 -3
- package/dist/config/config.resolver.d.ts +14 -0
- package/dist/config/config.resolver.js +31 -0
- package/dist/config/config.validator.js +82 -0
- package/dist/config/exact-framework-config.d.ts +5 -0
- package/dist/config/framework-config.d.ts +26 -1
- package/dist/contracts/client-contract-structure.validator.d.ts +8 -0
- package/dist/contracts/client-contract-structure.validator.js +6 -1
- package/dist/contracts/field.d.ts +11 -0
- package/dist/framework/contract-view.d.ts +41 -10
- package/dist/framework/framework.factory.js +39 -11
- package/dist/framework/operation-facade.d.ts +24 -5
- package/dist/framework/operation-facade.js +2 -1
- package/dist/index.d.ts +4 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/operations/operation-arguments.d.ts +1 -1
- package/dist/operations/operation-call.d.ts +9 -5
- package/dist/operations/operation-result.d.ts +5 -3
- package/dist/operations/operation-validator-core.js +6 -0
- package/dist/policy/policy-admission.d.ts +22 -0
- package/dist/policy/policy-admission.js +400 -0
- package/dist/policy/policy-after-update.runtime.d.ts +29 -0
- package/dist/policy/policy-after-update.runtime.js +131 -0
- package/dist/policy/policy-condition.d.ts +15 -0
- package/dist/policy/policy-condition.js +36 -0
- package/dist/policy/policy-engine.d.ts +111 -0
- package/dist/policy/policy-engine.js +1 -0
- package/dist/policy/policy-nested-write.runtime.d.ts +38 -0
- package/dist/policy/policy-nested-write.runtime.js +449 -0
- package/dist/policy/policy-read.runtime.d.ts +83 -0
- package/dist/policy/policy-read.runtime.js +494 -0
- package/dist/policy/policy-request.d.ts +37 -0
- package/dist/policy/policy-request.js +52 -0
- package/dist/policy/policy-session.d.ts +83 -0
- package/dist/policy/policy-session.js +234 -0
- package/dist/policy/policy-startup.validator.d.ts +8 -0
- package/dist/policy/policy-startup.validator.js +106 -0
- package/dist/policy/policy-write.runtime.d.ts +30 -0
- package/dist/policy/policy-write.runtime.js +394 -0
- package/dist/prebuilt/core-version.generated.d.ts +2 -0
- package/dist/prebuilt/core-version.generated.js +1 -0
- package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
- package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
- package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
- package/dist/prebuilt/prebuilt-contracts.js +1 -0
- package/dist/prebuilt/prebuilt-contracts.loader.d.ts +21 -0
- package/dist/prebuilt/prebuilt-contracts.loader.js +122 -0
- package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
- package/dist/prebuilt/prebuilt-inputs.fingerprint.js +192 -0
- package/dist/runtime/computed-read-runtime.d.ts +6 -1
- package/dist/runtime/computed-read-runtime.js +224 -45
- package/dist/runtime/context-executor.runtime.js +4 -2
- package/dist/runtime/execution-options.d.ts +8 -0
- package/dist/runtime/framework-warning.emitter.d.ts +6 -0
- package/dist/runtime/framework-warning.emitter.js +8 -0
- package/dist/runtime/level-lifecycle.runtime.js +3 -2
- package/dist/runtime/operation-code.guidance.js +3 -3
- package/dist/runtime/operation-request.d.ts +7 -0
- package/dist/runtime/operation-runtime.d.ts +9 -1
- package/dist/runtime/operation-runtime.js +114 -10
- package/dist/runtime/readonly-value.d.ts +5 -1
- package/dist/runtime/readonly-value.js +67 -28
- package/dist/transactions/transaction-runtime.d.ts +4 -1
- package/dist/transactions/transaction-runtime.js +27 -5
- package/package.json +2 -2
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { avzCheckOf } from "../avz/avz-check.registry.js";
|
|
2
|
+
import { jsonShapeOf } from "../avz/json-shape.registry.js";
|
|
3
|
+
import { hashCanonicalContractText } from "../contracts/contract-hash.js";
|
|
4
|
+
import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
|
|
5
|
+
import { CORE_VERSION } from "./core-version.generated.js";
|
|
6
|
+
import { PREBUILT_FORMAT } from "./prebuilt-contracts.js";
|
|
7
|
+
const LAYERS = ["root", "application", "client", "agent"];
|
|
8
|
+
const NOT_INPUTS = new Set(["adapter", "prebuilt"]);
|
|
9
|
+
const PER_RESOURCE = "#resources";
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function sortedRecord(record) {
|
|
14
|
+
const keys = Object.keys(record);
|
|
15
|
+
const prototype = Object.getPrototypeOf(record);
|
|
16
|
+
const plain = prototype === Object.prototype || prototype === null;
|
|
17
|
+
let written = plain;
|
|
18
|
+
for (let index = 0; written && index < keys.length; index += 1) {
|
|
19
|
+
const key = keys[index];
|
|
20
|
+
written =
|
|
21
|
+
!key.startsWith("#") &&
|
|
22
|
+
(index === 0 || keys[index - 1] < key);
|
|
23
|
+
}
|
|
24
|
+
if (written) {
|
|
25
|
+
return record;
|
|
26
|
+
}
|
|
27
|
+
const sorted = {};
|
|
28
|
+
if (!plain) {
|
|
29
|
+
const name = record.constructor?.name;
|
|
30
|
+
sorted["#class"] = typeof name === "string" ? name : "";
|
|
31
|
+
}
|
|
32
|
+
for (const key of keys.sort()) {
|
|
33
|
+
sorted[key.startsWith("#") ? `#${key}` : key] = record[key];
|
|
34
|
+
}
|
|
35
|
+
return sorted;
|
|
36
|
+
}
|
|
37
|
+
function inputMember(key, value) {
|
|
38
|
+
const raw = this[key];
|
|
39
|
+
switch (typeof raw) {
|
|
40
|
+
case "string":
|
|
41
|
+
return raw.startsWith("#") ? `#${raw}` : raw;
|
|
42
|
+
case "number":
|
|
43
|
+
return Number.isFinite(raw) ? raw : `#number(${raw})`;
|
|
44
|
+
case "bigint":
|
|
45
|
+
return `#bigint(${raw})`;
|
|
46
|
+
case "function":
|
|
47
|
+
return "#function";
|
|
48
|
+
case "symbol":
|
|
49
|
+
return `#symbol(${String(raw.description)})`;
|
|
50
|
+
case "undefined":
|
|
51
|
+
return "#undefined";
|
|
52
|
+
case "boolean":
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
if (raw === null) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
const check = avzCheckOf(raw);
|
|
59
|
+
if (check !== undefined) {
|
|
60
|
+
return `#avz(${inputText(check)})`;
|
|
61
|
+
}
|
|
62
|
+
const shape = jsonShapeOf(raw);
|
|
63
|
+
if (shape !== undefined) {
|
|
64
|
+
return `#shape(${inputText(shape.schema)})`;
|
|
65
|
+
}
|
|
66
|
+
if (raw instanceof Date) {
|
|
67
|
+
return `#date(${Number.isNaN(raw.getTime()) ? "invalid" : raw.toISOString()})`;
|
|
68
|
+
}
|
|
69
|
+
if (raw instanceof RegExp) {
|
|
70
|
+
return `#regexp(${JSON.stringify(raw.source)},${raw.flags})`;
|
|
71
|
+
}
|
|
72
|
+
if (raw instanceof Map || raw instanceof Set) {
|
|
73
|
+
return `#${raw instanceof Map ? "map" : "set"}(${inputText([...raw])})`;
|
|
74
|
+
}
|
|
75
|
+
if (value !== raw) {
|
|
76
|
+
return `#json(${inputText(value)})`;
|
|
77
|
+
}
|
|
78
|
+
return Array.isArray(raw) ? raw : sortedRecord(raw);
|
|
79
|
+
}
|
|
80
|
+
function inputText(value) {
|
|
81
|
+
return JSON.stringify(value, inputMember);
|
|
82
|
+
}
|
|
83
|
+
function modelText(value) {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.stringify(value) ?? "#undefined";
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return inputText(value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function recordAt(value, key) {
|
|
92
|
+
if (!isRecord(value) || !Object.hasOwn(value, key)) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const member = value[key];
|
|
96
|
+
return isRecord(member) ? member : undefined;
|
|
97
|
+
}
|
|
98
|
+
function layerOf(config, layer) {
|
|
99
|
+
return layer === "root"
|
|
100
|
+
? config
|
|
101
|
+
: Object.hasOwn(config, layer)
|
|
102
|
+
? config[layer]
|
|
103
|
+
: undefined;
|
|
104
|
+
}
|
|
105
|
+
function sharedLayer(layer) {
|
|
106
|
+
if (!isRecord(layer)) {
|
|
107
|
+
return layer;
|
|
108
|
+
}
|
|
109
|
+
const shared = { ...layer };
|
|
110
|
+
if (isRecord(layer.resources)) {
|
|
111
|
+
shared.resources = PER_RESOURCE;
|
|
112
|
+
}
|
|
113
|
+
const exemptions = layer.exemptions;
|
|
114
|
+
if (isRecord(exemptions) && isRecord(exemptions.resources)) {
|
|
115
|
+
shared.exemptions = { ...exemptions, resources: PER_RESOURCE };
|
|
116
|
+
}
|
|
117
|
+
return shared;
|
|
118
|
+
}
|
|
119
|
+
function policyInput(policy) {
|
|
120
|
+
if (!isRecord(policy) || !Object.hasOwn(policy, "engine")) {
|
|
121
|
+
return policy;
|
|
122
|
+
}
|
|
123
|
+
const engine = policy.engine;
|
|
124
|
+
const governed = engine !== null && typeof engine === "object"
|
|
125
|
+
? engine.governed
|
|
126
|
+
: undefined;
|
|
127
|
+
return { ...policy, engine: { governed: governed ?? null } };
|
|
128
|
+
}
|
|
129
|
+
function authoredInputs(config) {
|
|
130
|
+
const authored = config;
|
|
131
|
+
const root = {};
|
|
132
|
+
for (const key of Object.keys(authored)) {
|
|
133
|
+
if (!NOT_INPUTS.has(key)) {
|
|
134
|
+
root[key] = key === "policy" ? policyInput(authored[key]) : authored[key];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return root;
|
|
138
|
+
}
|
|
139
|
+
const HEADER = `aventara-prebuilt format ${PREBUILT_FORMAT} core ${CORE_VERSION} protocol ${AVENTARA_PROTOCOL_VERSION}`;
|
|
140
|
+
export function prebuiltInputsFingerprint(model, config) {
|
|
141
|
+
return hashCanonicalContractText(`${HEADER}\n${inputText(authoredInputs(config))}\n${modelText(model)}`);
|
|
142
|
+
}
|
|
143
|
+
export async function prebuiltPartFingerprints(model, config) {
|
|
144
|
+
const root = authoredInputs(config);
|
|
145
|
+
const names = new Set(Object.keys(model.resources));
|
|
146
|
+
for (const layer of LAYERS) {
|
|
147
|
+
const value = layerOf(root, layer);
|
|
148
|
+
for (const name of Object.keys(recordAt(value, "resources") ?? {})) {
|
|
149
|
+
names.add(name);
|
|
150
|
+
}
|
|
151
|
+
const exemptions = recordAt(value, "exemptions");
|
|
152
|
+
for (const name of Object.keys(recordAt(exemptions, "resources") ?? {})) {
|
|
153
|
+
names.add(name);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const sharedRoot = sharedLayer(root);
|
|
157
|
+
for (const layer of LAYERS) {
|
|
158
|
+
if (layer !== "root" && Object.hasOwn(root, layer)) {
|
|
159
|
+
sharedRoot[layer] = sharedLayer(root[layer]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const { resources: _resources, ...sharedModel } = model;
|
|
163
|
+
const shared = hashCanonicalContractText(`${HEADER}\n${inputText(sharedRoot)}\n${modelText(sharedModel)}`);
|
|
164
|
+
const sorted = [...names].sort();
|
|
165
|
+
const parts = sorted.map((name) => {
|
|
166
|
+
const entries = {};
|
|
167
|
+
for (const layer of LAYERS) {
|
|
168
|
+
const value = layerOf(root, layer);
|
|
169
|
+
const resources = recordAt(value, "resources");
|
|
170
|
+
const exemptions = recordAt(recordAt(value, "exemptions"), "resources");
|
|
171
|
+
const entry = {};
|
|
172
|
+
if (resources !== undefined && Object.hasOwn(resources, name)) {
|
|
173
|
+
entry.resources = resources[name];
|
|
174
|
+
}
|
|
175
|
+
if (exemptions !== undefined && Object.hasOwn(exemptions, name)) {
|
|
176
|
+
entry.exemptions = exemptions[name];
|
|
177
|
+
}
|
|
178
|
+
if (Object.keys(entry).length > 0) {
|
|
179
|
+
entries[layer] = entry;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const modelPart = Object.hasOwn(model.resources, name)
|
|
183
|
+
? modelText(model.resources[name])
|
|
184
|
+
: "#absent";
|
|
185
|
+
return hashCanonicalContractText(`${inputText(entries)}\n${modelPart}`);
|
|
186
|
+
});
|
|
187
|
+
const hashes = await Promise.all(parts);
|
|
188
|
+
return {
|
|
189
|
+
shared: await shared,
|
|
190
|
+
resources: Object.fromEntries(sorted.map((name, index) => [name, hashes[index]])),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -21,6 +21,11 @@ export type PlannedReadOperation = {
|
|
|
21
21
|
readonly projection: ReadProjectionPlan | undefined;
|
|
22
22
|
readonly materialize: boolean;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* The fields a level returns when it states no projection: every scalar of the
|
|
26
|
+
* active Contract whose `select` is `true` and that is not governed.
|
|
27
|
+
*/
|
|
28
|
+
export declare function defaultProjectionFields(active: AvContract, resourceName: string): readonly string[];
|
|
24
29
|
/**
|
|
25
30
|
* Clone the caller projection into the internal Adapter projection: the read
|
|
26
31
|
* set the Adapter is asked for, always explicit for a projecting variant, plus
|
|
@@ -33,5 +38,5 @@ export declare function planReadOperation(core: AvContractCore, model: AvAdapter
|
|
|
33
38
|
* Materialize computed scalar reads and remove internal dependency fields,
|
|
34
39
|
* against `active` — the Contract `planReadOperation` was given as `active`.
|
|
35
40
|
*/
|
|
36
|
-
export declare function materializeReadOperation(active: AvContract, compute: ResolvedComputeConfig | undefined,
|
|
41
|
+
export declare function materializeReadOperation(active: AvContract, compute: ResolvedComputeConfig | undefined, _request: OperationExecutionRequest, plan: PlannedReadOperation, value: unknown, contextFactory: ContextFactory): Promise<unknown>;
|
|
37
42
|
export {};
|
|
@@ -89,18 +89,27 @@ function projectionEntry(entry) {
|
|
|
89
89
|
function withoutProjectionChoice(value) {
|
|
90
90
|
return Object.fromEntries(Object.entries(value).filter(([key]) => key !== "select" && key !== "include"));
|
|
91
91
|
}
|
|
92
|
-
function
|
|
92
|
+
export function defaultProjectionFields(active, resourceName) {
|
|
93
93
|
const activeResource = lookupResource(active, resourceName);
|
|
94
|
+
if (activeResource === undefined) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const fields = [];
|
|
98
|
+
for (const [fieldName, field] of Object.entries(activeResource.fields)) {
|
|
99
|
+
if (field.kind === "scalar" &&
|
|
100
|
+
field.capabilities.select === true &&
|
|
101
|
+
field.governed !== true) {
|
|
102
|
+
fields.push(fieldName);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return fields;
|
|
106
|
+
}
|
|
107
|
+
function defaultProjectionLevel(core, model, active, compute, resourceName) {
|
|
94
108
|
const adapterSelect = [];
|
|
95
109
|
const requested = [];
|
|
96
110
|
const computed = [];
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (field.kind !== "scalar" || field.capabilities.select !== true) {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
addScalarProjection(core, model, active, compute, resourceName, fieldName, adapterSelect, requested, computed);
|
|
103
|
-
}
|
|
111
|
+
for (const fieldName of defaultProjectionFields(active, resourceName)) {
|
|
112
|
+
addScalarProjection(core, model, active, compute, resourceName, fieldName, adapterSelect, requested, computed);
|
|
104
113
|
}
|
|
105
114
|
return {
|
|
106
115
|
select: adapterSelect,
|
|
@@ -168,6 +177,12 @@ function planSelectLevel(core, model, active, compute, resourceName, select, col
|
|
|
168
177
|
}
|
|
169
178
|
for (const entry of select) {
|
|
170
179
|
if (typeof entry === "string") {
|
|
180
|
+
if (entry === "$defaults") {
|
|
181
|
+
for (const fieldName of defaultProjectionFields(active, resourceName)) {
|
|
182
|
+
addScalarProjection(core, model, active, compute, resourceName, fieldName, adapterSelect, requested, computed);
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
171
186
|
if (entry === "$count") {
|
|
172
187
|
adapterSelect.push(entry);
|
|
173
188
|
if (collectReducers) {
|
|
@@ -292,7 +307,175 @@ export function planReadOperation(core, model, active, compute, request, args) {
|
|
|
292
307
|
materialize: defaults.plan.computed.length > 0,
|
|
293
308
|
};
|
|
294
309
|
}
|
|
295
|
-
|
|
310
|
+
function resolveReadLevel(active, compute, plan, tail) {
|
|
311
|
+
const resource = lookupResource(active, plan.resource);
|
|
312
|
+
if (resource === undefined) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
const stages = [];
|
|
316
|
+
for (const fieldName of plan.computed) {
|
|
317
|
+
const field = lookupField(resource, fieldName);
|
|
318
|
+
const stage = readCompute(compute?.[plan.resource]?.[fieldName]?.read);
|
|
319
|
+
if (field?.kind !== "scalar" || stage === undefined) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
stages.push({
|
|
323
|
+
field: fieldName,
|
|
324
|
+
definition: field,
|
|
325
|
+
compute: stage.compute,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const relations = [];
|
|
329
|
+
let restFree = true;
|
|
330
|
+
for (const relation of [...plan.relations].reverse()) {
|
|
331
|
+
const level = resolveReadLevel(active, compute, relation.projection, tail && restFree);
|
|
332
|
+
relations.unshift({ relation, level });
|
|
333
|
+
restFree &&= level?.computeFree ?? true;
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
plan,
|
|
337
|
+
stages,
|
|
338
|
+
relations,
|
|
339
|
+
computeFree: stages.length === 0 && restFree,
|
|
340
|
+
atOnce: tail && stages.length <= 1 && restFree,
|
|
341
|
+
plainKeys: plan.requested.every((field) => !(field in Object.prototype)),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function isNestedContainer(value) {
|
|
345
|
+
return (value !== null &&
|
|
346
|
+
typeof value === "object" &&
|
|
347
|
+
!(value instanceof Date) &&
|
|
348
|
+
!(value instanceof Uint8Array));
|
|
349
|
+
}
|
|
350
|
+
function isThenable(value) {
|
|
351
|
+
return (value !== null &&
|
|
352
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
353
|
+
typeof value.then === "function");
|
|
354
|
+
}
|
|
355
|
+
function setOwn(target, key, value) {
|
|
356
|
+
if (key in target) {
|
|
357
|
+
Object.defineProperty(target, key, {
|
|
358
|
+
value,
|
|
359
|
+
writable: true,
|
|
360
|
+
enumerable: true,
|
|
361
|
+
configurable: true,
|
|
362
|
+
});
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
target[key] = value;
|
|
366
|
+
}
|
|
367
|
+
function answerOf(level, record) {
|
|
368
|
+
const answer = {};
|
|
369
|
+
for (const fieldName of level.plan.requested) {
|
|
370
|
+
if (!Object.hasOwn(record, fieldName)) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (level.plainKeys) {
|
|
374
|
+
answer[fieldName] = record[fieldName];
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
setOwn(answer, fieldName, record[fieldName]);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return answer;
|
|
381
|
+
}
|
|
382
|
+
function heldReadValue(active, level, stage, value) {
|
|
383
|
+
const validation = normalizeScalarFieldValue(active, stage.definition, value, "runtime", [
|
|
384
|
+
"resources",
|
|
385
|
+
level.plan.resource,
|
|
386
|
+
"fields",
|
|
387
|
+
stage.field,
|
|
388
|
+
"pipelines",
|
|
389
|
+
"compute",
|
|
390
|
+
"read",
|
|
391
|
+
]);
|
|
392
|
+
if (!validation.ok) {
|
|
393
|
+
throw new AvComputedValueError(level.plan.resource, stage.field, "read", validation.issues);
|
|
394
|
+
}
|
|
395
|
+
return validation.value;
|
|
396
|
+
}
|
|
397
|
+
function projectedAtOnce(level, value) {
|
|
398
|
+
if (!isRecord(value) || level === undefined) {
|
|
399
|
+
return value;
|
|
400
|
+
}
|
|
401
|
+
const record = { ...value };
|
|
402
|
+
relationsAtOnce(level, record);
|
|
403
|
+
return answerOf(level, record);
|
|
404
|
+
}
|
|
405
|
+
function relationsAtOnce(level, record) {
|
|
406
|
+
for (const { relation, level: nested } of level.relations) {
|
|
407
|
+
if (!Object.hasOwn(record, relation.field)) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const value = record[relation.field];
|
|
411
|
+
if (value === null || value === undefined) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (relation.cardinality === "many") {
|
|
415
|
+
if (Array.isArray(value)) {
|
|
416
|
+
record[relation.field] = Array.from(value, (entry) => projectedAtOnce(nested, entry));
|
|
417
|
+
}
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
record[relation.field] = projectedAtOnce(nested, value);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function computedAtOnce(active, level, stage, value, contextFactory) {
|
|
424
|
+
const record = { ...value };
|
|
425
|
+
const view = frozenCopy(record);
|
|
426
|
+
let computedValue;
|
|
427
|
+
try {
|
|
428
|
+
computedValue = stage.compute({
|
|
429
|
+
field: stage.field,
|
|
430
|
+
value: view[stage.field],
|
|
431
|
+
record: view,
|
|
432
|
+
ctx: contextFactory(),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
return Promise.reject(error);
|
|
437
|
+
}
|
|
438
|
+
const finish = (returned) => {
|
|
439
|
+
if (returned !== undefined) {
|
|
440
|
+
record[stage.field] = heldReadValue(active, level, stage, returned);
|
|
441
|
+
}
|
|
442
|
+
relationsAtOnce(level, record);
|
|
443
|
+
return answerOf(level, record);
|
|
444
|
+
};
|
|
445
|
+
const later = (error) => Promise.resolve().then(() => {
|
|
446
|
+
throw error;
|
|
447
|
+
});
|
|
448
|
+
let waits;
|
|
449
|
+
try {
|
|
450
|
+
waits = isThenable(computedValue);
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
return later(error);
|
|
454
|
+
}
|
|
455
|
+
if (waits) {
|
|
456
|
+
const pending = computedValue;
|
|
457
|
+
return (async () => finish(await pending))();
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
return finish(computedValue);
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return later(error);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function materializeRecord(active, level, value, contextFactory) {
|
|
467
|
+
if (level?.atOnce !== true) {
|
|
468
|
+
return materializeInTurn(active, level, value, contextFactory);
|
|
469
|
+
}
|
|
470
|
+
if (!isRecord(value)) {
|
|
471
|
+
return value;
|
|
472
|
+
}
|
|
473
|
+
const [stage] = level.stages;
|
|
474
|
+
return stage === undefined
|
|
475
|
+
? projectedAtOnce(level, value)
|
|
476
|
+
: computedAtOnce(active, level, stage, value, contextFactory);
|
|
477
|
+
}
|
|
478
|
+
async function materializeRelation(active, level, relation, value, contextFactory) {
|
|
296
479
|
if (value === null || value === undefined) {
|
|
297
480
|
return value;
|
|
298
481
|
}
|
|
@@ -300,66 +483,62 @@ async function materializeRelation(active, compute, request, relation, value, co
|
|
|
300
483
|
if (!Array.isArray(value)) {
|
|
301
484
|
return value;
|
|
302
485
|
}
|
|
303
|
-
return Promise.all(value.map((entry) => materializeRecord(active,
|
|
486
|
+
return Promise.all(value.map((entry) => materializeRecord(active, level, entry, contextFactory)));
|
|
304
487
|
}
|
|
305
|
-
return materializeRecord(active,
|
|
488
|
+
return materializeRecord(active, level, value, contextFactory);
|
|
306
489
|
}
|
|
307
|
-
async function
|
|
308
|
-
if (!isRecord(value)) {
|
|
309
|
-
return value;
|
|
310
|
-
}
|
|
311
|
-
const resource = lookupResource(active, plan.resource);
|
|
312
|
-
if (resource === undefined) {
|
|
490
|
+
async function materializeInTurn(active, level, value, contextFactory) {
|
|
491
|
+
if (!isRecord(value) || level === undefined) {
|
|
313
492
|
return value;
|
|
314
493
|
}
|
|
494
|
+
const { stages } = level;
|
|
315
495
|
const record = { ...value };
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
496
|
+
let told;
|
|
497
|
+
if (stages.length > 1) {
|
|
498
|
+
told = { ...record };
|
|
499
|
+
for (const key of Object.keys(told)) {
|
|
500
|
+
const nested = told[key];
|
|
501
|
+
if (isNestedContainer(nested)) {
|
|
502
|
+
told[key] = frozenCopy(nested, true);
|
|
503
|
+
}
|
|
321
504
|
}
|
|
322
|
-
|
|
505
|
+
}
|
|
506
|
+
for (const stage of stages) {
|
|
507
|
+
const view = frozenCopy(told ?? record);
|
|
323
508
|
const computedValue = await stage.compute({
|
|
324
|
-
field:
|
|
325
|
-
value: view[
|
|
509
|
+
field: stage.field,
|
|
510
|
+
value: view[stage.field],
|
|
326
511
|
record: view,
|
|
327
512
|
ctx: contextFactory(),
|
|
328
513
|
});
|
|
329
514
|
if (computedValue === undefined) {
|
|
330
515
|
continue;
|
|
331
516
|
}
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
fieldName,
|
|
337
|
-
"pipelines",
|
|
338
|
-
"compute",
|
|
339
|
-
"read",
|
|
340
|
-
]);
|
|
341
|
-
if (!validation.ok) {
|
|
342
|
-
throw new AvComputedValueError(plan.resource, fieldName, "read", validation.issues);
|
|
517
|
+
const held = heldReadValue(active, level, stage, computedValue);
|
|
518
|
+
record[stage.field] = held;
|
|
519
|
+
if (told !== undefined) {
|
|
520
|
+
told[stage.field] = held;
|
|
343
521
|
}
|
|
344
|
-
record[fieldName] = validation.value;
|
|
345
522
|
}
|
|
346
|
-
for (const relation of
|
|
523
|
+
for (const { relation, level: nested } of level.relations) {
|
|
347
524
|
if (!Object.hasOwn(record, relation.field)) {
|
|
348
525
|
continue;
|
|
349
526
|
}
|
|
350
|
-
record[relation.field] = await materializeRelation(active,
|
|
527
|
+
record[relation.field] = await materializeRelation(active, nested, relation, record[relation.field], contextFactory);
|
|
351
528
|
}
|
|
352
|
-
return
|
|
353
|
-
.filter((fieldName) => Object.hasOwn(record, fieldName))
|
|
354
|
-
.map((fieldName) => [fieldName, record[fieldName]]));
|
|
529
|
+
return answerOf(level, record);
|
|
355
530
|
}
|
|
356
|
-
export async function materializeReadOperation(active, compute,
|
|
531
|
+
export async function materializeReadOperation(active, compute, _request, plan, value, contextFactory) {
|
|
357
532
|
const projection = plan.projection;
|
|
358
533
|
if (!plan.materialize || projection === undefined) {
|
|
359
534
|
return value;
|
|
360
535
|
}
|
|
536
|
+
const level = resolveReadLevel(active, compute, projection, true);
|
|
361
537
|
if (Array.isArray(value)) {
|
|
362
|
-
|
|
538
|
+
const rows = value.map((entry) => materializeRecord(active, level, entry, contextFactory));
|
|
539
|
+
return rows.some((row) => row instanceof Promise)
|
|
540
|
+
? Promise.all(rows)
|
|
541
|
+
: Array.from(rows);
|
|
363
542
|
}
|
|
364
|
-
return materializeRecord(active,
|
|
543
|
+
return materializeRecord(active, level, value, contextFactory);
|
|
365
544
|
}
|
|
@@ -195,9 +195,10 @@ export function createContextExecutor(dependencies) {
|
|
|
195
195
|
const contextOf = (prototype, scope, facts, readOnly = false) => {
|
|
196
196
|
const context = Object.create(prototype);
|
|
197
197
|
Object.defineProperty(context, STATE, {
|
|
198
|
-
value: { scope, facts, resources:
|
|
198
|
+
value: { scope, facts, resources: undefined },
|
|
199
199
|
});
|
|
200
|
-
for (const
|
|
200
|
+
for (const key of Object.keys(facts)) {
|
|
201
|
+
const value = facts[key];
|
|
201
202
|
if (value !== undefined) {
|
|
202
203
|
Object.defineProperty(context, key, { value, enumerable: true });
|
|
203
204
|
}
|
|
@@ -232,6 +233,7 @@ export function createContextExecutor(dependencies) {
|
|
|
232
233
|
Object.defineProperty(prototype, resource, {
|
|
233
234
|
get() {
|
|
234
235
|
const state = stateOf(this);
|
|
236
|
+
state.resources ??= new Map();
|
|
235
237
|
let tree = state.resources.get(resource);
|
|
236
238
|
if (tree === undefined) {
|
|
237
239
|
tree = resourceTree(state.scope, resource, readOnly);
|
|
@@ -18,4 +18,12 @@ export type AvExecutionOptions = {
|
|
|
18
18
|
* object), the Framework starts the request with `{}`.
|
|
19
19
|
*/
|
|
20
20
|
readonly state?: AvState & object;
|
|
21
|
+
/**
|
|
22
|
+
* The principal the call or run is checked as by the configuration's
|
|
23
|
+
* `policy`. An application call names one to opt in: left out, it is server
|
|
24
|
+
* code and the policy does not check it. A `framework.client.*` call is
|
|
25
|
+
* checked either way, and `as` replaces the principal `policy.principal`
|
|
26
|
+
* would derive from its state. `null` is the anonymous principal.
|
|
27
|
+
*/
|
|
28
|
+
readonly as?: unknown;
|
|
21
29
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emits a framework warning: through `process.emitWarning` (type
|
|
3
|
+
* `AventaraWarning`, `code` as given) where the runtime has it, else through
|
|
4
|
+
* `console.warn`. A warning changes no behaviour.
|
|
5
|
+
*/
|
|
6
|
+
export declare function emitFrameworkWarning(message: string, code: string): void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function emitFrameworkWarning(message, code) {
|
|
2
|
+
const emitWarning = globalThis.process?.emitWarning;
|
|
3
|
+
if (typeof emitWarning === "function") {
|
|
4
|
+
emitWarning(message, { type: "AventaraWarning", code });
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
globalThis.console?.warn(`AventaraWarning: ${message}`);
|
|
8
|
+
}
|
|
@@ -20,8 +20,9 @@ export async function runLevelLifecycle(levels, context, inner, settle, reuse, r
|
|
|
20
20
|
}
|
|
21
21
|
const result = await settle(await inner());
|
|
22
22
|
const after = [...levels].reverse().flatMap((level) => level.hooks.after);
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const settled = after.length > 0 ? frozenCopy(result, true) : undefined;
|
|
24
|
+
for (const [index, hook] of after.entries()) {
|
|
25
|
+
await hook(index === after.length - 1 ? settled : frozenCopy(settled), context());
|
|
25
26
|
}
|
|
26
27
|
return result;
|
|
27
28
|
};
|
|
@@ -66,7 +66,7 @@ export const OPERATION_CODE_GUIDANCE = {
|
|
|
66
66
|
},
|
|
67
67
|
A2003: {
|
|
68
68
|
message: "Not found.",
|
|
69
|
-
cause: "A unique operation (find.unique, update.unique, delete.unique) matched no row, or a $connect names a record that does not exist. The operation fails, so no after hook runs.",
|
|
69
|
+
cause: "A unique operation (find.unique, update.unique, delete.unique) matched no row, or a $connect names a record that does not exist. The operation fails, so no after hook runs. Under a policy, a row outside the caller's read scope is a miss too, and so is a single-row update or delete of a row the caller cannot read, so the answer does not reveal that the row exists. A row the caller can read but not change answers A4002.",
|
|
70
70
|
fix: "Catch AvNotFoundError, or use a first-style operation, which answers A1001 with null. Check the $connect identifier.",
|
|
71
71
|
},
|
|
72
72
|
A2004: {
|
|
@@ -161,7 +161,7 @@ export const OPERATION_CODE_GUIDANCE = {
|
|
|
161
161
|
},
|
|
162
162
|
A4002: {
|
|
163
163
|
message: "Policy denied.",
|
|
164
|
-
cause: '
|
|
165
|
-
fix: "Read cause.message
|
|
164
|
+
cause: "The configuration's policy refused the request: its engine does not allow the operation, a field the request names, writes or returns, a row it reaches, a row it creates, or the row a change leaves (moved out of what the caller reads, or failing its grant's after condition, which rolls the write back); on a database without interactive transactions, a write the policy can only check after it runs; or a stage threw new AvError(\"A4002\", ...).",
|
|
165
|
+
fix: "Read cause.message, which names what the policy refused. Call as a principal the policy allows, leave the refused field or relation out (a governed one is named only where every row grants it), or change only rows the caller may change and still read afterwards.",
|
|
166
166
|
},
|
|
167
167
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AvOperationIdentity } from "../operations/operation-identity.js";
|
|
2
|
+
import type { PolicySession } from "../policy/policy-session.js";
|
|
2
3
|
import type { AvTransportContext } from "./pipeline.js";
|
|
3
4
|
import type { AvState } from "./request-state.js";
|
|
4
5
|
/**
|
|
@@ -63,5 +64,11 @@ export type OperationExecutionRequest = WithoutArguments<FrameworkOperationReque
|
|
|
63
64
|
*/
|
|
64
65
|
export type StatefulOperationRequest = OperationExecutionRequest & {
|
|
65
66
|
readonly state: AvState;
|
|
67
|
+
/**
|
|
68
|
+
* The request's policy session, when the policy checks it: a client's or an
|
|
69
|
+
* agent's request, or an application call made `as` a principal. Absent, nothing
|
|
70
|
+
* is checked: an application call, or a call a stage made through `ctx`.
|
|
71
|
+
*/
|
|
72
|
+
readonly policy?: PolicySession;
|
|
66
73
|
};
|
|
67
74
|
export {};
|
|
@@ -7,6 +7,7 @@ import type { ResolvedFrameworkConfig } from "../config/config.resolver.js";
|
|
|
7
7
|
import type { AvRequestOrigin, RemoteRequestOrigin } from "../config/config-layer.js";
|
|
8
8
|
import type { AvContract } from "../contracts/contract.js";
|
|
9
9
|
import type { OperationValidationOptions, SemanticOperationArguments } from "../operations/operation-validator.js";
|
|
10
|
+
import { type PolicyCaller } from "../policy/policy-request.js";
|
|
10
11
|
import type { FrameworkTransactionRequest } from "../transactions/transaction-request.js";
|
|
11
12
|
import { type ArgumentsSource } from "./caller-data.door.js";
|
|
12
13
|
import { type ContextExecutor, type ExecutorScope } from "./context-executor.runtime.js";
|
|
@@ -65,6 +66,11 @@ export declare function withRequestState<Request extends {
|
|
|
65
66
|
}>(request: Request): Request & {
|
|
66
67
|
readonly state: AvState;
|
|
67
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* `request` with its policy session when the policy checks it, or the failure
|
|
71
|
+
* deriving its principal answered.
|
|
72
|
+
*/
|
|
73
|
+
export declare function withRequestPolicy<M extends AvAdapterModel>(dependencies: Pick<OperationRuntimeDependencies<M>, "adapter" | "config" | "contracts">, request: StatefulOperationRequest, caller: PolicyCaller | undefined, report: DiagnosticsReporter): StatefulOperationRequest | AvOperationFailureResponse;
|
|
68
74
|
/**
|
|
69
75
|
* Builds what every operation's pipeline shares, the ctx executor included: its
|
|
70
76
|
* raw calls run the application's operation path with every developer stage
|
|
@@ -79,7 +85,9 @@ export declare function createPipelineDependencies<M extends AvAdapterModel>(dep
|
|
|
79
85
|
* request's arguments are the caller's own objects, copied at the door, or data
|
|
80
86
|
* no caller holds (see {@link ArgumentsSource}).
|
|
81
87
|
*/
|
|
82
|
-
export type FrameworkOperationExecutor = (request: FrameworkOperationRequest, report: DiagnosticsReporter, source: ArgumentsSource
|
|
88
|
+
export type FrameworkOperationExecutor = (request: FrameworkOperationRequest, report: DiagnosticsReporter, source: ArgumentsSource,
|
|
89
|
+
/** The principal an application call runs as (`{ as }`), when it names one. */
|
|
90
|
+
caller?: PolicyCaller) => Promise<AvOperationResponse>;
|
|
83
91
|
/** Selects the Contract of the request's origin. */
|
|
84
92
|
export declare function activeContract(contracts: AvContracts, request: FrameworkOperationRequest | FrameworkTransactionRequest): AvContract;
|
|
85
93
|
/** Creates the transport-independent operation executor owned by core. */
|