@aventara/core 1.0.0-rc.12 → 1.0.0-rc.14
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 +152 -1
- package/ERRORS.md +7 -6
- package/README.md +3 -2
- package/dist/avz/avz-check.factory.d.ts +2 -0
- package/dist/avz/avz-check.factory.js +12 -0
- package/dist/avz/avz.d.ts +6 -0
- package/dist/avz/avz.js +1 -0
- package/dist/compiler/contract-compiler-diagnostic.d.ts +10 -0
- package/dist/compiler/contract-compiler-diagnostic.guidance.js +5 -0
- package/dist/compiler/contract.compiler.js +102 -12
- package/dist/compiler/create-requirements.projector.d.ts +45 -3
- package/dist/compiler/create-requirements.projector.js +239 -28
- package/dist/compiler/managed-elsewhere.projector.d.ts +3 -0
- package/dist/compiler/managed-elsewhere.projector.js +1 -1
- package/dist/config/computed-fields.d.ts +8 -0
- package/dist/config/computed-fields.js +1 -1
- package/dist/config/config-diagnostic.guidance.js +2 -2
- package/dist/config/config.resolver.d.ts +2 -2
- package/dist/config/config.validator.js +15 -6
- package/dist/config/exact-framework-config.d.ts +1 -0
- package/dist/config/field-pipeline-config.d.ts +2 -2
- package/dist/config/forced-operation.validator.js +3 -2
- package/dist/config/framework-config.d.ts +8 -3
- package/dist/config/framework-exemptions-config.d.ts +13 -8
- package/dist/config/framework-resources-config.d.ts +11 -3
- package/dist/contracts/client-contract-structure.validator.d.ts +4 -1
- package/dist/contracts/client-contract-structure.validator.js +3 -2
- package/dist/contracts/contract-limits.d.ts +8 -1
- package/dist/contracts/contract-limits.js +1 -0
- package/dist/contracts/contract.d.ts +7 -3
- package/dist/contracts/transaction-capability.d.ts +8 -0
- package/dist/contracts/transaction-capability.js +7 -0
- package/dist/framework/access-facade.js +14 -6
- package/dist/framework/contract-view.d.ts +91 -9
- package/dist/framework/operation-facade.d.ts +8 -3
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/operations/validation-code.guidance.js +1 -1
- package/dist/policy/access-provider.interface.d.ts +7 -2
- package/dist/policy/access-question.runtime.js +2 -1
- package/dist/prebuilt/core-version.generated.d.ts +1 -1
- package/dist/prebuilt/core-version.generated.js +1 -1
- package/dist/protocol/absent-route.responder.d.ts +2 -2
- package/dist/protocol/absent-route.responder.js +2 -1
- package/dist/protocol/av-protocol.d.ts +1 -0
- package/dist/protocol/av-protocol.js +1 -0
- package/dist/protocol/protocol-surface.js +2 -1
- package/dist/protocol/protocol.handler.js +3 -1
- package/dist/runtime/context-executor.runtime.js +2 -1
- package/dist/runtime/create-requirement.d.ts +5 -1
- package/dist/runtime/create-requirement.js +10 -2
- package/dist/runtime/field-check.evaluator.js +39 -2
- package/dist/runtime/field-validator.runtime.d.ts +4 -3
- package/dist/runtime/field-validator.runtime.js +29 -0
- package/dist/runtime/mutation-planner.js +26 -9
- package/dist/runtime/operation-code.guidance.js +3 -3
- package/dist/runtime/operation-runtime.d.ts +10 -9
- package/dist/runtime/operation-runtime.js +20 -18
- package/dist/runtime/pipeline.d.ts +1 -15
- package/dist/runtime/state-refusal.mapper.d.ts +16 -0
- package/dist/runtime/state-refusal.mapper.js +40 -0
- package/dist/transactions/operation-fingerprint.d.ts +15 -2
- package/dist/transactions/transaction-plan-validator.js +31 -1
- package/dist/transactions/transaction-runtime.js +1 -1
- package/package.json +1 -1
- package/src/runtime/field-check.evaluator.ts +46 -2
|
@@ -12,11 +12,18 @@ type ProjectedResource = {
|
|
|
12
12
|
readonly fields: Readonly<Record<string, ProjectedField>>;
|
|
13
13
|
createRequirements?: AvCreateRequirements;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* The Adapter's statement with the layer's non-null added fields required too.
|
|
17
|
+
* The runtime reads it over the Core Contract (`recordWriteFactsOf`): what write
|
|
18
|
+
* compute must not leave missing.
|
|
19
|
+
*/
|
|
20
|
+
export declare function withVirtualRequirements(stated: AvCreateRequirements | undefined, resource: ProjectedResource): AvCreateRequirements | undefined;
|
|
15
21
|
/**
|
|
16
22
|
* Sets every Resource's `createRequirements` in one layer's projection to the
|
|
17
|
-
* Adapter's statement as that layer sees it,
|
|
18
|
-
*
|
|
19
|
-
* its compute stages
|
|
23
|
+
* Adapter's statement as that layer sees it, its non-null added (`VIRTUAL`)
|
|
24
|
+
* fields required too, and removes it where nothing is left. Reads the
|
|
25
|
+
* projection's final fields: its exposure, and the lifecycle its compute stages
|
|
26
|
+
* gave them.
|
|
20
27
|
*/
|
|
21
28
|
export declare function projectCreateRequirements(model: AvAdapterModel, resources: Readonly<Record<string, ProjectedResource>>): void;
|
|
22
29
|
/** A Resource's operations, as far as this check reads them. */
|
|
@@ -38,6 +45,41 @@ type ResourceWithOperations = ProjectedResource & {
|
|
|
38
45
|
* its final view.
|
|
39
46
|
*/
|
|
40
47
|
export declare function checkHiddenRequirements(model: AvAdapterModel, layer: AvConfigLayerName, universe: Readonly<Record<string, ProjectedResource>>, parent: Readonly<Record<string, ProjectedResource>>, projection: Readonly<Record<string, ResourceWithOperations>>, diagnostics: AvContractCompilerDiagnostic[]): void;
|
|
48
|
+
/**
|
|
49
|
+
* Refuses, for one layer, every relation that writes rows of a Resource the
|
|
50
|
+
* layer keeps read-only: a nested `$create`, `$connectOrCreate` or `$upsert`
|
|
51
|
+
* into a Resource with no create or upsert variant and no field writable on
|
|
52
|
+
* create, a nested `$update` or `$upsert` into one with no update or upsert
|
|
53
|
+
* variant and no field writable on update, or a nested `$delete` into one with
|
|
54
|
+
* no delete variant. Reported once, by the layer that makes it so: an overlay
|
|
55
|
+
* is not refused for a relation its parent was already refused for. `parent` is
|
|
56
|
+
* what the layer starts from; the root's is none, so an Adapter that offers such
|
|
57
|
+
* a write is refused at the root. An Omit layer has none left to refuse:
|
|
58
|
+
* {@link withholdReadOnlyNestedWrites} dropped them first.
|
|
59
|
+
*/
|
|
60
|
+
export declare function checkReadOnlyNestedWrites(layer: AvConfigLayerName, parent: Readonly<Record<string, ResourceWithOperations>> | undefined, projection: Readonly<Record<string, ResourceWithOperations>>, diagnostics: AvContractCompilerDiagnostic[]): void;
|
|
61
|
+
/** Why the narrowing dropped a directive: its target deletes, updates or creates nothing in the layer. */
|
|
62
|
+
export type WithheldReason = "delete" | "update" | "create";
|
|
63
|
+
/**
|
|
64
|
+
* The directives an Omit layer's narrowing dropped, by relation, stage and
|
|
65
|
+
* directive (`"posts.comments.update.$delete"`), each with why.
|
|
66
|
+
*/
|
|
67
|
+
export type WithheldWrites = ReadonlyMap<string, WithheldReason>;
|
|
68
|
+
/** The key of one dropped directive in {@link WithheldWrites}. */
|
|
69
|
+
export declare function withheldKey(relation: string, stage: string, directive: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Narrows an Omit layer's view so no relation writes rows of a Resource the
|
|
72
|
+
* view cannot write: `$create`, `$connectOrCreate` and `$upsert` into one that
|
|
73
|
+
* creates nothing (no create or upsert variant, no field writable on create),
|
|
74
|
+
* `$update` and `$upsert` into one that updates nothing (no update or upsert
|
|
75
|
+
* variant, no field writable on update), `$delete` into one that deletes
|
|
76
|
+
* nothing (no delete variant). A member emptied goes with its last directive.
|
|
77
|
+
* Dropping a directive can leave another Resource writing nothing (its only
|
|
78
|
+
* write capability was it), so the pass repeats to a fixed point. Returns
|
|
79
|
+
* every directive it dropped, with why. A Pick layer's view is not narrowed:
|
|
80
|
+
* {@link checkReadOnlyNestedWrites} refuses what it grants.
|
|
81
|
+
*/
|
|
82
|
+
export declare function withholdReadOnlyNestedWrites(resources: Readonly<Record<string, ResourceWithOperations>>): WithheldWrites;
|
|
41
83
|
/**
|
|
42
84
|
* Every name in the Adapter's create requirements that is not what its member
|
|
43
85
|
* says: a `required` name that is no field of the Resource, a group whose first
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { advertisedDirectives, advertisedDirectivesAt, } from "../contracts/field.js";
|
|
2
2
|
import { isOperationVariantAvailable, writeScopeOf, } from "../contracts/resource-operations.js";
|
|
3
3
|
import { isRecord } from "../runtime/record.guard.js";
|
|
4
|
+
import { keptDirectives } from "./managed-elsewhere.projector.js";
|
|
4
5
|
function own(record, key) {
|
|
5
6
|
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
6
7
|
}
|
|
@@ -32,6 +33,23 @@ function computedOnCreate(resource, name) {
|
|
|
32
33
|
Array.isArray(field.lifecycle) &&
|
|
33
34
|
field.lifecycle.includes("COMPUTED_ON_CREATE"));
|
|
34
35
|
}
|
|
36
|
+
function virtualRequirements(resource) {
|
|
37
|
+
return Object.entries(resource.fields)
|
|
38
|
+
.filter(([, field]) => field.kind === "scalar" &&
|
|
39
|
+
field.nullable === false &&
|
|
40
|
+
Array.isArray(field.lifecycle) &&
|
|
41
|
+
field.lifecycle.includes("VIRTUAL"))
|
|
42
|
+
.map(([name]) => name);
|
|
43
|
+
}
|
|
44
|
+
export function withVirtualRequirements(stated, resource) {
|
|
45
|
+
const [first, ...rest] = [
|
|
46
|
+
...(stated?.required ?? []),
|
|
47
|
+
...virtualRequirements(resource),
|
|
48
|
+
];
|
|
49
|
+
return first === undefined
|
|
50
|
+
? stated
|
|
51
|
+
: { ...stated, required: [first, ...rest] };
|
|
52
|
+
}
|
|
35
53
|
function projectOne(stated, resource, resources) {
|
|
36
54
|
const required = [];
|
|
37
55
|
const requireOnce = (name) => {
|
|
@@ -99,7 +117,7 @@ function projectOne(stated, resource, resources) {
|
|
|
99
117
|
export function projectCreateRequirements(model, resources) {
|
|
100
118
|
for (const [name, resource] of Object.entries(resources)) {
|
|
101
119
|
delete resource.createRequirements;
|
|
102
|
-
const stated = own(model.resources, name)?.createRequirements;
|
|
120
|
+
const stated = withVirtualRequirements(own(model.resources, name)?.createRequirements, resource);
|
|
103
121
|
if (stated === undefined) {
|
|
104
122
|
continue;
|
|
105
123
|
}
|
|
@@ -118,6 +136,7 @@ const UPDATING_DIRECTIVES = [
|
|
|
118
136
|
"$update",
|
|
119
137
|
"$upsert",
|
|
120
138
|
];
|
|
139
|
+
const DELETING_DIRECTIVES = ["$delete"];
|
|
121
140
|
function variantsOf(resource, families) {
|
|
122
141
|
const variants = [];
|
|
123
142
|
for (const family of families) {
|
|
@@ -159,6 +178,36 @@ function createVariantsOf(resource) {
|
|
|
159
178
|
return variants;
|
|
160
179
|
}
|
|
161
180
|
function createPaths(resources) {
|
|
181
|
+
const { created, updated } = writtenResources(resources);
|
|
182
|
+
const paths = new Map();
|
|
183
|
+
const add = (name, path) => {
|
|
184
|
+
const list = paths.get(name) ?? [];
|
|
185
|
+
list.push(path);
|
|
186
|
+
paths.set(name, list);
|
|
187
|
+
};
|
|
188
|
+
for (const [name, resource] of Object.entries(resources)) {
|
|
189
|
+
const variants = createVariantsOf(resource);
|
|
190
|
+
if (variants.length > 0) {
|
|
191
|
+
add(name, { kind: "operation", variants });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const [parent, resource] of Object.entries(resources)) {
|
|
195
|
+
for (const [relation, field] of Object.entries(resource.fields)) {
|
|
196
|
+
const target = field.target;
|
|
197
|
+
if (field.kind !== "relation" || typeof target !== "string") {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if ((created.has(parent) &&
|
|
201
|
+
advertises(field, "create", CREATING_DIRECTIVES)) ||
|
|
202
|
+
(updated.has(parent) &&
|
|
203
|
+
advertises(field, "update", CREATING_DIRECTIVES))) {
|
|
204
|
+
add(target, { kind: "nested", parent, relation });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return paths;
|
|
209
|
+
}
|
|
210
|
+
function writtenResources(resources) {
|
|
162
211
|
const created = new Set();
|
|
163
212
|
const updated = new Set();
|
|
164
213
|
for (const [name, resource] of Object.entries(resources)) {
|
|
@@ -197,33 +246,7 @@ function createPaths(resources) {
|
|
|
197
246
|
}
|
|
198
247
|
}
|
|
199
248
|
}
|
|
200
|
-
|
|
201
|
-
const add = (name, path) => {
|
|
202
|
-
const list = paths.get(name) ?? [];
|
|
203
|
-
list.push(path);
|
|
204
|
-
paths.set(name, list);
|
|
205
|
-
};
|
|
206
|
-
for (const [name, resource] of Object.entries(resources)) {
|
|
207
|
-
const variants = createVariantsOf(resource);
|
|
208
|
-
if (variants.length > 0) {
|
|
209
|
-
add(name, { kind: "operation", variants });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
for (const [parent, resource] of Object.entries(resources)) {
|
|
213
|
-
for (const [relation, field] of Object.entries(resource.fields)) {
|
|
214
|
-
const target = field.target;
|
|
215
|
-
if (field.kind !== "relation" || typeof target !== "string") {
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
if ((created.has(parent) &&
|
|
219
|
-
advertises(field, "create", CREATING_DIRECTIVES)) ||
|
|
220
|
-
(updated.has(parent) &&
|
|
221
|
-
advertises(field, "update", CREATING_DIRECTIVES))) {
|
|
222
|
-
add(target, { kind: "nested", parent, relation });
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return paths;
|
|
249
|
+
return { created, updated };
|
|
227
250
|
}
|
|
228
251
|
function pathLabel(path) {
|
|
229
252
|
return path.kind === "operation"
|
|
@@ -320,6 +343,7 @@ export function checkHiddenRequirements(model, layer, universe, parent, projecti
|
|
|
320
343
|
const parentResource = own(parent, name);
|
|
321
344
|
if (stated === undefined ||
|
|
322
345
|
routes.length === 0 ||
|
|
346
|
+
createsNothing(resource) ||
|
|
323
347
|
universeResource === undefined ||
|
|
324
348
|
parentResource === undefined) {
|
|
325
349
|
continue;
|
|
@@ -367,6 +391,193 @@ export function checkHiddenRequirements(model, layer, universe, parent, projecti
|
|
|
367
391
|
}
|
|
368
392
|
}
|
|
369
393
|
}
|
|
394
|
+
function createsNothing(resource) {
|
|
395
|
+
return (variantsOf(resource, ["create", "upsert"]).length === 0 &&
|
|
396
|
+
!Object.keys(resource.fields).some((name) => creatable(resource, name)));
|
|
397
|
+
}
|
|
398
|
+
function updatesNothing(resource) {
|
|
399
|
+
return (variantsOf(resource, ["update", "upsert"]).length === 0 &&
|
|
400
|
+
!Object.values(resource.fields).some((field) => {
|
|
401
|
+
const capabilities = isRecord(field.capabilities)
|
|
402
|
+
? field.capabilities
|
|
403
|
+
: undefined;
|
|
404
|
+
return field.kind === "scalar"
|
|
405
|
+
? capabilities?.update !== undefined
|
|
406
|
+
: advertisedDirectives(capabilities?.update) !== undefined;
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
function deletesNothing(resource) {
|
|
410
|
+
return variantsOf(resource, ["delete"]).length === 0;
|
|
411
|
+
}
|
|
412
|
+
function advertisedOf(field, stage, directives) {
|
|
413
|
+
const capabilities = isRecord(field.capabilities)
|
|
414
|
+
? field.capabilities
|
|
415
|
+
: undefined;
|
|
416
|
+
const advertised = advertisedDirectives(capabilities?.[stage]) ?? [];
|
|
417
|
+
return directives.filter((name) => advertised.includes(name));
|
|
418
|
+
}
|
|
419
|
+
function readOnlyNestedWrites(resources) {
|
|
420
|
+
const { created, updated } = writtenResources(resources);
|
|
421
|
+
const writes = [];
|
|
422
|
+
for (const [parent, resource] of Object.entries(resources)) {
|
|
423
|
+
for (const [relation, field] of Object.entries(resource.fields)) {
|
|
424
|
+
const target = field.target;
|
|
425
|
+
const targetResource = field.kind === "relation" && typeof target === "string"
|
|
426
|
+
? own(resources, target)
|
|
427
|
+
: undefined;
|
|
428
|
+
if (typeof target !== "string" || targetResource === undefined) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const reached = (directives) => [
|
|
432
|
+
...new Set([
|
|
433
|
+
...(created.has(parent)
|
|
434
|
+
? advertisedOf(field, "create", directives)
|
|
435
|
+
: []),
|
|
436
|
+
...(updated.has(parent)
|
|
437
|
+
? advertisedOf(field, "update", directives)
|
|
438
|
+
: []),
|
|
439
|
+
]),
|
|
440
|
+
];
|
|
441
|
+
const creating = createsNothing(targetResource)
|
|
442
|
+
? reached(CREATING_DIRECTIVES)
|
|
443
|
+
: [];
|
|
444
|
+
const updating = updatesNothing(targetResource)
|
|
445
|
+
? reached(UPDATING_DIRECTIVES)
|
|
446
|
+
: [];
|
|
447
|
+
const deleting = deletesNothing(targetResource)
|
|
448
|
+
? reached(DELETING_DIRECTIVES)
|
|
449
|
+
: [];
|
|
450
|
+
if (creating.length > 0 || updating.length > 0 || deleting.length > 0) {
|
|
451
|
+
writes.push({
|
|
452
|
+
parent,
|
|
453
|
+
relation,
|
|
454
|
+
target,
|
|
455
|
+
creating,
|
|
456
|
+
updating,
|
|
457
|
+
deleting,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return writes;
|
|
463
|
+
}
|
|
464
|
+
export function checkReadOnlyNestedWrites(layer, parent, projection, diagnostics) {
|
|
465
|
+
const inherited = new Map((parent === undefined ? [] : readOnlyNestedWrites(parent)).map((write) => [
|
|
466
|
+
`${write.parent}.${write.relation}`,
|
|
467
|
+
write,
|
|
468
|
+
]));
|
|
469
|
+
for (const found of readOnlyNestedWrites(projection)) {
|
|
470
|
+
const at = `${found.parent}.${found.relation}`;
|
|
471
|
+
const above = inherited.get(at);
|
|
472
|
+
const write = {
|
|
473
|
+
...found,
|
|
474
|
+
creating: (above?.creating.length ?? 0) > 0 ? [] : found.creating,
|
|
475
|
+
updating: (above?.updating.length ?? 0) > 0 ? [] : found.updating,
|
|
476
|
+
deleting: (above?.deleting.length ?? 0) > 0 ? [] : found.deleting,
|
|
477
|
+
};
|
|
478
|
+
if (write.creating.length === 0 &&
|
|
479
|
+
write.updating.length === 0 &&
|
|
480
|
+
write.deleting.length === 0) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const directives = [
|
|
484
|
+
...new Set([...write.creating, ...write.updating, ...write.deleting]),
|
|
485
|
+
];
|
|
486
|
+
const lacks = [
|
|
487
|
+
...(write.creating.length === 0
|
|
488
|
+
? []
|
|
489
|
+
: ["no create operation and no field writable on create"]),
|
|
490
|
+
...(write.updating.length === 0
|
|
491
|
+
? []
|
|
492
|
+
: ["no update operation and no field writable on update"]),
|
|
493
|
+
...(write.deleting.length === 0 ? [] : ["no delete operation"]),
|
|
494
|
+
];
|
|
495
|
+
const gives = [
|
|
496
|
+
...(write.creating.length === 0
|
|
497
|
+
? []
|
|
498
|
+
: ["a create operation or a field writable on create"]),
|
|
499
|
+
...(write.updating.length === 0
|
|
500
|
+
? []
|
|
501
|
+
: ["an update operation or a field writable on update"]),
|
|
502
|
+
...(write.deleting.length === 0 ? [] : ["a delete operation"]),
|
|
503
|
+
];
|
|
504
|
+
const keep = `take ${listed(directives)} off "${at}" (keep only the directives it may use, such as $connect)`;
|
|
505
|
+
diagnostics.push({
|
|
506
|
+
code: "COMPILER_READ_ONLY_NESTED_WRITE",
|
|
507
|
+
path: `${layer}.exemptions.resources.${write.parent}.fields.${write.relation}`,
|
|
508
|
+
message: write.creating.length === 0 && write.updating.length === 0
|
|
509
|
+
? `the ${layer} layer lets relation "${at}" delete rows of resource "${write.target}" ($delete), but "${write.target}" has no delete operation in that layer; ${keep}, or give "${write.target}" a delete operation`
|
|
510
|
+
: `the ${layer} layer lets relation "${at}" write rows of resource "${write.target}" (${listed(directives)}), but "${write.target}" is read-only in that layer: it has ${lacks.join(", and ")}; ${keep}, or give "${write.target}" ${gives.join(", and ")}`,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function dropDirectives(field, stage, dropped) {
|
|
515
|
+
if (!isRecord(field.capabilities)) {
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
const capabilities = field.capabilities;
|
|
519
|
+
const advertised = advertisedDirectives(capabilities[stage]) ?? [];
|
|
520
|
+
const removed = advertised.filter((name) => dropped.includes(name));
|
|
521
|
+
if (removed.length === 0) {
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
const narrowed = keptDirectives(capabilities[stage], advertised.filter((name) => !dropped.includes(name)));
|
|
525
|
+
if (narrowed === undefined) {
|
|
526
|
+
delete capabilities[stage];
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
capabilities[stage] = narrowed;
|
|
530
|
+
}
|
|
531
|
+
return removed;
|
|
532
|
+
}
|
|
533
|
+
function* relationsOf(resources) {
|
|
534
|
+
for (const [parent, resource] of Object.entries(resources)) {
|
|
535
|
+
for (const [relation, field] of Object.entries(resource.fields)) {
|
|
536
|
+
const target = field.target;
|
|
537
|
+
const targetResource = field.kind === "relation" && typeof target === "string"
|
|
538
|
+
? own(resources, target)
|
|
539
|
+
: undefined;
|
|
540
|
+
if (typeof target === "string" && targetResource !== undefined) {
|
|
541
|
+
yield [parent, relation, field, target, targetResource];
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
export function withheldKey(relation, stage, directive) {
|
|
547
|
+
return `${relation}.${stage}.${directive}`;
|
|
548
|
+
}
|
|
549
|
+
export function withholdReadOnlyNestedWrites(resources) {
|
|
550
|
+
const withheld = new Map();
|
|
551
|
+
const drop = (at, field, stage, directives, reason) => {
|
|
552
|
+
const removed = dropDirectives(field, stage, directives);
|
|
553
|
+
for (const directive of removed) {
|
|
554
|
+
withheld.set(withheldKey(at, stage, directive), reason);
|
|
555
|
+
}
|
|
556
|
+
return removed.length > 0;
|
|
557
|
+
};
|
|
558
|
+
let changed = true;
|
|
559
|
+
while (changed) {
|
|
560
|
+
changed = false;
|
|
561
|
+
for (const [parent, relation, field, , target] of relationsOf(resources)) {
|
|
562
|
+
const at = `${parent}.${relation}`;
|
|
563
|
+
for (const stage of ["create", "update"]) {
|
|
564
|
+
if (deletesNothing(target)) {
|
|
565
|
+
changed =
|
|
566
|
+
drop(at, field, stage, DELETING_DIRECTIVES, "delete") || changed;
|
|
567
|
+
}
|
|
568
|
+
if (createsNothing(target)) {
|
|
569
|
+
changed =
|
|
570
|
+
drop(at, field, stage, CREATING_DIRECTIVES, "create") || changed;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (updatesNothing(target)) {
|
|
574
|
+
changed =
|
|
575
|
+
drop(at, field, "update", UPDATING_DIRECTIVES, "update") || changed;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return withheld;
|
|
580
|
+
}
|
|
370
581
|
function misnamed(path, message) {
|
|
371
582
|
return { code: "COMPILER_UNKNOWN_FIELD", path, message };
|
|
372
583
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
2
|
+
import type { AvRelationMutationDirective } from "../contracts/capability.js";
|
|
2
3
|
import type { AvContractCompilerDiagnostic } from "./contract-compiler-diagnostic.js";
|
|
3
4
|
type ProjectedField = Record<string, unknown> & {
|
|
4
5
|
kind: "scalar" | "relation";
|
|
@@ -14,6 +15,8 @@ type ProjectedContract = {
|
|
|
14
15
|
};
|
|
15
16
|
/** The Resources the root's `resources` tree marks `managedElsewhere: true`. */
|
|
16
17
|
export declare function managedElsewhereOf(rootTree: unknown): ReadonlySet<string>;
|
|
18
|
+
/** A relation mutation capability with only `kept` of its directives (its scopes kept), or `undefined` when none is left. */
|
|
19
|
+
export declare function keptDirectives(capability: unknown, kept: readonly AvRelationMutationDirective[]): unknown;
|
|
17
20
|
/**
|
|
18
21
|
* Takes every write of a managed Resource away from `baseline`: its write
|
|
19
22
|
* families, its fields' `create`/`update`, and the directives of every relation
|
|
@@ -22,7 +22,7 @@ export function managedElsewhereOf(rootTree) {
|
|
|
22
22
|
}
|
|
23
23
|
return managed;
|
|
24
24
|
}
|
|
25
|
-
function keptDirectives(capability, kept) {
|
|
25
|
+
export function keptDirectives(capability, kept) {
|
|
26
26
|
if (Array.isArray(capability)) {
|
|
27
27
|
const left = capability.filter((directive) => kept.includes(directive));
|
|
28
28
|
return left.length > 0 ? left : undefined;
|
|
@@ -38,6 +38,14 @@ type CreateComputedOf<Layer> = Layer extends object ? {
|
|
|
38
38
|
export type AvPreComputeLift<M, L extends AvConfigLayerName> = M extends {
|
|
39
39
|
readonly "~computedFields"?: infer Facts;
|
|
40
40
|
} ? Exclude<Facts, undefined> extends infer Stated extends AvConfigComputedFields ? string extends keyof Stated["root"] ? string : CreateComputedOf<Stated[ComputingLayers<Stated, L>]> : string : string;
|
|
41
|
+
/**
|
|
42
|
+
* The create fields a ctx call need not write, as `"<Resource>.<field>"`: those
|
|
43
|
+
* the root's or the application's `compute.create` fills. `never` when the
|
|
44
|
+
* model's type carries no statement of them.
|
|
45
|
+
*/
|
|
46
|
+
export type AvContextComputeLift<M> = M extends {
|
|
47
|
+
readonly "~computedFields"?: infer Facts;
|
|
48
|
+
} ? Exclude<Facts, undefined> extends infer Stated extends AvConfigComputedFields ? string extends keyof Stated["root"] ? never : CreateComputedOf<Stated["root" | "application"]> : never : never;
|
|
41
49
|
/** The configuration's layers' `resources` trees and the agent's parent. */
|
|
42
50
|
type ComputedFieldsSource = {
|
|
43
51
|
readonly trees: Readonly<Record<AvConfigLayerName, unknown>>;
|
|
@@ -66,7 +66,7 @@ export function warnOnStaleComputedFields(generated, configured) {
|
|
|
66
66
|
if (drift.length === 0) {
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
-
const message = `generated types are stale: run aventara:prepare. The computed fields the pre-compute stages are typed with differ from the configuration's:\n${drift
|
|
69
|
+
const message = `generated types are stale: run aventara:prepare. The computed fields the pre-compute stages and ctx creates are typed with differ from the configuration's:\n${drift
|
|
70
70
|
.map((line) => ` - ${line}`)
|
|
71
71
|
.join("\n")}`;
|
|
72
72
|
emitFrameworkWarning(message, STALE_GENERATED_TYPES_WARNING);
|
|
@@ -26,7 +26,7 @@ export const CONFIG_DIAGNOSTIC_GUIDANCE = {
|
|
|
26
26
|
},
|
|
27
27
|
CONFIG_FORBIDDEN_IN_OVERLAY: {
|
|
28
28
|
message: "A root-only key is written in an overlay.",
|
|
29
|
-
cause: "A field definition in application, client or agent.",
|
|
30
|
-
fix: "
|
|
29
|
+
cause: "A field definition, or a Resource's managedElsewhere, in application, client or agent.",
|
|
30
|
+
fix: "State it once at the root (`resources.<R>.fields.<f>.definition`, `resources.<R>.managedElsewhere`); an overlay only chooses visibility.",
|
|
31
31
|
},
|
|
32
32
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AvAdapter } from "../adapters/adapter.interface.js";
|
|
2
2
|
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
3
3
|
import type { AvContractClientAccess } from "../contracts/contract-access.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { TransactionsStyle } from "../contracts/transaction-capability.js";
|
|
5
5
|
import type { AvPolicyEngine } from "../policy/policy-engine.js";
|
|
6
6
|
import type { AvDiagnosticsSink } from "../runtime/diagnostics-reporter.js";
|
|
7
7
|
import type { AvMisuseSink } from "../runtime/misuse-reporter.js";
|
|
@@ -45,7 +45,7 @@ export type ResolvedFrameworkLayerConfig = {
|
|
|
45
45
|
* registrations, then the overlay's, at each level.
|
|
46
46
|
*/
|
|
47
47
|
readonly pipelines: ResolvedPipelineTree;
|
|
48
|
-
readonly transactionsStyle:
|
|
48
|
+
readonly transactionsStyle: TransactionsStyle | undefined;
|
|
49
49
|
};
|
|
50
50
|
/**
|
|
51
51
|
* The MCP endpoint a Framework serves (`mcp` resolved): its mount path in
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAccessPath, isAccessUrl } from "../contracts/access-route.rules.js";
|
|
2
|
-
import {
|
|
2
|
+
import { TRANSACTIONS_STYLES } from "../contracts/transaction-capability.js";
|
|
3
3
|
import { ACCESS_VERB_NAMES, isAccessVerbName, providerVerb, } from "../policy/access-verb.table.js";
|
|
4
4
|
import { isPlainRecord } from "../runtime/record.guard.js";
|
|
5
5
|
import { normalizeEntrypoint } from "./entrypoint.normalizer.js";
|
|
@@ -69,13 +69,14 @@ const OPERATION_FAMILIES = new Set([
|
|
|
69
69
|
"upsert",
|
|
70
70
|
]);
|
|
71
71
|
const OPERATION_VARIANTS = new Set(["first", "unique", "many", "count", "one"]);
|
|
72
|
-
const
|
|
72
|
+
const TRANSACTIONS_STYLE_SET = new Set(TRANSACTIONS_STYLES);
|
|
73
73
|
const AVZ_FORMAT_SET = new Set(AVZ_FORMATS);
|
|
74
74
|
const LIMIT_PROPERTIES = new Set([
|
|
75
75
|
"maxRequestBytes",
|
|
76
76
|
"maxNestingDepth",
|
|
77
77
|
"maxListLimit",
|
|
78
78
|
"maxTransactionOperations",
|
|
79
|
+
"maxTransactionWrites",
|
|
79
80
|
"maxBooleanNodes",
|
|
80
81
|
"maxJsonDepth",
|
|
81
82
|
"maxAll",
|
|
@@ -436,6 +437,7 @@ function validateResources(value, path, isRoot, diagnostics) {
|
|
|
436
437
|
const EXEMPTIONS_PROPERTIES = new Set(["resources"]);
|
|
437
438
|
const RESOURCE_EXEMPTION_PROPERTIES = new Set(["operations", "fields"]);
|
|
438
439
|
const FLAG_CAPABILITIES = new Set(["select", "include", "order", "orderNulls"]);
|
|
440
|
+
const READ_EXEMPTION = "read";
|
|
439
441
|
function movedHidden(path) {
|
|
440
442
|
return diagnostic("CONFIG_MOVED_PROPERTY", path, `${path} moved in rc.1: an entry is hidden with \`true\` under exposed: true`);
|
|
441
443
|
}
|
|
@@ -492,9 +494,12 @@ function validateOperationsExemption(value, path, diagnostics) {
|
|
|
492
494
|
}
|
|
493
495
|
}
|
|
494
496
|
function validateFieldExemption(value, path, diagnostics) {
|
|
497
|
+
if (value === READ_EXEMPTION) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
495
500
|
const kind = validateExemptionValue(value, path, "a capability name", diagnostics);
|
|
496
501
|
if (kind === undefined) {
|
|
497
|
-
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", path, `${path} must be true, false, a list of capability names, or an object of them`));
|
|
502
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", path, `${path} must be true, false, "read", a list of capability names, or an object of them`));
|
|
498
503
|
return;
|
|
499
504
|
}
|
|
500
505
|
if (kind !== "object") {
|
|
@@ -543,7 +548,11 @@ function validateResourceExemption(value, path, diagnostics) {
|
|
|
543
548
|
validateOperationsExemption(entry, keyPath, diagnostics);
|
|
544
549
|
continue;
|
|
545
550
|
}
|
|
546
|
-
if (
|
|
551
|
+
if (entry === READ_EXEMPTION) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (!isPlainRecord(entry)) {
|
|
555
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", keyPath, `${keyPath} must be "read" or an object of field exemptions`));
|
|
547
556
|
continue;
|
|
548
557
|
}
|
|
549
558
|
for (const [field, fieldEntry] of Object.entries(entry)) {
|
|
@@ -616,9 +625,9 @@ function validateLayer(value, path, kind, diagnostics) {
|
|
|
616
625
|
validateExemptions(value.exemptions, exemptionsPath, diagnostics);
|
|
617
626
|
}
|
|
618
627
|
if (Object.hasOwn(value, "transactionsStyle") &&
|
|
619
|
-
!
|
|
628
|
+
!TRANSACTIONS_STYLE_SET.has(value.transactionsStyle)) {
|
|
620
629
|
const stylePath = childPath(path, "transactionsStyle");
|
|
621
|
-
diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", stylePath, `${stylePath} must be "none" or "interactive"`));
|
|
630
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", stylePath, `${stylePath} must be "none", "server" or "interactive"`));
|
|
622
631
|
}
|
|
623
632
|
if (Object.hasOwn(value, "limits")) {
|
|
624
633
|
validateLimits(value.limits, childPath(path, "limits"), diagnostics);
|
|
@@ -26,6 +26,7 @@ type LimitsSchema = {
|
|
|
26
26
|
readonly maxNestingDepth: Leaf;
|
|
27
27
|
readonly maxListLimit: Leaf;
|
|
28
28
|
readonly maxTransactionOperations: Leaf;
|
|
29
|
+
readonly maxTransactionWrites: Leaf;
|
|
29
30
|
readonly maxBooleanNodes: Leaf;
|
|
30
31
|
readonly maxJsonDepth: Leaf;
|
|
31
32
|
readonly maxAll: Leaf;
|
|
@@ -20,9 +20,9 @@ export type AvFieldCompute<M extends AvAdapterModel = AvAdapterModel, Cfg = AvCo
|
|
|
20
20
|
/**
|
|
21
21
|
* A scalar field's stages: validators, field pipes, compute and write guards.
|
|
22
22
|
* `Check` is the checks the field takes (any, when the field's type is not
|
|
23
|
-
* known here).
|
|
23
|
+
* known here), or the sentence a type that takes none refuses them with.
|
|
24
24
|
*/
|
|
25
|
-
export type AvScalarFieldPipeline<M extends AvAdapterModel = AvAdapterModel, Cfg = AvConfigAny, R extends AvConfigResourceKey<M> = AvConfigResourceKey<M>, Check extends AvZCheck = AvZCheck> = {
|
|
25
|
+
export type AvScalarFieldPipeline<M extends AvAdapterModel = AvAdapterModel, Cfg = AvConfigAny, R extends AvConfigResourceKey<M> = AvConfigResourceKey<M>, Check extends AvZCheck | `Error: ${string}` = AvZCheck> = {
|
|
26
26
|
/**
|
|
27
27
|
* Declarative checks on the value, built with `AvZ` and carried in the Contract;
|
|
28
28
|
* root's, then the overlay's. On a list field `AvZ.minItems` and `AvZ.maxItems`
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isOperationVariantAvailable } from "../contracts/resource-operations.js";
|
|
2
|
+
import { opensServerTransactions } from "../contracts/transaction-capability.js";
|
|
2
3
|
import { forcedOperationsOf } from "./pipeline-tree.resolver.js";
|
|
3
4
|
function reachable(contract, resource, family, variant) {
|
|
4
5
|
if (!Object.hasOwn(contract.resources, resource)) {
|
|
@@ -45,7 +46,7 @@ export function validateForcedOperations(config, contracts) {
|
|
|
45
46
|
: ["application", "client"];
|
|
46
47
|
for (const origin of origins) {
|
|
47
48
|
const contract = contracts[origin];
|
|
48
|
-
if (contract.transactions
|
|
49
|
+
if (opensServerTransactions(contract.transactions)) {
|
|
49
50
|
continue;
|
|
50
51
|
}
|
|
51
52
|
const forced = [
|
|
@@ -61,6 +62,6 @@ export function validateForcedOperations(config, contracts) {
|
|
|
61
62
|
return [...refused].map(([path, layers]) => ({
|
|
62
63
|
code: "CONFIG_INVALID_VALUE",
|
|
63
64
|
path,
|
|
64
|
-
message: `${path}: forceTransaction runs a plain call as a one-step transaction, and the effective transactionsStyle of the ${layers.join(" and ")} ${layers.length === 1 ? "layer is" : "layers are"} "none"; set transactionsStyle: "interactive" there (the adapter must offer interactive transactions) or remove forceTransaction`,
|
|
65
|
+
message: `${path}: forceTransaction runs a plain call as a one-step transaction, and the effective transactionsStyle of the ${layers.join(" and ")} ${layers.length === 1 ? "layer is" : "layers are"} "none"; set transactionsStyle: "server" (no plans from callers) or "interactive" there (the adapter must offer interactive transactions) or remove forceTransaction`,
|
|
65
66
|
}));
|
|
66
67
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AvAdapter } from "../adapters/adapter.interface.js";
|
|
2
2
|
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
3
3
|
import type { AvContractLimits } from "../contracts/contract-limits.js";
|
|
4
|
-
import type { AvTransactionCapability } from "../contracts/transaction-capability.js";
|
|
5
4
|
import type { AvAccessProvider } from "../policy/access-provider.interface.js";
|
|
6
5
|
import type { AvPolicyEngine } from "../policy/policy-engine.js";
|
|
7
6
|
import type { AvPrebuiltContracts } from "../prebuilt/prebuilt-contracts.js";
|
|
@@ -38,8 +37,14 @@ export type AvConfigLayer<M extends AvAdapterModel = AvAdapterModel, Cfg = AvCon
|
|
|
38
37
|
* the adapter's model, an overlay the root's view.
|
|
39
38
|
*/
|
|
40
39
|
readonly exposed?: boolean;
|
|
41
|
-
/**
|
|
42
|
-
|
|
40
|
+
/**
|
|
41
|
+
* `"interactive"`: the server opens transactions and callers send plans
|
|
42
|
+
* (`/_transactions`, a generated client's `tx`). `"server"`: only the server
|
|
43
|
+
* opens them (`forceTransaction`, a policy-checked write, `ctx.$transaction`);
|
|
44
|
+
* a plan answers as an absent route. `"none"`: no transactions. Both
|
|
45
|
+
* `"server"` and `"interactive"` need adapter support.
|
|
46
|
+
*/
|
|
47
|
+
readonly transactionsStyle?: "none" | "server" | "interactive";
|
|
43
48
|
/** Request limits; see {@link AvConfigLimits}. */
|
|
44
49
|
readonly limits?: AvConfigLimits;
|
|
45
50
|
/** Guards, pipes, hooks, interceptors and filters run around every operation of the layer. */
|