@aventara/core 1.0.0-rc.2 → 1.0.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +175 -0
- package/ERRORS.md +9 -9
- package/README.md +72 -12
- package/dist/adapters/adapter-operation.d.ts +7 -0
- package/dist/compiler/contract.compiler.d.ts +6 -0
- package/dist/compiler/contract.compiler.js +73 -12
- package/dist/config/computed-fields.js +2 -9
- package/dist/config/config-diagnostic.guidance.js +3 -3
- package/dist/config/config.resolver.d.ts +14 -0
- package/dist/config/config.resolver.js +31 -0
- package/dist/config/config.validator.js +82 -0
- package/dist/config/exact-framework-config.d.ts +8 -1
- package/dist/config/framework-config.d.ts +26 -1
- package/dist/contracts/client-contract-structure.validator.d.ts +8 -0
- package/dist/contracts/client-contract-structure.validator.js +6 -1
- package/dist/contracts/field.d.ts +11 -0
- package/dist/framework/contract-view.d.ts +41 -10
- package/dist/framework/framework.factory.js +39 -11
- package/dist/framework/operation-facade.d.ts +24 -5
- package/dist/framework/operation-facade.js +2 -1
- package/dist/index.d.ts +4 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/operations/database-refusal.d.ts +1 -1
- package/dist/operations/database-refusal.js +1 -1
- package/dist/operations/operation-arguments.d.ts +1 -1
- package/dist/operations/operation-call.d.ts +9 -5
- package/dist/operations/operation-result.d.ts +5 -3
- package/dist/operations/operation-validator-core.js +66 -33
- package/dist/operations/validation-code.guidance.js +2 -2
- package/dist/policy/policy-admission.d.ts +22 -0
- package/dist/policy/policy-admission.js +400 -0
- package/dist/policy/policy-after-update.runtime.d.ts +29 -0
- package/dist/policy/policy-after-update.runtime.js +131 -0
- package/dist/policy/policy-condition.d.ts +15 -0
- package/dist/policy/policy-condition.js +36 -0
- package/dist/policy/policy-engine.d.ts +111 -0
- package/dist/policy/policy-engine.js +1 -0
- package/dist/policy/policy-nested-write.runtime.d.ts +38 -0
- package/dist/policy/policy-nested-write.runtime.js +449 -0
- package/dist/policy/policy-read.runtime.d.ts +83 -0
- package/dist/policy/policy-read.runtime.js +494 -0
- package/dist/policy/policy-request.d.ts +37 -0
- package/dist/policy/policy-request.js +52 -0
- package/dist/policy/policy-session.d.ts +83 -0
- package/dist/policy/policy-session.js +234 -0
- package/dist/policy/policy-startup.validator.d.ts +8 -0
- package/dist/policy/policy-startup.validator.js +106 -0
- package/dist/policy/policy-write.runtime.d.ts +30 -0
- package/dist/policy/policy-write.runtime.js +394 -0
- package/dist/prebuilt/core-version.generated.d.ts +2 -0
- package/dist/prebuilt/core-version.generated.js +1 -0
- package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
- package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
- package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
- package/dist/prebuilt/prebuilt-contracts.js +1 -0
- package/dist/prebuilt/prebuilt-contracts.loader.d.ts +21 -0
- package/dist/prebuilt/prebuilt-contracts.loader.js +122 -0
- package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
- package/dist/prebuilt/prebuilt-inputs.fingerprint.js +192 -0
- package/dist/runtime/computed-read-runtime.d.ts +6 -1
- package/dist/runtime/computed-read-runtime.js +224 -45
- package/dist/runtime/context-executor.runtime.js +4 -2
- package/dist/runtime/execution-options.d.ts +8 -0
- package/dist/runtime/field-validator.runtime.d.ts +9 -0
- package/dist/runtime/field-validator.runtime.js +36 -2
- package/dist/runtime/framework-warning.emitter.d.ts +6 -0
- package/dist/runtime/framework-warning.emitter.js +8 -0
- package/dist/runtime/level-lifecycle.runtime.js +3 -2
- package/dist/runtime/operation-code.guidance.js +4 -4
- package/dist/runtime/operation-request.d.ts +7 -0
- package/dist/runtime/operation-runtime.d.ts +9 -1
- package/dist/runtime/operation-runtime.js +119 -11
- package/dist/runtime/readonly-value.d.ts +5 -1
- package/dist/runtime/readonly-value.js +67 -28
- package/dist/transactions/transaction-runtime.d.ts +4 -1
- package/dist/transactions/transaction-runtime.js +27 -5
- package/package.json +2 -2
|
@@ -66,12 +66,12 @@ export const OPERATION_CODE_GUIDANCE = {
|
|
|
66
66
|
},
|
|
67
67
|
A2003: {
|
|
68
68
|
message: "Not found.",
|
|
69
|
-
cause: "A unique operation (find.unique, update.unique, delete.unique) matched no row, or a $connect names a record that does not exist. The operation fails, so no after hook runs.",
|
|
69
|
+
cause: "A unique operation (find.unique, update.unique, delete.unique) matched no row, or a $connect names a record that does not exist. The operation fails, so no after hook runs. Under a policy, a row outside the caller's read scope is a miss too, and so is a single-row update or delete of a row the caller cannot read, so the answer does not reveal that the row exists. A row the caller can read but not change answers A4002.",
|
|
70
70
|
fix: "Catch AvNotFoundError, or use a first-style operation, which answers A1001 with null. Check the $connect identifier.",
|
|
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 out of its range (V1004), or holding a character it cannot store or a pattern it cannot read (V1001), with the issue at `["arguments"]
|
|
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.',
|
|
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: {
|
|
@@ -161,7 +161,7 @@ export const OPERATION_CODE_GUIDANCE = {
|
|
|
161
161
|
},
|
|
162
162
|
A4002: {
|
|
163
163
|
message: "Policy denied.",
|
|
164
|
-
cause: '
|
|
165
|
-
fix: "Read cause.message
|
|
164
|
+
cause: "The configuration's policy refused the request: its engine does not allow the operation, a field the request names, writes or returns, a row it reaches, a row it creates, or the row a change leaves (moved out of what the caller reads, or failing its grant's after condition, which rolls the write back); on a database without interactive transactions, a write the policy can only check after it runs; or a stage threw new AvError(\"A4002\", ...).",
|
|
165
|
+
fix: "Read cause.message, which names what the policy refused. Call as a principal the policy allows, leave the refused field or relation out (a governed one is named only where every row grants it), or change only rows the caller may change and still read afterwards.",
|
|
166
166
|
},
|
|
167
167
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AvOperationIdentity } from "../operations/operation-identity.js";
|
|
2
|
+
import type { PolicySession } from "../policy/policy-session.js";
|
|
2
3
|
import type { AvTransportContext } from "./pipeline.js";
|
|
3
4
|
import type { AvState } from "./request-state.js";
|
|
4
5
|
/**
|
|
@@ -63,5 +64,11 @@ export type OperationExecutionRequest = WithoutArguments<FrameworkOperationReque
|
|
|
63
64
|
*/
|
|
64
65
|
export type StatefulOperationRequest = OperationExecutionRequest & {
|
|
65
66
|
readonly state: AvState;
|
|
67
|
+
/**
|
|
68
|
+
* The request's policy session, when the policy checks it: a client's or an
|
|
69
|
+
* agent's request, or an application call made `as` a principal. Absent, nothing
|
|
70
|
+
* is checked: an application call, or a call a stage made through `ctx`.
|
|
71
|
+
*/
|
|
72
|
+
readonly policy?: PolicySession;
|
|
66
73
|
};
|
|
67
74
|
export {};
|
|
@@ -7,6 +7,7 @@ import type { ResolvedFrameworkConfig } from "../config/config.resolver.js";
|
|
|
7
7
|
import type { AvRequestOrigin, RemoteRequestOrigin } from "../config/config-layer.js";
|
|
8
8
|
import type { AvContract } from "../contracts/contract.js";
|
|
9
9
|
import type { OperationValidationOptions, SemanticOperationArguments } from "../operations/operation-validator.js";
|
|
10
|
+
import { type PolicyCaller } from "../policy/policy-request.js";
|
|
10
11
|
import type { FrameworkTransactionRequest } from "../transactions/transaction-request.js";
|
|
11
12
|
import { type ArgumentsSource } from "./caller-data.door.js";
|
|
12
13
|
import { type ContextExecutor, type ExecutorScope } from "./context-executor.runtime.js";
|
|
@@ -65,6 +66,11 @@ export declare function withRequestState<Request extends {
|
|
|
65
66
|
}>(request: Request): Request & {
|
|
66
67
|
readonly state: AvState;
|
|
67
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* `request` with its policy session when the policy checks it, or the failure
|
|
71
|
+
* deriving its principal answered.
|
|
72
|
+
*/
|
|
73
|
+
export declare function withRequestPolicy<M extends AvAdapterModel>(dependencies: Pick<OperationRuntimeDependencies<M>, "adapter" | "config" | "contracts">, request: StatefulOperationRequest, caller: PolicyCaller | undefined, report: DiagnosticsReporter): StatefulOperationRequest | AvOperationFailureResponse;
|
|
68
74
|
/**
|
|
69
75
|
* Builds what every operation's pipeline shares, the ctx executor included: its
|
|
70
76
|
* raw calls run the application's operation path with every developer stage
|
|
@@ -79,7 +85,9 @@ export declare function createPipelineDependencies<M extends AvAdapterModel>(dep
|
|
|
79
85
|
* request's arguments are the caller's own objects, copied at the door, or data
|
|
80
86
|
* no caller holds (see {@link ArgumentsSource}).
|
|
81
87
|
*/
|
|
82
|
-
export type FrameworkOperationExecutor = (request: FrameworkOperationRequest, report: DiagnosticsReporter, source: ArgumentsSource
|
|
88
|
+
export type FrameworkOperationExecutor = (request: FrameworkOperationRequest, report: DiagnosticsReporter, source: ArgumentsSource,
|
|
89
|
+
/** The principal an application call runs as (`{ as }`), when it names one. */
|
|
90
|
+
caller?: PolicyCaller) => Promise<AvOperationResponse>;
|
|
83
91
|
/** Selects the Contract of the request's origin. */
|
|
84
92
|
export declare function activeContract(contracts: AvContracts, request: FrameworkOperationRequest | FrameworkTransactionRequest): AvContract;
|
|
85
93
|
/** Creates the transport-independent operation executor owned by core. */
|
|
@@ -3,6 +3,9 @@ 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
5
|
import { findOperationAvailabilityIssue, validateOperationArguments, } from "../operations/operation-validator.js";
|
|
6
|
+
import { enforceReadPolicy, } from "../policy/policy-read.runtime.js";
|
|
7
|
+
import { admitRequestPolicy, openRequestPolicy, } from "../policy/policy-request.js";
|
|
8
|
+
import { enforceWritePolicy, writeNeedsTransaction, } from "../policy/policy-write.runtime.js";
|
|
6
9
|
import { transactionInfrastructureFailure } from "../transactions/transaction-response.js";
|
|
7
10
|
import { settleTransaction } from "../transactions/transaction-settlement.runtime.js";
|
|
8
11
|
import { TRANSACTION_PIPES, transactionFacts, } from "../transactions/transaction-stage.runtime.js";
|
|
@@ -15,7 +18,7 @@ import { createDiagnosticsReporter, } from "./diagnostics-reporter.js";
|
|
|
15
18
|
import { AdapterBoundary, OPERATION_ARGUMENTS_PATH, resolveOperationFailure, sanitizeOperationFailure, } from "./error-boundary.js";
|
|
16
19
|
import { planFieldStages } from "./field-stage.plan.js";
|
|
17
20
|
import { guardSentFields, pipeSentFields, valueAt, writeRecordPathsOf, } from "./field-stage.runtime.js";
|
|
18
|
-
import { fieldValidatorIssues } from "./field-validator.runtime.js";
|
|
21
|
+
import { fieldValidatorIssues, hiddenFieldRefusal, } from "./field-validator.runtime.js";
|
|
19
22
|
import { thrownFailure } from "./framework-error.factory.js";
|
|
20
23
|
import { AvInterceptorNextError } from "./interceptor-next.error.js";
|
|
21
24
|
import { runLevelLifecycle, } from "./level-lifecycle.runtime.js";
|
|
@@ -40,6 +43,16 @@ function scopeOf(request, binding) {
|
|
|
40
43
|
export function withRequestState(request) {
|
|
41
44
|
return { ...request, state: requestStateOf(request.state) };
|
|
42
45
|
}
|
|
46
|
+
export function withRequestPolicy(dependencies, request, caller, report) {
|
|
47
|
+
try {
|
|
48
|
+
const policy = openRequestPolicy(dependencies.config.policy, dependencies.contracts.core, request, caller, dependencies.adapter);
|
|
49
|
+
return policy === undefined ? request : { ...request, policy };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
report(error, "A3000");
|
|
53
|
+
return internalFailure();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
43
56
|
export function createPipelineDependencies(dependencies, model) {
|
|
44
57
|
const misuse = createMisuseReporter(dependencies.config.misuse);
|
|
45
58
|
const recordWrites = recordWriteFactsOf({
|
|
@@ -170,7 +183,7 @@ function outputAccepted(contract, request, projection, data, report) {
|
|
|
170
183
|
}
|
|
171
184
|
export function createOperationRuntime(dependencies, pipelineDependencies) {
|
|
172
185
|
const misuse = createMisuseReporter(dependencies.config.misuse);
|
|
173
|
-
return async (request, report, source) => {
|
|
186
|
+
return async (request, report, source, caller) => {
|
|
174
187
|
let validated;
|
|
175
188
|
try {
|
|
176
189
|
validated = validateOperation(dependencies, request, source, misuse);
|
|
@@ -184,7 +197,10 @@ export function createOperationRuntime(dependencies, pipelineDependencies) {
|
|
|
184
197
|
if (!validated.ok) {
|
|
185
198
|
return validated.response;
|
|
186
199
|
}
|
|
187
|
-
const stateful = withRequestState(request);
|
|
200
|
+
const stateful = withRequestPolicy(dependencies, withRequestState(request), caller, report);
|
|
201
|
+
if (!("state" in stateful)) {
|
|
202
|
+
return stateful;
|
|
203
|
+
}
|
|
188
204
|
const forced = isForcedOperation(activeScopeConfig(dependencies.config, request).pipelines, request);
|
|
189
205
|
const admitted = await admitOperation(pipelineDependencies, stateful, validated.contract, validated.arguments, OPERATION_ARGUMENTS_PATH, report, {
|
|
190
206
|
requestId: request.requestId,
|
|
@@ -194,8 +210,20 @@ export function createOperationRuntime(dependencies, pipelineDependencies) {
|
|
|
194
210
|
if (!admitted.ok) {
|
|
195
211
|
return admitted.response;
|
|
196
212
|
}
|
|
197
|
-
|
|
198
|
-
|
|
213
|
+
let checkedWrite = false;
|
|
214
|
+
if (stateful.policy !== undefined &&
|
|
215
|
+
request.family !== "find" &&
|
|
216
|
+
validated.contract.transactions === "interactive" &&
|
|
217
|
+
!forced) {
|
|
218
|
+
try {
|
|
219
|
+
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);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
return sanitizeOperationFailure(error, stateful.policy.execution.boundary.originOf(error), report, { path: OPERATION_ARGUMENTS_PATH, arguments: admitted.args });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (forced || checkedWrite) {
|
|
226
|
+
return executeForcedOperation(pipelineDependencies, dependencies.adapter, stateful, validated.contract, admitted.args, report, forced);
|
|
199
227
|
}
|
|
200
228
|
return executeAdmittedOperation(pipelineDependencies, stateful, validated.contract, admitted.args, OPERATION_ARGUMENTS_PATH, { kind: "standalone", executor: dependencies.adapter }, report);
|
|
201
229
|
};
|
|
@@ -346,6 +374,12 @@ export async function admitOperation(dependencies, request, contract, args, argu
|
|
|
346
374
|
if (absent.length > 0) {
|
|
347
375
|
return { ok: false, response: mapValidationOutcome(absent) };
|
|
348
376
|
}
|
|
377
|
+
if (request.policy !== undefined) {
|
|
378
|
+
const refused = await admitRequestPolicy(request.policy, dependencies.contracts.core, contract, request, current, report, { path: argumentsPath, arguments: current });
|
|
379
|
+
if (refused !== undefined) {
|
|
380
|
+
return { ok: false, response: refused };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
349
383
|
try {
|
|
350
384
|
return await admit();
|
|
351
385
|
}
|
|
@@ -441,7 +475,7 @@ export async function prepareStep(dependencies, request, contract, args, argumen
|
|
|
441
475
|
};
|
|
442
476
|
}
|
|
443
477
|
}
|
|
444
|
-
async function executeForcedOperation(dependencies, adapter, request, contract, args, report) {
|
|
478
|
+
async function executeForcedOperation(dependencies, adapter, request, contract, args, report, forced) {
|
|
445
479
|
const plan = [
|
|
446
480
|
{
|
|
447
481
|
resource: request.resource,
|
|
@@ -452,7 +486,9 @@ async function executeForcedOperation(dependencies, adapter, request, contract,
|
|
|
452
486
|
];
|
|
453
487
|
const settled = await settleTransaction({
|
|
454
488
|
adapter,
|
|
455
|
-
levels:
|
|
489
|
+
levels: forced
|
|
490
|
+
? transactionLevelsOf(activeScopeConfig(dependencies.config, request).pipelines, request.resource)
|
|
491
|
+
: [],
|
|
456
492
|
plan,
|
|
457
493
|
contextOf: (current, executor) => dependencies.contexts.transaction({
|
|
458
494
|
requestId: request.requestId,
|
|
@@ -539,7 +575,30 @@ export async function executeAdmittedOperation(dependencies, request, contract,
|
|
|
539
575
|
return mapValidationOutcome(refused);
|
|
540
576
|
}
|
|
541
577
|
}
|
|
578
|
+
const hiddenRefusal = hiddenFieldRefusal({ core: dependencies.contracts.core, active: contract }, dependencies.recordWrites.references, request, computed, argumentsPath);
|
|
579
|
+
if (hiddenRefusal !== undefined) {
|
|
580
|
+
throw hiddenRefusal;
|
|
581
|
+
}
|
|
542
582
|
args = deepFreeze(computed);
|
|
583
|
+
let policyWrite;
|
|
584
|
+
if (request.policy !== undefined && request.family !== "find") {
|
|
585
|
+
const planned = planReadOperation(dependencies.contracts.core, dependencies.config.adapter.model, contract, scopeConfig.compute, request, args);
|
|
586
|
+
const plannedArguments = await stripVirtualMutationInput(contracts, request, planned.arguments);
|
|
587
|
+
try {
|
|
588
|
+
policyWrite = await enforceWritePolicy(request.policy.on({ executor, boundary: adapterBoundary }), dependencies.contracts.core, dependencies.config.adapter.model, request, planned, plannedArguments, binding.kind === "transaction");
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
return sanitizeOperationFailure(error, adapterBoundary.originOf(error), report, { path: argumentsPath, arguments: args });
|
|
592
|
+
}
|
|
593
|
+
if (policyWrite.kind === "refused") {
|
|
594
|
+
return policyWrite.response;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
let policyFailure;
|
|
598
|
+
const refuseWrite = (response) => {
|
|
599
|
+
policyFailure = response;
|
|
600
|
+
throw thrownFailure(response);
|
|
601
|
+
};
|
|
543
602
|
let readMaterialized = false;
|
|
544
603
|
let executedReadPlan;
|
|
545
604
|
const materialize = async (data, plan) => {
|
|
@@ -548,24 +607,73 @@ export async function executeAdmittedOperation(dependencies, request, contract,
|
|
|
548
607
|
return materializeReadOperation(contract, scopeConfig.compute, request, plan, data, computeContext);
|
|
549
608
|
};
|
|
550
609
|
const invoke = async () => {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
610
|
+
let readPlan = planReadOperation(dependencies.contracts.core, dependencies.config.adapter.model, contract, scopeConfig.compute, request, args);
|
|
611
|
+
let adapterArguments = await stripVirtualMutationInput(contracts, request, readPlan.arguments);
|
|
612
|
+
let operation = request;
|
|
613
|
+
let settle;
|
|
614
|
+
let writeMiss;
|
|
615
|
+
if (policyWrite !== undefined && policyWrite.kind !== "refused") {
|
|
616
|
+
writeMiss = policyWrite.miss;
|
|
617
|
+
if (policyWrite.kind === "empty") {
|
|
618
|
+
if (writeMiss !== undefined && policyWrite.data === null) {
|
|
619
|
+
refuseWrite(await writeMiss());
|
|
620
|
+
}
|
|
621
|
+
return materialize(policyWrite.data, readPlan);
|
|
622
|
+
}
|
|
623
|
+
operation = policyWrite.identity;
|
|
624
|
+
adapterArguments = policyWrite.arguments;
|
|
625
|
+
readPlan = policyWrite.plan;
|
|
626
|
+
settle = policyWrite.settle;
|
|
627
|
+
}
|
|
628
|
+
else if (request.policy !== undefined && request.family === "find") {
|
|
629
|
+
const read = await enforceReadPolicy(request.policy.on({ executor, boundary: adapterBoundary }), dependencies.contracts.core, dependencies.config.adapter.model, request, readPlan, adapterArguments);
|
|
630
|
+
if (read.kind === "refused") {
|
|
631
|
+
throw thrownFailure(read.response);
|
|
632
|
+
}
|
|
633
|
+
if (read.kind === "empty") {
|
|
634
|
+
const miss = uniqueMissFailure(request, read.data);
|
|
635
|
+
if (miss !== undefined) {
|
|
636
|
+
throw thrownFailure(miss);
|
|
637
|
+
}
|
|
638
|
+
return materialize(read.data, readPlan);
|
|
639
|
+
}
|
|
640
|
+
operation = read.identity;
|
|
641
|
+
adapterArguments = read.arguments;
|
|
642
|
+
readPlan = read.plan;
|
|
643
|
+
settle = read.settle;
|
|
644
|
+
}
|
|
645
|
+
const { policy: _session, ...identity } = operation;
|
|
646
|
+
let adapterData = await adapterBoundary.execute(() => executor.execute({
|
|
555
647
|
...identity,
|
|
556
648
|
resource: request.resource,
|
|
557
649
|
arguments: adapterArguments,
|
|
558
650
|
}));
|
|
651
|
+
if (writeMiss !== undefined && adapterData === null) {
|
|
652
|
+
refuseWrite(await writeMiss());
|
|
653
|
+
}
|
|
559
654
|
const miss = uniqueMissFailure(request, adapterData);
|
|
560
655
|
if (miss !== undefined) {
|
|
561
656
|
throw thrownFailure(miss);
|
|
562
657
|
}
|
|
658
|
+
if (settle !== undefined) {
|
|
659
|
+
const settled = await settle(adapterData);
|
|
660
|
+
if (!settled.ok) {
|
|
661
|
+
if (policyWrite !== undefined) {
|
|
662
|
+
refuseWrite(settled.response);
|
|
663
|
+
}
|
|
664
|
+
throw thrownFailure(settled.response);
|
|
665
|
+
}
|
|
666
|
+
adapterData = settled.data;
|
|
667
|
+
}
|
|
563
668
|
return materialize(adapterData, readPlan);
|
|
564
669
|
};
|
|
565
670
|
const materialized = async (data) => readMaterialized
|
|
566
671
|
? data
|
|
567
672
|
: materialize(data, planReadOperation(dependencies.contracts.core, dependencies.config.adapter.model, contract, scopeConfig.compute, request, args));
|
|
568
673
|
const data = await materialized(await runLevelLifecycle(levels, context, invoke, materialized, reuse, () => new AvInterceptorNextError()));
|
|
674
|
+
if (policyFailure !== undefined) {
|
|
675
|
+
return policyFailure;
|
|
676
|
+
}
|
|
569
677
|
if (!outputAccepted(contract, request, executedReadPlan?.projection, data, report)) {
|
|
570
678
|
return internalFailure();
|
|
571
679
|
}
|
|
@@ -32,8 +32,12 @@ export declare function deepFreeze<T>(value: T): T;
|
|
|
32
32
|
* only reads is told — the validated arguments after validation, a result in an
|
|
33
33
|
* after hook — so neither what is written nor the result the caller receives can
|
|
34
34
|
* be changed through it.
|
|
35
|
+
*
|
|
36
|
+
* With `snapshot`, a copied container found to hold no `Date` or `Uint8Array`
|
|
37
|
+
* is remembered as such at once, so a later `frozenCopy` of the copy shares it
|
|
38
|
+
* without walking it: for a copy that is itself copied again for each stage.
|
|
35
39
|
*/
|
|
36
|
-
export declare function frozenCopy<T>(value: T): AvDeepReadonly<T>;
|
|
40
|
+
export declare function frozenCopy<T>(value: T, snapshot?: boolean): AvDeepReadonly<T>;
|
|
37
41
|
/**
|
|
38
42
|
* A read-only view of `value`: reads see the live value, nested plain objects
|
|
39
43
|
* and arrays as views too, and any write, definition or deletion throws a
|
|
@@ -37,38 +37,79 @@ function scalarCopy(value) {
|
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
39
39
|
const SCALAR_FREE = new WeakSet();
|
|
40
|
-
|
|
40
|
+
function scalarFree(value) {
|
|
41
|
+
return Object.isFrozen(value) && SCALAR_FREE.has(value);
|
|
42
|
+
}
|
|
43
|
+
const WALKING = Symbol("walking");
|
|
44
|
+
function place(target, key, value, enumerable) {
|
|
45
|
+
if (enumerable && typeof key === "string" && !(key in target)) {
|
|
46
|
+
target[key] = value;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
Object.defineProperty(target, key, {
|
|
50
|
+
value,
|
|
51
|
+
enumerable,
|
|
52
|
+
writable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function frozenCopy(value, snapshot = false) {
|
|
57
|
+
const scalar = scalarCopy(value);
|
|
58
|
+
if (scalar !== undefined) {
|
|
59
|
+
return scalar;
|
|
60
|
+
}
|
|
61
|
+
if (!isContainer(value) || scalarFree(value)) {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
41
64
|
const copies = new Map();
|
|
42
|
-
const walking = new Set();
|
|
43
65
|
let cycled = false;
|
|
66
|
+
let holdsScalar = false;
|
|
44
67
|
const copy = (current) => {
|
|
68
|
+
if (current === null || typeof current !== "object") {
|
|
69
|
+
return current;
|
|
70
|
+
}
|
|
45
71
|
const scalar = scalarCopy(current);
|
|
46
72
|
if (scalar !== undefined) {
|
|
73
|
+
holdsScalar = true;
|
|
47
74
|
return scalar;
|
|
48
75
|
}
|
|
49
|
-
if (!isContainer(current) ||
|
|
76
|
+
if (!isContainer(current) || scalarFree(current)) {
|
|
50
77
|
return current;
|
|
51
78
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
79
|
+
const known = copies.get(current);
|
|
80
|
+
if (known === WALKING) {
|
|
81
|
+
cycled = true;
|
|
82
|
+
return current;
|
|
83
|
+
}
|
|
84
|
+
if (known !== undefined) {
|
|
85
|
+
holdsScalar ||= !scalarFree(known);
|
|
86
|
+
return known;
|
|
55
87
|
}
|
|
56
|
-
copies.set(current,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
let changed = !DEEP_FROZEN.has(current);
|
|
88
|
+
copies.set(current, WALKING);
|
|
89
|
+
const outer = holdsScalar;
|
|
90
|
+
holdsScalar = false;
|
|
91
|
+
let changed = !(Object.isFrozen(current) && DEEP_FROZEN.has(current));
|
|
92
|
+
const array = Array.isArray(current);
|
|
93
|
+
const prototype = Object.getPrototypeOf(current);
|
|
94
|
+
const target = array
|
|
95
|
+
? new Array(current.length)
|
|
96
|
+
: prototype === Object.prototype
|
|
97
|
+
? {}
|
|
98
|
+
: Object.create(prototype);
|
|
60
99
|
for (const key of Reflect.ownKeys(current)) {
|
|
100
|
+
if (array && key === "length") {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
61
103
|
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
62
|
-
if (descriptor === undefined ||
|
|
63
|
-
!("value" in descriptor) ||
|
|
64
|
-
(Array.isArray(current) && key === "length")) {
|
|
104
|
+
if (descriptor === undefined || !("value" in descriptor)) {
|
|
65
105
|
continue;
|
|
66
106
|
}
|
|
67
107
|
const next = copy(descriptor.value);
|
|
68
108
|
changed ||= next !== descriptor.value;
|
|
69
|
-
|
|
109
|
+
place(target, key, next, descriptor.enumerable === true);
|
|
70
110
|
}
|
|
71
|
-
|
|
111
|
+
const held = holdsScalar;
|
|
112
|
+
holdsScalar = outer || held;
|
|
72
113
|
if (!changed) {
|
|
73
114
|
copies.set(current, current);
|
|
74
115
|
if (!cycled) {
|
|
@@ -76,20 +117,18 @@ export function frozenCopy(value) {
|
|
|
76
117
|
}
|
|
77
118
|
return current;
|
|
78
119
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
120
|
+
Object.freeze(target);
|
|
121
|
+
DEEP_FROZEN.add(target);
|
|
122
|
+
if (cycled) {
|
|
123
|
+
for (const key of Reflect.ownKeys(target)) {
|
|
124
|
+
deepFreeze(target[key]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (snapshot && !held && !cycled) {
|
|
128
|
+
SCALAR_FREE.add(target);
|
|
89
129
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return frozen;
|
|
130
|
+
copies.set(current, target);
|
|
131
|
+
return target;
|
|
93
132
|
};
|
|
94
133
|
return copy(value);
|
|
95
134
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
2
|
+
import { type PolicyCaller } from "../policy/policy-request.js";
|
|
2
3
|
import type { TransactionDiagnosticsChannel } from "../runtime/diagnostics-reporter.js";
|
|
3
4
|
import type { OperationPipelineDependencies, OperationRuntimeDependencies } from "../runtime/operation-runtime.js";
|
|
4
5
|
import type { PlanArguments } from "./transaction-plan-validator.js";
|
|
@@ -13,7 +14,9 @@ import type { AvTransactionResponse } from "./transaction-response.js";
|
|
|
13
14
|
* runner's mixed plans), and `planArguments` whether the steps' arguments are
|
|
14
15
|
* copied at the door; see `validateTransactionPlan`.
|
|
15
16
|
*/
|
|
16
|
-
export type FrameworkTransactionExecutor = (request: FrameworkTransactionRequest, diagnostics: TransactionDiagnosticsChannel, origins: readonly TransactionOperationOrigin[] | undefined, planArguments: PlanArguments
|
|
17
|
+
export type FrameworkTransactionExecutor = (request: FrameworkTransactionRequest, diagnostics: TransactionDiagnosticsChannel, origins: readonly TransactionOperationOrigin[] | undefined, planArguments: PlanArguments,
|
|
18
|
+
/** The principal an application run's steps run as (`{ as }`), when it names one. */
|
|
19
|
+
caller?: PolicyCaller) => Promise<AvTransactionResponse>;
|
|
17
20
|
/**
|
|
18
21
|
* Validates a whole plan through the Framework Validation Layer, then runs it inside
|
|
19
22
|
* exactly one Adapter transaction. The touched Resources' transaction stages and
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { transactionLevelsOf } from "../config/pipeline-tree.resolver.js";
|
|
2
|
+
import { openRequestPolicy, } from "../policy/policy-request.js";
|
|
2
3
|
import { AdapterBoundary, planOperationArgumentsPath, planStepPath, sanitizeOperationFailure, } from "../runtime/error-boundary.js";
|
|
3
4
|
import { internalFailure, operationSuccess, } from "../runtime/operation-response.js";
|
|
4
5
|
import { admitOperation, EVERY_FIELD, executeAdmittedOperation, prepareStep, STEP_PIPES, withRequestState, } from "../runtime/operation-runtime.js";
|
|
@@ -20,13 +21,13 @@ export function createTransactionRuntime(dependencies, pipelineDependencies) {
|
|
|
20
21
|
cascades: resolveCascadeClosure(adapter.referentialActions),
|
|
21
22
|
sqlStatements: adapter.sqlStatements === true,
|
|
22
23
|
};
|
|
23
|
-
return async (seeded, diagnostics, origins, planArguments) => {
|
|
24
|
+
return async (seeded, diagnostics, origins, planArguments, caller) => {
|
|
24
25
|
const request = withRequestState(seeded);
|
|
25
26
|
const validation = await validateTransactionPlan(planDependencies, request, origins, planArguments);
|
|
26
27
|
if (!validation.ok) {
|
|
27
28
|
return validation.response;
|
|
28
29
|
}
|
|
29
|
-
return executeTransactionPlan(
|
|
30
|
+
return executeTransactionPlan(dependencies, pipelineDependencies, request, validation.plan, request.state, diagnostics, caller);
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
function planOrigin(request, plan) {
|
|
@@ -79,8 +80,21 @@ function standingReferences(core, operation, args, steps, subject) {
|
|
|
79
80
|
}
|
|
80
81
|
return { ok: false, ...refused };
|
|
81
82
|
}
|
|
82
|
-
async function executeTransactionPlan(
|
|
83
|
-
const
|
|
83
|
+
async function executeTransactionPlan(runtime, dependencies, request, plan, state, diagnostics, caller) {
|
|
84
|
+
const { adapter } = runtime;
|
|
85
|
+
const steps = new Map();
|
|
86
|
+
const stateful = (operation) => {
|
|
87
|
+
const known = steps.get(operation);
|
|
88
|
+
if (known !== undefined) {
|
|
89
|
+
return known;
|
|
90
|
+
}
|
|
91
|
+
const policy = openRequestPolicy(runtime.config.policy, runtime.contracts.core, { origin: operation.request.origin, state }, caller, adapter);
|
|
92
|
+
const step = policy === undefined
|
|
93
|
+
? { ...operation.request, state }
|
|
94
|
+
: { ...operation.request, state, policy };
|
|
95
|
+
steps.set(operation, step);
|
|
96
|
+
return step;
|
|
97
|
+
};
|
|
84
98
|
const admitted = [];
|
|
85
99
|
const standing = [];
|
|
86
100
|
for (const step of plan.operations) {
|
|
@@ -90,7 +104,15 @@ async function executeTransactionPlan(adapter, dependencies, request, plan, stat
|
|
|
90
104
|
continue;
|
|
91
105
|
}
|
|
92
106
|
const report = diagnostics.operation(step.request, step.index);
|
|
93
|
-
|
|
107
|
+
let stepRequest;
|
|
108
|
+
try {
|
|
109
|
+
stepRequest = stateful(step);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
report(error, "A3000");
|
|
113
|
+
return transactionOperationFailure(internalFailure(), step.index);
|
|
114
|
+
}
|
|
115
|
+
const admission = await admitOperation(dependencies, stepRequest, step.contract, step.arguments, planOperationArgumentsPath(step.index), report, { requestId: request.requestId, transaction: undefined, state }, {
|
|
94
116
|
select: EVERY_FIELD,
|
|
95
117
|
validation: { transactionReferences: true },
|
|
96
118
|
beforeBegin: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/core",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "Core runtime, contract compiler, and extension APIs for Aventara.",
|
|
6
6
|
"type": "module",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"access": "public"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
|
-
"build": "node ../../scripts/build/build-output.cleaner.ts dist && tsc -p tsconfig.json --removeComments --declaration false && tsc -p tsconfig.json --emitDeclarationOnly && node ../../scripts/build/declaration-comments.sanitizer.ts dist && node scripts/errors-markdown.generator.ts",
|
|
48
|
+
"build": "node ../../scripts/build/build-output.cleaner.ts dist && node scripts/core-version.generator.ts && tsc -p tsconfig.json --removeComments --declaration false && tsc -p tsconfig.json --emitDeclarationOnly && node ../../scripts/build/declaration-comments.sanitizer.ts dist && node scripts/errors-markdown.generator.ts",
|
|
49
49
|
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
|
|
50
50
|
"test": "vitest run --typecheck --config ../../vitest.config.mts --root .",
|
|
51
51
|
"lint": "biome lint src scripts"
|