@aventara/core 1.0.0-rc.1 → 1.0.0-rc.3

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 (97) hide show
  1. package/CHANGELOG.md +341 -1
  2. package/ERRORS.md +142 -0
  3. package/README.md +24 -1
  4. package/dist/adapters/adapter-discovery-diagnostic.guidance.d.ts +6 -0
  5. package/dist/adapters/adapter-discovery-diagnostic.guidance.js +7 -0
  6. package/dist/adapters/adapter-field.d.ts +14 -0
  7. package/dist/adapters/adapter-model.d.ts +9 -0
  8. package/dist/adapters/adapter.interface.d.ts +10 -0
  9. package/dist/compiler/contract-compiler-diagnostic.guidance.d.ts +6 -0
  10. package/dist/compiler/contract-compiler-diagnostic.guidance.js +97 -0
  11. package/dist/compiler/contract.compiler.js +94 -6
  12. package/dist/config/computed-fields.d.ts +62 -0
  13. package/dist/config/computed-fields.js +80 -0
  14. package/dist/config/config-diagnostic.guidance.d.ts +6 -0
  15. package/dist/config/config-diagnostic.guidance.js +32 -0
  16. package/dist/config/define-resource.d.ts +1 -1
  17. package/dist/config/exact-framework-config.d.ts +50 -20
  18. package/dist/config/field-compute.resolver.d.ts +2 -0
  19. package/dist/config/field-compute.resolver.js +8 -0
  20. package/dist/config/framework-config.d.ts +7 -6
  21. package/dist/config/framework-resources-config.d.ts +18 -12
  22. package/dist/config/pipeline-config.d.ts +4 -4
  23. package/dist/contracts/capability.d.ts +10 -1
  24. package/dist/contracts/capability.js +2 -0
  25. package/dist/contracts/client-contract-structure.validator.d.ts +65 -35
  26. package/dist/contracts/client-contract-structure.validator.js +4 -2
  27. package/dist/contracts/contract-canonicalizer.d.ts +7 -0
  28. package/dist/contracts/contract-canonicalizer.js +18 -4
  29. package/dist/contracts/field.d.ts +30 -1
  30. package/dist/framework/contract-view.d.ts +4 -4
  31. package/dist/framework/framework-construction.error.d.ts +12 -3
  32. package/dist/framework/framework-construction.error.js +5 -1
  33. package/dist/framework/framework.factory.d.ts +1 -1
  34. package/dist/framework/framework.factory.js +9 -2
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +1 -0
  37. package/dist/internal.d.ts +2 -0
  38. package/dist/internal.js +1 -0
  39. package/dist/operations/database-refusal.d.ts +51 -0
  40. package/dist/operations/database-refusal.js +45 -0
  41. package/dist/operations/operation-arguments.d.ts +102 -31
  42. package/dist/operations/operation-validator-core.js +145 -36
  43. package/dist/operations/validation-code.guidance.d.ts +6 -0
  44. package/dist/operations/validation-code.guidance.js +90 -0
  45. package/dist/operations/write-scope.resolver.d.ts +4 -0
  46. package/dist/operations/write-scope.resolver.js +2 -0
  47. package/dist/protocol/protocol.handler.js +55 -16
  48. package/dist/runtime/caller-data.copier.d.ts +17 -0
  49. package/dist/runtime/caller-data.copier.js +18 -1
  50. package/dist/runtime/caller-data.door.d.ts +11 -5
  51. package/dist/runtime/computed-read-runtime.js +15 -2
  52. package/dist/runtime/computed-value.error.d.ts +3 -2
  53. package/dist/runtime/computed-value.error.js +3 -2
  54. package/dist/runtime/create-requirement.d.ts +41 -5
  55. package/dist/runtime/create-requirement.js +137 -20
  56. package/dist/runtime/diagnostics-reporter.d.ts +2 -1
  57. package/dist/runtime/error-boundary.js +4 -0
  58. package/dist/runtime/error-guidance.d.ts +12 -0
  59. package/dist/runtime/error-guidance.js +1 -0
  60. package/dist/runtime/error.catalog.d.ts +49 -0
  61. package/dist/runtime/error.catalog.js +49 -0
  62. package/dist/runtime/field-stage.runtime.d.ts +2 -0
  63. package/dist/runtime/field-stage.runtime.js +8 -4
  64. package/dist/runtime/field-validator.runtime.d.ts +18 -1
  65. package/dist/runtime/field-validator.runtime.js +47 -8
  66. package/dist/runtime/json-value.d.ts +10 -5
  67. package/dist/runtime/level-lifecycle.runtime.d.ts +2 -2
  68. package/dist/runtime/level-lifecycle.runtime.js +2 -5
  69. package/dist/runtime/mutation-planner.js +3 -14
  70. package/dist/runtime/operation-code.guidance.d.ts +6 -0
  71. package/dist/runtime/operation-code.guidance.js +167 -0
  72. package/dist/runtime/operation-runtime.d.ts +20 -6
  73. package/dist/runtime/operation-runtime.js +82 -36
  74. package/dist/runtime/owned-execution.registry.d.ts +12 -2
  75. package/dist/runtime/pipe-output.validator.d.ts +10 -0
  76. package/dist/runtime/pipe-output.validator.js +4 -2
  77. package/dist/runtime/pipeline.d.ts +35 -8
  78. package/dist/runtime/readonly-value.d.ts +14 -3
  79. package/dist/runtime/readonly-value.js +10 -1
  80. package/dist/runtime/scalar-runtime.d.ts +5 -1
  81. package/dist/runtime/scalar-runtime.js +86 -16
  82. package/dist/runtime/unsent-value.normalizer.d.ts +15 -0
  83. package/dist/runtime/unsent-value.normalizer.js +128 -0
  84. package/dist/transactions/operation-fingerprint.js +2 -2
  85. package/dist/transactions/sql-statement.d.ts +8 -0
  86. package/dist/transactions/sql-statement.validator.js +36 -3
  87. package/dist/transactions/transaction-plan-assembler.d.ts +1 -1
  88. package/dist/transactions/transaction-plan-assembler.js +33 -10
  89. package/dist/transactions/transaction-plan-validator.d.ts +16 -0
  90. package/dist/transactions/transaction-plan-validator.js +27 -1
  91. package/dist/transactions/transaction-reference-validator.d.ts +42 -0
  92. package/dist/transactions/transaction-reference-validator.js +82 -56
  93. package/dist/transactions/transaction-reference.js +5 -0
  94. package/dist/transactions/transaction-runtime.js +47 -22
  95. package/dist/transactions/transaction-stage.runtime.d.ts +2 -1
  96. package/dist/transactions/transaction-stage.runtime.js +7 -4
  97. package/package.json +4 -3
@@ -268,7 +268,7 @@ function pathSegment(array, key) {
268
268
  }
269
269
  return key;
270
270
  }
271
- function mirrorControlData(value) {
271
+ function mirrorControlData(value, members) {
272
272
  const shape = reflectNode(value);
273
273
  if (shape === undefined) {
274
274
  throw new AvCallerDataError("unreadable-value", []);
@@ -375,6 +375,20 @@ function mirrorControlData(value) {
375
375
  continue;
376
376
  }
377
377
  const memberLink = childLink(link, pathSegment(shape.array, key));
378
+ const byValue = members?.copyAt(pathOf(memberLink));
379
+ if (byValue !== undefined) {
380
+ try {
381
+ store(copyCallerData(member, byValue));
382
+ }
383
+ catch (error) {
384
+ if (!(error instanceof AvCallerDataError)) {
385
+ throw error;
386
+ }
387
+ members?.refused(pathOf(memberLink), error);
388
+ store(undefined);
389
+ }
390
+ continue;
391
+ }
378
392
  const memberShape = reflectNode(member);
379
393
  if (memberShape === undefined) {
380
394
  store(UNREADABLE);
@@ -408,6 +422,9 @@ function mirrorControlData(value) {
408
422
  }
409
423
  return root;
410
424
  }
425
+ export function mirrorCallerData(value, members) {
426
+ return mirrorControlData(value, members);
427
+ }
411
428
  export function locateCallerDataError(error, copy) {
412
429
  const within = error.within;
413
430
  if (within === undefined) {
@@ -6,13 +6,19 @@ import type { AvMisuseEvent, MisuseReporter } from "./misuse-reporter.js";
6
6
  *
7
7
  * - `"caller"`: a server-side caller's own objects — copied at the door.
8
8
  * - `"owned"`: data no caller holds a reference to — a body the protocol decoder
9
- * parsed with `JSON.parse` for this request alone, or a plan the typed runner
10
- * assembled from copies its builders took. It is never copied again.
9
+ * parsed with `JSON.parse` for this request alone, a host's parsed body the
10
+ * protocol's shortcut copied at the call, or a plan the typed runner assembled
11
+ * from copies its builders took. It is never copied again.
11
12
  *
12
- * Only core's own modules choose `"owned"`: the public `framework.execute` and
13
- * `executeTransaction` always pass `"caller"`.
13
+ * - `{ refused }`: a body the protocol's shortcut copied at the call, whose copy
14
+ * failed — answered at the door as `"caller"` would answer it, never read again.
15
+ *
16
+ * Only core's own modules choose anything but `"caller"`: the public
17
+ * `framework.execute` and `executeTransaction` always pass `"caller"`.
14
18
  */
15
- export type ArgumentsSource = "caller" | "owned";
19
+ export type ArgumentsSource = "caller" | "owned" | {
20
+ readonly refused: AvCallerDataError;
21
+ };
16
22
  /** The operation identity a misuse event may name, as far as it could be read. */
17
23
  export type MisuseIdentity = Omit<AvMisuseEvent, "kind" | "path">;
18
24
  /**
@@ -1,5 +1,6 @@
1
1
  import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
2
2
  import { AvComputedValueError } from "./computed-value.error.js";
3
+ import { frozenCopy } from "./readonly-value.js";
3
4
  import { normalizeScalarFieldValue } from "./scalar-runtime.js";
4
5
  function isRecord(value) {
5
6
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -36,6 +37,12 @@ function appendUniqueProjection(values, value) {
36
37
  values.push(value);
37
38
  }
38
39
  }
40
+ function adapterIdentifier(model, resource) {
41
+ const resources = model.resources;
42
+ return ((Object.hasOwn(resources, resource)
43
+ ? resources[resource]?.identifiers?.[0]
44
+ : undefined) ?? []);
45
+ }
39
46
  function supportsProjection(request) {
40
47
  if (request.family === "upsert") {
41
48
  return true;
@@ -51,6 +58,11 @@ function addScalarProjection(core, model, active, compute, resourceName, fieldNa
51
58
  const stage = readCompute(compute?.[resourceName]?.[fieldName]?.read);
52
59
  if (stage !== undefined) {
53
60
  appendUnique(computed, fieldName);
61
+ if (stage.dependsOn.length === 0) {
62
+ for (const member of adapterIdentifier(model, resourceName)) {
63
+ appendUniqueProjection(adapterSelect, member);
64
+ }
65
+ }
54
66
  for (const dependency of stage.dependsOn) {
55
67
  if (storedScalar(model, resourceName, dependency)) {
56
68
  appendUniqueProjection(adapterSelect, dependency);
@@ -307,10 +319,11 @@ async function materializeRecord(active, compute, request, plan, value, contextF
307
319
  if (field?.kind !== "scalar" || stage === undefined) {
308
320
  continue;
309
321
  }
322
+ const view = frozenCopy(record);
310
323
  const computedValue = await stage.compute({
311
324
  field: fieldName,
312
- value: record[fieldName],
313
- record,
325
+ value: view[fieldName],
326
+ record: view,
314
327
  ctx: contextFactory(),
315
328
  });
316
329
  if (computedValue === undefined) {
@@ -1,7 +1,8 @@
1
1
  import type { AvValidationIssue } from "../operations/validation-code.js";
2
2
  /**
3
3
  * What the diagnostics channel receives when a computed value — a read or write
4
- * compute stage's return — is one its Contract refuses: the developer's broken code,
4
+ * compute stage's return — is one its Contract refuses, or a `compute.create`
5
+ * stage returned `undefined` for a field a create cannot do without (rc.2): the developer's broken code,
5
6
  * never the caller's. It is thrown, not a deliberate outcome, so the framework's
6
7
  * boundary answers the caller the generic `A3000` and reports this error, with the
7
8
  * issues, exactly once.
@@ -9,5 +10,5 @@ import type { AvValidationIssue } from "../operations/validation-code.js";
9
10
  export declare class AvComputedValueError extends Error {
10
11
  readonly issues: readonly AvValidationIssue[];
11
12
  readonly name = "AvComputedValueError";
12
- constructor(resource: string, field: string, stage: "read" | "create" | "update", issues: readonly AvValidationIssue[]);
13
+ constructor(resource: string, field: string, stage: "read" | "create" | "update", issues: readonly AvValidationIssue[], message?: string);
13
14
  }
@@ -1,9 +1,10 @@
1
1
  export class AvComputedValueError extends Error {
2
2
  issues;
3
3
  name = "AvComputedValueError";
4
- constructor(resource, field, stage, issues) {
4
+ constructor(resource, field, stage, issues, message) {
5
5
  const [first] = issues;
6
- super(`The compute.${stage} stage of ${resource}.${field} returned a value its Contract refuses${first === undefined ? "." : ` (${first.code}: ${first.message})`}`);
6
+ super(message ??
7
+ `The compute.${stage} stage of ${resource}.${field} returned a value its Contract refuses${first === undefined ? "." : ` (${first.code}: ${first.message})`}`);
7
8
  this.issues = issues;
8
9
  }
9
10
  }
@@ -1,20 +1,32 @@
1
1
  import type { AvContract } from "../contracts/contract.js";
2
+ import type { AvCreateRequirements } from "../contracts/create-requirements.js";
2
3
  import type { AvOperationIdentity } from "../operations/operation-identity.js";
3
4
  import type { AvValidationIssue } from "../operations/validation-code.js";
4
5
  import type { ReferentialActionTable } from "./referential-action.js";
5
6
  type Path = readonly (string | number)[];
7
+ /** One required field a `compute.create` stage was to fill and left missing. */
8
+ export type ComputedRequirementGap = {
9
+ readonly resource: string;
10
+ readonly field: string;
11
+ readonly issue: AvValidationIssue;
12
+ };
6
13
  /**
7
- * What the Adapter stated beside its model that the check reads: its
8
- * referential edges, for the fields that hold each reference. Read once when
9
- * the Framework is constructed. The requirements themselves are the active
10
- * Contract's.
14
+ * What the Adapter stated that the checks read: its referential edges, for the
15
+ * fields that hold each reference, and its own create requirements per
16
+ * Resource, before any layer projected them (what write compute must not leave
17
+ * missing). Read once when the Framework is constructed. The requirements the
18
+ * caller is held to are the active Contract's.
11
19
  */
12
20
  export type RecordWriteFacts = {
13
21
  readonly references: ReferentialActionTable;
22
+ readonly stated: (resource: string) => AvCreateRequirements | undefined;
14
23
  };
15
24
  /** The facts an Adapter stated, read off it once; an absent table states nothing. */
16
25
  export declare function recordWriteFactsOf(adapter: {
17
26
  readonly referentialActions?: ReferentialActionTable;
27
+ readonly model?: {
28
+ readonly resources: Readonly<Record<string, unknown>>;
29
+ };
18
30
  }): RecordWriteFacts;
19
31
  /**
20
32
  * The `V1000` issues for every create position in one operation's arguments
@@ -24,7 +36,31 @@ export declare function recordWriteFactsOf(adapter: {
24
36
  * reference both, in argument order. Empty when there are none. Total over
25
37
  * malformed input.
26
38
  */
27
- export declare function createRequirementIssues(contract: AvContract, facts: RecordWriteFacts, request: AvOperationIdentity & {
39
+ export declare function createRequirementIssues(contract: AvContract, facts: Pick<RecordWriteFacts, "references">, request: AvOperationIdentity & {
28
40
  readonly resource: string;
29
41
  }, args: unknown, argumentsPath: Path): readonly AvValidationIssue[];
42
+ /**
43
+ * The `V1000` issues for every create position of one operation's arguments
44
+ * that lacks a value the active Contract requires, and the `V1012` issues for
45
+ * every write record that sets a relation and its reference both, on the
46
+ * arguments as the caller sent them, before any pipeline stage runs. Empty when
47
+ * there are none.
48
+ */
49
+ export declare function preliminaryRecordIssues(contract: AvContract, facts: Pick<RecordWriteFacts, "references">, request: AvOperationIdentity & {
50
+ readonly resource: string;
51
+ }, args: unknown, argumentsPath: Path): readonly AvValidationIssue[];
52
+ /**
53
+ * Every required field a `compute.create` stage was to fill and that is still
54
+ * missing after write compute, at every create position of one operation's
55
+ * arguments (`create.*`'s `data`, `upsert`'s `create`, every nested create),
56
+ * in argument order, read against the Adapter's own statement of each
57
+ * Resource's create requirements; `computed` names the fields a Resource's
58
+ * `compute.create` stages fill.
59
+ * A field the parent supplies to a nested create, a group whose relation the
60
+ * record writes, and a value the caller sent are not missing. Empty when
61
+ * nothing is.
62
+ */
63
+ export declare function computedRequirementGapsOf(contract: AvContract, facts: RecordWriteFacts, request: AvOperationIdentity & {
64
+ readonly resource: string;
65
+ }, args: unknown, argumentsPath: Path, computed: (resource: string) => ReadonlySet<string>): readonly ComputedRequirementGap[];
30
66
  export {};
@@ -1,6 +1,6 @@
1
1
  import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
2
2
  import { advertisedDirectivesAt } from "../contracts/field.js";
3
- import { NESTED_WRITE_SCOPE, resolveWriteScope, } from "../operations/write-scope.resolver.js";
3
+ import { LOOPBACK_WRITE_SCOPE, NESTED_WRITE_SCOPE, resolveWriteScope, } from "../operations/write-scope.resolver.js";
4
4
  import { isRelationMutationDirective, RELATION_MUTATION_RECORD_SITES, } from "./mutation-planner.js";
5
5
  function isRecord(value) {
6
6
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -63,9 +63,19 @@ function checkCreateRecord(context, resource, record, path, scope, via) {
63
63
  if (!isRecord(record)) {
64
64
  return;
65
65
  }
66
+ if (context.computed !== undefined) {
67
+ computedRequirementGaps(context, resource, record, path, via);
68
+ walkNestedCreates(context, resource, record, path);
69
+ return;
70
+ }
66
71
  const { contract, issues } = context;
67
- const conflicted = referenceConflicts(context, resource, record, path);
68
- mixedReferenceWrites(context, resource, record, path, conflicted);
72
+ const looped = loopbackWrites(context, resource, record, path, via, "create");
73
+ const conflicted = looped.size > 0
74
+ ? looped
75
+ : referenceConflicts(context, resource, record, path);
76
+ if (looped.size === 0) {
77
+ mixedReferenceWrites(context, resource, record, path, conflicted);
78
+ }
69
79
  const requirements = requirementsOf(contract, resource);
70
80
  if (requirements !== undefined) {
71
81
  const supplied = suppliedThrough(requirements, via);
@@ -157,14 +167,93 @@ function mixedReferenceWrites(context, resource, record, path, conflicted) {
157
167
  });
158
168
  }
159
169
  }
160
- function checkUpdateRecord(context, resource, record, path) {
170
+ function checkUpdateRecord(context, resource, record, path, via) {
161
171
  if (!isRecord(record)) {
162
172
  return;
163
173
  }
164
- const conflicted = referenceConflicts(context, resource, record, path);
165
- mixedReferenceWrites(context, resource, record, path, conflicted);
174
+ if (context.computed === undefined &&
175
+ loopbackWrites(context, resource, record, path, via, "update").size === 0) {
176
+ const conflicted = referenceConflicts(context, resource, record, path);
177
+ mixedReferenceWrites(context, resource, record, path, conflicted);
178
+ }
166
179
  walkNestedCreates(context, resource, record, path);
167
180
  }
181
+ function loopbackWrites(context, resource, record, path, via, stage) {
182
+ const refused = new Set();
183
+ if (via === undefined) {
184
+ return refused;
185
+ }
186
+ const { contract } = context;
187
+ const edge = lookupField(lookupResource(contract, via.resource), via.relation);
188
+ if (edge?.kind !== "relation" || typeof edge.inverse !== "string") {
189
+ return refused;
190
+ }
191
+ const inverse = edge.inverse;
192
+ const other = lookupField(lookupResource(contract, resource), inverse);
193
+ if (other?.kind === "relation" &&
194
+ advertisedDirectivesAt(other.capabilities[stage], LOOPBACK_WRITE_SCOPE) !==
195
+ undefined) {
196
+ return refused;
197
+ }
198
+ const references = edgesOf(context.references, resource).find((candidate) => candidate.field === inverse) ?? undefined;
199
+ for (const field of [
200
+ inverse,
201
+ ...(references === undefined ? [] : referencesOf(references)),
202
+ ]) {
203
+ if (!written(record, field)) {
204
+ continue;
205
+ }
206
+ refused.add(field);
207
+ context.issues.push({
208
+ code: "V1012",
209
+ path: [...path, field],
210
+ message: field === inverse
211
+ ? `Relation "${field}" is the relation this ${resource} is written through, from its other end ("${via.resource}.${via.relation}"): the write it sits in supplies it.`
212
+ : `Field "${field}" holds the reference of "${inverse}", the relation this ${resource} is written through ("${via.resource}.${via.relation}"): the write it sits in supplies it.`,
213
+ });
214
+ }
215
+ return refused;
216
+ }
217
+ function computedRequirementGaps(context, resource, record, path, via) {
218
+ const reader = context.computed;
219
+ const computed = reader?.computed(resource);
220
+ const stated = reader?.stated(resource);
221
+ if (computed === undefined || computed.size === 0 || stated === undefined) {
222
+ return;
223
+ }
224
+ const supplied = suppliedThrough(stated, via);
225
+ const gap = (field) => {
226
+ context.gaps?.push({
227
+ resource,
228
+ field,
229
+ issue: {
230
+ code: "V1000",
231
+ path: [...path, field],
232
+ message: `compute.create on ${resource}.${field} returned undefined for a required field.`,
233
+ },
234
+ });
235
+ };
236
+ for (const field of namesOf(stated.required)) {
237
+ if (computed.has(field) &&
238
+ !supplied.has(field) &&
239
+ !written(record, field)) {
240
+ gap(field);
241
+ }
242
+ }
243
+ const groups = Array.isArray(stated.oneOf) ? stated.oneOf : [];
244
+ for (const group of groups) {
245
+ const [relation, ...references] = namesOf(group);
246
+ const left = references.find((field) => computed.has(field) && !written(record, field));
247
+ if (relation === undefined ||
248
+ left === undefined ||
249
+ supplied.has(relation) ||
250
+ references.some((field) => supplied.has(field)) ||
251
+ written(record, relation)) {
252
+ continue;
253
+ }
254
+ gap(left);
255
+ }
256
+ }
168
257
  function walkNestedCreates(context, resource, record, path) {
169
258
  if (!isRecord(record)) {
170
259
  return;
@@ -196,7 +285,7 @@ function walkNestedCreates(context, resource, record, path) {
196
285
  checkCreateRecord(context, field.target, siteRecord, sitePath, NESTED_WRITE_SCOPE, via);
197
286
  }
198
287
  else {
199
- checkUpdateRecord(context, field.target, siteRecord, sitePath);
288
+ checkUpdateRecord(context, field.target, siteRecord, sitePath, via);
200
289
  }
201
290
  }
202
291
  }
@@ -204,17 +293,52 @@ function walkNestedCreates(context, resource, record, path) {
204
293
  }
205
294
  }
206
295
  export function recordWriteFactsOf(adapter) {
207
- return { references: adapter.referentialActions ?? {} };
296
+ const resources = adapter.model?.resources ?? {};
297
+ return {
298
+ references: adapter.referentialActions ?? {},
299
+ stated: (resource) => {
300
+ const found = Object.hasOwn(resources, resource)
301
+ ? resources[resource]
302
+ : undefined;
303
+ const stated = isRecord(found) ? found.createRequirements : undefined;
304
+ return isRecord(stated) ? stated : undefined;
305
+ },
306
+ };
208
307
  }
209
308
  export function createRequirementIssues(contract, facts, request, args, argumentsPath) {
210
- if (!isRecord(args)) {
211
- return [];
212
- }
213
309
  const context = {
214
310
  contract,
215
311
  references: facts.references,
216
312
  issues: [],
217
313
  };
314
+ walkOperation(context, request, args, argumentsPath);
315
+ return context.issues;
316
+ }
317
+ export function preliminaryRecordIssues(contract, facts, request, args, argumentsPath) {
318
+ const context = {
319
+ contract,
320
+ references: facts.references,
321
+ issues: [],
322
+ };
323
+ walkOperation(context, request, args, argumentsPath);
324
+ return context.issues;
325
+ }
326
+ export function computedRequirementGapsOf(contract, facts, request, args, argumentsPath, computed) {
327
+ const gaps = [];
328
+ walkOperation({
329
+ contract,
330
+ references: facts.references,
331
+ issues: [],
332
+ computed: { stated: facts.stated, computed },
333
+ gaps,
334
+ }, request, args, argumentsPath);
335
+ return gaps;
336
+ }
337
+ function walkOperation(context, request, args, argumentsPath) {
338
+ if (!isRecord(args)) {
339
+ return;
340
+ }
341
+ const contract = context.contract;
218
342
  const scope = resolveWriteScope(lookupResource(contract, request.resource), request);
219
343
  if (request.family === "create" && written(args, "data")) {
220
344
  const data = args.data;
@@ -229,16 +353,9 @@ export function createRequirementIssues(contract, facts, request, args, argument
229
353
  }
230
354
  if (request.family === "upsert") {
231
355
  checkCreateRecord(context, request.resource, args.create, [...argumentsPath, "create"], scope, undefined);
232
- checkUpdateRecord(context, request.resource, args.update, [
233
- ...argumentsPath,
234
- "update",
235
- ]);
356
+ checkUpdateRecord(context, request.resource, args.update, [...argumentsPath, "update"], undefined);
236
357
  }
237
358
  if (request.family === "update") {
238
- checkUpdateRecord(context, request.resource, args.data, [
239
- ...argumentsPath,
240
- "data",
241
- ]);
359
+ checkUpdateRecord(context, request.resource, args.data, [...argumentsPath, "data"], undefined);
242
360
  }
243
- return context.issues;
244
361
  }
@@ -47,7 +47,8 @@ export type AvTransactionDiagnostic = {
47
47
  };
48
48
  /**
49
49
  * One unplanned failure of the MCP endpoint itself, outside any tool call's
50
- * operation: the raw error, answered `500` with no detail.
50
+ * operation: the raw error, answered `500` with no detail, or `401` when it is
51
+ * the throw of `mcp.authorization.verify`.
51
52
  */
52
53
  export type AvMcpDiagnostic = {
53
54
  readonly kind: "mcp";
@@ -1,3 +1,4 @@
1
+ import { isDatabaseRefusalMessage } from "../operations/database-refusal.js";
1
2
  import { VALIDATION_CODE_MESSAGES, VALIDATION_CODES, } from "../operations/validation-code.js";
2
3
  import { AvError } from "./framework-error.js";
3
4
  import { INTERNAL_FAILURE_MESSAGE, internalFailure, OPERATION_CODES, operationFailure, } from "./operation-response.js";
@@ -164,6 +165,9 @@ function projectIssue(value, unvouched, view, author) {
164
165
  unvouched.push(supplied);
165
166
  }
166
167
  }
168
+ else if (isDatabaseRefusalMessage(code, supplied)) {
169
+ message = supplied;
170
+ }
167
171
  if (rawPath === undefined) {
168
172
  return { code, message };
169
173
  }
@@ -0,0 +1,12 @@
1
+ /** Why a code is answered, and what the one who meets it should do. */
2
+ export type ErrorCodeGuidance = {
3
+ /** What typically produced the code, in a sentence or two. */
4
+ readonly cause: string;
5
+ /** What to change so it does not happen again. */
6
+ readonly fix: string;
7
+ };
8
+ /** Guidance for a code whose table holds no message of its own. */
9
+ export type ErrorCodeDescription = ErrorCodeGuidance & {
10
+ /** The code's meaning, in one short sentence. */
11
+ readonly message: string;
12
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import type { AvAdapterDiscoveryDiagnosticCode } from "../adapters/adapter-discovery-diagnostic.js";
2
+ import type { AvContractCompilerDiagnosticCode } from "../compiler/contract-compiler-diagnostic.js";
3
+ import type { AvConfigDiagnosticCode } from "../config/config-diagnostic.js";
4
+ import type { AvConstructionStage } from "../framework/framework-construction.error.js";
5
+ import { type AvValidationCode } from "../operations/validation-code.js";
6
+ import { type AvOperationCode } from "./operation-response.js";
7
+ /**
8
+ * The documentation page every catalog entry links into. Each code has its own
9
+ * anchor there: the code in lower case (`#a2004`, `#config_required`).
10
+ */
11
+ export declare const ERROR_CODES_DOCS_URL = "https://aventara-framework.vercel.app/docs/error-codes";
12
+ /**
13
+ * What kind of code an entry is: an outcome code's family (`success` `A1xxx`,
14
+ * `request` `A2xxx`, `internal` `A3xxx`, `auth` `A4xxx`), a `validation` issue
15
+ * code (`V1xxx`), or a startup code, by the construction stage that reports it.
16
+ */
17
+ export type AvErrorKind = "success" | "request" | "internal" | "auth" | "validation" | AvConstructionStage;
18
+ /** Every code the catalog describes: outcome, validation and startup codes. */
19
+ export type AvErrorCatalogCode = AvOperationCode | AvValidationCode | AvConfigDiagnosticCode | AvAdapterDiscoveryDiagnosticCode | AvContractCompilerDiagnosticCode;
20
+ /** One code: what it means, why it happens, what to do, and where it is documented. */
21
+ export type AvErrorCatalogEntry<C extends AvErrorCatalogCode = AvErrorCatalogCode> = {
22
+ readonly code: C;
23
+ readonly kind: AvErrorKind;
24
+ /** Its meaning in one sentence; for a V-code, the framework's message for it. */
25
+ readonly message: string;
26
+ /** What typically produces it. */
27
+ readonly cause: string;
28
+ /** What to change. */
29
+ readonly fix: string;
30
+ /** Its entry on the error-codes page. */
31
+ readonly docs: string;
32
+ };
33
+ /** The error catalog's type: one entry per code. */
34
+ export type AvErrorCatalog = {
35
+ readonly [C in AvErrorCatalogCode]: AvErrorCatalogEntry<C>;
36
+ };
37
+ /** The documentation URL of one code: the error-codes page at its anchor. */
38
+ export declare function errorDocsUrl(code: AvErrorCatalogCode): string;
39
+ /**
40
+ * Every code Aventara answers or refuses with, by code: its `kind`, `message`,
41
+ * `cause`, `fix` and `docs` URL. An operation's `code`, a validation issue's
42
+ * `code` and a startup diagnostic's `code` are all keys of it:
43
+ *
44
+ * ```ts
45
+ * AvErrorCatalog[error.code].fix;
46
+ * AvErrorCatalog.V1000.docs; // https://aventara-framework.vercel.app/docs/error-codes#v1000
47
+ * ```
48
+ */
49
+ export declare const AvErrorCatalog: AvErrorCatalog;
@@ -0,0 +1,49 @@
1
+ import { ADAPTER_DISCOVERY_DIAGNOSTIC_GUIDANCE } from "../adapters/adapter-discovery-diagnostic.guidance.js";
2
+ import { CONTRACT_COMPILER_DIAGNOSTIC_GUIDANCE } from "../compiler/contract-compiler-diagnostic.guidance.js";
3
+ import { CONFIG_DIAGNOSTIC_GUIDANCE } from "../config/config-diagnostic.guidance.js";
4
+ import { VALIDATION_CODE_GUIDANCE } from "../operations/validation-code.guidance.js";
5
+ import { VALIDATION_CODE_MESSAGES, VALIDATION_CODES, } from "../operations/validation-code.js";
6
+ import { OPERATION_CODE_GUIDANCE } from "./operation-code.guidance.js";
7
+ import { OPERATION_CODES } from "./operation-response.js";
8
+ export const ERROR_CODES_DOCS_URL = "https://aventara-framework.vercel.app/docs/error-codes";
9
+ export function errorDocsUrl(code) {
10
+ return `${ERROR_CODES_DOCS_URL}#${code.toLowerCase()}`;
11
+ }
12
+ const OUTCOME_KIND = {
13
+ "1": "success",
14
+ "2": "request",
15
+ "3": "internal",
16
+ "4": "auth",
17
+ };
18
+ function entry(code, kind, description) {
19
+ return Object.freeze({
20
+ code,
21
+ kind,
22
+ message: description.message,
23
+ cause: description.cause,
24
+ fix: description.fix,
25
+ docs: errorDocsUrl(code),
26
+ });
27
+ }
28
+ function entriesOf(table, kind) {
29
+ return Object.keys(table).map((code) => [
30
+ code,
31
+ entry(code, kind, table[code]),
32
+ ]);
33
+ }
34
+ export const AvErrorCatalog = Object.freeze(Object.fromEntries([
35
+ ...OPERATION_CODES.map((code) => [
36
+ code,
37
+ entry(code, OUTCOME_KIND[code[1]], OPERATION_CODE_GUIDANCE[code]),
38
+ ]),
39
+ ...VALIDATION_CODES.map((code) => [
40
+ code,
41
+ entry(code, "validation", {
42
+ message: VALIDATION_CODE_MESSAGES[code],
43
+ ...VALIDATION_CODE_GUIDANCE[code],
44
+ }),
45
+ ]),
46
+ ...entriesOf(CONFIG_DIAGNOSTIC_GUIDANCE, "configuration"),
47
+ ...entriesOf(ADAPTER_DISCOVERY_DIAGNOSTIC_GUIDANCE, "discovery"),
48
+ ...entriesOf(CONTRACT_COMPILER_DIAGNOSTIC_GUIDANCE, "contract-compilation"),
49
+ ]));
@@ -6,6 +6,8 @@ export type Path = readonly (string | number)[];
6
6
  type UnknownRecord = Readonly<Record<string, unknown>>;
7
7
  /** A plain object: what a write record, or a directive's operand, is. */
8
8
  export declare function isPlainRecord(value: unknown): value is UnknownRecord;
9
+ /** The containers a family writes, by argument key. */
10
+ export declare const WRITE_KEYS: Readonly<Partial<Record<AvOperationFamily, readonly string[]>>>;
9
11
  /** The fields of `resource` with stages, in the Resource's field order. */
10
12
  export declare function stagedFieldsOf(contract: AvContract, resource: string, stages: Readonly<Record<string, ResolvedFieldStages>>, kind: "pipes" | "guards"): readonly string[];
11
13
  /** The value at `path` in `args`, or `undefined` when nothing is there. */
@@ -1,3 +1,4 @@
1
+ import { frozenCopy } from "./readonly-value.js";
1
2
  export function isPlainRecord(value) {
2
3
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
3
4
  return false;
@@ -5,7 +6,7 @@ export function isPlainRecord(value) {
5
6
  const prototype = Object.getPrototypeOf(value);
6
7
  return prototype === Object.prototype || prototype === null;
7
8
  }
8
- const WRITE_KEYS = {
9
+ export const WRITE_KEYS = {
9
10
  create: ["data"],
10
11
  update: ["data"],
11
12
  upsert: ["create", "update"],
@@ -104,9 +105,12 @@ export async function pipeSentFields(stages, fields, records, args, select) {
104
105
  }
105
106
  let value = record[field];
106
107
  for (const pipe of stages[field]?.pipes ?? []) {
107
- const next = await pipe(value);
108
+ const told = frozenCopy(value);
109
+ const next = await pipe(told);
108
110
  if (next !== undefined) {
109
- value = next;
111
+ if (next !== told) {
112
+ value = next;
113
+ }
110
114
  changed = true;
111
115
  }
112
116
  }
@@ -133,7 +137,7 @@ export async function guardSentFields(stages, fields, records, args, context) {
133
137
  }
134
138
  for (const guard of stages[field]?.guards ?? []) {
135
139
  const told = context();
136
- if ((await guard(record[field], told)) === false) {
140
+ if ((await guard(frozenCopy(record[field]), told)) === false) {
137
141
  return false;
138
142
  }
139
143
  }
@@ -1,11 +1,28 @@
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";
7
+ /** The write phase of a record: a created row (an upsert's `create` branch too) or an updated one. */
8
+ export type WriteStage = "create" | "update";
9
+ /**
10
+ * The fields a judgment leaves for later: a plan step's (or a forced call's)
11
+ * literal values are judged before BEGIN, except a field a write compute stage
12
+ * of that phase may still change, which is judged after write compute.
13
+ */
14
+ export type DeferredField = (resource: string, stage: WriteStage, field: string) => boolean;
6
15
  /**
7
16
  * Every way the sent fields `select` admits break their validators: the
8
17
  * operation's own records, then each nested write target's. Paths are the
9
18
  * request's: `argumentsPath` and then the value's place in the arguments.
10
19
  */
11
- export declare function fieldValidatorIssues(contract: AvContract, references: ReferentialActionTable, request: OperationExecutionRequest, args: unknown, argumentsPath: Path, select: (path: Path) => boolean): readonly AvValidationIssue[];
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;