@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,394 @@
|
|
|
1
|
+
import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
|
|
2
|
+
import { uniqueMissFailure } from "../runtime/outcome-mapper.js";
|
|
3
|
+
import { frozenCopy } from "../runtime/readonly-value.js";
|
|
4
|
+
import { policyRefusal } from "./policy-admission.js";
|
|
5
|
+
import { afterCheckFields, afterUpdate, checkChanged, } from "./policy-after-update.runtime.js";
|
|
6
|
+
import { allOf, everyOf, noneOf, someOf, whereOf, } from "./policy-condition.js";
|
|
7
|
+
import { planNestedWrites, writesNested, } from "./policy-nested-write.runtime.js";
|
|
8
|
+
import { enforceReadPolicy, identifiedRows, identifierOf, returnedRowChecks, } from "./policy-read.runtime.js";
|
|
9
|
+
const UNVERIFIABLE = "cannot be verified on this database: it has no interactive transaction to roll the write back in";
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function refused(message) {
|
|
14
|
+
return { kind: "refused", response: policyRefusal(message) };
|
|
15
|
+
}
|
|
16
|
+
function rowOf(core, resource, record) {
|
|
17
|
+
const owner = lookupResource(core, resource);
|
|
18
|
+
const row = {};
|
|
19
|
+
if (isRecord(record)) {
|
|
20
|
+
for (const [field, value] of Object.entries(record)) {
|
|
21
|
+
if (value !== undefined &&
|
|
22
|
+
lookupField(owner, field)?.kind !== "relation") {
|
|
23
|
+
row[field] = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return frozenCopy(row);
|
|
28
|
+
}
|
|
29
|
+
function rowsOf(data) {
|
|
30
|
+
if (Array.isArray(data)) {
|
|
31
|
+
return data.filter(isRecord);
|
|
32
|
+
}
|
|
33
|
+
return isRecord(data) ? [data] : [];
|
|
34
|
+
}
|
|
35
|
+
function offers(core, resource, family, variant) {
|
|
36
|
+
const operations = lookupResource(core, resource)?.operations;
|
|
37
|
+
return operations?.[family]?.[variant] !== undefined;
|
|
38
|
+
}
|
|
39
|
+
function missOf(session, request, selector) {
|
|
40
|
+
return async () => {
|
|
41
|
+
const read = await session.scope(request.resource, "read");
|
|
42
|
+
const reach = allOf(isRecord(selector) ? selector : true, read);
|
|
43
|
+
if (reach !== false &&
|
|
44
|
+
(await session.renewed().exists(request.resource, whereOf(reach)))) {
|
|
45
|
+
return policyRefusal(`The policy does not allow ${request.family} on this row of Resource "${request.resource}".`);
|
|
46
|
+
}
|
|
47
|
+
return (uniqueMissFailure(request, null) ??
|
|
48
|
+
policyRefusal(`The policy does not allow ${request.family} on this row of Resource "${request.resource}".`));
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function checkCreated(session, core, model, resource, grants, created) {
|
|
52
|
+
if (created.length === 0) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const these = identifiedRows(core, identifierOf(model, resource), resource, created);
|
|
56
|
+
const missed = allOf(these, someOf([...grants.values()].map(noneOf)));
|
|
57
|
+
if (missed !== false &&
|
|
58
|
+
(await session.renewed().exists(resource, whereOf(missed)))) {
|
|
59
|
+
return policyRefusal(`The policy does not allow writing ${fieldNames(grants)} of Resource "${resource}" on the row this request creates.`);
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
function fieldNames(grants) {
|
|
64
|
+
return [...grants.keys()].map((field) => `"${field}"`).join(", ");
|
|
65
|
+
}
|
|
66
|
+
function uncheckableCreate(model, request, grants, inTransaction) {
|
|
67
|
+
if (grants.size === 0) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const what = `The policy allows writing ${fieldNames(grants)} of Resource "${request.resource}" on some rows only, and checks the row this request creates against them after the insert`;
|
|
71
|
+
if (!inTransaction) {
|
|
72
|
+
return refused(`${what}, which ${UNVERIFIABLE}.`);
|
|
73
|
+
}
|
|
74
|
+
if (request.variant === "count" ||
|
|
75
|
+
identifierOf(model, request.resource).length === 0) {
|
|
76
|
+
return refused(`${what}, and this create does not return the rows it creates to check by.`);
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
function unverifiable(read, request, inTransaction) {
|
|
81
|
+
if (inTransaction || read.kind !== "run") {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const rootChecked = request.family !== "delete" && read.checks.root.length > 0;
|
|
85
|
+
return rootChecked || read.checks.nested
|
|
86
|
+
? refused(`The policy checks the rows this ${request.family} on Resource "${request.resource}" returns after it runs, which ${UNVERIFIABLE}. Select fewer fields or relations, or read them in a request of their own.`)
|
|
87
|
+
: undefined;
|
|
88
|
+
}
|
|
89
|
+
async function nestedWritesOf(session, core, model, request, record, parent, inTransaction) {
|
|
90
|
+
if (!isRecord(record) || !writesNested(core, request.resource, record)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
if (!inTransaction) {
|
|
94
|
+
return {
|
|
95
|
+
kind: "refused",
|
|
96
|
+
response: policyRefusal(`The policy checks the nested writes of this ${request.family} on Resource "${request.resource}" inside its transaction, which ${UNVERIFIABLE}. Write the related rows in their own operations.`),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return planNestedWrites(session, core, model, request.resource, record, parent);
|
|
100
|
+
}
|
|
101
|
+
function withNestedWrites(run, key, nested) {
|
|
102
|
+
const runArguments = isRecord(run.arguments) ? run.arguments : {};
|
|
103
|
+
const narrowed = runArguments[key] === nested.record
|
|
104
|
+
? run
|
|
105
|
+
: { ...run, arguments: { ...runArguments, [key]: nested.record } };
|
|
106
|
+
return withChangedRows(narrowed, (data) => nested.after(rowsOf(data)));
|
|
107
|
+
}
|
|
108
|
+
function countOf(run) {
|
|
109
|
+
return {
|
|
110
|
+
...run,
|
|
111
|
+
settle: async (data) => {
|
|
112
|
+
const settled = await run.settle(data);
|
|
113
|
+
return settled.ok ? { ok: true, data: rowsOf(data).length } : settled;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function withoutCarried(run, asked, carried) {
|
|
118
|
+
if (carried === undefined || !Array.isArray(asked.select)) {
|
|
119
|
+
return run;
|
|
120
|
+
}
|
|
121
|
+
const kept = asked.select;
|
|
122
|
+
const dropped = carried.filter((field) => !kept.includes(field));
|
|
123
|
+
if (dropped.length === 0) {
|
|
124
|
+
return run;
|
|
125
|
+
}
|
|
126
|
+
const trimmed = (row) => {
|
|
127
|
+
if (!isRecord(row)) {
|
|
128
|
+
return row;
|
|
129
|
+
}
|
|
130
|
+
const answer = {};
|
|
131
|
+
for (const key of Object.keys(row)) {
|
|
132
|
+
if (!dropped.includes(key)) {
|
|
133
|
+
answer[key] = row[key];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return answer;
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
...run,
|
|
140
|
+
settle: async (data) => {
|
|
141
|
+
const settled = await run.settle(data);
|
|
142
|
+
if (!settled.ok) {
|
|
143
|
+
return settled;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
data: Array.isArray(settled.data)
|
|
148
|
+
? settled.data.map(trimmed)
|
|
149
|
+
: trimmed(settled.data),
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function withChangedRows(read, check) {
|
|
155
|
+
return {
|
|
156
|
+
...read,
|
|
157
|
+
settle: async (data) => {
|
|
158
|
+
const settled = await read.settle(data);
|
|
159
|
+
if (!settled.ok) {
|
|
160
|
+
return settled;
|
|
161
|
+
}
|
|
162
|
+
const response = await check(data);
|
|
163
|
+
return response === undefined ? settled : { ok: false, response };
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export async function writeNeedsTransaction(session, core, request, planned, adapterArguments) {
|
|
168
|
+
switch (request.family) {
|
|
169
|
+
case "find":
|
|
170
|
+
return false;
|
|
171
|
+
case "update":
|
|
172
|
+
case "upsert":
|
|
173
|
+
return true;
|
|
174
|
+
case "create": {
|
|
175
|
+
const args = isRecord(adapterArguments) ? adapterArguments : {};
|
|
176
|
+
if (session.createGrants().size > 0 ||
|
|
177
|
+
(isRecord(args.data) && writesNested(core, request.resource, args.data))) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case "delete":
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
const plan = planned();
|
|
186
|
+
const checks = await returnedRowChecks(session, core, plan);
|
|
187
|
+
return [...checks.keys()].some((level) => level !== plan.projection || request.family !== "delete");
|
|
188
|
+
}
|
|
189
|
+
export async function enforceWritePolicy(session, core, model, request, plan, adapterArguments, inTransaction) {
|
|
190
|
+
const args = isRecord(adapterArguments) ? adapterArguments : {};
|
|
191
|
+
switch (request.family) {
|
|
192
|
+
case "create":
|
|
193
|
+
return enforceCreate(session, core, model, request, plan, adapterArguments, args, inTransaction);
|
|
194
|
+
case "update":
|
|
195
|
+
case "delete":
|
|
196
|
+
return enforceScopedWrite(session, core, model, request, plan, adapterArguments, args, inTransaction);
|
|
197
|
+
case "upsert":
|
|
198
|
+
return enforceUpsert(session, core, model, request, plan, adapterArguments, args, inTransaction);
|
|
199
|
+
case "find":
|
|
200
|
+
return enforceReadPolicy(session, core, model, request, plan, adapterArguments);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function enforceCreate(session, core, model, request, plan, adapterArguments, args, inTransaction) {
|
|
204
|
+
const resource = request.resource;
|
|
205
|
+
const records = Array.isArray(args.data) ? args.data : [args.data];
|
|
206
|
+
const nested = await nestedWritesOf(session, core, model, request, args.data, { kind: "new" }, inTransaction);
|
|
207
|
+
if (nested?.kind === "refused") {
|
|
208
|
+
return nested;
|
|
209
|
+
}
|
|
210
|
+
if (nested === undefined) {
|
|
211
|
+
const answers = await Promise.all(records
|
|
212
|
+
.filter(isRecord)
|
|
213
|
+
.map((record) => session.can(resource, "create", rowOf(core, resource, record))));
|
|
214
|
+
if (answers.includes(false)) {
|
|
215
|
+
return refused(`The policy does not allow creating this row of Resource "${resource}".`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const grants = session.createGrants();
|
|
219
|
+
const uncheckable = uncheckableCreate(model, request, grants, inTransaction);
|
|
220
|
+
if (uncheckable !== undefined) {
|
|
221
|
+
return uncheckable;
|
|
222
|
+
}
|
|
223
|
+
const read = await enforceReadPolicy(session, core, model, request, plan, adapterArguments, { scope: undefined, identified: grants.size > 0 || nested !== undefined });
|
|
224
|
+
const blocked = unverifiable(read, request, inTransaction);
|
|
225
|
+
if (blocked !== undefined || read.kind !== "run") {
|
|
226
|
+
return blocked ?? read;
|
|
227
|
+
}
|
|
228
|
+
let run = read;
|
|
229
|
+
if (grants.size > 0) {
|
|
230
|
+
run = withChangedRows(run, (data) => checkCreated(session, core, model, resource, grants, rowsOf(data)));
|
|
231
|
+
}
|
|
232
|
+
if (nested !== undefined) {
|
|
233
|
+
run = withNestedWrites(run, "data", nested);
|
|
234
|
+
}
|
|
235
|
+
return run;
|
|
236
|
+
}
|
|
237
|
+
async function enforceScopedWrite(session, core, model, request, plan, adapterArguments, args, inTransaction) {
|
|
238
|
+
const resource = request.resource;
|
|
239
|
+
const action = request.family === "delete" ? "delete" : "update";
|
|
240
|
+
const scope = allOf(await session.scope(resource, action), await session.scope(resource, "read"));
|
|
241
|
+
const changes = action === "update"
|
|
242
|
+
? await afterUpdate(session, core, model, resource, args.data, inTransaction)
|
|
243
|
+
: undefined;
|
|
244
|
+
if (changes !== undefined && !inTransaction) {
|
|
245
|
+
return refused(`The policy checks this update on Resource "${resource}" after it runs (${changes.after ? "its grant's after condition" : "whether it moves a row out of what the caller may read"}), which ${UNVERIFIABLE}.`);
|
|
246
|
+
}
|
|
247
|
+
if (changes !== undefined && identifierOf(model, resource).length === 0) {
|
|
248
|
+
return refused(`The policy checks this update on Resource "${resource}" after it runs, and the Resource has no identifier to find the changed rows by.`);
|
|
249
|
+
}
|
|
250
|
+
const nested = action === "update"
|
|
251
|
+
? await nestedWritesOf(session, core, model, request, args.data, {
|
|
252
|
+
kind: "existing",
|
|
253
|
+
reach: allOf(isRecord(args.where) ? args.where : true, scope),
|
|
254
|
+
}, inTransaction)
|
|
255
|
+
: undefined;
|
|
256
|
+
if (nested?.kind === "refused") {
|
|
257
|
+
return nested;
|
|
258
|
+
}
|
|
259
|
+
const miss = request.variant === "unique"
|
|
260
|
+
? missOf(session, request, args.where)
|
|
261
|
+
: undefined;
|
|
262
|
+
const carried = changes?.after === true
|
|
263
|
+
? afterCheckFields(core, model, resource)
|
|
264
|
+
: undefined;
|
|
265
|
+
const read = await enforceReadPolicy(session, core, model, request, plan, adapterArguments, {
|
|
266
|
+
scope,
|
|
267
|
+
identified: changes !== undefined || nested !== undefined,
|
|
268
|
+
...(carried === undefined ? {} : { carried }),
|
|
269
|
+
});
|
|
270
|
+
const blocked = unverifiable(read, request, inTransaction);
|
|
271
|
+
if (blocked !== undefined) {
|
|
272
|
+
return blocked;
|
|
273
|
+
}
|
|
274
|
+
if (read.kind !== "run") {
|
|
275
|
+
return miss === undefined ? read : { ...read, miss };
|
|
276
|
+
}
|
|
277
|
+
let run = read;
|
|
278
|
+
if (action === "delete" && run.checks.root.length > 0) {
|
|
279
|
+
const where = isRecord(run.arguments) ? run.arguments.where : undefined;
|
|
280
|
+
const reach = allOf(isRecord(where) ? where : true, someOf(run.checks.root));
|
|
281
|
+
if (reach !== false && (await session.exists(resource, whereOf(reach)))) {
|
|
282
|
+
return refused("The policy does not allow reading every row or field this request returns.");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (changes !== undefined) {
|
|
286
|
+
let changed;
|
|
287
|
+
const counted = request.variant === "count";
|
|
288
|
+
if (counted && offers(core, resource, "update", "many")) {
|
|
289
|
+
const runArgs = isRecord(run.arguments) ? run.arguments : {};
|
|
290
|
+
const asMany = {
|
|
291
|
+
...run,
|
|
292
|
+
identity: { family: "update", variant: "many" },
|
|
293
|
+
arguments: {
|
|
294
|
+
...runArgs,
|
|
295
|
+
select: changes.after
|
|
296
|
+
? [...afterCheckFields(core, model, resource)]
|
|
297
|
+
: [...identifierOf(model, resource)],
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
run = withChangedRows(asMany, (data) => checkChanged(session, core, model, resource, changes, rowsOf(data), changes.after));
|
|
301
|
+
if (nested !== undefined) {
|
|
302
|
+
run = withNestedWrites(run, "data", nested);
|
|
303
|
+
}
|
|
304
|
+
return countOf(run);
|
|
305
|
+
}
|
|
306
|
+
if (counted) {
|
|
307
|
+
const runArgs = isRecord(run.arguments) ? run.arguments : {};
|
|
308
|
+
const identifier = identifierOf(model, resource);
|
|
309
|
+
changed = await session.rows(resource, isRecord(runArgs.where) ? runArgs.where : {}, identifier);
|
|
310
|
+
if (changed.length === 0) {
|
|
311
|
+
return { kind: "empty", data: 0 };
|
|
312
|
+
}
|
|
313
|
+
const these = identifiedRows(core, identifier, resource, changed);
|
|
314
|
+
run = {
|
|
315
|
+
...run,
|
|
316
|
+
arguments: {
|
|
317
|
+
...runArgs,
|
|
318
|
+
where: everyOf([
|
|
319
|
+
these,
|
|
320
|
+
isRecord(runArgs.where) ? runArgs.where : true,
|
|
321
|
+
]),
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const known = changed;
|
|
326
|
+
run = withChangedRows(run, (data) => checkChanged(session, core, model, resource, changes, known ?? rowsOf(data), known === undefined && changes.after));
|
|
327
|
+
}
|
|
328
|
+
if (nested !== undefined) {
|
|
329
|
+
run = withNestedWrites(run, "data", nested);
|
|
330
|
+
}
|
|
331
|
+
run = withoutCarried(run, args, carried);
|
|
332
|
+
return miss === undefined ? run : { ...run, miss };
|
|
333
|
+
}
|
|
334
|
+
async function enforceUpsert(session, core, model, request, plan, adapterArguments, args, inTransaction) {
|
|
335
|
+
const resource = request.resource;
|
|
336
|
+
const selector = isRecord(args.where) ? args.where : true;
|
|
337
|
+
const scope = allOf(await session.scope(resource, "update"), await session.scope(resource, "read"));
|
|
338
|
+
const within = allOf(selector, scope);
|
|
339
|
+
const updates = within !== false && (await session.exists(resource, whereOf(within)));
|
|
340
|
+
let changes;
|
|
341
|
+
let grants = new Map();
|
|
342
|
+
const exists = !updates && (await session.exists(resource, whereOf(selector)));
|
|
343
|
+
const nested = exists
|
|
344
|
+
? undefined
|
|
345
|
+
: await nestedWritesOf(session, core, model, request, updates ? args.update : args.create, updates ? { kind: "existing", reach: within } : { kind: "new" }, inTransaction);
|
|
346
|
+
if (nested?.kind === "refused") {
|
|
347
|
+
return nested;
|
|
348
|
+
}
|
|
349
|
+
if (updates) {
|
|
350
|
+
changes = await afterUpdate(session, core, model, resource, args.update, inTransaction);
|
|
351
|
+
if (changes !== undefined && !inTransaction) {
|
|
352
|
+
return refused(`The policy checks this upsert's update on Resource "${resource}" after it runs (${changes.after ? "its grant's after condition" : "whether it moves a row out of what the caller may read"}), which ${UNVERIFIABLE}.`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else if (exists) {
|
|
356
|
+
return refused(`The policy does not allow update on this row of Resource "${resource}".`);
|
|
357
|
+
}
|
|
358
|
+
else if (nested === undefined &&
|
|
359
|
+
!(await session.can(resource, "create", rowOf(core, resource, args.create)))) {
|
|
360
|
+
return refused(`The policy does not allow creating this row of Resource "${resource}".`);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
grants = session.createGrants();
|
|
364
|
+
const uncheckable = uncheckableCreate(model, request, grants, inTransaction);
|
|
365
|
+
if (uncheckable !== undefined) {
|
|
366
|
+
return uncheckable;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const carried = changes?.after === true
|
|
370
|
+
? afterCheckFields(core, model, resource)
|
|
371
|
+
: undefined;
|
|
372
|
+
const read = await enforceReadPolicy(session, core, model, request, plan, adapterArguments, {
|
|
373
|
+
scope: undefined,
|
|
374
|
+
identified: changes !== undefined || grants.size > 0 || nested !== undefined,
|
|
375
|
+
...(carried === undefined ? {} : { carried }),
|
|
376
|
+
});
|
|
377
|
+
const blocked = unverifiable(read, request, inTransaction);
|
|
378
|
+
if (blocked !== undefined || read.kind !== "run") {
|
|
379
|
+
return blocked ?? read;
|
|
380
|
+
}
|
|
381
|
+
let run = read;
|
|
382
|
+
const checks = changes;
|
|
383
|
+
if (checks !== undefined) {
|
|
384
|
+
run = withChangedRows(run, (data) => checkChanged(session, core, model, resource, checks, rowsOf(data), checks.after));
|
|
385
|
+
}
|
|
386
|
+
const created = grants;
|
|
387
|
+
if (created.size > 0) {
|
|
388
|
+
run = withChangedRows(run, (data) => checkCreated(session, core, model, resource, created, rowsOf(data)));
|
|
389
|
+
}
|
|
390
|
+
if (nested !== undefined) {
|
|
391
|
+
run = withNestedWrites(run, updates ? "update" : "create", nested);
|
|
392
|
+
}
|
|
393
|
+
return withoutCarried(run, args, carried);
|
|
394
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CORE_VERSION = "1.0.0-rc.4";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** The file format this core writes and reads. */
|
|
2
|
+
export declare const PREBUILT_FORMAT = 1;
|
|
3
|
+
/**
|
|
4
|
+
* Contracts compiled ahead of time: the value of a generated
|
|
5
|
+
* `contracts.generated.ts`, passed to `createFramework` as `prebuilt`. The
|
|
6
|
+
* framework uses them only when they were compiled by the same core, for the
|
|
7
|
+
* same protocol, from the same configuration and model; otherwise it compiles
|
|
8
|
+
* live and warns (`AVENTARA_PREBUILT_STALE`). Generated, never written by hand.
|
|
9
|
+
*/
|
|
10
|
+
export type AvPrebuiltContracts = {
|
|
11
|
+
/** The file format's version. */
|
|
12
|
+
readonly format: number;
|
|
13
|
+
/** The version of `@aventara/core` that compiled the Contracts. */
|
|
14
|
+
readonly core: string;
|
|
15
|
+
/** The protocol version the Contracts were compiled for. */
|
|
16
|
+
readonly protocol: number;
|
|
17
|
+
/**
|
|
18
|
+
* `sha256:` over every input: the configuration (without `adapter` and
|
|
19
|
+
* `prebuilt`), the adapter's model, the core version, the protocol version
|
|
20
|
+
* and the file format.
|
|
21
|
+
*/
|
|
22
|
+
readonly fingerprint: string;
|
|
23
|
+
/** The same inputs, fingerprinted in parts, so a stale file can say what changed. */
|
|
24
|
+
readonly fingerprints: {
|
|
25
|
+
/** The inputs that belong to no single Resource. */
|
|
26
|
+
readonly shared: string;
|
|
27
|
+
/** Each Resource's inputs: its model, and its entries in every layer. */
|
|
28
|
+
readonly resources: Readonly<Record<string, string>>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Each Contract's canonical JSON text. The Client and Agent texts carry
|
|
32
|
+
* their hash and are the bytes `/_contract` serves.
|
|
33
|
+
*/
|
|
34
|
+
readonly contracts: {
|
|
35
|
+
readonly core: string;
|
|
36
|
+
readonly application: string;
|
|
37
|
+
readonly client: string;
|
|
38
|
+
readonly agent: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
2
|
+
import type { AvContracts } from "../compiler/contract.compiler.js";
|
|
3
|
+
import { type AvPrebuiltContracts } from "./prebuilt-contracts.js";
|
|
4
|
+
/**
|
|
5
|
+
* The prebuilt file's value for Contracts `compileContracts` compiled from
|
|
6
|
+
* `model` and `config` (the configuration as authored; its `adapter` and
|
|
7
|
+
* `prebuilt`, if any, are no inputs): each Contract's canonical text, and the
|
|
8
|
+
* fingerprints of the inputs.
|
|
9
|
+
*/
|
|
10
|
+
export declare function prebuiltContractsOf(model: AvAdapterModel, config: object, contracts: AvContracts): Promise<AvPrebuiltContracts>;
|
|
11
|
+
/**
|
|
12
|
+
* The generated module that exports `prebuilt` as `aventaraContracts`. Each
|
|
13
|
+
* Contract stays a string, so loading it is one `JSON.parse`; the type is
|
|
14
|
+
* imported for checking only.
|
|
15
|
+
*/
|
|
16
|
+
export declare function emitPrebuiltContracts(prebuilt: AvPrebuiltContracts): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { canonicalContractTextOf } from "../contracts/contract-canonicalizer.js";
|
|
2
|
+
import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
|
|
3
|
+
import { CORE_VERSION } from "./core-version.generated.js";
|
|
4
|
+
import { PREBUILT_FORMAT, } from "./prebuilt-contracts.js";
|
|
5
|
+
import { prebuiltInputsFingerprint, prebuiltPartFingerprints, } from "./prebuilt-inputs.fingerprint.js";
|
|
6
|
+
export async function prebuiltContractsOf(model, config, contracts) {
|
|
7
|
+
const [fingerprint, fingerprints] = await Promise.all([
|
|
8
|
+
prebuiltInputsFingerprint(model, config),
|
|
9
|
+
prebuiltPartFingerprints(model, config),
|
|
10
|
+
]);
|
|
11
|
+
return {
|
|
12
|
+
format: PREBUILT_FORMAT,
|
|
13
|
+
core: CORE_VERSION,
|
|
14
|
+
protocol: AVENTARA_PROTOCOL_VERSION,
|
|
15
|
+
fingerprint,
|
|
16
|
+
fingerprints,
|
|
17
|
+
contracts: {
|
|
18
|
+
core: canonicalContractTextOf(contracts.core),
|
|
19
|
+
application: canonicalContractTextOf(contracts.application),
|
|
20
|
+
client: canonicalContractTextOf(contracts.client),
|
|
21
|
+
agent: canonicalContractTextOf(contracts.agent),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const literal = (value) => JSON.stringify(value);
|
|
26
|
+
const key = (name) => name === "__proto__" ? `[${literal(name)}]` : literal(name);
|
|
27
|
+
export function emitPrebuiltContracts(prebuilt) {
|
|
28
|
+
const resources = Object.entries(prebuilt.fingerprints.resources).map(([name, fingerprint]) => `\t\t\t${key(name)}: ${literal(fingerprint)},\n`);
|
|
29
|
+
return `// Generated by @aventara/core from the configuration and the adapter's model. Do not edit: regenerate it.
|
|
30
|
+
import type { AvPrebuiltContracts } from "@aventara/core";
|
|
31
|
+
|
|
32
|
+
export const aventaraContracts: AvPrebuiltContracts = {
|
|
33
|
+
\tformat: ${prebuilt.format},
|
|
34
|
+
\tcore: ${literal(prebuilt.core)},
|
|
35
|
+
\tprotocol: ${prebuilt.protocol},
|
|
36
|
+
\tfingerprint: ${literal(prebuilt.fingerprint)},
|
|
37
|
+
\tfingerprints: {
|
|
38
|
+
\t\tshared: ${literal(prebuilt.fingerprints.shared)},
|
|
39
|
+
\t\tresources: {
|
|
40
|
+
${resources.join("")}\t\t},
|
|
41
|
+
\t},
|
|
42
|
+
\tcontracts: {
|
|
43
|
+
\t\tcore: ${literal(prebuilt.contracts.core)},
|
|
44
|
+
\t\tapplication: ${literal(prebuilt.contracts.application)},
|
|
45
|
+
\t\tclient: ${literal(prebuilt.contracts.client)},
|
|
46
|
+
\t\tagent: ${literal(prebuilt.contracts.agent)},
|
|
47
|
+
\t},
|
|
48
|
+
};
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PREBUILT_FORMAT = 1;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
2
|
+
import { type AvContracts } from "../compiler/contract.compiler.js";
|
|
3
|
+
import type { ResolvedFrameworkConfig } from "../config/config.resolver.js";
|
|
4
|
+
/** The warning's code, as `process.emitWarning` reports it. */
|
|
5
|
+
export declare const PREBUILT_STALE_WARNING = "AVENTARA_PREBUILT_STALE";
|
|
6
|
+
/** The loaded Contracts, or why the file cannot be used. */
|
|
7
|
+
export type PrebuiltLoad = {
|
|
8
|
+
readonly ok: true;
|
|
9
|
+
readonly contracts: AvContracts;
|
|
10
|
+
} | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly reason: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Loads the Contracts of a prebuilt file when it was compiled by this core, for
|
|
16
|
+
* this protocol, from `config` (as authored) and `model`; binds them to the
|
|
17
|
+
* runtime pieces of the live `resolved` configuration.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadPrebuiltContracts<M extends AvAdapterModel>(prebuilt: unknown, model: M, config: object, resolved: ResolvedFrameworkConfig<M>): Promise<PrebuiltLoad>;
|
|
20
|
+
/** Warns, once for the framework being created, that its prebuilt file was not used, and why. */
|
|
21
|
+
export declare function warnStalePrebuilt(reason: string): void;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { bindPrebuiltContracts, } from "../compiler/contract.compiler.js";
|
|
2
|
+
import { recordCanonicalContractText } from "../contracts/contract-canonicalizer.js";
|
|
3
|
+
import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
|
|
4
|
+
import { emitFrameworkWarning } from "../runtime/framework-warning.emitter.js";
|
|
5
|
+
import { CORE_VERSION } from "./core-version.generated.js";
|
|
6
|
+
import { PREBUILT_FORMAT } from "./prebuilt-contracts.js";
|
|
7
|
+
import { prebuiltInputsFingerprint, prebuiltPartFingerprints, } from "./prebuilt-inputs.fingerprint.js";
|
|
8
|
+
export const PREBUILT_STALE_WARNING = "AVENTARA_PREBUILT_STALE";
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function listed(names) {
|
|
13
|
+
return names.length < 2
|
|
14
|
+
? names.join("")
|
|
15
|
+
: `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
16
|
+
}
|
|
17
|
+
async function changedInputs(stored, model, config) {
|
|
18
|
+
const live = await prebuiltPartFingerprints(model, config);
|
|
19
|
+
const storedResources = isRecord(stored) ? stored.resources : undefined;
|
|
20
|
+
const before = isRecord(storedResources) ? storedResources : {};
|
|
21
|
+
const changed = [];
|
|
22
|
+
const added = [];
|
|
23
|
+
const removed = [];
|
|
24
|
+
for (const [name, fingerprint] of Object.entries(live.resources)) {
|
|
25
|
+
if (!Object.hasOwn(before, name)) {
|
|
26
|
+
added.push(name);
|
|
27
|
+
}
|
|
28
|
+
else if (before[name] !== fingerprint) {
|
|
29
|
+
changed.push(name);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const name of Object.keys(before)) {
|
|
33
|
+
if (!Object.hasOwn(live.resources, name)) {
|
|
34
|
+
removed.push(name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const parts = [];
|
|
38
|
+
if (changed.length > 0) {
|
|
39
|
+
parts.push(`${changed.length === 1 ? "Resource" : "Resources"} ${listed(changed)} changed`);
|
|
40
|
+
}
|
|
41
|
+
if (added.length > 0) {
|
|
42
|
+
parts.push(`${added.length === 1 ? "Resource" : "Resources"} ${listed(added)} ${added.length === 1 ? "is" : "are"} new`);
|
|
43
|
+
}
|
|
44
|
+
if (removed.length > 0) {
|
|
45
|
+
parts.push(`${removed.length === 1 ? "Resource" : "Resources"} ${listed(removed)} ${removed.length === 1 ? "is" : "are"} gone`);
|
|
46
|
+
}
|
|
47
|
+
if (!isRecord(stored) || stored.shared !== live.shared) {
|
|
48
|
+
parts.push("the configuration or model outside the Resources changed");
|
|
49
|
+
}
|
|
50
|
+
return parts.length === 0
|
|
51
|
+
? "its fingerprint does not match the configuration and model"
|
|
52
|
+
: `${listed(parts)} since it was built`;
|
|
53
|
+
}
|
|
54
|
+
function textAt(record, key) {
|
|
55
|
+
const value = Object.hasOwn(record, key) ? record[key] : undefined;
|
|
56
|
+
return typeof value === "string" ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
function parsedContracts(texts) {
|
|
59
|
+
if (!isRecord(texts)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const parsed = {};
|
|
63
|
+
for (const origin of ["core", "application", "client", "agent"]) {
|
|
64
|
+
const text = textAt(texts, origin);
|
|
65
|
+
if (text === undefined) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
let contract;
|
|
69
|
+
try {
|
|
70
|
+
contract = JSON.parse(text);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (!isRecord(contract)) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
parsed[origin] = contract;
|
|
79
|
+
}
|
|
80
|
+
const contracts = parsed;
|
|
81
|
+
recordCanonicalContractText(contracts.client, textAt(texts, "client") ?? "");
|
|
82
|
+
recordCanonicalContractText(contracts.agent, textAt(texts, "agent") ?? "");
|
|
83
|
+
return contracts;
|
|
84
|
+
}
|
|
85
|
+
export async function loadPrebuiltContracts(prebuilt, model, config, resolved) {
|
|
86
|
+
if (!isRecord(prebuilt)) {
|
|
87
|
+
return { ok: false, reason: "it is not a prebuilt Contracts file" };
|
|
88
|
+
}
|
|
89
|
+
if (prebuilt.format !== PREBUILT_FORMAT) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: `it is written in file format ${String(prebuilt.format)}, and this @aventara/core reads format ${PREBUILT_FORMAT}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (prebuilt.core !== CORE_VERSION) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
reason: `it was built by @aventara/core ${String(prebuilt.core)}, and this is ${CORE_VERSION}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (prebuilt.protocol !== AVENTARA_PROTOCOL_VERSION) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
reason: `it was built for protocol version ${String(prebuilt.protocol)}, and this @aventara/core speaks version ${AVENTARA_PROTOCOL_VERSION}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if ((await prebuiltInputsFingerprint(model, config)) !== prebuilt.fingerprint) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
reason: await changedInputs(prebuilt.fingerprints, model, config),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const contracts = parsedContracts(prebuilt.contracts);
|
|
114
|
+
if (contracts === undefined) {
|
|
115
|
+
return { ok: false, reason: "its Contracts are not readable JSON objects" };
|
|
116
|
+
}
|
|
117
|
+
bindPrebuiltContracts(model, resolved, contracts);
|
|
118
|
+
return { ok: true, contracts };
|
|
119
|
+
}
|
|
120
|
+
export function warnStalePrebuilt(reason) {
|
|
121
|
+
emitFrameworkWarning(`prebuilt Contracts are stale and were not used: ${reason}. The Contracts were compiled at startup instead; regenerate the file with aventara-build.`, PREBUILT_STALE_WARNING);
|
|
122
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
2
|
+
/**
|
|
3
|
+
* The fingerprint of the inputs Contracts are compiled from: `config` as
|
|
4
|
+
* authored (its `adapter` and `prebuilt` left out), `model`, this core's
|
|
5
|
+
* version, the protocol version and the file format.
|
|
6
|
+
*/
|
|
7
|
+
export declare function prebuiltInputsFingerprint(model: AvAdapterModel, config: object): Promise<string>;
|
|
8
|
+
/** The inputs' parts, each fingerprinted on its own: what a file stores to name what changed. */
|
|
9
|
+
export type PrebuiltPartFingerprints = {
|
|
10
|
+
/** The inputs that belong to no single Resource. */
|
|
11
|
+
readonly shared: string;
|
|
12
|
+
/** Each Resource's inputs (its model, and its entries in every layer), by name, sorted. */
|
|
13
|
+
readonly resources: Readonly<Record<string, string>>;
|
|
14
|
+
};
|
|
15
|
+
/** The parts of the same inputs as {@link prebuiltInputsFingerprint}'s, each fingerprinted. */
|
|
16
|
+
export declare function prebuiltPartFingerprints(model: AvAdapterModel, config: object): Promise<PrebuiltPartFingerprints>;
|