@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +152 -1
  2. package/ERRORS.md +7 -6
  3. package/README.md +3 -2
  4. package/dist/avz/avz-check.factory.d.ts +2 -0
  5. package/dist/avz/avz-check.factory.js +12 -0
  6. package/dist/avz/avz.d.ts +6 -0
  7. package/dist/avz/avz.js +1 -0
  8. package/dist/compiler/contract-compiler-diagnostic.d.ts +10 -0
  9. package/dist/compiler/contract-compiler-diagnostic.guidance.js +5 -0
  10. package/dist/compiler/contract.compiler.js +102 -12
  11. package/dist/compiler/create-requirements.projector.d.ts +45 -3
  12. package/dist/compiler/create-requirements.projector.js +239 -28
  13. package/dist/compiler/managed-elsewhere.projector.d.ts +3 -0
  14. package/dist/compiler/managed-elsewhere.projector.js +1 -1
  15. package/dist/config/computed-fields.d.ts +8 -0
  16. package/dist/config/computed-fields.js +1 -1
  17. package/dist/config/config-diagnostic.guidance.js +2 -2
  18. package/dist/config/config.resolver.d.ts +2 -2
  19. package/dist/config/config.validator.js +15 -6
  20. package/dist/config/exact-framework-config.d.ts +1 -0
  21. package/dist/config/field-pipeline-config.d.ts +2 -2
  22. package/dist/config/forced-operation.validator.js +3 -2
  23. package/dist/config/framework-config.d.ts +8 -3
  24. package/dist/config/framework-exemptions-config.d.ts +13 -8
  25. package/dist/config/framework-resources-config.d.ts +11 -3
  26. package/dist/contracts/client-contract-structure.validator.d.ts +4 -1
  27. package/dist/contracts/client-contract-structure.validator.js +3 -2
  28. package/dist/contracts/contract-limits.d.ts +8 -1
  29. package/dist/contracts/contract-limits.js +1 -0
  30. package/dist/contracts/contract.d.ts +7 -3
  31. package/dist/contracts/transaction-capability.d.ts +8 -0
  32. package/dist/contracts/transaction-capability.js +7 -0
  33. package/dist/framework/access-facade.js +14 -6
  34. package/dist/framework/contract-view.d.ts +91 -9
  35. package/dist/framework/operation-facade.d.ts +8 -3
  36. package/dist/internal.d.ts +1 -0
  37. package/dist/internal.js +1 -0
  38. package/dist/operations/validation-code.guidance.js +1 -1
  39. package/dist/policy/access-provider.interface.d.ts +7 -2
  40. package/dist/policy/access-question.runtime.js +2 -1
  41. package/dist/prebuilt/core-version.generated.d.ts +1 -1
  42. package/dist/prebuilt/core-version.generated.js +1 -1
  43. package/dist/protocol/absent-route.responder.d.ts +2 -2
  44. package/dist/protocol/absent-route.responder.js +2 -1
  45. package/dist/protocol/av-protocol.d.ts +1 -0
  46. package/dist/protocol/av-protocol.js +1 -0
  47. package/dist/protocol/protocol-surface.js +2 -1
  48. package/dist/protocol/protocol.handler.js +3 -1
  49. package/dist/runtime/context-executor.runtime.js +2 -1
  50. package/dist/runtime/create-requirement.d.ts +5 -1
  51. package/dist/runtime/create-requirement.js +10 -2
  52. package/dist/runtime/field-check.evaluator.js +39 -2
  53. package/dist/runtime/field-validator.runtime.d.ts +4 -3
  54. package/dist/runtime/field-validator.runtime.js +29 -0
  55. package/dist/runtime/mutation-planner.js +26 -9
  56. package/dist/runtime/operation-code.guidance.js +3 -3
  57. package/dist/runtime/operation-runtime.d.ts +10 -9
  58. package/dist/runtime/operation-runtime.js +20 -18
  59. package/dist/runtime/pipeline.d.ts +1 -15
  60. package/dist/runtime/state-refusal.mapper.d.ts +16 -0
  61. package/dist/runtime/state-refusal.mapper.js +40 -0
  62. package/dist/transactions/operation-fingerprint.d.ts +15 -2
  63. package/dist/transactions/transaction-plan-validator.js +31 -1
  64. package/dist/transactions/transaction-runtime.js +1 -1
  65. package/package.json +1 -1
  66. 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, and removes it where nothing is
18
- * left. Reads the projection's final fields: its exposure, and the lifecycle
19
- * its compute stages gave them.
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
- const paths = new Map();
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: "Define the field once at the root (`resources.<R>.fields.<f>.definition`); an overlay only chooses its visibility.",
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 { AvTransactionCapability } from "../contracts/transaction-capability.js";
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: AvTransactionCapability | undefined;
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 { TRANSACTION_CAPABILITIES } from "../contracts/transaction-capability.js";
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 TRANSACTION_CAPABILITY_SET = new Set(TRANSACTION_CAPABILITIES);
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 (!requireRecord(entry, keyPath, diagnostics)) {
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
- !TRANSACTION_CAPABILITY_SET.has(value.transactionsStyle)) {
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 !== "none") {
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
- /** `"none"` turns transactions off; `"interactive"` needs adapter support. */
42
- readonly transactionsStyle?: AvTransactionCapability;
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. */