@aventara/core 1.0.0-rc.12 → 1.0.0-rc.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +152 -1
  2. package/ERRORS.md +7 -6
  3. package/README.md +3 -2
  4. package/dist/avz/avz-check.factory.d.ts +2 -0
  5. package/dist/avz/avz-check.factory.js +12 -0
  6. package/dist/avz/avz.d.ts +6 -0
  7. package/dist/avz/avz.js +1 -0
  8. package/dist/compiler/contract-compiler-diagnostic.d.ts +10 -0
  9. package/dist/compiler/contract-compiler-diagnostic.guidance.js +5 -0
  10. package/dist/compiler/contract.compiler.js +102 -12
  11. package/dist/compiler/create-requirements.projector.d.ts +45 -3
  12. package/dist/compiler/create-requirements.projector.js +239 -28
  13. package/dist/compiler/managed-elsewhere.projector.d.ts +3 -0
  14. package/dist/compiler/managed-elsewhere.projector.js +1 -1
  15. package/dist/config/computed-fields.d.ts +8 -0
  16. package/dist/config/computed-fields.js +1 -1
  17. package/dist/config/config-diagnostic.guidance.js +2 -2
  18. package/dist/config/config.resolver.d.ts +2 -2
  19. package/dist/config/config.validator.js +15 -6
  20. package/dist/config/exact-framework-config.d.ts +1 -0
  21. package/dist/config/field-pipeline-config.d.ts +2 -2
  22. package/dist/config/forced-operation.validator.js +3 -2
  23. package/dist/config/framework-config.d.ts +8 -3
  24. package/dist/config/framework-exemptions-config.d.ts +13 -8
  25. package/dist/config/framework-resources-config.d.ts +11 -3
  26. package/dist/contracts/client-contract-structure.validator.d.ts +4 -1
  27. package/dist/contracts/client-contract-structure.validator.js +3 -2
  28. package/dist/contracts/contract-limits.d.ts +8 -1
  29. package/dist/contracts/contract-limits.js +1 -0
  30. package/dist/contracts/contract.d.ts +7 -3
  31. package/dist/contracts/transaction-capability.d.ts +8 -0
  32. package/dist/contracts/transaction-capability.js +7 -0
  33. package/dist/framework/access-facade.js +14 -6
  34. package/dist/framework/contract-view.d.ts +91 -9
  35. package/dist/framework/operation-facade.d.ts +8 -3
  36. package/dist/internal.d.ts +1 -0
  37. package/dist/internal.js +1 -0
  38. package/dist/operations/validation-code.guidance.js +1 -1
  39. package/dist/policy/access-provider.interface.d.ts +7 -2
  40. package/dist/policy/access-question.runtime.js +2 -1
  41. package/dist/prebuilt/core-version.generated.d.ts +1 -1
  42. package/dist/prebuilt/core-version.generated.js +1 -1
  43. package/dist/protocol/absent-route.responder.d.ts +2 -2
  44. package/dist/protocol/absent-route.responder.js +2 -1
  45. package/dist/protocol/av-protocol.d.ts +1 -0
  46. package/dist/protocol/av-protocol.js +1 -0
  47. package/dist/protocol/protocol-surface.js +2 -1
  48. package/dist/protocol/protocol.handler.js +3 -1
  49. package/dist/runtime/context-executor.runtime.js +2 -1
  50. package/dist/runtime/create-requirement.d.ts +5 -1
  51. package/dist/runtime/create-requirement.js +10 -2
  52. package/dist/runtime/field-check.evaluator.js +39 -2
  53. package/dist/runtime/field-validator.runtime.d.ts +4 -3
  54. package/dist/runtime/field-validator.runtime.js +29 -0
  55. package/dist/runtime/mutation-planner.js +26 -9
  56. package/dist/runtime/operation-code.guidance.js +3 -3
  57. package/dist/runtime/operation-runtime.d.ts +10 -9
  58. package/dist/runtime/operation-runtime.js +20 -18
  59. package/dist/runtime/pipeline.d.ts +1 -15
  60. package/dist/runtime/state-refusal.mapper.d.ts +16 -0
  61. package/dist/runtime/state-refusal.mapper.js +40 -0
  62. package/dist/transactions/operation-fingerprint.d.ts +15 -2
  63. package/dist/transactions/transaction-plan-validator.js +31 -1
  64. package/dist/transactions/transaction-runtime.js +1 -1
  65. package/package.json +1 -1
  66. package/src/runtime/field-check.evaluator.ts +46 -2
@@ -21,12 +21,16 @@ export type RecordWriteFacts = {
21
21
  readonly references: ReferentialActionTable;
22
22
  readonly stated: (resource: string) => AvCreateRequirements | undefined;
23
23
  };
24
- /** The facts an Adapter stated, read off it once; an absent table states nothing. */
24
+ /**
25
+ * The facts an Adapter stated, read off it once, with the non-null added
26
+ * (`VIRTUAL`) fields of `core` required too; an absent table states nothing.
27
+ */
25
28
  export declare function recordWriteFactsOf(adapter: {
26
29
  readonly referentialActions?: ReferentialActionTable;
27
30
  readonly model?: {
28
31
  readonly resources: Readonly<Record<string, unknown>>;
29
32
  };
33
+ readonly core?: AvContract;
30
34
  }): RecordWriteFacts;
31
35
  /**
32
36
  * The `V1000` issues for every create position in one operation's arguments
@@ -1,3 +1,4 @@
1
+ import { withVirtualRequirements } from "../compiler/create-requirements.projector.js";
1
2
  import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
2
3
  import { advertisedDirectivesAt } from "../contracts/field.js";
3
4
  import { LOOPBACK_WRITE_SCOPE, NESTED_WRITE_SCOPE, resolveWriteScope, } from "../operations/write-scope.resolver.js";
@@ -292,14 +293,21 @@ function walkNestedCreates(context, resource, record, path) {
292
293
  }
293
294
  export function recordWriteFactsOf(adapter) {
294
295
  const resources = adapter.model?.resources ?? {};
296
+ const contracted = adapter.core?.resources ?? {};
295
297
  return {
296
298
  references: adapter.referentialActions ?? {},
297
299
  stated: (resource) => {
298
300
  const found = Object.hasOwn(resources, resource)
299
301
  ? resources[resource]
300
302
  : undefined;
301
- const stated = isRecord(found) ? found.createRequirements : undefined;
302
- return isRecord(stated) ? stated : undefined;
303
+ const own = isRecord(found) ? found.createRequirements : undefined;
304
+ const stated = isRecord(own) ? own : undefined;
305
+ const fields = Object.hasOwn(contracted, resource)
306
+ ? contracted[resource]?.fields
307
+ : undefined;
308
+ return fields === undefined
309
+ ? stated
310
+ : withVirtualRequirements(stated, { fields });
303
311
  },
304
312
  };
305
313
  }
@@ -418,10 +418,47 @@ const FORMAT_LABELS = {
418
418
  function plural(count, one, many) {
419
419
  return `${count} ${count === 1 ? one : many}`;
420
420
  }
421
+ const PATTERN_SYNTAX = "\\^$.*+?()[]{}|/";
422
+ function listedValues(pattern) {
423
+ const { source, flags } = pattern;
424
+ if (flags !== "" || !source.startsWith("^(?:") || !source.endsWith(")$")) {
425
+ return undefined;
426
+ }
427
+ const body = source.slice(4, -2);
428
+ const values = [];
429
+ let value = "";
430
+ for (let index = 0; index < body.length; index += 1) {
431
+ const character = body.charAt(index);
432
+ if (character === "\\") {
433
+ const escaped = body.charAt(index + 1);
434
+ if (escaped === "" || !PATTERN_SYNTAX.includes(escaped)) {
435
+ return undefined;
436
+ }
437
+ value += escaped;
438
+ index += 1;
439
+ }
440
+ else if (character === "|") {
441
+ values.push(value);
442
+ value = "";
443
+ }
444
+ else if (PATTERN_SYNTAX.includes(character)) {
445
+ return undefined;
446
+ }
447
+ else {
448
+ value += character;
449
+ }
450
+ }
451
+ values.push(value);
452
+ return values;
453
+ }
421
454
  function formatMessage(check) {
422
455
  switch (check.format) {
423
- case "regex":
424
- return `Value must match the pattern /${check.pattern.source}/${check.pattern.flags}.`;
456
+ case "regex": {
457
+ const listed = listedValues(check.pattern);
458
+ return listed === undefined
459
+ ? `Value must match the pattern /${check.pattern.source}/${check.pattern.flags}.`
460
+ : `Value must be one of ${listed.map((value) => JSON.stringify(value)).join(", ")}.`;
461
+ }
425
462
  case "includes":
426
463
  return `Value must include ${JSON.stringify(check.includes)}.`;
427
464
  case "starts_with":
@@ -7,11 +7,12 @@ import type { ReferentialActionTable } from "./referential-action.js";
7
7
  /** The write phase of a record: a created row (an upsert's `create` branch too) or an updated one. */
8
8
  type WriteStage = "create" | "update";
9
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.
10
+ * The fields a judgment leaves out (`hiddenFieldRefusal`: the ones the active
11
+ * layer shows, which its own judgment covers).
13
12
  */
14
13
  type DeferredField = (resource: string, stage: WriteStage, field: string) => boolean;
14
+ /** Whether the value at a path differs between `before` and `after`. */
15
+ export declare function valuesChangedBetween(before: unknown, after: unknown): (path: Path) => boolean;
15
16
  /**
16
17
  * Every way the sent fields `select` admits break their validators: the
17
18
  * operation's own records, then each nested write target's. Paths are the
@@ -1,5 +1,6 @@
1
1
  import { hasFieldValidators, validatedFieldsOf, } from "../compiler/field-validator.compiler.js";
2
2
  import { AvComputedValueError } from "./computed-value.error.js";
3
+ import { isAvDecimal } from "./decimal.js";
3
4
  import { entryCheckIssues, fieldCheckIssues } from "./field-check.evaluator.js";
4
5
  import { valueAt, writeRecordPathsOf, } from "./field-stage.runtime.js";
5
6
  import { nestedWriteTargetsOf } from "./nested-write.walker.js";
@@ -41,6 +42,34 @@ function entryIssues(rules, entry, at) {
41
42
  const { compiled } = rules;
42
43
  return entryCheckIssues(compiled.checks, entry, compiled.subject ?? "string", at);
43
44
  }
45
+ function sameWrittenValue(left, right) {
46
+ if (Object.is(left, right)) {
47
+ return true;
48
+ }
49
+ if (left instanceof Date && right instanceof Date) {
50
+ return Object.is(left.getTime(), right.getTime());
51
+ }
52
+ if (isAvDecimal(left) && isAvDecimal(right)) {
53
+ return left.toString() === right.toString();
54
+ }
55
+ if (left instanceof Uint8Array && right instanceof Uint8Array) {
56
+ return (left.length === right.length &&
57
+ left.every((byte, index) => byte === right[index]));
58
+ }
59
+ if (Array.isArray(left) && Array.isArray(right)) {
60
+ return (left.length === right.length &&
61
+ left.every((entry, index) => sameWrittenValue(entry, right[index])));
62
+ }
63
+ if (isPlainRecord(left) && isPlainRecord(right)) {
64
+ const keys = Object.keys(left);
65
+ return (keys.length === Object.keys(right).length &&
66
+ keys.every((key) => Object.hasOwn(right, key) && sameWrittenValue(left[key], right[key])));
67
+ }
68
+ return false;
69
+ }
70
+ export function valuesChangedBetween(before, after) {
71
+ return (path) => !sameWrittenValue(valueAt(before, path), valueAt(after, path));
72
+ }
44
73
  function recordIssues(contract, resource, records, args, argumentsPath, select, deferred) {
45
74
  const compiled = validatedFieldsOf(contract, resource);
46
75
  const issues = [];
@@ -42,6 +42,7 @@ async function computeRecord(contracts, compute, resourceName, stage, data, cont
42
42
  return data;
43
43
  }
44
44
  const logicalData = cloneRecord(data);
45
+ let changed = false;
45
46
  const resourceCompute = compute[resourceName];
46
47
  if (resourceCompute !== undefined) {
47
48
  for (const [fieldName, fieldCompute] of Object.entries(resourceCompute)) {
@@ -79,9 +80,10 @@ async function computeRecord(contracts, compute, resourceName, stage, data, cont
79
80
  throw new AvComputedValueError(resourceName, fieldName, stage, validation.issues);
80
81
  }
81
82
  logicalData[fieldName] = validation.value;
83
+ changed = true;
82
84
  }
83
85
  }
84
- return logicalData;
86
+ return changed ? logicalData : data;
85
87
  }
86
88
  async function mapRelationMutationRecords(relation, value, leaf) {
87
89
  if (!isRecord(value)) {
@@ -121,23 +123,36 @@ export async function applyWriteCompute(contracts, compute, request, args, conte
121
123
  return args;
122
124
  }
123
125
  const logicalArgs = cloneRecord(args);
126
+ let changed = false;
127
+ const computeList = async (records, stage) => {
128
+ const computed = await Promise.all(records.map((entry) => computeRecord(contracts, compute, request.resource, stage, entry, contextFactory)));
129
+ return computed.every((entry, index) => entry === records[index])
130
+ ? records
131
+ : computed;
132
+ };
133
+ const computeAt = async (key, stage) => {
134
+ const record = logicalArgs[key];
135
+ const computed = Array.isArray(record) && request.family === "create"
136
+ ? await computeList(record, stage)
137
+ : await computeRecord(contracts, compute, request.resource, stage, record, contextFactory);
138
+ changed ||= computed !== record;
139
+ logicalArgs[key] = computed;
140
+ };
124
141
  if (request.family === "create" && "data" in logicalArgs) {
125
- logicalArgs.data = Array.isArray(logicalArgs.data)
126
- ? await Promise.all(logicalArgs.data.map((entry) => computeRecord(contracts, compute, request.resource, "create", entry, contextFactory)))
127
- : await computeRecord(contracts, compute, request.resource, "create", logicalArgs.data, contextFactory);
142
+ await computeAt("data", "create");
128
143
  }
129
144
  if (request.family === "update" && "data" in logicalArgs) {
130
- logicalArgs.data = await computeRecord(contracts, compute, request.resource, "update", logicalArgs.data, contextFactory);
145
+ await computeAt("data", "update");
131
146
  }
132
147
  if (request.family === "upsert") {
133
148
  if ("create" in logicalArgs) {
134
- logicalArgs.create = await computeRecord(contracts, compute, request.resource, "create", logicalArgs.create, contextFactory);
149
+ await computeAt("create", "create");
135
150
  }
136
151
  if ("update" in logicalArgs) {
137
- logicalArgs.update = await computeRecord(contracts, compute, request.resource, "update", logicalArgs.update, contextFactory);
152
+ await computeAt("update", "update");
138
153
  }
139
154
  }
140
- return logicalArgs;
155
+ return changed ? logicalArgs : args;
141
156
  }
142
157
  export async function applyNestedWriteCompute(contracts, compute, args, targets, context) {
143
158
  if (compute === undefined) {
@@ -154,7 +169,9 @@ export async function applyNestedWriteCompute(contracts, compute, args, targets,
154
169
  continue;
155
170
  }
156
171
  const computed = await computeRecord(contracts, compute, target.resource, site.stage, record, () => context(target, valueAt(current, target.path)));
157
- current = replaceAt(current, site.path, computed);
172
+ if (computed !== record) {
173
+ current = replaceAt(current, site.path, computed);
174
+ }
158
175
  }
159
176
  }
160
177
  return current;
@@ -71,7 +71,7 @@ export const OPERATION_CODE_GUIDANCE = {
71
71
  },
72
72
  A2004: {
73
73
  message: "Validation failed.",
74
- cause: 'The arguments break a contract rule or a field validator. cause.issues lists each problem by V-code and path; a validator\'s issue also has check and params. The validators judge the values after the pipes and write compute, so a computed value they refuse is here too, at its field. A required create field left out is here (V1000). So is a value the database itself refused: too long for its column or its index, or out of its range (V1004), or holding a character it cannot store or a pattern it cannot read (V1001), with the issue at `["arguments"]`, or at the value where the adapter can place it (a bigint its driver cannot bind), and its message saying what the database refused. So is a record written through a relation that writes that relation, or a field holding its reference, back (V1012, at that field), where the adapter admits no such write.',
74
+ cause: 'The arguments break a contract rule or a field validator. cause.issues lists each problem by V-code and path; a validator\'s issue also has check and params. The validators judge every value the caller sent after its pipes and before write compute, a field with a compute stage included, so a bad value is refused even when compute would replace it; then a value write compute set or changed, so a computed value they refuse is here too, at its field. A required create field left out is here (V1000). So is a value the database itself refused: too long for its column or its index, or out of its range (V1004), or holding a character it cannot store or a pattern it cannot read (V1001), with the issue at `["arguments"]`, or at the value where the adapter can place it (a bigint its driver cannot bind), and its message saying what the database refused. So is a record written through a relation that writes that relation, or a field holding its reference, back (V1012, at that field), where the adapter admits no such write.',
75
75
  fix: "Fix the argument each issue's path points at, as its V-code says. A field the server must fill takes a compute.create stage. For a value the database refused, send one its column holds. For a nested write that writes its relation back, leave that field out: the parent supplies it.",
76
76
  },
77
77
  A2005: {
@@ -151,12 +151,12 @@ export const OPERATION_CODE_GUIDANCE = {
151
151
  },
152
152
  A4000: {
153
153
  message: "Unauthenticated.",
154
- cause: 'A guard threw new AvError("A4000", ...): authentication is required or failed. The framework never authenticates by itself.',
154
+ cause: 'A guard threw new AvError("A4000", ...), or the host\'s or access provider\'s state rejected with an object whose status or statusCode is the number 401 (an authentication library\'s refusal, such as Better Auth\'s statusCode 401): authentication is required or failed. Such a refusal answers {"data":null,"code":"A4000","cause":{"message":"Unauthenticated."}}, nothing of the library\'s error, and is not sent to diagnostics. A string status ("401"), another number, or a status and statusCode that are two different numbers is no refusal and answers 500 A3000; a number beside a string is read by the number. The rule holds on every protocol route (operations, cached reads, /_transactions, /_all, /_access/can), on MCP tool calls and in Next.js; in process (avClient in a Server Component or Server Action, serverClient.$access.can given headers) the call rejects with an AvAuthError. The framework never authenticates by itself.',
155
155
  fix: "Send the credentials your guards expect, such as the Authorization header.",
156
156
  },
157
157
  A4001: {
158
158
  message: "Forbidden.",
159
- cause: 'A guard returned false ("Operation denied by a framework guard."), a field write guard or a nested write\'s target guard included, or a stage threw A4001.',
159
+ cause: "A guard returned false (\"Operation denied by a framework guard.\"), a field write guard or a nested write's target guard included, a stage threw A4001, or the host's or access provider's state rejected with an object whose status or statusCode is the number 403, read by the same rule as A4000's 401 (answered with this message only, not sent to diagnostics).",
160
160
  fix: "Call as a caller the guard admits, or stop sending the guarded field. To answer another code, throw an AvError from the guard.",
161
161
  },
162
162
  A4002: {
@@ -139,25 +139,26 @@ type AdmissionScope = {
139
139
  /** Before BEGIN a plan step's `$ref` slots are unresolved: these admit them. */
140
140
  readonly validation?: OperationValidationOptions;
141
141
  /**
142
- * A plan step or a forced call, admitted before BEGIN: the validators judge
143
- * there the sent values no later stage can change (a field with no write
144
- * compute stage of its record's phase), so a caller's refusal opens no
145
- * transaction. Everything is judged again after write compute.
142
+ * The sent values the validators judge, when not the ones `select` admits: at
143
+ * a step a transaction pipe changed, its resolved `$ref` values and every value
144
+ * the pipe changed since the step's admission.
146
145
  */
147
- readonly beforeBegin?: boolean;
146
+ readonly judge?: (path: readonly (string | number)[]) => boolean;
148
147
  };
149
148
  /**
150
- * Runs an operation's admission — its guards and pipes, and before BEGIN the
151
- * validators of the values nothing after it changes — and answers the
152
- * deep-frozen arguments the rest of the operation reads, or the refusal.
149
+ * Runs an operation's admission — its guards, pipes and field pipes, then the
150
+ * validators of the values the caller sent — and answers the deep-frozen
151
+ * arguments the rest of the operation reads, or the refusal.
153
152
  */
154
153
  export declare function admitOperation<M extends AvAdapterModel>(dependencies: OperationPipelineDependencies<M>, request: StatefulOperationRequest, contract: AvContract, args: unknown, argumentsPath: readonly (string | number)[], report: DiagnosticsReporter, scope: ExecutorScope<M>, admission?: AdmissionScope): Promise<AdmittedOperation>;
155
154
  /** How a plan step's arguments were changed between their admission and the step. */
156
155
  type StepChanges = {
157
156
  /** The sent `$ref` values written at this step: their field stages run now. */
158
157
  readonly written: (path: readonly (string | number)[]) => boolean;
159
- /** A transaction pipe changed the step: every value is judged again. */
158
+ /** A transaction pipe changed the step: what it changed is judged again. */
160
159
  readonly transactionPiped: boolean;
160
+ /** The step's arguments as admitted before BEGIN, what a transaction pipe's change is told apart from. */
161
+ readonly admitted: unknown;
161
162
  /** An operation pipe ran before BEGIN on a step with `$ref`s: the resolved arguments are checked again. */
162
163
  readonly pipedAroundReferences: boolean;
163
164
  };
@@ -2,6 +2,7 @@ import { hasFieldValidators } from "../compiler/field-validator.compiler.js";
2
2
  import { createComputedFields } from "../config/field-compute.resolver.js";
3
3
  import { fieldStagesOf, isForcedOperation, nestedTargetLevels, operationLevels, rawPipelineTree, transactionLevelsOf, } from "../config/pipeline-tree.resolver.js";
4
4
  import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
5
+ import { opensServerTransactions } from "../contracts/transaction-capability.js";
5
6
  import { findOperationAvailabilityIssue, validateOperationArguments, } from "../operations/operation-validator.js";
6
7
  import { enforceReadPolicy, } from "../policy/policy-read.runtime.js";
7
8
  import { admitRequestPolicy, openRequestPolicy, } from "../policy/policy-request.js";
@@ -18,7 +19,7 @@ import { createDiagnosticsReporter, } from "./diagnostics-reporter.js";
18
19
  import { AdapterBoundary, OPERATION_ARGUMENTS_PATH, resolveOperationFailure, sanitizeOperationFailure, } from "./error-boundary.js";
19
20
  import { planFieldStages } from "./field-stage.plan.js";
20
21
  import { guardSentFields, pipeSentFields, valueAt, writeRecordPathsOf, } from "./field-stage.runtime.js";
21
- import { fieldValidatorIssues, hiddenFieldRefusal, } from "./field-validator.runtime.js";
22
+ import { fieldValidatorIssues, hiddenFieldRefusal, valuesChangedBetween, } from "./field-validator.runtime.js";
22
23
  import { thrownFailure } from "./framework-error.factory.js";
23
24
  import { AvInterceptorNextError } from "./interceptor-next.error.js";
24
25
  import { runLevelLifecycle, } from "./level-lifecycle.runtime.js";
@@ -66,6 +67,7 @@ export function createPipelineDependencies(dependencies, model) {
66
67
  ? {}
67
68
  : { referentialActions: dependencies.adapter.referentialActions }),
68
69
  model,
70
+ core: dependencies.contracts.core,
69
71
  });
70
72
  const contexts = createContextExecutor({
71
73
  model,
@@ -213,14 +215,14 @@ export function createOperationRuntime(dependencies, pipelineDependencies) {
213
215
  requestId: request.requestId,
214
216
  transaction: undefined,
215
217
  state: stateful.state,
216
- }, forced ? FORCED_CALL : SINGLE_CALL);
218
+ });
217
219
  if (!admitted.ok) {
218
220
  return admitted.response;
219
221
  }
220
222
  let checkedWrite = false;
221
223
  if (stateful.policy !== undefined &&
222
224
  request.family !== "find" &&
223
- validated.contract.transactions === "interactive" &&
225
+ opensServerTransactions(validated.contract.transactions) &&
224
226
  !forced) {
225
227
  try {
226
228
  checkedWrite = await writeNeedsTransaction(stateful.policy, dependencies.contracts.core, stateful, () => planReadOperation(dependencies.contracts.core, dependencies.config.adapter.model, validated.contract, activeScopeConfig(dependencies.config, request).compute, stateful, admitted.args), admitted.args);
@@ -309,7 +311,6 @@ function filtersOf(levels) {
309
311
  return levels.flatMap((level) => level.filters);
310
312
  }
311
313
  const SINGLE_CALL = { select: EVERY_FIELD };
312
- const FORCED_CALL = { select: EVERY_FIELD, beforeBegin: true };
313
314
  export async function admitOperation(dependencies, request, contract, args, argumentsPath, report, scope, admission = SINGLE_CALL) {
314
315
  const contexts = dependencies.contexts;
315
316
  const tree = activeScopeConfig(dependencies.config, request).pipelines;
@@ -402,7 +403,7 @@ async function runFieldInputStages(dependencies, request, contract, args, argume
402
403
  const tree = scopeConfig.pipelines;
403
404
  const planned = dependencies.fieldStages[request.origin];
404
405
  const pipes = planned.anyPipes;
405
- const judge = admission.beforeBegin === true && hasFieldValidators(contract);
406
+ const judge = hasFieldValidators(contract);
406
407
  if (!pipes && !judge) {
407
408
  return { ok: true, args, piped: false };
408
409
  }
@@ -432,22 +433,17 @@ async function runFieldInputStages(dependencies, request, contract, args, argume
432
433
  }
433
434
  }
434
435
  if (judge) {
435
- const compute = scopeConfig.compute;
436
- const broken = fieldValidatorIssues(contract, dependencies.recordWrites.references, request, current, argumentsPath, admission.select, (resource, stage, field) => {
437
- const fields = compute !== undefined && Object.hasOwn(compute, resource)
438
- ? compute[resource]
439
- : undefined;
440
- const phases = fields !== undefined && Object.hasOwn(fields, field)
441
- ? fields[field]
442
- : undefined;
443
- return phases?.[stage] !== undefined;
444
- });
436
+ const broken = fieldValidatorIssues(contract, dependencies.recordWrites.references, request, current, argumentsPath, admission.judge ?? admission.select);
445
437
  if (broken.length > 0) {
446
438
  return { ok: false, response: mapValidationOutcome(broken) };
447
439
  }
448
440
  }
449
441
  return { ok: true, args: current, piped: changed };
450
442
  }
443
+ function transactionPipeChanges(changes, current) {
444
+ const changed = valuesChangedBetween(changes.admitted, current);
445
+ return (path) => changes.written(path) || changed(path);
446
+ }
451
447
  export async function prepareStep(dependencies, request, contract, args, argumentsPath, report, executor, changes, outer) {
452
448
  let current = deepFreeze(withoutUnsentWriteValues(dependencies.contracts.core, request, args));
453
449
  const scope = {
@@ -456,7 +452,12 @@ export async function prepareStep(dependencies, request, contract, args, argumen
456
452
  state: request.state,
457
453
  };
458
454
  try {
459
- const resolved = await runFieldInputStages(dependencies, request, contract, current, argumentsPath, report, { select: changes.written });
455
+ const resolved = await runFieldInputStages(dependencies, request, contract, current, argumentsPath, report, changes.transactionPiped
456
+ ? {
457
+ select: changes.written,
458
+ judge: transactionPipeChanges(changes, current),
459
+ }
460
+ : { select: changes.written });
460
461
  if (!resolved.ok) {
461
462
  return resolved;
462
463
  }
@@ -510,6 +511,7 @@ async function executeForcedOperation(dependencies, adapter, request, contract,
510
511
  const prepared = await prepareStep(dependencies, request, contract, stepArguments, OPERATION_ARGUMENTS_PATH, report, executor, {
511
512
  written: () => false,
512
513
  transactionPiped: true,
514
+ admitted: args,
513
515
  pipedAroundReferences: false,
514
516
  }, outer);
515
517
  if (!prepared.ok) {
@@ -576,8 +578,8 @@ export async function executeAdmittedOperation(dependencies, request, contract,
576
578
  if (firstUnfilled !== undefined) {
577
579
  throw new AvComputedValueError(firstUnfilled.resource, firstUnfilled.field, "create", unfilled.map((gap) => gap.issue), `compute.create on ${firstUnfilled.resource}.${firstUnfilled.field} returned undefined for a required field`);
578
580
  }
579
- if (hasFieldValidators(contract)) {
580
- const refused = fieldValidatorIssues(contract, dependencies.recordWrites.references, request, computed, argumentsPath, EVERY_FIELD);
581
+ if (computed !== args && hasFieldValidators(contract)) {
582
+ const refused = fieldValidatorIssues(contract, dependencies.recordWrites.references, request, computed, argumentsPath, valuesChangedBetween(args, computed));
581
583
  if (refused.length > 0) {
582
584
  return mapValidationOutcome(refused);
583
585
  }
@@ -5,7 +5,7 @@ import type { AvRequestOrigin } from "../config/config-layer.js";
5
5
  import type { AvConfigResourceKey } from "../config/framework-field-definition.js";
6
6
  import type { AvContract } from "../contracts/contract.js";
7
7
  import type { AvApplicationScalarForms } from "../contracts/scalar-value-type.js";
8
- import type { AvContractApplicationView, RequirementsLessComputed } from "../framework/contract-view.js";
8
+ import type { AvContractApplicationView, PreComputeView } from "../framework/contract-view.js";
9
9
  import type { AvContextOperations } from "../framework/operation-facade.js";
10
10
  import type { AvOperationArgumentsFor, AvResourceKey } from "../operations/operation-arguments.js";
11
11
  import type { AvOperationFamily, AvOperationIdentity } from "../operations/operation-identity.js";
@@ -59,20 +59,6 @@ type StageArgumentsOf<C extends AvContract, R extends string, F extends AvOperat
59
59
  * `compute.create` fills (`Lift`, as `"<Resource>.<field>"`; `string`: any).
60
60
  */
61
61
  export type AvPreComputeStageArguments<M extends AvAdapterModel, R extends string, F extends AvOperationFamily, V extends string, Lift extends string = AvPreComputeLift<M, "root">> = F extends "find" | "delete" ? AvStageArguments<M, R, F, V> : [Lift] extends [never] ? AvStageArguments<M, R, F, V> : StageArgumentsOf<PreComputeView<AvContractApplicationView<M>, Lift>, R, F, V>;
62
- /** A Contract view with the create requirements `Lift` names lifted. */
63
- type PreComputeView<C, Lift extends string> = Extract<{
64
- readonly [P in keyof C]: P extends "resources" ? string extends Lift ? {
65
- readonly [R in keyof C[P]]: Omit<C[P][R], "createRequirements">;
66
- } : {
67
- readonly [R in keyof C[P]]: LiftedResource<C[P][R], LiftedFieldOf<Lift, R>>;
68
- } : C[P];
69
- }, AvContract>;
70
- /** The fields of Resource `R` that `Lift` names. */
71
- type LiftedFieldOf<Lift extends string, R> = Lift extends `${R & string}.${infer Field}` ? Field : never;
72
- /** One Resource with the create requirements on `Fields` lifted; itself when none. */
73
- type LiftedResource<Resource, Fields> = [Fields] extends [never] ? Resource : {
74
- readonly [P in keyof Resource]: P extends "createRequirements" ? RequirementsLessComputed<Exclude<Resource[P], undefined>, Fields> : Resource[P];
75
- };
76
62
  /**
77
63
  * What a pipeline callback is told about the operation it is wrapping, and the
78
64
  * framework's executor: `ctx.<Resource>.<family>.<variant>(args)`, `ctx.$sql` and
@@ -0,0 +1,16 @@
1
+ import { type AvOperationFailureResponse } from "./operation-response.js";
2
+ /**
3
+ * The outcome a seed's rejection carries by its shape: `A4000` for an object
4
+ * whose `status` or `statusCode` is `401`, `A4001` for `403`, each with core's
5
+ * own cause. `undefined` for anything else: an `AvError` (it is its own
6
+ * outcome), two numeric fields that disagree, and a value whose fields cannot
7
+ * be read. Total: it never throws.
8
+ */
9
+ export declare function stateRefusalOf(error: unknown): AvOperationFailureResponse | undefined;
10
+ /**
11
+ * What an in-process call rejects with when the `state` seeding it rejected
12
+ * with `error`: the `AvAuthError` of `A4000` or `A4001`, with core's own cause,
13
+ * for a rejection {@link stateRefusalOf} reads as a refusal; `error` itself
14
+ * otherwise, an `AvError` included. Total: it never throws.
15
+ */
16
+ export declare function inProcessStateRejection(error: unknown): unknown;
@@ -0,0 +1,40 @@
1
+ import { thrownFailure } from "./framework-error.factory.js";
2
+ import { AvError } from "./framework-error.js";
3
+ import { OPERATION_CODE_GUIDANCE } from "./operation-code.guidance.js";
4
+ import { operationFailure, } from "./operation-response.js";
5
+ const REFUSALS = {
6
+ 401: "A4000",
7
+ 403: "A4001",
8
+ };
9
+ const isRefusalStatus = (value) => value === 401 || value === 403;
10
+ export function stateRefusalOf(error) {
11
+ try {
12
+ if (typeof error !== "object" ||
13
+ error === null ||
14
+ error instanceof AvError) {
15
+ return undefined;
16
+ }
17
+ const { status, statusCode } = error;
18
+ if (typeof status === "number" &&
19
+ typeof statusCode === "number" &&
20
+ status !== statusCode) {
21
+ return undefined;
22
+ }
23
+ const statuses = new Set([status, statusCode].filter(isRefusalStatus));
24
+ const [only] = statuses;
25
+ if (statuses.size !== 1 || only === undefined) {
26
+ return undefined;
27
+ }
28
+ const code = REFUSALS[only];
29
+ return operationFailure(code, {
30
+ message: OPERATION_CODE_GUIDANCE[code].message,
31
+ });
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ export function inProcessStateRejection(error) {
38
+ const refusal = stateRefusalOf(error);
39
+ return refusal === undefined ? error : thrownFailure(refusal);
40
+ }
@@ -8,5 +8,18 @@ export type OperationFingerprintNode = {
8
8
  };
9
9
  /** Whether `value` is an operation fingerprint (`fp1:…`). */
10
10
  export declare function isOperationFingerprint(value: unknown): value is OperationFingerprint;
11
- /** Computes an operation's fingerprint from its wire-form arguments. */
12
- export declare function computeOperationFingerprint(node: OperationFingerprintNode): Promise<OperationFingerprint>;
11
+ /**
12
+ * The fingerprint of one transaction plan step, `fp1:` and 22 base64url
13
+ * characters: the RFC 8785 canonical JSON of `{ resource, family, variant, args }`
14
+ * over the step's wire-form arguments (a key holding `undefined` left out, an
15
+ * array element holding it written `null`), its UTF-8 bytes hashed with SHA-256,
16
+ * the first 16 bytes of the digest in base64url without padding. Rejects when the
17
+ * arguments are not JSON: a lone surrogate, a non-finite number, a `bigint`, a
18
+ * function, a symbol, or an object that is not a plain object or array.
19
+ */
20
+ export declare function computeOperationFingerprint(node: {
21
+ readonly resource: string;
22
+ readonly family: string;
23
+ readonly variant: string;
24
+ readonly args: unknown;
25
+ }): Promise<`fp1:${string}`>;
@@ -1,5 +1,6 @@
1
1
  import { AvCanonicalJsonError } from "../contracts/contract-canonicalizer.js";
2
2
  import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
3
+ import { takesTransactionPlans } from "../contracts/transaction-capability.js";
3
4
  import { parseOperationIdentity } from "../operations/operation-identity.js";
4
5
  import { findOperationAvailabilityIssue, operationUnavailableIssue, unavailableOperationKey, } from "../operations/operation-validator.js";
5
6
  import { resourceUnavailableIssue } from "../operations/operation-validator-core.js";
@@ -51,6 +52,31 @@ export function capturePlanAtCall(plan) {
51
52
  throw error;
52
53
  }
53
54
  }
55
+ const WRITE_FAMILIES = new Set([
56
+ "create",
57
+ "update",
58
+ "upsert",
59
+ "delete",
60
+ ]);
61
+ function writeLimitFailure(steps, planContracts) {
62
+ const maxTransactionWrites = Math.min(...planContracts.map((contract) => contract.limits.maxTransactionWrites));
63
+ let writes = 0;
64
+ for (const step of steps) {
65
+ if (step.ok &&
66
+ (step.kind === "sql" || WRITE_FAMILIES.has(step.node.get("family")))) {
67
+ writes += 1;
68
+ }
69
+ }
70
+ if (writes <= maxTransactionWrites) {
71
+ return undefined;
72
+ }
73
+ return operationFailure("A2009", {
74
+ message: "Transaction write count exceeds the active Contract limit.",
75
+ issues: [
76
+ validationIssue("V1014", ["operations"], `Transaction write count exceeds maxTransactionWrites (${maxTransactionWrites}).`),
77
+ ],
78
+ });
79
+ }
54
80
  function captureSteps(operations, operationCount, planArguments) {
55
81
  const steps = [];
56
82
  const statements = planArguments.source === "owned" ? planArguments.statements : undefined;
@@ -269,7 +295,7 @@ export async function validateTransactionPlan(dependencies, request, origins, pl
269
295
  const planContracts = origins === undefined || origins.length === 0
270
296
  ? [activeContract(dependencies.contracts, request)]
271
297
  : originContracts(dependencies.contracts, origins);
272
- if (planContracts.some((contract) => contract.transactions === "none")) {
298
+ if (planContracts.some((contract) => !takesTransactionPlans(contract.transactions))) {
273
299
  return { ok: false, response: transactionsUnavailableFailure() };
274
300
  }
275
301
  const planEnvelope = inspectTransactionEnvelope(request.plan, {
@@ -307,6 +333,10 @@ export async function validateTransactionPlan(dependencies, request, origins, pl
307
333
  };
308
334
  }
309
335
  const steps = captureSteps(operations, operationCount, planArguments);
336
+ const writes = writeLimitFailure(steps, planContracts);
337
+ if (writes !== undefined) {
338
+ return { ok: false, response: writes };
339
+ }
310
340
  const misuse = createMisuseReporter(dependencies.config.misuse);
311
341
  const validated = [];
312
342
  const cascaded = new Set();
@@ -148,7 +148,6 @@ async function executeTransactionPlan(runtime, dependencies, request, plan, stat
148
148
  const admission = await admitOperation(dependencies, stepRequest, step.contract, step.arguments, planOperationArgumentsPath(step.index), report, { requestId: request.requestId, transaction: undefined, state }, {
149
149
  select: EVERY_FIELD,
150
150
  validation: { transactionReferences: true },
151
- beforeBegin: true,
152
151
  });
153
152
  if (!admission.ok) {
154
153
  return transactionOperationFailure(admission.response, step.index);
@@ -199,6 +198,7 @@ async function executeTransactionPlan(runtime, dependencies, request, plan, stat
199
198
  const prepared = await prepareStep(dependencies, stepRequest, operation.contract, resolved.arguments, argumentsPath, report, executor, {
200
199
  written: (path) => written.has(pathKey(path)),
201
200
  transactionPiped,
201
+ admitted: admitted[operation.index]?.args,
202
202
  pipedAroundReferences: admitted[operation.index]?.piped === true &&
203
203
  (live.length > 0 || operation.references.length > 0),
204
204
  }, outer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/core",
3
- "version": "1.0.0-rc.12",
3
+ "version": "1.0.0-rc.14",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "Core runtime, contract compiler, and extension APIs for Aventara.",
6
6
  "type": "module",