@aventara/core 1.0.0-rc.11 → 1.0.0-rc.13
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.
- package/CHANGELOG.md +157 -0
- package/ERRORS.md +8 -7
- package/README.md +3 -2
- package/dist/avz/avz-check.factory.d.ts +2 -0
- package/dist/avz/avz-check.factory.js +12 -0
- package/dist/avz/avz.d.ts +6 -0
- package/dist/avz/avz.js +1 -0
- package/dist/compiler/contract-compiler-diagnostic.d.ts +13 -2
- package/dist/compiler/contract-compiler-diagnostic.guidance.js +7 -2
- package/dist/compiler/contract.compiler.js +110 -15
- package/dist/compiler/create-requirements.projector.d.ts +45 -3
- package/dist/compiler/create-requirements.projector.js +239 -28
- package/dist/compiler/managed-elsewhere.projector.d.ts +3 -0
- package/dist/compiler/managed-elsewhere.projector.js +1 -1
- package/dist/config/computed-fields.d.ts +8 -0
- package/dist/config/computed-fields.js +1 -1
- package/dist/config/config-diagnostic.guidance.js +2 -2
- package/dist/config/config.resolver.d.ts +2 -2
- package/dist/config/config.validator.js +15 -6
- package/dist/config/exact-framework-config.d.ts +1 -0
- package/dist/config/field-pipeline-config.d.ts +2 -2
- package/dist/config/forced-operation.validator.js +3 -2
- package/dist/config/framework-config.d.ts +8 -3
- package/dist/config/framework-exemptions-config.d.ts +13 -8
- package/dist/config/framework-resources-config.d.ts +11 -3
- package/dist/contracts/client-contract-structure.validator.d.ts +4 -1
- package/dist/contracts/client-contract-structure.validator.js +3 -2
- package/dist/contracts/contract-limits.d.ts +8 -1
- package/dist/contracts/contract-limits.js +1 -0
- package/dist/contracts/contract.d.ts +7 -3
- package/dist/contracts/transaction-capability.d.ts +8 -0
- package/dist/contracts/transaction-capability.js +7 -0
- package/dist/framework/access-facade.js +14 -6
- package/dist/framework/contract-view.d.ts +104 -12
- package/dist/framework/operation-facade.d.ts +8 -3
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/operations/validation-code.guidance.js +1 -1
- package/dist/policy/access-provider.interface.d.ts +7 -2
- package/dist/policy/access-question.runtime.js +2 -1
- package/dist/prebuilt/core-version.generated.d.ts +1 -1
- package/dist/prebuilt/core-version.generated.js +1 -1
- package/dist/protocol/absent-route.responder.d.ts +2 -2
- package/dist/protocol/absent-route.responder.js +2 -1
- package/dist/protocol/av-protocol.d.ts +1 -0
- package/dist/protocol/av-protocol.js +1 -0
- package/dist/protocol/protocol-surface.js +2 -1
- package/dist/protocol/protocol.handler.js +3 -1
- package/dist/runtime/context-executor.runtime.js +2 -1
- package/dist/runtime/create-requirement.d.ts +5 -1
- package/dist/runtime/create-requirement.js +10 -2
- package/dist/runtime/field-check.evaluator.js +39 -2
- package/dist/runtime/field-validator.runtime.d.ts +4 -3
- package/dist/runtime/field-validator.runtime.js +29 -0
- package/dist/runtime/mutation-planner.js +26 -9
- package/dist/runtime/operation-code.guidance.js +3 -3
- package/dist/runtime/operation-runtime.d.ts +10 -9
- package/dist/runtime/operation-runtime.js +20 -18
- package/dist/runtime/pipeline.d.ts +1 -15
- package/dist/runtime/state-refusal.mapper.d.ts +16 -0
- package/dist/runtime/state-refusal.mapper.js +40 -0
- package/dist/transactions/operation-fingerprint.d.ts +15 -2
- package/dist/transactions/transaction-plan-validator.js +31 -1
- package/dist/transactions/transaction-runtime.js +1 -1
- package/package.json +1 -1
- package/src/runtime/field-check.evaluator.ts +46 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
145
|
+
await computeAt("data", "update");
|
|
131
146
|
}
|
|
132
147
|
if (request.family === "upsert") {
|
|
133
148
|
if ("create" in logicalArgs) {
|
|
134
|
-
|
|
149
|
+
await computeAt("create", "create");
|
|
135
150
|
}
|
|
136
151
|
if ("update" in logicalArgs) {
|
|
137
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 401 (an authentication library's refusal, answered with this message only): authentication is required or failed. 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:
|
|
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 403 (answered with this message only).",
|
|
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
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
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
|
|
146
|
+
readonly judge?: (path: readonly (string | number)[]) => boolean;
|
|
148
147
|
};
|
|
149
148
|
/**
|
|
150
|
-
* Runs an operation's admission — its guards and pipes,
|
|
151
|
-
* validators of the values
|
|
152
|
-
*
|
|
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:
|
|
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
|
-
}
|
|
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
|
|
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 =
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
/**
|
|
12
|
-
|
|
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
|
|
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
|
@@ -579,10 +579,54 @@ function plural(count: number, one: string, many: string): string {
|
|
|
579
579
|
return `${count} ${count === 1 ? one : many}`;
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
+
/** The characters a pattern reads as syntax: `AvZ.oneOf` escapes each in a value. */
|
|
583
|
+
const PATTERN_SYNTAX = "\\^$.*+?()[]{}|/";
|
|
584
|
+
|
|
585
|
+
// Source: rc.13 (CORE-08). `AvZ.oneOf` writes its values as `^(?:a|b)$`, each
|
|
586
|
+
// syntax character escaped, no flags: read back exactly, that form lists them.
|
|
587
|
+
// Any other pattern (an unescaped syntax character, a flag) lists nothing.
|
|
588
|
+
/** The values an `AvZ.oneOf` pattern lists, or `undefined` for any other pattern. */
|
|
589
|
+
function listedValues(pattern: {
|
|
590
|
+
readonly source: string;
|
|
591
|
+
readonly flags: string;
|
|
592
|
+
}): readonly string[] | undefined {
|
|
593
|
+
const { source, flags } = pattern;
|
|
594
|
+
if (flags !== "" || !source.startsWith("^(?:") || !source.endsWith(")$")) {
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
const body = source.slice(4, -2);
|
|
598
|
+
const values: string[] = [];
|
|
599
|
+
let value = "";
|
|
600
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
601
|
+
const character = body.charAt(index);
|
|
602
|
+
if (character === "\\") {
|
|
603
|
+
const escaped = body.charAt(index + 1);
|
|
604
|
+
if (escaped === "" || !PATTERN_SYNTAX.includes(escaped)) {
|
|
605
|
+
return undefined;
|
|
606
|
+
}
|
|
607
|
+
value += escaped;
|
|
608
|
+
index += 1;
|
|
609
|
+
} else if (character === "|") {
|
|
610
|
+
values.push(value);
|
|
611
|
+
value = "";
|
|
612
|
+
} else if (PATTERN_SYNTAX.includes(character)) {
|
|
613
|
+
return undefined;
|
|
614
|
+
} else {
|
|
615
|
+
value += character;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
values.push(value);
|
|
619
|
+
return values;
|
|
620
|
+
}
|
|
621
|
+
|
|
582
622
|
function formatMessage(check: AvFieldFormatCheck): string {
|
|
583
623
|
switch (check.format) {
|
|
584
|
-
case "regex":
|
|
585
|
-
|
|
624
|
+
case "regex": {
|
|
625
|
+
const listed = listedValues(check.pattern);
|
|
626
|
+
return listed === undefined
|
|
627
|
+
? `Value must match the pattern /${check.pattern.source}/${check.pattern.flags}.`
|
|
628
|
+
: `Value must be one of ${listed.map((value) => JSON.stringify(value)).join(", ")}.`;
|
|
629
|
+
}
|
|
586
630
|
case "includes":
|
|
587
631
|
return `Value must include ${JSON.stringify(check.includes)}.`;
|
|
588
632
|
case "starts_with":
|