@aventara/core 1.0.0-rc.2 → 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 +175 -0
- package/ERRORS.md +9 -9
- 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 +8 -1
- 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/database-refusal.d.ts +1 -1
- package/dist/operations/database-refusal.js +1 -1
- 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 +66 -33
- package/dist/operations/validation-code.guidance.js +2 -2
- 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/field-validator.runtime.d.ts +9 -0
- package/dist/runtime/field-validator.runtime.js +36 -2
- 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 +4 -4
- package/dist/runtime/operation-request.d.ts +7 -0
- package/dist/runtime/operation-runtime.d.ts +9 -1
- package/dist/runtime/operation-runtime.js +119 -11
- 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
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AvContract } from "../contracts/contract.js";
|
|
2
2
|
import type { AvValidationIssue } from "../operations/validation-code.js";
|
|
3
|
+
import { AvComputedValueError } from "./computed-value.error.js";
|
|
3
4
|
import { type Path } from "./field-stage.runtime.js";
|
|
4
5
|
import type { OperationExecutionRequest } from "./operation-request.js";
|
|
5
6
|
import type { ReferentialActionTable } from "./referential-action.js";
|
|
@@ -17,3 +18,11 @@ export type DeferredField = (resource: string, stage: WriteStage, field: string)
|
|
|
17
18
|
* request's: `argumentsPath` and then the value's place in the arguments.
|
|
18
19
|
*/
|
|
19
20
|
export declare function fieldValidatorIssues(contract: AvContract, references: ReferentialActionTable, request: OperationExecutionRequest, args: unknown, argumentsPath: Path, select: (path: Path) => boolean, deferred?: DeferredField): readonly AvValidationIssue[];
|
|
21
|
+
/**
|
|
22
|
+
* The server's defect when a value written to a field the active layer hides
|
|
23
|
+
* breaks its validators or its Json shape, or `undefined`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function hiddenFieldRefusal(contracts: {
|
|
26
|
+
readonly core: AvContract;
|
|
27
|
+
readonly active: AvContract;
|
|
28
|
+
}, references: ReferentialActionTable, request: OperationExecutionRequest, args: unknown, argumentsPath: Path): AvComputedValueError | undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { validatedFieldsOf, } from "../compiler/field-validator.compiler.js";
|
|
1
|
+
import { hasFieldValidators, validatedFieldsOf, } from "../compiler/field-validator.compiler.js";
|
|
2
|
+
import { AvComputedValueError } from "./computed-value.error.js";
|
|
2
3
|
import { entryCheckIssues, fieldCheckIssues } from "./field-check.evaluator.js";
|
|
3
4
|
import { isPlainRecord, valueAt, writeRecordPathsOf, } from "./field-stage.runtime.js";
|
|
4
5
|
import { nestedWriteTargetsOf } from "./nested-write.walker.js";
|
|
@@ -14,7 +15,10 @@ function rulesOf(contract, compiled, resource, name) {
|
|
|
14
15
|
const field = fields !== undefined && Object.hasOwn(fields, name)
|
|
15
16
|
? fields[name]
|
|
16
17
|
: undefined;
|
|
17
|
-
if (field
|
|
18
|
+
if (field === undefined) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
if (field.kind === "scalar" && field.json !== undefined) {
|
|
18
22
|
return { kind: "shape", field };
|
|
19
23
|
}
|
|
20
24
|
const checks = Object.hasOwn(compiled, name) ? compiled[name] : undefined;
|
|
@@ -90,3 +94,33 @@ export function fieldValidatorIssues(contract, references, request, args, argume
|
|
|
90
94
|
}
|
|
91
95
|
return issues;
|
|
92
96
|
}
|
|
97
|
+
function holdsField(contract, resource, field) {
|
|
98
|
+
const fields = Object.hasOwn(contract.resources, resource)
|
|
99
|
+
? contract.resources[resource]?.fields
|
|
100
|
+
: undefined;
|
|
101
|
+
return fields !== undefined && Object.hasOwn(fields, field);
|
|
102
|
+
}
|
|
103
|
+
export function hiddenFieldRefusal(contracts, references, request, args, argumentsPath) {
|
|
104
|
+
const { core, active } = contracts;
|
|
105
|
+
if (core === active || !hasFieldValidators(core)) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const judged = [];
|
|
109
|
+
const issues = fieldValidatorIssues(core, references, request, args, argumentsPath, () => true, (resource, stage, field) => {
|
|
110
|
+
if (holdsField(active, resource, field)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
judged.push({ resource, stage, field });
|
|
114
|
+
return false;
|
|
115
|
+
});
|
|
116
|
+
const [first] = issues;
|
|
117
|
+
if (first === undefined) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const path = first.path ?? [];
|
|
121
|
+
const at = judged.find(({ field }) => path.includes(field)) ?? judged[0];
|
|
122
|
+
if (at === undefined) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return new AvComputedValueError(at.resource, at.field, at.stage, issues, `compute.${at.stage} on ${at.resource}.${at.field}, a field the ${request.origin} layer hides, returned a value its validators refuse (${first.code}: ${first.message})`);
|
|
126
|
+
}
|
|
@@ -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
|
};
|