@aventara/core 1.0.0-rc.1 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +295 -1
- package/ERRORS.md +142 -0
- package/README.md +24 -1
- package/dist/adapters/adapter-discovery-diagnostic.guidance.d.ts +6 -0
- package/dist/adapters/adapter-discovery-diagnostic.guidance.js +7 -0
- package/dist/adapters/adapter-field.d.ts +14 -0
- package/dist/adapters/adapter-model.d.ts +9 -0
- package/dist/adapters/adapter.interface.d.ts +10 -0
- package/dist/compiler/contract-compiler-diagnostic.guidance.d.ts +6 -0
- package/dist/compiler/contract-compiler-diagnostic.guidance.js +97 -0
- package/dist/compiler/contract.compiler.js +94 -6
- package/dist/config/computed-fields.d.ts +62 -0
- package/dist/config/computed-fields.js +80 -0
- package/dist/config/config-diagnostic.guidance.d.ts +6 -0
- package/dist/config/config-diagnostic.guidance.js +32 -0
- package/dist/config/define-resource.d.ts +1 -1
- package/dist/config/exact-framework-config.d.ts +48 -20
- package/dist/config/field-compute.resolver.d.ts +2 -0
- package/dist/config/field-compute.resolver.js +8 -0
- package/dist/config/framework-config.d.ts +7 -6
- package/dist/config/framework-resources-config.d.ts +18 -12
- package/dist/config/pipeline-config.d.ts +4 -4
- package/dist/contracts/capability.d.ts +10 -1
- package/dist/contracts/capability.js +2 -0
- package/dist/contracts/client-contract-structure.validator.d.ts +65 -35
- package/dist/contracts/client-contract-structure.validator.js +4 -2
- package/dist/contracts/contract-canonicalizer.d.ts +7 -0
- package/dist/contracts/contract-canonicalizer.js +18 -4
- package/dist/contracts/field.d.ts +30 -1
- package/dist/framework/contract-view.d.ts +4 -4
- package/dist/framework/framework-construction.error.d.ts +12 -3
- package/dist/framework/framework-construction.error.js +5 -1
- package/dist/framework/framework.factory.d.ts +1 -1
- package/dist/framework/framework.factory.js +9 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +1 -0
- package/dist/operations/database-refusal.d.ts +51 -0
- package/dist/operations/database-refusal.js +45 -0
- package/dist/operations/operation-arguments.d.ts +102 -31
- package/dist/operations/operation-validator-core.js +86 -4
- package/dist/operations/validation-code.guidance.d.ts +6 -0
- package/dist/operations/validation-code.guidance.js +90 -0
- package/dist/operations/write-scope.resolver.d.ts +4 -0
- package/dist/operations/write-scope.resolver.js +2 -0
- package/dist/protocol/protocol.handler.js +55 -16
- package/dist/runtime/caller-data.copier.d.ts +17 -0
- package/dist/runtime/caller-data.copier.js +18 -1
- package/dist/runtime/caller-data.door.d.ts +11 -5
- package/dist/runtime/computed-read-runtime.js +15 -2
- package/dist/runtime/computed-value.error.d.ts +3 -2
- package/dist/runtime/computed-value.error.js +3 -2
- package/dist/runtime/create-requirement.d.ts +41 -5
- package/dist/runtime/create-requirement.js +137 -20
- package/dist/runtime/diagnostics-reporter.d.ts +2 -1
- package/dist/runtime/error-boundary.js +4 -0
- package/dist/runtime/error-guidance.d.ts +12 -0
- package/dist/runtime/error-guidance.js +1 -0
- package/dist/runtime/error.catalog.d.ts +49 -0
- package/dist/runtime/error.catalog.js +49 -0
- package/dist/runtime/field-stage.runtime.d.ts +2 -0
- package/dist/runtime/field-stage.runtime.js +8 -4
- package/dist/runtime/field-validator.runtime.d.ts +9 -1
- package/dist/runtime/field-validator.runtime.js +11 -6
- package/dist/runtime/json-value.d.ts +10 -5
- package/dist/runtime/level-lifecycle.runtime.d.ts +2 -2
- package/dist/runtime/level-lifecycle.runtime.js +2 -5
- package/dist/runtime/mutation-planner.js +3 -14
- package/dist/runtime/operation-code.guidance.d.ts +6 -0
- package/dist/runtime/operation-code.guidance.js +167 -0
- package/dist/runtime/operation-runtime.d.ts +20 -6
- package/dist/runtime/operation-runtime.js +77 -35
- package/dist/runtime/owned-execution.registry.d.ts +12 -2
- package/dist/runtime/pipe-output.validator.d.ts +10 -0
- package/dist/runtime/pipe-output.validator.js +4 -2
- package/dist/runtime/pipeline.d.ts +35 -8
- package/dist/runtime/readonly-value.d.ts +14 -3
- package/dist/runtime/readonly-value.js +10 -1
- package/dist/runtime/scalar-runtime.d.ts +5 -1
- package/dist/runtime/scalar-runtime.js +86 -16
- package/dist/runtime/unsent-value.normalizer.d.ts +15 -0
- package/dist/runtime/unsent-value.normalizer.js +128 -0
- package/dist/transactions/operation-fingerprint.js +2 -2
- package/dist/transactions/sql-statement.d.ts +8 -0
- package/dist/transactions/sql-statement.validator.js +36 -3
- package/dist/transactions/transaction-plan-assembler.d.ts +1 -1
- package/dist/transactions/transaction-plan-assembler.js +33 -10
- package/dist/transactions/transaction-plan-validator.d.ts +16 -0
- package/dist/transactions/transaction-plan-validator.js +27 -1
- package/dist/transactions/transaction-reference-validator.d.ts +42 -0
- package/dist/transactions/transaction-reference-validator.js +82 -56
- package/dist/transactions/transaction-reference.js +5 -0
- package/dist/transactions/transaction-runtime.js +47 -22
- package/dist/transactions/transaction-stage.runtime.d.ts +2 -1
- package/dist/transactions/transaction-stage.runtime.js +7 -4
- package/package.json +4 -3
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export const OPERATION_CODE_GUIDANCE = {
|
|
2
|
+
A1000: {
|
|
3
|
+
message: "Success.",
|
|
4
|
+
cause: "A read, a count or another operation without a more specific success code succeeded.",
|
|
5
|
+
fix: "Nothing: the call succeeded.",
|
|
6
|
+
},
|
|
7
|
+
A1001: {
|
|
8
|
+
message: "No match.",
|
|
9
|
+
cause: "A first-style operation (find.first, update.first, delete.first) matched no row; data is null and the generated client resolves null.",
|
|
10
|
+
fix: "Nothing: handle the null result. A unique operation answers A2003 instead.",
|
|
11
|
+
},
|
|
12
|
+
A1002: {
|
|
13
|
+
message: "Created.",
|
|
14
|
+
cause: "create.one created a row.",
|
|
15
|
+
fix: "Nothing: the call succeeded.",
|
|
16
|
+
},
|
|
17
|
+
A1003: {
|
|
18
|
+
message: "Created many.",
|
|
19
|
+
cause: "create.many or create.count created rows.",
|
|
20
|
+
fix: "Nothing: the call succeeded.",
|
|
21
|
+
},
|
|
22
|
+
A1004: {
|
|
23
|
+
message: "Updated.",
|
|
24
|
+
cause: "update.first or update.unique updated a row.",
|
|
25
|
+
fix: "Nothing: the call succeeded.",
|
|
26
|
+
},
|
|
27
|
+
A1005: {
|
|
28
|
+
message: "Updated many.",
|
|
29
|
+
cause: "update.many or update.count updated rows.",
|
|
30
|
+
fix: "Nothing: the call succeeded.",
|
|
31
|
+
},
|
|
32
|
+
A1006: {
|
|
33
|
+
message: "Deleted.",
|
|
34
|
+
cause: "delete.first or delete.unique deleted a row.",
|
|
35
|
+
fix: "Nothing: the call succeeded.",
|
|
36
|
+
},
|
|
37
|
+
A1007: {
|
|
38
|
+
message: "Deleted many.",
|
|
39
|
+
cause: "delete.count deleted rows.",
|
|
40
|
+
fix: "Nothing: the call succeeded.",
|
|
41
|
+
},
|
|
42
|
+
A1008: {
|
|
43
|
+
message: "Upserted.",
|
|
44
|
+
cause: "upsert.unique created or updated a row.",
|
|
45
|
+
fix: "Nothing: the call succeeded.",
|
|
46
|
+
},
|
|
47
|
+
A1009: {
|
|
48
|
+
message: "Transaction committed.",
|
|
49
|
+
cause: "A whole transaction plan committed; data holds one result per step.",
|
|
50
|
+
fix: "Nothing: the plan committed.",
|
|
51
|
+
},
|
|
52
|
+
A2000: {
|
|
53
|
+
message: "Invalid request.",
|
|
54
|
+
cause: "The HTTP request cannot be interpreted: malformed JSON, a body that is not a JSON object, an unknown `/_*` path, a body that does not decode in its Content-Encoding, or a missing Aventara-Protocol-Version or Aventara-Contract-Hash header (the message names it).",
|
|
55
|
+
fix: "Send a JSON object with both identity headers. The generated client does this for you.",
|
|
56
|
+
},
|
|
57
|
+
A2001: {
|
|
58
|
+
message: "Unknown Resource.",
|
|
59
|
+
cause: 'The Resource is not in the caller\'s contract: misspelt, hidden by exposure, or added to the schema after aventara:prepare last ran. cause.message is "The requested Resource is not available." and the issue is V1005 at `["resource"]`.',
|
|
60
|
+
fix: "Use the model's exact name. After a schema change run the adapter's generate step (`npm run aventara:prepare` in a scaffolded project), restart, and regenerate the client. A Resource hidden on purpose is reached from server code (framework.application).",
|
|
61
|
+
},
|
|
62
|
+
A2002: {
|
|
63
|
+
message: "Unknown operation.",
|
|
64
|
+
cause: 'The operation is not available on that Resource: a typo, removed by exposure, an operation the adapter does not offer (such as delete.many), or a ctx.$sql, appTx.$sql or ctx.$transaction the adapter or the layer cannot run. The issue is V1006 at `["family"]` or `["variant"]`.',
|
|
65
|
+
fix: "Call an operation the contract lists (`GET <api>/_contract`), regenerate a stale client, and check the layer's exemptions and transactionsStyle.",
|
|
66
|
+
},
|
|
67
|
+
A2003: {
|
|
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.",
|
|
70
|
+
fix: "Catch AvNotFoundError, or use a first-style operation, which answers A1001 with null. Check the $connect identifier.",
|
|
71
|
+
},
|
|
72
|
+
A2004: {
|
|
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"]` 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
|
+
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
|
+
},
|
|
77
|
+
A2005: {
|
|
78
|
+
message: "Contract mismatch.",
|
|
79
|
+
cause: "The caller's Aventara-Contract-Hash is not the server's current one: the schema, the configuration, the limits or the exposure changed, or the client targets another deployment.",
|
|
80
|
+
fix: "Regenerate the client against the running server: `npm run avclient:generate`.",
|
|
81
|
+
},
|
|
82
|
+
A2006: {
|
|
83
|
+
message: "Protocol mismatch.",
|
|
84
|
+
cause: "Aventara-Protocol-Version is not 1.",
|
|
85
|
+
fix: "Send `Aventara-Protocol-Version: 1`, or upgrade the client.",
|
|
86
|
+
},
|
|
87
|
+
A2007: {
|
|
88
|
+
message: "Invalid transaction reference.",
|
|
89
|
+
cause: "A transaction $ref or fingerprint is invalid: the issues are V1010, V1011, V1016, V1017 or V1018, and cause.operation is the step. A $ref with no fingerprint is A2004 / V1000.",
|
|
90
|
+
fix: "Let the generated client or framework.appTx build plans; check the $ref path and that the referenced step comes earlier.",
|
|
91
|
+
},
|
|
92
|
+
A2008: {
|
|
93
|
+
message: "Conflict.",
|
|
94
|
+
cause: "A uniqueness or reference constraint failed: a duplicate value, a missing parent, or a $disconnect or $set that would orphan a required child (also through update.first or a developer SQL statement). Sending it again unchanged fails the same way.",
|
|
95
|
+
fix: "Change the data. For a duplicate, update the existing row or use upsert.unique.",
|
|
96
|
+
},
|
|
97
|
+
A2009: {
|
|
98
|
+
message: "Limit exceeded.",
|
|
99
|
+
cause: "A contract limit was crossed: nesting depth (V1013), list or plan size (V1014), boolean nodes (V1015) or Json value depth (V1021).",
|
|
100
|
+
fix: "Ask for less (paginate, split the plan), or raise the limit under `limits`.",
|
|
101
|
+
},
|
|
102
|
+
A2010: {
|
|
103
|
+
message: "Payload too large.",
|
|
104
|
+
cause: "The decoded request body exceeds maxRequestBytes (default 1048576).",
|
|
105
|
+
fix: "Send less, or raise limits.maxRequestBytes.",
|
|
106
|
+
},
|
|
107
|
+
A2011: {
|
|
108
|
+
message: "Unsupported media type.",
|
|
109
|
+
cause: "The body is not application/json, or its Content-Encoding is not gzip, deflate or br.",
|
|
110
|
+
fix: "Send `Content-Type: application/json`.",
|
|
111
|
+
},
|
|
112
|
+
A2012: {
|
|
113
|
+
message: "Method not allowed.",
|
|
114
|
+
cause: "The HTTP method does not fit the route; the Allow header names the right one (`POST` on operations and `/_transactions`, `GET` on `/_contract`).",
|
|
115
|
+
fix: "Use the method the Allow header names.",
|
|
116
|
+
},
|
|
117
|
+
A2013: {
|
|
118
|
+
message: "Concurrent modification.",
|
|
119
|
+
cause: "A competing writer reached the state first; nothing was written.",
|
|
120
|
+
fix: "Retry. No retry is automatic.",
|
|
121
|
+
},
|
|
122
|
+
A2014: {
|
|
123
|
+
message: "Stale selection.",
|
|
124
|
+
cause: "The selected state changed before the write; nothing was written.",
|
|
125
|
+
fix: "Read again, then retry.",
|
|
126
|
+
},
|
|
127
|
+
A3000: {
|
|
128
|
+
message: "Internal error.",
|
|
129
|
+
cause: 'An unexpected failure on the server: a stage threw an error that is not an AvError, an interceptor called next() twice, a compute stage reached a write through its read-only ctx, a compute stage returned a value of the wrong type, or a compute.create returned undefined for a required field. cause.message is always "Internal framework error."',
|
|
130
|
+
fix: "Read the real error in the diagnostics sink (the root `diagnostics` key) or the server log line for the request id, and fix that stage. To answer with a code of your choice, throw an AvError.",
|
|
131
|
+
},
|
|
132
|
+
A3001: {
|
|
133
|
+
message: "Adapter error.",
|
|
134
|
+
cause: 'A database or ORM failure that is not something the caller can fix. cause.message is always "Adapter execution failed."',
|
|
135
|
+
fix: "Read the raw error in the diagnostics sink; check the database, its connection and the migration state.",
|
|
136
|
+
},
|
|
137
|
+
A3002: {
|
|
138
|
+
message: "Transaction error.",
|
|
139
|
+
cause: "Transaction infrastructure failed (commit or rollback, or the transaction expired or was closed under a step), or a Resource transaction interceptor called next() twice or resolved without calling it.",
|
|
140
|
+
fix: "Read the diagnostics sink. Call next() exactly once in a transaction interceptor; retry an infrastructure failure.",
|
|
141
|
+
},
|
|
142
|
+
A3003: {
|
|
143
|
+
message: "Serialization error.",
|
|
144
|
+
cause: "A result could not be encoded for transport.",
|
|
145
|
+
fix: "Read the diagnostics sink; make compute.read stages and Json values return plain data of the field's type.",
|
|
146
|
+
},
|
|
147
|
+
A3004: {
|
|
148
|
+
message: "Pipe output invalid.",
|
|
149
|
+
cause: 'A server pipe, field pipe or transaction pipe (or write compute) returned arguments the server\'s own contract refuses: a field the Resource does not have, a wrong type, a dropped required field, a reference written together with its relation (V1012), or a changed plan. cause.message is always "A server pipeline produced invalid arguments."',
|
|
150
|
+
fix: "Read the server log line, which names the pipe, the operation and the path, and fix the pipe. A pipe returns a new object of the same shape, or undefined to keep the arguments.",
|
|
151
|
+
},
|
|
152
|
+
A4000: {
|
|
153
|
+
message: "Unauthenticated.",
|
|
154
|
+
cause: 'A guard threw new AvError("A4000", ...): authentication is required or failed. The framework never authenticates by itself.',
|
|
155
|
+
fix: "Send the credentials your guards expect, such as the Authorization header.",
|
|
156
|
+
},
|
|
157
|
+
A4001: {
|
|
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.',
|
|
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
|
+
},
|
|
162
|
+
A4002: {
|
|
163
|
+
message: "Policy denied.",
|
|
164
|
+
cause: 'A guard or hook threw new AvError("A4002", ...) with a policy reason of your own.',
|
|
165
|
+
fix: "Read cause.message for the policy's reason.",
|
|
166
|
+
},
|
|
167
|
+
};
|
|
@@ -104,7 +104,13 @@ export type ValidatedOperationArguments = {
|
|
|
104
104
|
};
|
|
105
105
|
/** Validates one operation's arguments through the Framework Validation Layer. */
|
|
106
106
|
export declare function validateOperationRequest(contract: AvContract, request: FrameworkOperationRequest, options?: OperationValidationOptions, write?: NormalizationWrite): ValidatedOperationArguments;
|
|
107
|
-
/**
|
|
107
|
+
/** The subject a refused output of an operation's pipes is reported under at a plan step. */
|
|
108
|
+
export declare const STEP_PIPES = "Pipes";
|
|
109
|
+
/**
|
|
110
|
+
* Every sent field's path. A `$ref` is an object of `$`-keys, which no field
|
|
111
|
+
* pipe or validator takes, so a plan step's literal values before BEGIN are
|
|
112
|
+
* every field too, wherever a pipe left a reference or a literal.
|
|
113
|
+
*/
|
|
108
114
|
export declare const EVERY_FIELD: () => boolean;
|
|
109
115
|
/**
|
|
110
116
|
* What admission leaves: the deep-frozen arguments every later stage reads, and
|
|
@@ -123,16 +129,24 @@ export type AdmittedOperation = {
|
|
|
123
129
|
export type AdmissionScope = {
|
|
124
130
|
/**
|
|
125
131
|
* The sent values the field pipes and validators take: every one for a single
|
|
126
|
-
* call
|
|
127
|
-
* at the step.
|
|
132
|
+
* call and for a plan step before BEGIN (an unresolved `$ref` is an object of
|
|
133
|
+
* `$`-keys, which they never take), its resolved `$ref` values at the step.
|
|
128
134
|
*/
|
|
129
135
|
readonly select: (path: readonly (string | number)[]) => boolean;
|
|
130
136
|
/** Before BEGIN a plan step's `$ref` slots are unresolved: these admit them. */
|
|
131
137
|
readonly validation?: OperationValidationOptions;
|
|
138
|
+
/**
|
|
139
|
+
* A plan step or a forced call, admitted before BEGIN: the validators judge
|
|
140
|
+
* there the sent values no later stage can change (a field with no write
|
|
141
|
+
* compute stage of its record's phase), so a caller's refusal opens no
|
|
142
|
+
* transaction. Everything is judged again after write compute.
|
|
143
|
+
*/
|
|
144
|
+
readonly beforeBegin?: boolean;
|
|
132
145
|
};
|
|
133
146
|
/**
|
|
134
|
-
* Runs an operation's admission — its guards
|
|
135
|
-
* the
|
|
147
|
+
* Runs an operation's admission — its guards and pipes, and before BEGIN the
|
|
148
|
+
* validators of the values nothing after it changes — and answers the
|
|
149
|
+
* deep-frozen arguments the rest of the operation reads, or the refusal.
|
|
136
150
|
*/
|
|
137
151
|
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>;
|
|
138
152
|
/** How a plan step's arguments were changed between their admission and the step. */
|
|
@@ -146,7 +160,7 @@ export type StepChanges = {
|
|
|
146
160
|
};
|
|
147
161
|
/**
|
|
148
162
|
* A plan step's (or a forced call's) arguments at the step: resolved values
|
|
149
|
-
* through their field
|
|
163
|
+
* through their field pipes, and re-checked when a stage changed them.
|
|
150
164
|
*/
|
|
151
165
|
export declare function prepareStep<M extends AvAdapterModel>(dependencies: OperationPipelineDependencies<M>, request: StatefulOperationRequest, contract: AvContract, args: unknown, argumentsPath: readonly (string | number)[], report: DiagnosticsReporter, executor: AvAdapterTransactionExecutor<M>, changes: StepChanges, outer: OuterFailureLayer): Promise<AdmittedOperation>;
|
|
152
166
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasFieldValidators } from "../compiler/field-validator.compiler.js";
|
|
2
|
+
import { createComputedFields } from "../config/field-compute.resolver.js";
|
|
2
3
|
import { fieldStagesOf, isForcedOperation, nestedTargetLevels, operationLevels, rawPipelineTree, transactionLevelsOf, } from "../config/pipeline-tree.resolver.js";
|
|
3
4
|
import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
|
|
4
5
|
import { findOperationAvailabilityIssue, validateOperationArguments, } from "../operations/operation-validator.js";
|
|
@@ -7,8 +8,9 @@ import { settleTransaction } from "../transactions/transaction-settlement.runtim
|
|
|
7
8
|
import { TRANSACTION_PIPES, transactionFacts, } from "../transactions/transaction-stage.runtime.js";
|
|
8
9
|
import { copyAtDoor, misuseIdentityOf, refuseCallerData, } from "./caller-data.door.js";
|
|
9
10
|
import { materializeReadOperation, planReadOperation, } from "./computed-read-runtime.js";
|
|
11
|
+
import { AvComputedValueError } from "./computed-value.error.js";
|
|
10
12
|
import { createContextExecutor, } from "./context-executor.runtime.js";
|
|
11
|
-
import { createRequirementIssues, recordWriteFactsOf, } from "./create-requirement.js";
|
|
13
|
+
import { computedRequirementGapsOf, createRequirementIssues, preliminaryRecordIssues, recordWriteFactsOf, } from "./create-requirement.js";
|
|
12
14
|
import { createDiagnosticsReporter, } from "./diagnostics-reporter.js";
|
|
13
15
|
import { AdapterBoundary, OPERATION_ARGUMENTS_PATH, resolveOperationFailure, sanitizeOperationFailure, } from "./error-boundary.js";
|
|
14
16
|
import { planFieldStages } from "./field-stage.plan.js";
|
|
@@ -23,10 +25,11 @@ import { nestedWriteTargetsOf, } from "./nested-write.walker.js";
|
|
|
23
25
|
import { internalFailure, operationFailure } from "./operation-response.js";
|
|
24
26
|
import { mapOperationOutcome, mapValidationOutcome, uniqueMissFailure, } from "./outcome-mapper.js";
|
|
25
27
|
import { AvOutputValidationError, validateOperationOutput, } from "./output-validator.js";
|
|
26
|
-
import { refusePipeOutput, refuseStageOutput, } from "./pipe-output.validator.js";
|
|
28
|
+
import { AvPipeOutputError, PIPE_OUTPUT_FAILURE_MESSAGE, refusePipeOutput, refuseStageOutput, } from "./pipe-output.validator.js";
|
|
27
29
|
import { deepFreeze, frozenCopy } from "./readonly-value.js";
|
|
28
30
|
import { requestStateOf } from "./request-state.resolver.js";
|
|
29
31
|
import { normalizeOperationScalarValues, } from "./scalar-runtime.js";
|
|
32
|
+
import { withoutUnsentWriteValues } from "./unsent-value.normalizer.js";
|
|
30
33
|
function scopeOf(request, binding) {
|
|
31
34
|
return {
|
|
32
35
|
requestId: request.requestId,
|
|
@@ -39,7 +42,12 @@ export function withRequestState(request) {
|
|
|
39
42
|
}
|
|
40
43
|
export function createPipelineDependencies(dependencies, model) {
|
|
41
44
|
const misuse = createMisuseReporter(dependencies.config.misuse);
|
|
42
|
-
const recordWrites = recordWriteFactsOf(
|
|
45
|
+
const recordWrites = recordWriteFactsOf({
|
|
46
|
+
...(dependencies.adapter.referentialActions === undefined
|
|
47
|
+
? {}
|
|
48
|
+
: { referentialActions: dependencies.adapter.referentialActions }),
|
|
49
|
+
model,
|
|
50
|
+
});
|
|
43
51
|
const contexts = createContextExecutor({
|
|
44
52
|
model,
|
|
45
53
|
adapter: dependencies.adapter,
|
|
@@ -121,7 +129,7 @@ function operationFacts(contract, request, args) {
|
|
|
121
129
|
family: request.family,
|
|
122
130
|
variant: request.variant,
|
|
123
131
|
},
|
|
124
|
-
$args: args,
|
|
132
|
+
$args: frozenCopy(args),
|
|
125
133
|
$origin: request.origin,
|
|
126
134
|
$requestId: request.requestId,
|
|
127
135
|
$contract: contract,
|
|
@@ -177,15 +185,16 @@ export function createOperationRuntime(dependencies, pipelineDependencies) {
|
|
|
177
185
|
return validated.response;
|
|
178
186
|
}
|
|
179
187
|
const stateful = withRequestState(request);
|
|
188
|
+
const forced = isForcedOperation(activeScopeConfig(dependencies.config, request).pipelines, request);
|
|
180
189
|
const admitted = await admitOperation(pipelineDependencies, stateful, validated.contract, validated.arguments, OPERATION_ARGUMENTS_PATH, report, {
|
|
181
190
|
requestId: request.requestId,
|
|
182
191
|
transaction: undefined,
|
|
183
192
|
state: stateful.state,
|
|
184
|
-
});
|
|
193
|
+
}, forced ? FORCED_CALL : SINGLE_CALL);
|
|
185
194
|
if (!admitted.ok) {
|
|
186
195
|
return admitted.response;
|
|
187
196
|
}
|
|
188
|
-
if (
|
|
197
|
+
if (forced) {
|
|
189
198
|
return executeForcedOperation(pipelineDependencies, dependencies.adapter, stateful, validated.contract, admitted.args, report);
|
|
190
199
|
}
|
|
191
200
|
return executeAdmittedOperation(pipelineDependencies, stateful, validated.contract, admitted.args, OPERATION_ARGUMENTS_PATH, { kind: "standalone", executor: dependencies.adapter }, report);
|
|
@@ -198,9 +207,11 @@ function validateOperation(dependencies, request, source, misuse) {
|
|
|
198
207
|
}
|
|
199
208
|
const contract = activeContract(dependencies.contracts, request);
|
|
200
209
|
let owned = request;
|
|
201
|
-
if (source
|
|
210
|
+
if (source !== "owned" &&
|
|
202
211
|
findOperationAvailabilityIssue(contract, request) === undefined) {
|
|
203
|
-
const copy =
|
|
212
|
+
const copy = source === "caller"
|
|
213
|
+
? copyAtDoor(request.arguments)
|
|
214
|
+
: { ok: false, error: source.refused };
|
|
204
215
|
if (!copy.ok) {
|
|
205
216
|
return rejected(mapValidationOutcome([
|
|
206
217
|
refuseCallerData(copy.error, OPERATION_ARGUMENTS_PATH, misuseIdentityOf(request), misuse),
|
|
@@ -235,6 +246,12 @@ export function protocolIdentityFailure(contracts, protocol, origin) {
|
|
|
235
246
|
return undefined;
|
|
236
247
|
}
|
|
237
248
|
export function validateOperationRequest(contract, request, options, write = "copy") {
|
|
249
|
+
if (findOperationAvailabilityIssue(contract, request) === undefined) {
|
|
250
|
+
const sent = withoutUnsentWriteValues(contract, request, request.arguments);
|
|
251
|
+
if (sent !== request.arguments) {
|
|
252
|
+
request = { ...request, arguments: sent };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
238
255
|
const validation = validateOperationArguments(contract, request, options);
|
|
239
256
|
if (!validation.ok) {
|
|
240
257
|
return { ok: false, response: mapValidationOutcome(validation.issues) };
|
|
@@ -251,12 +268,13 @@ function rejected(response) {
|
|
|
251
268
|
return { ok: false, response };
|
|
252
269
|
}
|
|
253
270
|
const FIELD_PIPES = "Field pipes";
|
|
254
|
-
const STEP_PIPES = "Pipes";
|
|
271
|
+
export const STEP_PIPES = "Pipes";
|
|
255
272
|
export const EVERY_FIELD = () => true;
|
|
256
273
|
function filtersOf(levels) {
|
|
257
274
|
return levels.flatMap((level) => level.filters);
|
|
258
275
|
}
|
|
259
276
|
const SINGLE_CALL = { select: EVERY_FIELD };
|
|
277
|
+
const FORCED_CALL = { select: EVERY_FIELD, beforeBegin: true };
|
|
260
278
|
export async function admitOperation(dependencies, request, contract, args, argumentsPath, report, scope, admission = SINGLE_CALL) {
|
|
261
279
|
const contexts = dependencies.contexts;
|
|
262
280
|
const tree = activeScopeConfig(dependencies.config, request).pipelines;
|
|
@@ -299,8 +317,10 @@ export async function admitOperation(dependencies, request, contract, args, argu
|
|
|
299
317
|
let pipeIndex = 0;
|
|
300
318
|
for (const level of levels) {
|
|
301
319
|
for (const pipe of level.pipes) {
|
|
302
|
-
const
|
|
303
|
-
|
|
320
|
+
const told = frozenCopy(current);
|
|
321
|
+
const output = await pipe(told, context());
|
|
322
|
+
if (output !== undefined && output !== told && output !== current) {
|
|
323
|
+
const returned = withoutUnsentWriteValues(dependencies.contracts.core, request, output);
|
|
304
324
|
const refused = refusePipeOutput(contracts, request, returned, pipe, pipeIndex, admission.validation);
|
|
305
325
|
if (refused !== undefined) {
|
|
306
326
|
report(refused.error, "A3004");
|
|
@@ -322,6 +342,10 @@ export async function admitOperation(dependencies, request, contract, args, argu
|
|
|
322
342
|
piped: piped || fields.piped,
|
|
323
343
|
};
|
|
324
344
|
};
|
|
345
|
+
const absent = preliminaryRecordIssues(contract, dependencies.recordWrites, request, current, argumentsPath);
|
|
346
|
+
if (absent.length > 0) {
|
|
347
|
+
return { ok: false, response: mapValidationOutcome(absent) };
|
|
348
|
+
}
|
|
325
349
|
try {
|
|
326
350
|
return await admit();
|
|
327
351
|
}
|
|
@@ -333,10 +357,12 @@ export async function admitOperation(dependencies, request, contract, args, argu
|
|
|
333
357
|
}
|
|
334
358
|
}
|
|
335
359
|
async function runFieldInputStages(dependencies, request, contract, args, argumentsPath, report, admission) {
|
|
336
|
-
const
|
|
360
|
+
const scopeConfig = activeScopeConfig(dependencies.config, request);
|
|
361
|
+
const tree = scopeConfig.pipelines;
|
|
337
362
|
const planned = dependencies.fieldStages[request.origin];
|
|
338
363
|
const pipes = planned.anyPipes;
|
|
339
|
-
|
|
364
|
+
const judge = admission.beforeBegin === true && hasFieldValidators(contract);
|
|
365
|
+
if (!pipes && !judge) {
|
|
340
366
|
return { ok: true, args, piped: false };
|
|
341
367
|
}
|
|
342
368
|
let current = args;
|
|
@@ -364,14 +390,25 @@ async function runFieldInputStages(dependencies, request, contract, args, argume
|
|
|
364
390
|
}
|
|
365
391
|
}
|
|
366
392
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
393
|
+
if (judge) {
|
|
394
|
+
const compute = scopeConfig.compute;
|
|
395
|
+
const broken = fieldValidatorIssues(contract, dependencies.recordWrites.references, request, current, argumentsPath, admission.select, (resource, stage, field) => {
|
|
396
|
+
const fields = compute !== undefined && Object.hasOwn(compute, resource)
|
|
397
|
+
? compute[resource]
|
|
398
|
+
: undefined;
|
|
399
|
+
const phases = fields !== undefined && Object.hasOwn(fields, field)
|
|
400
|
+
? fields[field]
|
|
401
|
+
: undefined;
|
|
402
|
+
return phases?.[stage] !== undefined;
|
|
403
|
+
});
|
|
404
|
+
if (broken.length > 0) {
|
|
405
|
+
return { ok: false, response: mapValidationOutcome(broken) };
|
|
406
|
+
}
|
|
370
407
|
}
|
|
371
408
|
return { ok: true, args: current, piped: changed };
|
|
372
409
|
}
|
|
373
410
|
export async function prepareStep(dependencies, request, contract, args, argumentsPath, report, executor, changes, outer) {
|
|
374
|
-
let current = deepFreeze(args);
|
|
411
|
+
let current = deepFreeze(withoutUnsentWriteValues(dependencies.contracts.core, request, args));
|
|
375
412
|
const scope = {
|
|
376
413
|
requestId: request.requestId,
|
|
377
414
|
transaction: executor,
|
|
@@ -395,12 +432,6 @@ export async function prepareStep(dependencies, request, contract, args, argumen
|
|
|
395
432
|
return { ok: false, response: refused.response };
|
|
396
433
|
}
|
|
397
434
|
}
|
|
398
|
-
if (changes.transactionPiped) {
|
|
399
|
-
const broken = fieldValidatorIssues(contract, dependencies.recordWrites.references, request, current, argumentsPath, EVERY_FIELD);
|
|
400
|
-
if (broken.length > 0) {
|
|
401
|
-
return { ok: false, response: mapValidationOutcome(broken) };
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
435
|
return { ok: true, args: deepFreeze(current), piped: resolved.piped };
|
|
405
436
|
}
|
|
406
437
|
catch (error) {
|
|
@@ -465,15 +496,8 @@ export async function executeAdmittedOperation(dependencies, request, contract,
|
|
|
465
496
|
const adapterBoundary = new AdapterBoundary();
|
|
466
497
|
const contracts = { core: dependencies.contracts.core, active: contract };
|
|
467
498
|
let args = deepFreeze(admittedArguments);
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
if (told === undefined || told.of !== args) {
|
|
471
|
-
told = { of: args, view: frozenCopy(args) };
|
|
472
|
-
}
|
|
473
|
-
return told.view;
|
|
474
|
-
};
|
|
475
|
-
const context = () => contexts.operation(scope, operationFacts(contract, request, view()));
|
|
476
|
-
const computeContext = () => contexts.compute(scope, operationFacts(contract, request, view()));
|
|
499
|
+
const context = () => contexts.operation(scope, operationFacts(contract, request, args));
|
|
500
|
+
const computeContext = () => contexts.compute(scope, operationFacts(contract, request, args));
|
|
477
501
|
const reuse = { refusal: undefined };
|
|
478
502
|
const refusedReuse = (refusal) => {
|
|
479
503
|
report(refusal, "A3000");
|
|
@@ -493,9 +517,27 @@ export async function executeAdmittedOperation(dependencies, request, contract,
|
|
|
493
517
|
async function runLifecycle() {
|
|
494
518
|
let computed = await applyWriteCompute(contracts, scopeConfig.compute, request, args, computeContext);
|
|
495
519
|
computed = await applyNestedWriteCompute(contracts, scopeConfig.compute, computed, nestedWriteTargetsOf(contract, dependencies.recordWrites.references, request, computed), (target, payload) => contexts.compute(scope, nestedFacts(contract, request, target, payload)));
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
520
|
+
const broken = createRequirementIssues(contract, dependencies.recordWrites, request, computed, argumentsPath);
|
|
521
|
+
const [firstBroken] = broken;
|
|
522
|
+
if (firstBroken !== undefined) {
|
|
523
|
+
const what = firstBroken.code === "V1000"
|
|
524
|
+
? "dropped a required field"
|
|
525
|
+
: "wrote a reference twice";
|
|
526
|
+
report(new AvPipeOutputError(`Pipes or write compute ${what} of ${request.resource}.${request.family}.${request.variant} at "${(firstBroken.path ?? []).join(".")}" (${firstBroken.code}: ${firstBroken.message})`, broken), "A3004");
|
|
527
|
+
return operationFailure("A3004", {
|
|
528
|
+
message: PIPE_OUTPUT_FAILURE_MESSAGE,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
const unfilled = computedRequirementGapsOf(contract, dependencies.recordWrites, request, computed, argumentsPath, (resource) => createComputedFields(scopeConfig.compute, resource));
|
|
532
|
+
const [firstUnfilled] = unfilled;
|
|
533
|
+
if (firstUnfilled !== undefined) {
|
|
534
|
+
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`);
|
|
535
|
+
}
|
|
536
|
+
if (hasFieldValidators(contract)) {
|
|
537
|
+
const refused = fieldValidatorIssues(contract, dependencies.recordWrites.references, request, computed, argumentsPath, EVERY_FIELD);
|
|
538
|
+
if (refused.length > 0) {
|
|
539
|
+
return mapValidationOutcome(refused);
|
|
540
|
+
}
|
|
499
541
|
}
|
|
500
542
|
args = deepFreeze(computed);
|
|
501
543
|
let readMaterialized = false;
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import type { AgentTransactionRequest, ClientTransactionRequest } from "../transactions/transaction-request.js";
|
|
2
2
|
import type { AvTransactionResponse } from "../transactions/transaction-response.js";
|
|
3
|
+
import type { AvCallerDataError } from "./caller-data.copier.js";
|
|
3
4
|
import type { RemoteOperationRequest } from "./operation-request.js";
|
|
4
5
|
import type { AvOperationResponse } from "./operation-response.js";
|
|
5
6
|
/** The two entries that run a request the protocol decoded itself, uncopied. */
|
|
6
7
|
export type OwnedExecution = {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Runs `request` uncopied; with `refused`, its arguments are a host's parsed body
|
|
10
|
+
* whose copy at the call failed, answered as the door answers that failure.
|
|
11
|
+
*/
|
|
12
|
+
readonly execute: (request: RemoteOperationRequest, refused?: AvCallerDataError) => Promise<AvOperationResponse>;
|
|
13
|
+
/**
|
|
14
|
+
* Runs `request` uncopied; with `refused`, its plan is a host's parsed plan taken
|
|
15
|
+
* at the call, and these steps' arguments could not be copied there: each is
|
|
16
|
+
* answered at its turn as the door answers that failure.
|
|
17
|
+
*/
|
|
18
|
+
readonly executeTransaction: (request: ClientTransactionRequest | AgentTransactionRequest, refused?: ReadonlyMap<number, AvCallerDataError>) => Promise<AvTransactionResponse>;
|
|
9
19
|
};
|
|
10
20
|
/** Records `framework`'s owned entries; called once, at construction. */
|
|
11
21
|
export declare function recordOwnedExecution(framework: object, execution: OwnedExecution): void;
|
|
@@ -24,6 +24,8 @@ import type { AvOperationFailureResponse } from "./operation-response.js";
|
|
|
24
24
|
*/
|
|
25
25
|
/** The one text every `A3004` carries on the wire. */
|
|
26
26
|
export declare const PIPE_OUTPUT_FAILURE_MESSAGE = "A server pipeline produced invalid arguments.";
|
|
27
|
+
/** The Contract a stage's output is held to: Core's Resources plus what `active` adds. */
|
|
28
|
+
export declare function serverContractFor(core: AvContractCore, active: AvContract): AvContract;
|
|
27
29
|
/** The error the diagnostics channel receives for a refused pipe output. */
|
|
28
30
|
export declare class AvPipeOutputError extends Error {
|
|
29
31
|
readonly issues: readonly AvValidationIssue[];
|
|
@@ -61,3 +63,11 @@ export declare function refuseStageOutput(contracts: {
|
|
|
61
63
|
}, request: AvOperationIdentity & {
|
|
62
64
|
readonly resource: string;
|
|
63
65
|
}, args: unknown, subject: string, options?: OperationValidationOptions): RefusedStageOutput | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* The `A3004` for a stage output the server's rules raised `issues` against
|
|
68
|
+
* (paths under `["arguments", …]`), the first named in the diagnostic;
|
|
69
|
+
* `undefined` when there are none.
|
|
70
|
+
*/
|
|
71
|
+
export declare function refusedStageOutput(request: AvOperationIdentity & {
|
|
72
|
+
readonly resource: string;
|
|
73
|
+
}, issues: readonly AvValidationIssue[], subject: string): RefusedStageOutput | undefined;
|
|
@@ -19,7 +19,7 @@ function serverContract(core, active) {
|
|
|
19
19
|
return { ...core, resources };
|
|
20
20
|
}
|
|
21
21
|
const SERVER_VIEWS = new WeakMap();
|
|
22
|
-
function serverContractFor(core, active) {
|
|
22
|
+
export function serverContractFor(core, active) {
|
|
23
23
|
let view = SERVER_VIEWS.get(active);
|
|
24
24
|
if (view === undefined) {
|
|
25
25
|
view = serverContract(core, active);
|
|
@@ -64,7 +64,9 @@ export function refusePipeOutput(contracts, request, args, pipe, index, options)
|
|
|
64
64
|
return refuseStageOutput(contracts, request, args, `Pipe ${pipeLabel(pipe, index)}`, options);
|
|
65
65
|
}
|
|
66
66
|
export function refuseStageOutput(contracts, request, args, subject, options) {
|
|
67
|
-
|
|
67
|
+
return refusedStageOutput(request, serverIssues(serverContractFor(contracts.core, contracts.active), request, args, options), subject);
|
|
68
|
+
}
|
|
69
|
+
export function refusedStageOutput(request, issues, subject) {
|
|
68
70
|
const [first] = issues;
|
|
69
71
|
if (first === undefined) {
|
|
70
72
|
return undefined;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
2
2
|
import type { AvAdapterSqlValue } from "../adapters/adapter-sql-statement.js";
|
|
3
|
+
import type { AvPreComputeLift } from "../config/computed-fields.js";
|
|
3
4
|
import type { AvRequestOrigin } from "../config/config-layer.js";
|
|
4
5
|
import type { AvConfigResourceKey } from "../config/framework-field-definition.js";
|
|
5
6
|
import type { AvContract } from "../contracts/contract.js";
|
|
6
7
|
import type { AvApplicationScalarForms } from "../contracts/scalar-value-type.js";
|
|
7
|
-
import type { AvContractApplicationView } from "../framework/contract-view.js";
|
|
8
|
+
import type { AvContractApplicationView, RequirementsLessComputed } from "../framework/contract-view.js";
|
|
8
9
|
import type { AvContextOperations } from "../framework/operation-facade.js";
|
|
9
10
|
import type { AvOperationArgumentsFor, AvResourceKey } from "../operations/operation-arguments.js";
|
|
10
11
|
import type { AvOperationFamily, AvOperationIdentity } from "../operations/operation-identity.js";
|
|
11
12
|
import type { AvTransactionReference } from "../transactions/transaction-reference.js";
|
|
12
13
|
import type { AvError } from "./framework-error.js";
|
|
13
|
-
import type { AvDeepReadonly, AvDeepReadonlyLeaf } from "./readonly-value.js";
|
|
14
|
+
import type { ArrayForm, AvDeepReadonly, AvDeepReadonlyLeaf } from "./readonly-value.js";
|
|
14
15
|
import type { AvState } from "./request-state.js";
|
|
15
16
|
/**
|
|
16
17
|
* What a pipeline is told about the HTTP request a client-origin operation arrived on.
|
|
@@ -50,10 +51,32 @@ export type AvContextSql = <Row = unknown>(strings: TemplateStringsArray, ...val
|
|
|
50
51
|
* position admitting an unresolved `$ref`. `unknown` for a model whose Resource
|
|
51
52
|
* names are not literal.
|
|
52
53
|
*/
|
|
53
|
-
export type AvStageArguments<M extends AvAdapterModel, R extends string, F extends AvOperationFamily, V extends string> =
|
|
54
|
+
export type AvStageArguments<M extends AvAdapterModel, R extends string, F extends AvOperationFamily, V extends string> = StageArgumentsOf<AvContractApplicationView<M>, R, F, V>;
|
|
55
|
+
/** The arguments of operation `F`.`V` on `R` under the Contract view `C`. */
|
|
56
|
+
type StageArgumentsOf<C extends AvContract, R extends string, F extends AvOperationFamily, V extends string> = string extends R ? unknown : R extends AvResourceKey<C> ? AvOperationArgumentsFor<C, R, Extract<AvOperationIdentity, {
|
|
54
57
|
readonly family: F;
|
|
55
58
|
readonly variant: V;
|
|
56
59
|
}>, AvApplicationScalarForms, AvApplicationScalarForms> : unknown;
|
|
60
|
+
/**
|
|
61
|
+
* The arguments a variant's guards, pipes and filters are told: the operation's
|
|
62
|
+
* arguments before write compute has run, so a create need not hold a field
|
|
63
|
+
* `compute.create` fills (`Lift`, as `"<Resource>.<field>"`; `string`: any).
|
|
64
|
+
*/
|
|
65
|
+
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>;
|
|
66
|
+
/** A Contract view with the create requirements `Lift` names lifted. */
|
|
67
|
+
type PreComputeView<C, Lift extends string> = Extract<{
|
|
68
|
+
readonly [P in keyof C]: P extends "resources" ? string extends Lift ? {
|
|
69
|
+
readonly [R in keyof C[P]]: Omit<C[P][R], "createRequirements">;
|
|
70
|
+
} : {
|
|
71
|
+
readonly [R in keyof C[P]]: LiftedResource<C[P][R], LiftedFieldOf<Lift, R>>;
|
|
72
|
+
} : C[P];
|
|
73
|
+
}, AvContract>;
|
|
74
|
+
/** The fields of Resource `R` that `Lift` names. */
|
|
75
|
+
type LiftedFieldOf<Lift extends string, R> = Lift extends `${R & string}.${infer Field}` ? Field : never;
|
|
76
|
+
/** One Resource with the create requirements on `Fields` lifted; itself when none. */
|
|
77
|
+
type LiftedResource<Resource, Fields> = [Fields] extends [never] ? Resource : {
|
|
78
|
+
readonly [P in keyof Resource]: P extends "createRequirements" ? RequirementsLessComputed<Exclude<Resource[P], undefined>, Fields> : Resource[P];
|
|
79
|
+
};
|
|
57
80
|
/**
|
|
58
81
|
* What a pipeline callback is told about the operation it is wrapping, and the
|
|
59
82
|
* framework's executor: `ctx.<Resource>.<family>.<variant>(args)`, `ctx.$sql` and
|
|
@@ -83,8 +106,9 @@ export type AvContext<M extends AvAdapterModel = AvAdapterModel, R extends strin
|
|
|
83
106
|
* returned them in a pipe, and the validated arguments the operation executes in
|
|
84
107
|
* an interceptor, a hook or a filter. Only a pipe changes them, by returning new
|
|
85
108
|
* ones; writing to them throws a `TypeError`. At a variant, typed as that
|
|
86
|
-
* operation's arguments ({@link AvStageArguments}), deep read-only;
|
|
87
|
-
* `
|
|
109
|
+
* operation's arguments ({@link AvStageArguments}), deep read-only; in a guard or
|
|
110
|
+
* a filter a create may lack the fields `compute.create` fills ({@link
|
|
111
|
+
* AvPreComputeStageArguments}: write compute has not run); above it, `unknown`.
|
|
88
112
|
*/
|
|
89
113
|
readonly $args: AvDeepReadonly<A>;
|
|
90
114
|
readonly $sql: AvContextSql;
|
|
@@ -103,8 +127,9 @@ export type AvGuard<M extends AvAdapterModel = AvAdapterModel, R extends string
|
|
|
103
127
|
* Transforms the arguments before the validators judge them: the only stage
|
|
104
128
|
* that changes them. `args` is deep-frozen, so a pipe returns new arguments;
|
|
105
129
|
* returning `undefined` keeps them as they are. At a variant, `args` and the
|
|
106
|
-
* return are typed as that operation's arguments
|
|
107
|
-
*
|
|
130
|
+
* return are typed as that operation's arguments before write compute runs
|
|
131
|
+
* ({@link AvPreComputeStageArguments}: a create may lack the fields
|
|
132
|
+
* `compute.create` fills), `args` deep read-only.
|
|
108
133
|
*/
|
|
109
134
|
export type AvPipe<M extends AvAdapterModel = AvAdapterModel, R extends string = AvConfigResourceKey<M>, F extends AvOperationFamily = AvOperationFamily, V extends string = string, A = unknown> = {
|
|
110
135
|
pipe(args: PipeArguments<A>, context: AvContext<M, R, F, V, A>): PipeResult<PipeArguments<A>>;
|
|
@@ -113,7 +138,9 @@ export type AvPipe<M extends AvAdapterModel = AvAdapterModel, R extends string =
|
|
|
113
138
|
* A variant pipe's arguments and return: the operation's arguments, deep
|
|
114
139
|
* read-only, each member closed to inference.
|
|
115
140
|
*/
|
|
116
|
-
type PipeArguments<T> = T extends AvDeepReadonlyLeaf | AvTransactionReference ? T : T extends
|
|
141
|
+
type PipeArguments<T> = T extends AvDeepReadonlyLeaf | AvTransactionReference ? T : T extends readonly unknown[] ? ArrayForm<T> extends "array" ? readonly NoInfer<PipeArguments<Exclude<T[number], undefined>>>[] : {
|
|
142
|
+
readonly [K in keyof T]: NoInfer<PipeArguments<Exclude<T[K], undefined>>>;
|
|
143
|
+
} : T extends object ? {
|
|
117
144
|
readonly [K in keyof T]: NoInfer<PipeArguments<Exclude<T[K], undefined>>>;
|
|
118
145
|
} : T;
|
|
119
146
|
/** What a pipe returns: arguments of type `A`, or `undefined` to keep them. */
|