@aventara/core 1.0.0-rc.2 → 1.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/ERRORS.md +9 -9
  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 +8 -1
  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/database-refusal.d.ts +1 -1
  25. package/dist/operations/database-refusal.js +1 -1
  26. package/dist/operations/operation-arguments.d.ts +1 -1
  27. package/dist/operations/operation-call.d.ts +9 -5
  28. package/dist/operations/operation-result.d.ts +5 -3
  29. package/dist/operations/operation-validator-core.js +66 -33
  30. package/dist/operations/validation-code.guidance.js +2 -2
  31. package/dist/policy/policy-admission.d.ts +22 -0
  32. package/dist/policy/policy-admission.js +400 -0
  33. package/dist/policy/policy-after-update.runtime.d.ts +29 -0
  34. package/dist/policy/policy-after-update.runtime.js +131 -0
  35. package/dist/policy/policy-condition.d.ts +15 -0
  36. package/dist/policy/policy-condition.js +36 -0
  37. package/dist/policy/policy-engine.d.ts +111 -0
  38. package/dist/policy/policy-engine.js +1 -0
  39. package/dist/policy/policy-nested-write.runtime.d.ts +38 -0
  40. package/dist/policy/policy-nested-write.runtime.js +449 -0
  41. package/dist/policy/policy-read.runtime.d.ts +83 -0
  42. package/dist/policy/policy-read.runtime.js +494 -0
  43. package/dist/policy/policy-request.d.ts +37 -0
  44. package/dist/policy/policy-request.js +52 -0
  45. package/dist/policy/policy-session.d.ts +83 -0
  46. package/dist/policy/policy-session.js +234 -0
  47. package/dist/policy/policy-startup.validator.d.ts +8 -0
  48. package/dist/policy/policy-startup.validator.js +106 -0
  49. package/dist/policy/policy-write.runtime.d.ts +30 -0
  50. package/dist/policy/policy-write.runtime.js +394 -0
  51. package/dist/prebuilt/core-version.generated.d.ts +2 -0
  52. package/dist/prebuilt/core-version.generated.js +1 -0
  53. package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
  54. package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
  55. package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
  56. package/dist/prebuilt/prebuilt-contracts.js +1 -0
  57. package/dist/prebuilt/prebuilt-contracts.loader.d.ts +21 -0
  58. package/dist/prebuilt/prebuilt-contracts.loader.js +122 -0
  59. package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
  60. package/dist/prebuilt/prebuilt-inputs.fingerprint.js +192 -0
  61. package/dist/runtime/computed-read-runtime.d.ts +6 -1
  62. package/dist/runtime/computed-read-runtime.js +224 -45
  63. package/dist/runtime/context-executor.runtime.js +4 -2
  64. package/dist/runtime/execution-options.d.ts +8 -0
  65. package/dist/runtime/field-validator.runtime.d.ts +9 -0
  66. package/dist/runtime/field-validator.runtime.js +36 -2
  67. package/dist/runtime/framework-warning.emitter.d.ts +6 -0
  68. package/dist/runtime/framework-warning.emitter.js +8 -0
  69. package/dist/runtime/level-lifecycle.runtime.js +3 -2
  70. package/dist/runtime/operation-code.guidance.js +4 -4
  71. package/dist/runtime/operation-request.d.ts +7 -0
  72. package/dist/runtime/operation-runtime.d.ts +9 -1
  73. package/dist/runtime/operation-runtime.js +119 -11
  74. package/dist/runtime/readonly-value.d.ts +5 -1
  75. package/dist/runtime/readonly-value.js +67 -28
  76. package/dist/transactions/transaction-runtime.d.ts +4 -1
  77. package/dist/transactions/transaction-runtime.js +27 -5
  78. 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
  };
@@ -1,5 +1,6 @@
1
1
  import type { AvContract } from "../contracts/contract.js";
2
2
  import type { AvValidationIssue } from "../operations/validation-code.js";
3
+ import { AvComputedValueError } from "./computed-value.error.js";
3
4
  import { type Path } from "./field-stage.runtime.js";
4
5
  import type { OperationExecutionRequest } from "./operation-request.js";
5
6
  import type { ReferentialActionTable } from "./referential-action.js";
@@ -17,3 +18,11 @@ export type DeferredField = (resource: string, stage: WriteStage, field: string)
17
18
  * request's: `argumentsPath` and then the value's place in the arguments.
18
19
  */
19
20
  export declare function fieldValidatorIssues(contract: AvContract, references: ReferentialActionTable, request: OperationExecutionRequest, args: unknown, argumentsPath: Path, select: (path: Path) => boolean, deferred?: DeferredField): readonly AvValidationIssue[];
21
+ /**
22
+ * The server's defect when a value written to a field the active layer hides
23
+ * breaks its validators or its Json shape, or `undefined`.
24
+ */
25
+ export declare function hiddenFieldRefusal(contracts: {
26
+ readonly core: AvContract;
27
+ readonly active: AvContract;
28
+ }, references: ReferentialActionTable, request: OperationExecutionRequest, args: unknown, argumentsPath: Path): AvComputedValueError | undefined;
@@ -1,4 +1,5 @@
1
- import { validatedFieldsOf, } from "../compiler/field-validator.compiler.js";
1
+ import { hasFieldValidators, validatedFieldsOf, } from "../compiler/field-validator.compiler.js";
2
+ import { AvComputedValueError } from "./computed-value.error.js";
2
3
  import { entryCheckIssues, fieldCheckIssues } from "./field-check.evaluator.js";
3
4
  import { isPlainRecord, valueAt, writeRecordPathsOf, } from "./field-stage.runtime.js";
4
5
  import { nestedWriteTargetsOf } from "./nested-write.walker.js";
@@ -14,7 +15,10 @@ function rulesOf(contract, compiled, resource, name) {
14
15
  const field = fields !== undefined && Object.hasOwn(fields, name)
15
16
  ? fields[name]
16
17
  : undefined;
17
- if (field?.kind === "scalar" && field.json !== undefined) {
18
+ if (field === undefined) {
19
+ return undefined;
20
+ }
21
+ if (field.kind === "scalar" && field.json !== undefined) {
18
22
  return { kind: "shape", field };
19
23
  }
20
24
  const checks = Object.hasOwn(compiled, name) ? compiled[name] : undefined;
@@ -90,3 +94,33 @@ export function fieldValidatorIssues(contract, references, request, args, argume
90
94
  }
91
95
  return issues;
92
96
  }
97
+ function holdsField(contract, resource, field) {
98
+ const fields = Object.hasOwn(contract.resources, resource)
99
+ ? contract.resources[resource]?.fields
100
+ : undefined;
101
+ return fields !== undefined && Object.hasOwn(fields, field);
102
+ }
103
+ export function hiddenFieldRefusal(contracts, references, request, args, argumentsPath) {
104
+ const { core, active } = contracts;
105
+ if (core === active || !hasFieldValidators(core)) {
106
+ return undefined;
107
+ }
108
+ const judged = [];
109
+ const issues = fieldValidatorIssues(core, references, request, args, argumentsPath, () => true, (resource, stage, field) => {
110
+ if (holdsField(active, resource, field)) {
111
+ return true;
112
+ }
113
+ judged.push({ resource, stage, field });
114
+ return false;
115
+ });
116
+ const [first] = issues;
117
+ if (first === undefined) {
118
+ return undefined;
119
+ }
120
+ const path = first.path ?? [];
121
+ const at = judged.find(({ field }) => path.includes(field)) ?? judged[0];
122
+ if (at === undefined) {
123
+ return undefined;
124
+ }
125
+ return new AvComputedValueError(at.resource, at.field, at.stage, issues, `compute.${at.stage} on ${at.resource}.${at.field}, a field the ${request.origin} layer hides, returned a value its validators refuse (${first.code}: ${first.message})`);
126
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Emits a framework warning: through `process.emitWarning` (type
3
+ * `AventaraWarning`, `code` as given) where the runtime has it, else through
4
+ * `console.warn`. A warning changes no behaviour.
5
+ */
6
+ export declare function emitFrameworkWarning(message: string, code: string): void;
@@ -0,0 +1,8 @@
1
+ export function emitFrameworkWarning(message, code) {
2
+ const emitWarning = globalThis.process?.emitWarning;
3
+ if (typeof emitWarning === "function") {
4
+ emitWarning(message, { type: "AventaraWarning", code });
5
+ return;
6
+ }
7
+ globalThis.console?.warn(`AventaraWarning: ${message}`);
8
+ }
@@ -20,8 +20,9 @@ export async function runLevelLifecycle(levels, context, inner, settle, reuse, r
20
20
  }
21
21
  const result = await settle(await inner());
22
22
  const after = [...levels].reverse().flatMap((level) => level.hooks.after);
23
- 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
  };