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