@aventara/client 1.0.0-rc.0 → 1.0.0-rc.10
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 +651 -0
- package/README.md +195 -141
- package/dist/cli/command.parser.d.ts +7 -6
- package/dist/cli/command.parser.js +13 -13
- package/dist/cli/generation-failure.renderer.d.ts +2 -1
- package/dist/cli/generation-failure.renderer.js +24 -24
- package/dist/cli/generation-success.renderer.d.ts +2 -1
- package/dist/cli/step.reporter.d.ts +2 -1
- package/dist/cli/terminal.prompter.d.ts +2 -1
- package/dist/config/client-config.interface.d.ts +24 -7
- package/dist/config/config.loader.d.ts +12 -7
- package/dist/config/config.loader.js +26 -8
- package/dist/config/config.resolver.d.ts +16 -9
- package/dist/config/config.resolver.js +40 -12
- package/dist/config/env.cascade.d.ts +4 -8
- package/dist/config/env.cascade.js +7 -7
- package/dist/config/module-style.resolver.d.ts +4 -7
- package/dist/config/module-style.resolver.js +4 -4
- package/dist/config/tsconfig.locator.d.ts +3 -2
- package/dist/config/tsconfig.locator.js +16 -4
- package/dist/contract/contract.acceptance.d.ts +4 -5
- package/dist/contract/contract.acceptance.js +5 -5
- package/dist/contract/contract.fetcher.d.ts +5 -4
- package/dist/contract/contract.fetcher.js +6 -6
- package/dist/contract/contract.loader.d.ts +9 -8
- package/dist/contract/contract.loader.js +2 -2
- package/dist/emit/access.emitter.d.ts +12 -0
- package/dist/emit/access.emitter.js +263 -0
- package/dist/emit/banner.emitter.d.ts +6 -1
- package/dist/emit/banner.emitter.js +12 -8
- package/dist/emit/client-surface.emitter.d.ts +2 -2
- package/dist/emit/client-surface.emitter.js +136 -41
- package/dist/emit/client-tree.emitter.d.ts +11 -4
- package/dist/emit/client-tree.emitter.js +51 -17
- package/dist/emit/contract-carrier.emitter.d.ts +3 -3
- package/dist/emit/derivation.emitter.d.ts +3 -3
- package/dist/emit/derivation.emitter.js +7 -3
- package/dist/emit/descriptor.emitter.d.ts +2 -2
- package/dist/emit/descriptor.emitter.js +1 -1
- package/dist/emit/emitted-tree.interface.d.ts +1 -1
- package/dist/emit/enum.emitter.d.ts +2 -2
- package/dist/emit/field-check.emitter.d.ts +22 -0
- package/dist/emit/field-check.emitter.js +592 -0
- package/dist/emit/json-shape.emitter.d.ts +2 -2
- package/dist/emit/module-specifier.scanner.d.ts +2 -1
- package/dist/emit/name.deriver.d.ts +8 -9
- package/dist/emit/name.deriver.js +18 -15
- package/dist/emit/named-type.emitter.d.ts +2 -2
- package/dist/emit/named-type.emitter.js +12 -2
- package/dist/emit/runtime.emitter.d.ts +6 -5
- package/dist/emit/runtime.emitter.js +215 -116
- package/dist/emit/scalar.codec.d.ts +1 -1
- package/dist/emit/scalar.codec.js +246 -29
- package/dist/emit/transaction.emitter.js +30 -14
- package/dist/generate.d.ts +5 -4
- package/dist/generate.js +3 -3
- package/dist/hydration/hydration-tag.format.d.ts +11 -0
- package/dist/hydration/hydration-tag.format.js +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/init/client-init.errors.d.ts +4 -4
- package/dist/init/client-init.errors.js +4 -4
- package/dist/init/client-init.orchestrator.js +7 -7
- package/dist/init/client-init.planner.d.ts +3 -2
- package/dist/init/client-init.questions.d.ts +5 -16
- package/dist/init/client-init.questions.js +12 -12
- package/dist/init/client-project.inspector.d.ts +2 -2
- package/dist/init/client-project.inspector.js +3 -3
- package/dist/internal.d.ts +25 -0
- package/dist/internal.js +11 -0
- package/dist/output/output.validator.d.ts +3 -4
- package/dist/output/output.validator.js +2 -2
- package/dist/output/output.writer.d.ts +8 -7
- package/dist/output/output.writer.js +12 -12
- package/package.json +9 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { OPERATION_CODES, VALIDATION_CODES } from "@aventara/core/internal";
|
|
1
|
+
import { FRAMEWORK_ERROR_CLASS_OF, OPERATION_CODES, VALIDATION_CODES, } from "@aventara/core/internal";
|
|
2
2
|
import { AvProtocol } from "@aventara/core/protocol";
|
|
3
3
|
import { entrypointHref } from "../config/config.resolver.js";
|
|
4
|
+
import { AVCLIENT_REGENERATE, } from "./banner.emitter.js";
|
|
4
5
|
import { moduleSpecifierWriter, } from "./module-style.interface.js";
|
|
5
6
|
import { wireGrammarLiteral } from "./scalar.codec.js";
|
|
6
7
|
export function emitDecimalModule() {
|
|
@@ -14,7 +15,7 @@ export function emitDecimalModule() {
|
|
|
14
15
|
* It holds the decimal string it was made from, digit for digit, and is sent as
|
|
15
16
|
* that string. It does no arithmetic.
|
|
16
17
|
*/
|
|
17
|
-
export class
|
|
18
|
+
export class AvDecimal {
|
|
18
19
|
readonly #value: string;
|
|
19
20
|
|
|
20
21
|
/** @throws TypeError when the value is not a decimal string. */
|
|
@@ -30,7 +31,7 @@ export class Decimal {
|
|
|
30
31
|
return this.#value;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/** The decimal string, exactly as given: what JSON.stringify writes for
|
|
34
|
+
/** The decimal string, exactly as given: what JSON.stringify writes for an AvDecimal. */
|
|
34
35
|
toJSON(): string {
|
|
35
36
|
return this.#value;
|
|
36
37
|
}
|
|
@@ -51,51 +52,29 @@ export function emitMetadataModule(protocol, entrypoint) {
|
|
|
51
52
|
`export const DEFAULT_ENTRYPOINT = ${JSON.stringify(entrypointHref(entrypoint))};\n`,
|
|
52
53
|
};
|
|
53
54
|
}
|
|
55
|
+
function contractMismatchSummary(regenerate) {
|
|
56
|
+
return `This client was generated against a ClientContract other than the one the server serves. Regenerate it with ${regenerate}.`;
|
|
57
|
+
}
|
|
54
58
|
const FRAMEWORK_ERROR_CLASSES = {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
};
|
|
63
|
-
const FRAMEWORK_ERROR_CLASS_OF = {
|
|
64
|
-
A2000: "ProtocolError",
|
|
65
|
-
A2001: "ProtocolError",
|
|
66
|
-
A2002: "ProtocolError",
|
|
67
|
-
A2003: "NotFoundError",
|
|
68
|
-
A2004: "ValidationError",
|
|
69
|
-
A2005: "ContractMismatchError",
|
|
70
|
-
A2006: "ProtocolError",
|
|
71
|
-
A2007: "ValidationError",
|
|
72
|
-
A2008: "ConflictError",
|
|
73
|
-
A2009: "ValidationError",
|
|
74
|
-
A2010: "ProtocolError",
|
|
75
|
-
A2011: "ProtocolError",
|
|
76
|
-
A2012: "ProtocolError",
|
|
77
|
-
A2013: "ConflictError",
|
|
78
|
-
A2014: "ConflictError",
|
|
79
|
-
A3000: "InternalError",
|
|
80
|
-
A3001: "InternalError",
|
|
81
|
-
A3002: "InternalError",
|
|
82
|
-
A3003: "InternalError",
|
|
83
|
-
A3004: "InternalError",
|
|
84
|
-
A4000: "AuthError",
|
|
85
|
-
A4001: "AuthError",
|
|
86
|
-
A4002: "AuthError",
|
|
59
|
+
AvAuthError: "The caller is not authenticated, or is not permitted to run the operation.",
|
|
60
|
+
AvConflictError: "The operation conflicted with stored state, and nothing was written. A2008 fails the same way if sent again; A2013 and A2014 may succeed if sent again, and no retry is automatic.",
|
|
61
|
+
AvContractMismatchError: contractMismatchSummary(AVCLIENT_REGENERATE),
|
|
62
|
+
AvInternalError: "The server failed. Nothing in its cause is actionable; the diagnostics are in the server's logs.",
|
|
63
|
+
AvNotFoundError: "A strict unique target does not exist.",
|
|
64
|
+
AvProtocolError: "The request was not one the server could route or interpret: an unknown Resource or operation, a protocol version it does not speak, or a body, media type, size or method it refuses.",
|
|
65
|
+
AvValidationError: "The request broke a Contract rule: a field, type, capability, projection, reference or limit. The cause's issues say which, by code and path.",
|
|
87
66
|
};
|
|
88
|
-
export const FRAMEWORK_ERROR_CLASS_NAMES = Object.keys(FRAMEWORK_ERROR_CLASSES);
|
|
67
|
+
export const FRAMEWORK_ERROR_CLASS_NAMES = Object.keys(FRAMEWORK_ERROR_CLASSES).sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
89
68
|
const ERROR_CODES = OPERATION_CODES.filter((code) => !code.startsWith("A1"));
|
|
90
69
|
export function errorsModuleExports() {
|
|
91
70
|
return [
|
|
92
71
|
...FRAMEWORK_ERROR_CLASS_NAMES,
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"type
|
|
96
|
-
"type
|
|
97
|
-
"type
|
|
98
|
-
"type
|
|
72
|
+
"AvError",
|
|
73
|
+
"AvTransportError",
|
|
74
|
+
"type AvCause",
|
|
75
|
+
"type AvOperationCode",
|
|
76
|
+
"type AvValidationCode",
|
|
77
|
+
"type AvValidationIssue",
|
|
99
78
|
].sort((left, right) => {
|
|
100
79
|
const a = left.replace(/^type /, "");
|
|
101
80
|
const b = right.replace(/^type /, "");
|
|
@@ -108,17 +87,24 @@ function codeArray(codes) {
|
|
|
108
87
|
const ERRORS_MODULE_HEAD = `/**
|
|
109
88
|
* The outcome codes and error classes of Aventara protocol version 1.
|
|
110
89
|
*
|
|
111
|
-
* A1xxx resolves; every other code throws the
|
|
90
|
+
* A1xxx resolves; every other code throws the AvError subclass of its
|
|
112
91
|
* class; a response that is not a framework envelope throws
|
|
113
|
-
*
|
|
92
|
+
* AvTransportError. Branch on the class or on \`code\`, never on the message.
|
|
114
93
|
*/
|
|
115
94
|
`;
|
|
116
95
|
const ERRORS_MODULE_TYPES = `
|
|
117
96
|
/** One actionable validation issue; \`path\` locates it in the request. */
|
|
118
|
-
export interface
|
|
97
|
+
export interface AvValidationIssue {
|
|
119
98
|
readonly path?: readonly (string | number)[];
|
|
120
|
-
readonly code:
|
|
99
|
+
readonly code: AvValidationCode;
|
|
121
100
|
readonly message: string;
|
|
101
|
+
/**
|
|
102
|
+
* The failed check's name (max_length, string_format, …): present, with
|
|
103
|
+
* \`params\`, only on the issue of a failed field validator or Json Shape check.
|
|
104
|
+
*/
|
|
105
|
+
readonly check?: string;
|
|
106
|
+
/** That check's parameters, as the ClientContract states them ({ maximum: 80 }). */
|
|
107
|
+
readonly params?: Readonly<Record<string, unknown>>;
|
|
122
108
|
}
|
|
123
109
|
|
|
124
110
|
/**
|
|
@@ -126,26 +112,26 @@ export interface ValidationIssue {
|
|
|
126
112
|
* a member the protocol does not define is never carried, so no stack, SQL or
|
|
127
113
|
* server path the server should not have sent reaches it.
|
|
128
114
|
*/
|
|
129
|
-
export interface
|
|
115
|
+
export interface AvCause {
|
|
130
116
|
readonly message: string;
|
|
131
|
-
readonly issues?: readonly
|
|
117
|
+
readonly issues?: readonly AvValidationIssue[];
|
|
132
118
|
/** The index of the failing operation in a transaction plan. */
|
|
133
119
|
readonly operation?: number;
|
|
134
120
|
}
|
|
135
121
|
|
|
136
122
|
/**
|
|
137
123
|
* A framework outcome that throws: every code outside A1xxx. It is
|
|
138
|
-
* always thrown as the subclass of its code's class; catch
|
|
124
|
+
* always thrown as the subclass of its code's class; catch AvError to
|
|
139
125
|
* catch them all.
|
|
140
126
|
*/
|
|
141
|
-
export class
|
|
142
|
-
override readonly name: string = "
|
|
127
|
+
export class AvError<C extends AvOperationErrorCode = AvOperationErrorCode> extends Error {
|
|
128
|
+
override readonly name: string = "AvError";
|
|
143
129
|
/** The outcome code. Branch on this, never on the message. */
|
|
144
130
|
readonly code: C;
|
|
145
131
|
/** The server's cause: its message, and its issues or failing operation when it gave them. */
|
|
146
|
-
override readonly cause:
|
|
132
|
+
override readonly cause: AvCause;
|
|
147
133
|
|
|
148
|
-
constructor(code: C, cause:
|
|
134
|
+
constructor(code: C, cause: AvCause) {
|
|
149
135
|
super(cause.message);
|
|
150
136
|
this.code = code;
|
|
151
137
|
this.cause = cause;
|
|
@@ -155,12 +141,12 @@ export class FrameworkError<C extends OperationErrorCode = OperationErrorCode> e
|
|
|
155
141
|
const ERRORS_MODULE_TAIL = `
|
|
156
142
|
/**
|
|
157
143
|
* A failure that never produced a framework envelope: no response
|
|
158
|
-
* arrived, or the one that did is not an Aventara response envelope
|
|
159
|
-
* HTML error page, a truncated body, JSON of another shape. It carries no
|
|
144
|
+
* arrived, or the one that did is not an Aventara response envelope (a proxy's
|
|
145
|
+
* HTML error page, a truncated body, JSON of another shape). It carries no
|
|
160
146
|
* cause: nothing in such a response is the framework's to report.
|
|
161
147
|
*/
|
|
162
|
-
export class
|
|
163
|
-
override readonly name: string = "
|
|
148
|
+
export class AvTransportError extends Error {
|
|
149
|
+
override readonly name: string = "AvTransportError";
|
|
164
150
|
/** The HTTP status that arrived, or null when no response arrived at all. */
|
|
165
151
|
readonly status: number | null;
|
|
166
152
|
|
|
@@ -170,11 +156,15 @@ export class TransportError extends Error {
|
|
|
170
156
|
}
|
|
171
157
|
}
|
|
172
158
|
`;
|
|
173
|
-
export function emitErrorsModule() {
|
|
174
|
-
const
|
|
159
|
+
export function emitErrorsModule(regenerate = AVCLIENT_REGENERATE) {
|
|
160
|
+
const summaries = {
|
|
161
|
+
...FRAMEWORK_ERROR_CLASSES,
|
|
162
|
+
AvContractMismatchError: contractMismatchSummary(regenerate),
|
|
163
|
+
};
|
|
164
|
+
const classes = Object.entries(summaries).map(([name, summary]) => {
|
|
175
165
|
const codes = ERROR_CODES.filter((code) => FRAMEWORK_ERROR_CLASS_OF[code] === name);
|
|
176
166
|
return (`\n/** ${summary} Thrown for ${codes.join(", ")}. */\n` +
|
|
177
|
-
`export class ${name} extends
|
|
167
|
+
`export class ${name} extends AvError<${codes.map((code) => JSON.stringify(code)).join(" | ")}> {\n` +
|
|
178
168
|
`\toverride readonly name: string = ${JSON.stringify(name)};\n}\n`);
|
|
179
169
|
});
|
|
180
170
|
const table = ERROR_CODES.map((code) => `\t${code}: ${FRAMEWORK_ERROR_CLASS_OF[code]},\n`).join("");
|
|
@@ -184,32 +174,69 @@ export function emitErrorsModule() {
|
|
|
184
174
|
"\n/** The outcome codes of the protocol, in allocation order. */\n" +
|
|
185
175
|
`export const OPERATION_CODES = ${codeArray(OPERATION_CODES)};\n` +
|
|
186
176
|
"\n/** An outcome code. */\n" +
|
|
187
|
-
"export type
|
|
177
|
+
"export type AvOperationCode = (typeof OPERATION_CODES)[number];\n" +
|
|
188
178
|
"\n/** An outcome code that throws: every code outside A1xxx. */\n" +
|
|
189
|
-
`export type
|
|
179
|
+
`export type AvOperationErrorCode = Exclude<AvOperationCode, \`A1\${string}\`>;\n` +
|
|
190
180
|
"\n/** The validation codes of the protocol, in allocation order. */\n" +
|
|
191
181
|
`export const VALIDATION_CODES = ${codeArray(VALIDATION_CODES)};\n` +
|
|
192
182
|
"\n/** A validation code: what one issue in a cause is about. */\n" +
|
|
193
|
-
"export type
|
|
183
|
+
"export type AvValidationCode = (typeof VALIDATION_CODES)[number];\n" +
|
|
194
184
|
ERRORS_MODULE_TYPES +
|
|
195
185
|
classes.join("") +
|
|
196
186
|
ERRORS_MODULE_TAIL +
|
|
197
187
|
"\nconst ERROR_CLASS_OF: {\n" +
|
|
198
|
-
"\treadonly [C in
|
|
188
|
+
"\treadonly [C in AvOperationErrorCode]: new (code: C, cause: AvCause) => AvError;\n" +
|
|
199
189
|
`} = {\n${table}};\n` +
|
|
200
|
-
"\n/** The
|
|
201
|
-
"export function frameworkErrorOf<C extends
|
|
190
|
+
"\n/** The AvError subclass a failure envelope's code throws. */\n" +
|
|
191
|
+
"export function frameworkErrorOf<C extends AvOperationErrorCode>(code: C, cause: AvCause): AvError {\n" +
|
|
202
192
|
"\tconst ErrorClass = ERROR_CLASS_OF[code];\n" +
|
|
203
193
|
"\treturn new ErrorClass(code, cause);\n" +
|
|
204
194
|
"}\n",
|
|
205
195
|
};
|
|
206
196
|
}
|
|
207
|
-
export function emitTransportModule(style) {
|
|
197
|
+
export function emitTransportModule(style, access = false) {
|
|
208
198
|
return {
|
|
209
199
|
path: "runtime/transport.ts",
|
|
210
|
-
source: transportModuleSource(moduleSpecifierWriter(style))
|
|
200
|
+
source: transportModuleSource(moduleSpecifierWriter(style)) +
|
|
201
|
+
(access ? ASK_CAN_SOURCE : ""),
|
|
211
202
|
};
|
|
212
203
|
}
|
|
204
|
+
const ASK_CAN_SOURCE = `
|
|
205
|
+
/**
|
|
206
|
+
* Asks the policy every question in one request (\`POST /_access/can\`), each
|
|
207
|
+
* question's \`where\` in its wire forms, and resolves one boolean each, in order.
|
|
208
|
+
*
|
|
209
|
+
* @throws as {@link execute} does: the AvError subclass of a refusal (A2001 for a
|
|
210
|
+
* Resource this client cannot reach, A2004 for a malformed question, A2009 past
|
|
211
|
+
* the batch limit), AvTransportError when no framework envelope arrived.
|
|
212
|
+
*/
|
|
213
|
+
export async function askCan(
|
|
214
|
+
connection: TransportConnection,
|
|
215
|
+
questions: readonly unknown[],
|
|
216
|
+
options: AvCallOptions = {},
|
|
217
|
+
): Promise<boolean[]> {
|
|
218
|
+
const operation = "$access.can";
|
|
219
|
+
if (!Array.isArray(questions)) {
|
|
220
|
+
throw new TypeError("$access.can takes a list of questions.");
|
|
221
|
+
}
|
|
222
|
+
const headers = requestHeaders(options);
|
|
223
|
+
const body = serializeWireBody({
|
|
224
|
+
questions: questions.map((question: unknown) =>
|
|
225
|
+
isRecord(question) && Object.hasOwn(question, "where")
|
|
226
|
+
? { ...question, where: encodeArguments(question["where"]) }
|
|
227
|
+
: question,
|
|
228
|
+
),
|
|
229
|
+
});
|
|
230
|
+
const response = await post(connection, "/_access/can", operation, headers, body, options);
|
|
231
|
+
return decoded(operation, response.status, () => {
|
|
232
|
+
const data: unknown = response.data;
|
|
233
|
+
if (!Array.isArray(data) || data.length !== questions.length || !data.every((answer) => typeof answer === "boolean")) {
|
|
234
|
+
throw new TypeError("The answer must hold one boolean per question.");
|
|
235
|
+
}
|
|
236
|
+
return data as boolean[];
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
`;
|
|
213
240
|
const IDENTITY_HEADERS = (() => {
|
|
214
241
|
const { protocolVersion, contractHash, requestId } = AvProtocol.headers;
|
|
215
242
|
const prefix = protocolVersion.slice(0, protocolVersion.indexOf("-"));
|
|
@@ -227,26 +254,26 @@ const IDENTITY_HEADERS = (() => {
|
|
|
227
254
|
})();
|
|
228
255
|
function transportModuleSource(from) {
|
|
229
256
|
return `import { CLIENT_CONTRACT_HASH, PROTOCOL_VERSION } from ${from("../metadata")};
|
|
230
|
-
import { decodeResult, encodeArguments, readArgument, serializeWireBody } from ${from("./codec")};
|
|
257
|
+
import { decodeResult, encodeArguments, readArgument, serializeWireBody, unsentValues } from ${from("./codec")};
|
|
231
258
|
import { DECODE_TABLE } from ${from("./descriptor")};
|
|
232
259
|
import {
|
|
233
|
-
type
|
|
260
|
+
type AvCause,
|
|
234
261
|
frameworkErrorOf,
|
|
235
262
|
OPERATION_CODES,
|
|
236
|
-
type
|
|
237
|
-
type
|
|
238
|
-
|
|
263
|
+
type AvOperationCode,
|
|
264
|
+
type AvOperationErrorCode,
|
|
265
|
+
AvTransportError,
|
|
239
266
|
VALIDATION_CODES,
|
|
240
|
-
type
|
|
241
|
-
type
|
|
267
|
+
type AvValidationCode,
|
|
268
|
+
type AvValidationIssue,
|
|
242
269
|
} from ${from("./errors")};
|
|
243
270
|
|
|
244
271
|
/**
|
|
245
272
|
* The transport of Aventara protocol version 1: one POST per operation,
|
|
246
273
|
* sent with the protocol version and ClientContract hash, its response
|
|
247
|
-
* read by the protocol's rule
|
|
248
|
-
* code throws its
|
|
249
|
-
* envelope throws
|
|
274
|
+
* read by the protocol's rule: A1xxx resolves with the envelope's data, every other
|
|
275
|
+
* code throws its AvError subclass, and anything that is not a framework
|
|
276
|
+
* envelope throws AvTransportError. No request is ever retried.
|
|
250
277
|
*
|
|
251
278
|
* # A stale client
|
|
252
279
|
*
|
|
@@ -254,9 +281,9 @@ import {
|
|
|
254
281
|
* regenerate. The server checks the identity this transport sends before it
|
|
255
282
|
* routes, so a request under a stale hash, to an operation the deployment still
|
|
256
283
|
* advertises or to one it no longer does, is answered 409 A2005, which throws
|
|
257
|
-
*
|
|
284
|
+
* AvContractMismatchError naming the remedy. A response that is not a framework
|
|
258
285
|
* envelope, such as a host's own 404 for a path it does not mount, is a
|
|
259
|
-
*
|
|
286
|
+
* AvTransportError and names no remedy, because nothing in it says this client is
|
|
260
287
|
* stale.
|
|
261
288
|
*/
|
|
262
289
|
|
|
@@ -270,7 +297,7 @@ const FRAMEWORK_HEADERS: Readonly<Record<string, string>> = {
|
|
|
270
297
|
[WIRE_PREFIX + ${JSON.stringify(IDENTITY_HEADERS.contractHash)}]: CLIENT_CONTRACT_HASH,
|
|
271
298
|
};
|
|
272
299
|
|
|
273
|
-
/** The request id header:
|
|
300
|
+
/** The request id header: AvCallOptions.requestId sends it. */
|
|
274
301
|
const REQUEST_ID_HEADER = WIRE_PREFIX + ${JSON.stringify(IDENTITY_HEADERS.requestId)};
|
|
275
302
|
|
|
276
303
|
/** What the transport reads of an AbortSignal, where the consumer's lib declares none. */
|
|
@@ -292,16 +319,16 @@ export interface FetchResponse {
|
|
|
292
319
|
text(): Promise<string>;
|
|
293
320
|
}
|
|
294
321
|
|
|
295
|
-
/** The
|
|
322
|
+
/** The AvFetch API as far as the transport calls it, where the consumer's lib declares none. */
|
|
296
323
|
export type StructuralFetch = (url: string, init: FetchInit) => Promise<FetchResponse>;
|
|
297
324
|
|
|
298
325
|
/**
|
|
299
326
|
* The fetch this client calls: the consumer's own platform fetch type when
|
|
300
|
-
* their lib declares one
|
|
327
|
+
* their lib declares one (DOM's, Node's), so the platform fetch is accepted
|
|
301
328
|
* without a cast, and the structural fetch above where it declares none. Read
|
|
302
329
|
* through typeof globalThis, so this module names neither lib.
|
|
303
330
|
*/
|
|
304
|
-
export type
|
|
331
|
+
export type AvFetch = typeof globalThis extends { readonly fetch: infer F } ? F : StructuralFetch;
|
|
305
332
|
|
|
306
333
|
/** The consumer's AbortSignal type when their lib declares one, else the structural signal. */
|
|
307
334
|
export type PlatformSignal = typeof globalThis extends {
|
|
@@ -314,11 +341,11 @@ export type PlatformSignal = typeof globalThis extends {
|
|
|
314
341
|
export interface TransportConnection {
|
|
315
342
|
/** The framework entrypoint, an absolute URL without a trailing slash. */
|
|
316
343
|
readonly entrypoint: string;
|
|
317
|
-
readonly fetch:
|
|
344
|
+
readonly fetch: AvFetch;
|
|
318
345
|
}
|
|
319
346
|
|
|
320
347
|
/** Per-call options. Never serialized into the operation body. */
|
|
321
|
-
export interface
|
|
348
|
+
export interface AvCallOptions {
|
|
322
349
|
/** Aborts the request; the call rejects with what fetch rejected with. */
|
|
323
350
|
readonly signal?: PlatformSignal;
|
|
324
351
|
/** Extra request headers. The framework's own headers cannot be set here. */
|
|
@@ -330,19 +357,65 @@ export interface CallOptions {
|
|
|
330
357
|
readonly requestId?: string;
|
|
331
358
|
}
|
|
332
359
|
|
|
333
|
-
type SuccessCode = Exclude<
|
|
360
|
+
type SuccessCode = Exclude<AvOperationCode, AvOperationErrorCode>;
|
|
334
361
|
|
|
335
362
|
/** A framework envelope, read and checked, told apart by \`outcome\`. */
|
|
336
363
|
type Envelope =
|
|
337
364
|
| { readonly outcome: "data"; readonly code: SuccessCode; readonly data: unknown }
|
|
338
|
-
| { readonly outcome: "failure"; readonly code:
|
|
365
|
+
| { readonly outcome: "failure"; readonly code: AvOperationErrorCode; readonly cause: AvCause };
|
|
366
|
+
|
|
367
|
+
/** What a refusal of an operation's arguments says. */
|
|
368
|
+
const REFUSAL_MESSAGE = "Operation arguments failed framework validation.";
|
|
369
|
+
|
|
370
|
+
/** What an undefined value the request would leave out is told. */
|
|
371
|
+
const UNSENT_MESSAGE =
|
|
372
|
+
"Value must not be undefined: sent as JSON it would be left out, and the request would reach more rows.";
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Refuses arguments that hold an undefined value whose leaving out would reach more
|
|
376
|
+
* rows than the caller wrote (in a filter, a unique selector or a cursor, or a where,
|
|
377
|
+
* cursor, limit or offset left undefined), before anything is sent: JSON would drop
|
|
378
|
+
* it. Throws the AvValidationError (A2004) the server answers when it is handed such
|
|
379
|
+
* a value: one issue at each, with the server's code, its path under \`at\`
|
|
380
|
+
* (["arguments"] for a call, ["operations", i, "args"] for a plan step, whose index
|
|
381
|
+
* the cause names).
|
|
382
|
+
*/
|
|
383
|
+
export function refuseUnsentValues(
|
|
384
|
+
resource: string,
|
|
385
|
+
family: string,
|
|
386
|
+
variant: string,
|
|
387
|
+
args: unknown,
|
|
388
|
+
at: readonly (string | number)[],
|
|
389
|
+
operation?: number,
|
|
390
|
+
): void {
|
|
391
|
+
const unsent = unsentValues(DECODE_TABLE, resource, family, variant, args);
|
|
392
|
+
if (unsent.length === 0) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const issues: readonly AvValidationIssue[] = Object.freeze(
|
|
396
|
+
unsent.map(({ path, code }) =>
|
|
397
|
+
Object.freeze({ path: Object.freeze([...at, ...path]), code, message: UNSENT_MESSAGE }),
|
|
398
|
+
),
|
|
399
|
+
);
|
|
400
|
+
throw frameworkErrorOf(
|
|
401
|
+
"A2004",
|
|
402
|
+
Object.freeze(
|
|
403
|
+
operation === undefined ? { message: REFUSAL_MESSAGE, issues } : { message: REFUSAL_MESSAGE, issues, operation },
|
|
404
|
+
),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
339
407
|
|
|
340
408
|
/**
|
|
341
409
|
* Runs one operation: POSTs its arguments and settles its envelope.
|
|
342
410
|
*
|
|
343
|
-
* @
|
|
344
|
-
*
|
|
345
|
-
*
|
|
411
|
+
* @param check the client's own checks of the arguments, run before anything is sent
|
|
412
|
+
* (the ClientContract's field validators, when it has any).
|
|
413
|
+
* @returns the envelope's data when the code is A1xxx; null for A1001.
|
|
414
|
+
* @throws the AvError subclass of any other code; AvTransportError when no
|
|
415
|
+
* framework envelope arrived; AvValidationError (A2004), before anything is sent,
|
|
416
|
+
* when the arguments hold an undefined value JSON would leave out to widen the
|
|
417
|
+
* request (see refuseUnsentValues), or when
|
|
418
|
+
* \`check\` refuses the arguments; TypeError, before anything is sent, when the
|
|
346
419
|
* arguments are not a JSON-safe object or a request header is the framework's.
|
|
347
420
|
* A caller's abort rejects with what fetch rejected with.
|
|
348
421
|
*/
|
|
@@ -352,8 +425,11 @@ export async function execute(
|
|
|
352
425
|
family: string,
|
|
353
426
|
variant: string,
|
|
354
427
|
args: Readonly<Record<string, unknown>>,
|
|
355
|
-
options:
|
|
428
|
+
options: AvCallOptions = {},
|
|
429
|
+
check?: (resource: string, family: string, args: unknown) => void,
|
|
356
430
|
): Promise<unknown> {
|
|
431
|
+
refuseUnsentValues(resource, family, variant, args, ["arguments"]);
|
|
432
|
+
check?.(resource, family, args);
|
|
357
433
|
const operation = JSON.stringify(resource) + " " + family + "." + variant;
|
|
358
434
|
const headers = requestHeaders(options);
|
|
359
435
|
const body = serializeWireBody(encodeArguments(argumentObject(args)));
|
|
@@ -368,13 +444,13 @@ export async function execute(
|
|
|
368
444
|
* by its own operation's Resource (\`resources\`, in plan order).
|
|
369
445
|
*
|
|
370
446
|
* @throws as {@link execute} does; a rolled-back plan throws its failing
|
|
371
|
-
* operation's
|
|
447
|
+
* operation's AvError subclass, its cause naming the operation.
|
|
372
448
|
*/
|
|
373
449
|
export async function executePlan(
|
|
374
450
|
connection: TransportConnection,
|
|
375
451
|
plan: { readonly operations: readonly unknown[] },
|
|
376
452
|
resources: readonly string[],
|
|
377
|
-
options:
|
|
453
|
+
options: AvCallOptions = {},
|
|
378
454
|
): Promise<unknown[]> {
|
|
379
455
|
const operation = "the transaction";
|
|
380
456
|
const headers = requestHeaders(options);
|
|
@@ -395,7 +471,7 @@ async function post(
|
|
|
395
471
|
operation: string,
|
|
396
472
|
headers: Readonly<Record<string, string>>,
|
|
397
473
|
body: string,
|
|
398
|
-
options:
|
|
474
|
+
options: AvCallOptions,
|
|
399
475
|
): Promise<{ readonly status: number; readonly data: unknown }> {
|
|
400
476
|
const url = connection.entrypoint + path;
|
|
401
477
|
const init: FetchInit =
|
|
@@ -412,7 +488,7 @@ async function post(
|
|
|
412
488
|
if (options.signal?.aborted === true) {
|
|
413
489
|
throw error;
|
|
414
490
|
}
|
|
415
|
-
throw new
|
|
491
|
+
throw new AvTransportError("The request for " + operation + " failed before any response arrived.", null);
|
|
416
492
|
}
|
|
417
493
|
let text: string;
|
|
418
494
|
try {
|
|
@@ -421,7 +497,7 @@ async function post(
|
|
|
421
497
|
if (options.signal?.aborted === true) {
|
|
422
498
|
throw error;
|
|
423
499
|
}
|
|
424
|
-
throw new
|
|
500
|
+
throw new AvTransportError(
|
|
425
501
|
"The response to " + operation + " (HTTP " + response.status + ") could not be read.",
|
|
426
502
|
response.status,
|
|
427
503
|
);
|
|
@@ -430,14 +506,14 @@ async function post(
|
|
|
430
506
|
}
|
|
431
507
|
|
|
432
508
|
/**
|
|
433
|
-
* The decoded data, or
|
|
434
|
-
* otherwise valid envelope
|
|
509
|
+
* The decoded data, or an AvTransportError when a scalar in \`data\` does not
|
|
510
|
+
* decode inside an otherwise valid envelope.
|
|
435
511
|
*/
|
|
436
512
|
function decoded<T>(operation: string, status: number, decode: () => T): T {
|
|
437
513
|
try {
|
|
438
514
|
return decode();
|
|
439
515
|
} catch (error) {
|
|
440
|
-
throw new
|
|
516
|
+
throw new AvTransportError(
|
|
441
517
|
"The response to " + operation + " (HTTP " + status + ") holds a value this client cannot read: " +
|
|
442
518
|
(error instanceof Error ? error.message : String(error)),
|
|
443
519
|
status,
|
|
@@ -448,14 +524,14 @@ function decoded<T>(operation: string, status: number, decode: () => T): T {
|
|
|
448
524
|
/**
|
|
449
525
|
* Settles one response by the protocol's rule: the envelope's data, still in wire form,
|
|
450
526
|
* which the caller decodes by the decode table.
|
|
451
|
-
* A scalar in \`data\` that fails to decode, inside an otherwise valid envelope, throws
|
|
452
|
-
*
|
|
453
|
-
*
|
|
527
|
+
* A scalar in \`data\` that fails to decode, inside an otherwise valid envelope, throws AvTransportError.
|
|
528
|
+
* It is no AvError, because the server reported no failure: the response is one
|
|
529
|
+
* this client cannot read, which is what AvTransportError means.
|
|
454
530
|
*/
|
|
455
531
|
function settle(operation: string, status: number, text: string): unknown {
|
|
456
532
|
const envelope = envelopeOf(parseJson(text));
|
|
457
533
|
if (envelope === undefined) {
|
|
458
|
-
throw new
|
|
534
|
+
throw new AvTransportError(
|
|
459
535
|
"The response to " + operation + " (HTTP " + status + ") is not an Aventara response envelope.",
|
|
460
536
|
status,
|
|
461
537
|
);
|
|
@@ -466,7 +542,7 @@ function settle(operation: string, status: number, text: string): unknown {
|
|
|
466
542
|
return envelope.data;
|
|
467
543
|
}
|
|
468
544
|
|
|
469
|
-
/** The parsed body, or undefined
|
|
545
|
+
/** The parsed body, or undefined (which no envelope is) when it is not JSON. */
|
|
470
546
|
function parseJson(text: string): unknown {
|
|
471
547
|
try {
|
|
472
548
|
return JSON.parse(text);
|
|
@@ -479,15 +555,15 @@ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
|
479
555
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
480
556
|
}
|
|
481
557
|
|
|
482
|
-
function isOperationCode(value: unknown): value is
|
|
558
|
+
function isOperationCode(value: unknown): value is AvOperationCode {
|
|
483
559
|
return OPERATION_CODES.some((code) => code === value);
|
|
484
560
|
}
|
|
485
561
|
|
|
486
|
-
function isValidationCode(value: unknown): value is
|
|
562
|
+
function isValidationCode(value: unknown): value is AvValidationCode {
|
|
487
563
|
return VALIDATION_CODES.some((code) => code === value);
|
|
488
564
|
}
|
|
489
565
|
|
|
490
|
-
function isSuccessCode(code:
|
|
566
|
+
function isSuccessCode(code: AvOperationCode): code is SuccessCode {
|
|
491
567
|
return code.startsWith("A1");
|
|
492
568
|
}
|
|
493
569
|
|
|
@@ -511,9 +587,9 @@ function envelopeOf(body: unknown): Envelope | undefined {
|
|
|
511
587
|
/**
|
|
512
588
|
* The cause rebuilt from the members the protocol defines, or undefined when one of them
|
|
513
589
|
* is malformed. Every other member is dropped, so whatever else a server put in
|
|
514
|
-
* its cause
|
|
590
|
+
* its cause (a stack, SQL, a file path) is never carried into a thrown error.
|
|
515
591
|
*/
|
|
516
|
-
function causeOf(value: unknown):
|
|
592
|
+
function causeOf(value: unknown): AvCause | undefined {
|
|
517
593
|
if (!isRecord(value)) {
|
|
518
594
|
return undefined;
|
|
519
595
|
}
|
|
@@ -523,12 +599,12 @@ function causeOf(value: unknown): Cause | undefined {
|
|
|
523
599
|
if (typeof message !== "string") {
|
|
524
600
|
return undefined;
|
|
525
601
|
}
|
|
526
|
-
let cause:
|
|
602
|
+
let cause: AvCause = { message };
|
|
527
603
|
if (issues !== undefined) {
|
|
528
604
|
if (!Array.isArray(issues)) {
|
|
529
605
|
return undefined;
|
|
530
606
|
}
|
|
531
|
-
const rebuilt:
|
|
607
|
+
const rebuilt: AvValidationIssue[] = [];
|
|
532
608
|
for (const issue of issues) {
|
|
533
609
|
const copy = issueOf(issue);
|
|
534
610
|
if (copy === undefined) {
|
|
@@ -547,18 +623,30 @@ function causeOf(value: unknown): Cause | undefined {
|
|
|
547
623
|
return Object.freeze(cause);
|
|
548
624
|
}
|
|
549
625
|
|
|
550
|
-
|
|
626
|
+
/**
|
|
627
|
+
* One issue rebuilt from the members the protocol defines (its path, code and
|
|
628
|
+
* message, and a failed check's name and parameters), or undefined when one of
|
|
629
|
+
* them is malformed.
|
|
630
|
+
*/
|
|
631
|
+
function issueOf(value: unknown): AvValidationIssue | undefined {
|
|
551
632
|
if (!isRecord(value)) {
|
|
552
633
|
return undefined;
|
|
553
634
|
}
|
|
554
635
|
const path = value["path"];
|
|
555
636
|
const code = value["code"];
|
|
556
637
|
const message = value["message"];
|
|
638
|
+
const check = value["check"];
|
|
639
|
+
const params = value["params"];
|
|
557
640
|
if (!isValidationCode(code) || typeof message !== "string") {
|
|
558
641
|
return undefined;
|
|
559
642
|
}
|
|
643
|
+
const checked = check === undefined && params === undefined;
|
|
644
|
+
if (!checked && (typeof check !== "string" || !isRecord(params))) {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
const failed = checked ? {} : { check: check as string, params: frozen(params) as Readonly<Record<string, unknown>> };
|
|
560
648
|
if (path === undefined) {
|
|
561
|
-
return Object.freeze({ code, message });
|
|
649
|
+
return Object.freeze({ code, message, ...failed });
|
|
562
650
|
}
|
|
563
651
|
if (!Array.isArray(path)) {
|
|
564
652
|
return undefined;
|
|
@@ -570,7 +658,18 @@ function issueOf(value: unknown): ValidationIssue | undefined {
|
|
|
570
658
|
}
|
|
571
659
|
segments.push(segment);
|
|
572
660
|
}
|
|
573
|
-
return Object.freeze({ path: Object.freeze(segments), code, message });
|
|
661
|
+
return Object.freeze({ path: Object.freeze(segments), code, message, ...failed });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** A parsed JSON value, frozen at every depth. */
|
|
665
|
+
function frozen(value: unknown): unknown {
|
|
666
|
+
if (typeof value === "object" && value !== null) {
|
|
667
|
+
for (const member of Object.values(value)) {
|
|
668
|
+
frozen(member);
|
|
669
|
+
}
|
|
670
|
+
Object.freeze(value);
|
|
671
|
+
}
|
|
672
|
+
return value;
|
|
574
673
|
}
|
|
575
674
|
|
|
576
675
|
/** The arguments, refused unless they are an object: the body is always one. */
|
|
@@ -586,7 +685,7 @@ function argumentObject(args: unknown): unknown {
|
|
|
586
685
|
* the framework's; then requestId, which overwrites any request id header the
|
|
587
686
|
* caller set.
|
|
588
687
|
*/
|
|
589
|
-
function requestHeaders(options:
|
|
688
|
+
function requestHeaders(options: AvCallOptions): Record<string, string> {
|
|
590
689
|
const headers: Record<string, string> = { ...FRAMEWORK_HEADERS };
|
|
591
690
|
const owned = new Set(Object.keys(FRAMEWORK_HEADERS).map((name) => name.toLowerCase()));
|
|
592
691
|
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
@@ -3,7 +3,7 @@ import type { EmittedModule } from "./emitted-tree.interface.js";
|
|
|
3
3
|
import { type ClientModuleStyle } from "./module-style.interface.js";
|
|
4
4
|
export declare function wireGrammarLiteral(scalar: keyof typeof AvProtocol.scalarFormats): string;
|
|
5
5
|
/**
|
|
6
|
-
* `runtime/codec.ts`: the `
|
|
6
|
+
* `runtime/codec.ts`: the `AvBuiltInScalar` union and the codec table in core's
|
|
7
7
|
* `BUILT_IN_SCALARS` order, the helpers the codecs share, and the three
|
|
8
8
|
* functions the generated transport calls. Nothing here is re-exported from the
|
|
9
9
|
* tree's `AvClient.ts`: the codec is the runtime's, not the consumer's.
|