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

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 +295 -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 +48 -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 +86 -4
  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 +9 -1
  65. package/dist/runtime/field-validator.runtime.js +11 -6
  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 +77 -35
  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
@@ -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
  }
@@ -3,9 +3,17 @@ import type { AvValidationIssue } from "../operations/validation-code.js";
3
3
  import { type Path } from "./field-stage.runtime.js";
4
4
  import type { OperationExecutionRequest } from "./operation-request.js";
5
5
  import type { ReferentialActionTable } from "./referential-action.js";
6
+ /** The write phase of a record: a created row (an upsert's `create` branch too) or an updated one. */
7
+ export type WriteStage = "create" | "update";
8
+ /**
9
+ * The fields a judgment leaves for later: a plan step's (or a forced call's)
10
+ * literal values are judged before BEGIN, except a field a write compute stage
11
+ * of that phase may still change, which is judged after write compute.
12
+ */
13
+ export type DeferredField = (resource: string, stage: WriteStage, field: string) => boolean;
6
14
  /**
7
15
  * Every way the sent fields `select` admits break their validators: the
8
16
  * operation's own records, then each nested write target's. Paths are the
9
17
  * request's: `argumentsPath` and then the value's place in the arguments.
10
18
  */
11
- export declare function fieldValidatorIssues(contract: AvContract, references: ReferentialActionTable, request: OperationExecutionRequest, args: unknown, argumentsPath: Path, select: (path: Path) => boolean): readonly AvValidationIssue[];
19
+ export declare function fieldValidatorIssues(contract: AvContract, references: ReferentialActionTable, request: OperationExecutionRequest, args: unknown, argumentsPath: Path, select: (path: Path) => boolean, deferred?: DeferredField): readonly AvValidationIssue[];
@@ -36,17 +36,17 @@ function entryIssues(rules, entry, at) {
36
36
  const { compiled } = rules;
37
37
  return entryCheckIssues(compiled.checks, entry, compiled.subject ?? "string", at);
38
38
  }
39
- function recordIssues(contract, resource, records, args, argumentsPath, select) {
39
+ function recordIssues(contract, resource, records, args, argumentsPath, select, deferred) {
40
40
  const compiled = validatedFieldsOf(contract, resource);
41
41
  const issues = [];
42
- for (const path of records) {
42
+ for (const { path, stage } of records) {
43
43
  const record = valueAt(args, path);
44
44
  if (!isPlainRecord(record)) {
45
45
  continue;
46
46
  }
47
47
  for (const name of Object.keys(record)) {
48
48
  const rules = rulesOf(contract, compiled, resource, name);
49
- if (rules === undefined) {
49
+ if (rules === undefined || deferred?.(resource, stage, name) === true) {
50
50
  continue;
51
51
  }
52
52
  const fieldPath = [...path, name];
@@ -78,10 +78,15 @@ function recordIssues(contract, resource, records, args, argumentsPath, select)
78
78
  }
79
79
  return issues;
80
80
  }
81
- export function fieldValidatorIssues(contract, references, request, args, argumentsPath, select) {
82
- const issues = recordIssues(contract, request.resource, writeRecordPathsOf(request.family, args), args, argumentsPath, select);
81
+ export function fieldValidatorIssues(contract, references, request, args, argumentsPath, select, deferred) {
82
+ const issues = recordIssues(contract, request.resource, writeRecordPathsOf(request.family, args).map((path) => ({
83
+ path,
84
+ stage: request.family === "create" || path[0] === "create"
85
+ ? "create"
86
+ : "update",
87
+ })), args, argumentsPath, select, deferred);
83
88
  for (const target of nestedWriteTargetsOf(contract, references, request, args)) {
84
- issues.push(...recordIssues(contract, target.resource, target.records.map((site) => site.path), args, argumentsPath, select));
89
+ issues.push(...recordIssues(contract, target.resource, target.records, args, argumentsPath, select, deferred));
85
90
  }
86
91
  return issues;
87
92
  }
@@ -1,15 +1,20 @@
1
1
  /**
2
2
  * A JSON value: the form of the `json` scalar, in arguments and results.
3
3
  *
4
- * A `Date`, a `bigint`, a `Uint8Array`, a {@link AvDecimal}, any other class
5
- * instance and an `undefined` property are refused (`V1001`), as are a
6
- * non-finite `number` (`V1004`) and a cycle (`V1001`).
4
+ * A `Date`, a `bigint`, a `Uint8Array`, a {@link AvDecimal} and any other class
5
+ * instance are refused (`V1001`), as are a non-finite `number` (`V1004`) and a
6
+ * cycle (`V1001`). An `undefined` inside the value is what JSON makes of it: a key
7
+ * holding it is left out, an array element holding it (or a hole) is `null`. The
8
+ * type admits a key holding `undefined`, so an object with an optional property is
9
+ * a JSON value; an array element stays typed without it, as JSON has no such
10
+ * value.
7
11
  *
8
- * Results are fresh plain copies, so mutating one is safe.
12
+ * Results are fresh plain copies, so mutating one is safe; a result never holds a
13
+ * key whose value is `undefined`.
9
14
  *
10
15
  * A `json` field's value is this without its top-level `null`: a field-level
11
16
  * `null` is the field's absence, which only `nullable: true` admits.
12
17
  */
13
18
  export type AvJsonValue = string | number | boolean | null | readonly AvJsonValue[] | {
14
- readonly [key: string]: AvJsonValue;
19
+ readonly [key: string]: AvJsonValue | undefined;
15
20
  };
@@ -17,8 +17,8 @@ export type NextReuse<Refusal extends Error = Error> = {
17
17
  * Runs `levels`' lifecycle around `inner`: every interceptor, outer to inner,
18
18
  * around every before hook, `inner`, and every after hook. `settle` turns what
19
19
  * `inner` or an interceptor below resolved into what the stage above sees (the
20
- * operation read-materializes it). An after hook is told a deep-frozen copy of
21
- * the result; the result itself goes on to the interceptors, which may answer a
20
+ * operation read-materializes it). Each after hook is told its own deep-frozen
21
+ * copy of the result; the result itself goes on to the interceptors, which may answer a
22
22
  * different one. Every `next()` is single use: a second call rejects with
23
23
  * `refuse()`'s error, recorded in `reuse`, and runs nothing.
24
24
  */
@@ -20,11 +20,8 @@ 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
- if (after.length > 0) {
24
- const told = frozenCopy(result);
25
- for (const hook of after) {
26
- await hook(told, context());
27
- }
23
+ for (const hook of after) {
24
+ await hook(frozenCopy(result), context());
28
25
  }
29
26
  return result;
30
27
  };
@@ -1,7 +1,8 @@
1
1
  import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
2
2
  import { AvComputedValueError } from "./computed-value.error.js";
3
3
  import { replaceAt, valueAt } from "./field-stage.runtime.js";
4
- import { jsonShapeIssues, normalizeScalarFieldValue, } from "./scalar-runtime.js";
4
+ import { frozenCopy } from "./readonly-value.js";
5
+ import { normalizeScalarFieldValue } from "./scalar-runtime.js";
5
6
  function writeScalarField(contracts, resourceName, fieldName) {
6
7
  const field = lookupField(lookupResource(contracts.core, resourceName), fieldName) ??
7
8
  lookupField(lookupResource(contracts.active, resourceName), fieldName);
@@ -57,7 +58,7 @@ async function computeRecord(contracts, compute, resourceName, stage, data, cont
57
58
  if (field === undefined) {
58
59
  continue;
59
60
  }
60
- const snapshot = Object.freeze({ ...logicalData });
61
+ const snapshot = frozenCopy(logicalData);
61
62
  const computed = await callback({
62
63
  field: fieldName,
63
64
  value: snapshot[fieldName],
@@ -79,18 +80,6 @@ async function computeRecord(contracts, compute, resourceName, stage, data, cont
79
80
  if (!validation.ok) {
80
81
  throw new AvComputedValueError(resourceName, fieldName, stage, validation.issues);
81
82
  }
82
- const shapeIssues = jsonShapeIssues(field, validation.value, [
83
- "resources",
84
- resourceName,
85
- "fields",
86
- fieldName,
87
- "pipelines",
88
- "compute",
89
- stage,
90
- ]);
91
- if (shapeIssues.length > 0) {
92
- throw new AvComputedValueError(resourceName, fieldName, stage, shapeIssues);
93
- }
94
83
  logicalData[fieldName] = validation.value;
95
84
  }
96
85
  }
@@ -0,0 +1,6 @@
1
+ import type { ErrorCodeDescription } from "./error-guidance.js";
2
+ import type { AvOperationCode } from "./operation-response.js";
3
+ /** Meaning, cause and fix of every outcome code. */
4
+ export declare const OPERATION_CODE_GUIDANCE: {
5
+ readonly [C in AvOperationCode]: ErrorCodeDescription;
6
+ };