@aventara/client 0.1.0-pilot.4 → 1.0.0-rc.1
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 +679 -0
- package/README.md +168 -132
- package/dist/cli/command.parser.d.ts +6 -4
- package/dist/cli/command.parser.js +22 -11
- package/dist/cli/generate.command.d.ts +6 -1
- package/dist/cli/generate.command.js +12 -1
- package/dist/cli/generation-failure.renderer.js +24 -24
- package/dist/cli/step.reporter.d.ts +31 -0
- package/dist/cli/step.reporter.js +87 -0
- package/dist/cli.js +2 -0
- package/dist/config/client-config.interface.d.ts +7 -7
- package/dist/config/config.loader.d.ts +4 -4
- package/dist/config/config.loader.js +7 -7
- package/dist/config/config.resolver.d.ts +7 -7
- package/dist/config/config.resolver.js +12 -12
- package/dist/config/env.cascade.d.ts +2 -2
- package/dist/config/env.cascade.js +4 -4
- package/dist/config/module-style.resolver.d.ts +1 -1
- package/dist/config/module-style.resolver.js +2 -2
- package/dist/config/tsconfig.locator.d.ts +1 -1
- package/dist/config/tsconfig.locator.js +2 -2
- package/dist/contract/contract.acceptance.d.ts +4 -4
- package/dist/contract/contract.acceptance.js +4 -4
- package/dist/contract/contract.fetcher.d.ts +3 -3
- package/dist/contract/contract.fetcher.js +6 -6
- package/dist/contract/contract.loader.d.ts +6 -6
- package/dist/contract/contract.loader.js +2 -2
- package/dist/emit/client-surface.emitter.d.ts +2 -2
- package/dist/emit/client-surface.emitter.js +77 -43
- package/dist/emit/client-tree.emitter.d.ts +3 -3
- package/dist/emit/client-tree.emitter.js +8 -5
- package/dist/emit/contract-carrier.emitter.d.ts +3 -3
- package/dist/emit/contract-carrier.emitter.js +1 -1
- package/dist/emit/derivation.emitter.js +1 -0
- package/dist/emit/descriptor.emitter.d.ts +2 -2
- package/dist/emit/descriptor.emitter.js +2 -2
- 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 +21 -0
- package/dist/emit/field-check.emitter.js +603 -0
- package/dist/emit/json-shape.emitter.d.ts +16 -0
- package/dist/emit/json-shape.emitter.js +88 -0
- package/dist/emit/name.deriver.d.ts +6 -6
- package/dist/emit/name.deriver.js +15 -15
- package/dist/emit/named-type.emitter.d.ts +2 -2
- package/dist/emit/runtime.emitter.d.ts +2 -2
- package/dist/emit/runtime.emitter.js +117 -112
- package/dist/emit/scalar.codec.d.ts +1 -1
- package/dist/emit/scalar.codec.js +78 -35
- package/dist/emit/transaction.emitter.js +131 -83
- package/dist/generate.d.ts +11 -3
- package/dist/generate.js +40 -26
- 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 +37 -22
- package/dist/init/client-init.questions.d.ts +5 -5
- package/dist/init/client-init.questions.js +10 -10
- package/dist/init/client-project.inspector.d.ts +2 -2
- package/dist/init/client-project.inspector.js +3 -3
- package/dist/init/command.runner.d.ts +2 -0
- package/dist/init/command.runner.js +4 -1
- package/dist/output/output.writer.d.ts +5 -5
- package/dist/output/output.writer.js +12 -12
- package/package.json +6 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OPERATION_CODES, VALIDATION_CODES, } from "@aventara/core";
|
|
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
4
|
import { moduleSpecifierWriter, } from "./module-style.interface.js";
|
|
@@ -14,7 +14,7 @@ export function emitDecimalModule() {
|
|
|
14
14
|
* It holds the decimal string it was made from, digit for digit, and is sent as
|
|
15
15
|
* that string. It does no arithmetic.
|
|
16
16
|
*/
|
|
17
|
-
export class
|
|
17
|
+
export class AvDecimal {
|
|
18
18
|
readonly #value: string;
|
|
19
19
|
|
|
20
20
|
/** @throws TypeError when the value is not a decimal string. */
|
|
@@ -30,7 +30,7 @@ export class Decimal {
|
|
|
30
30
|
return this.#value;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** The decimal string, exactly as given: what JSON.stringify writes for
|
|
33
|
+
/** The decimal string, exactly as given: what JSON.stringify writes for an AvDecimal. */
|
|
34
34
|
toJSON(): string {
|
|
35
35
|
return this.#value;
|
|
36
36
|
}
|
|
@@ -52,50 +52,25 @@ export function emitMetadataModule(protocol, entrypoint) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
const FRAMEWORK_ERROR_CLASSES = {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
AvAuthError: "The caller is not authenticated, or is not permitted to run the operation.",
|
|
56
|
+
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.",
|
|
57
|
+
AvContractMismatchError: "This client was generated against a ClientContract other than the one the server serves. Regenerate it with `avclient generate`.",
|
|
58
|
+
AvInternalError: "The server failed. Nothing in its cause is actionable; the diagnostics are in the server's logs.",
|
|
59
|
+
AvNotFoundError: "A strict unique target does not exist.",
|
|
60
|
+
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.",
|
|
61
|
+
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.",
|
|
62
62
|
};
|
|
63
|
-
const
|
|
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",
|
|
87
|
-
};
|
|
88
|
-
export const FRAMEWORK_ERROR_CLASS_NAMES = Object.keys(FRAMEWORK_ERROR_CLASSES);
|
|
63
|
+
export const FRAMEWORK_ERROR_CLASS_NAMES = Object.keys(FRAMEWORK_ERROR_CLASSES).sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
89
64
|
const ERROR_CODES = OPERATION_CODES.filter((code) => !code.startsWith("A1"));
|
|
90
65
|
export function errorsModuleExports() {
|
|
91
66
|
return [
|
|
92
67
|
...FRAMEWORK_ERROR_CLASS_NAMES,
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"type
|
|
96
|
-
"type
|
|
97
|
-
"type
|
|
98
|
-
"type
|
|
68
|
+
"AvError",
|
|
69
|
+
"AvTransportError",
|
|
70
|
+
"type AvCause",
|
|
71
|
+
"type AvOperationCode",
|
|
72
|
+
"type AvValidationCode",
|
|
73
|
+
"type AvValidationIssue",
|
|
99
74
|
].sort((left, right) => {
|
|
100
75
|
const a = left.replace(/^type /, "");
|
|
101
76
|
const b = right.replace(/^type /, "");
|
|
@@ -108,17 +83,24 @@ function codeArray(codes) {
|
|
|
108
83
|
const ERRORS_MODULE_HEAD = `/**
|
|
109
84
|
* The outcome codes and error classes of Aventara protocol version 1.
|
|
110
85
|
*
|
|
111
|
-
* A1xxx resolves; every other code throws the
|
|
86
|
+
* A1xxx resolves; every other code throws the AvError subclass of its
|
|
112
87
|
* class; a response that is not a framework envelope throws
|
|
113
|
-
*
|
|
88
|
+
* AvTransportError. Branch on the class or on \`code\`, never on the message.
|
|
114
89
|
*/
|
|
115
90
|
`;
|
|
116
91
|
const ERRORS_MODULE_TYPES = `
|
|
117
92
|
/** One actionable validation issue; \`path\` locates it in the request. */
|
|
118
|
-
export interface
|
|
93
|
+
export interface AvValidationIssue {
|
|
119
94
|
readonly path?: readonly (string | number)[];
|
|
120
|
-
readonly code:
|
|
95
|
+
readonly code: AvValidationCode;
|
|
121
96
|
readonly message: string;
|
|
97
|
+
/**
|
|
98
|
+
* The failed check's name (max_length, string_format, …): present, with
|
|
99
|
+
* \`params\`, only on the issue of a failed field validator or Json Shape check.
|
|
100
|
+
*/
|
|
101
|
+
readonly check?: string;
|
|
102
|
+
/** That check's parameters, as the ClientContract states them ({ maximum: 80 }). */
|
|
103
|
+
readonly params?: Readonly<Record<string, unknown>>;
|
|
122
104
|
}
|
|
123
105
|
|
|
124
106
|
/**
|
|
@@ -126,26 +108,26 @@ export interface ValidationIssue {
|
|
|
126
108
|
* a member the protocol does not define is never carried, so no stack, SQL or
|
|
127
109
|
* server path the server should not have sent reaches it.
|
|
128
110
|
*/
|
|
129
|
-
export interface
|
|
111
|
+
export interface AvCause {
|
|
130
112
|
readonly message: string;
|
|
131
|
-
readonly issues?: readonly
|
|
113
|
+
readonly issues?: readonly AvValidationIssue[];
|
|
132
114
|
/** The index of the failing operation in a transaction plan. */
|
|
133
115
|
readonly operation?: number;
|
|
134
116
|
}
|
|
135
117
|
|
|
136
118
|
/**
|
|
137
119
|
* A framework outcome that throws: every code outside A1xxx. It is
|
|
138
|
-
* always thrown as the subclass of its code's class; catch
|
|
120
|
+
* always thrown as the subclass of its code's class; catch AvError to
|
|
139
121
|
* catch them all.
|
|
140
122
|
*/
|
|
141
|
-
export class
|
|
142
|
-
override readonly name: string = "
|
|
123
|
+
export class AvError<C extends AvOperationErrorCode = AvOperationErrorCode> extends Error {
|
|
124
|
+
override readonly name: string = "AvError";
|
|
143
125
|
/** The outcome code. Branch on this, never on the message. */
|
|
144
126
|
readonly code: C;
|
|
145
127
|
/** The server's cause: its message, and its issues or failing operation when it gave them. */
|
|
146
|
-
override readonly cause:
|
|
128
|
+
override readonly cause: AvCause;
|
|
147
129
|
|
|
148
|
-
constructor(code: C, cause:
|
|
130
|
+
constructor(code: C, cause: AvCause) {
|
|
149
131
|
super(cause.message);
|
|
150
132
|
this.code = code;
|
|
151
133
|
this.cause = cause;
|
|
@@ -155,12 +137,12 @@ export class FrameworkError<C extends OperationErrorCode = OperationErrorCode> e
|
|
|
155
137
|
const ERRORS_MODULE_TAIL = `
|
|
156
138
|
/**
|
|
157
139
|
* 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
|
|
140
|
+
* arrived, or the one that did is not an Aventara response envelope (a proxy's
|
|
141
|
+
* HTML error page, a truncated body, JSON of another shape). It carries no
|
|
160
142
|
* cause: nothing in such a response is the framework's to report.
|
|
161
143
|
*/
|
|
162
|
-
export class
|
|
163
|
-
override readonly name: string = "
|
|
144
|
+
export class AvTransportError extends Error {
|
|
145
|
+
override readonly name: string = "AvTransportError";
|
|
164
146
|
/** The HTTP status that arrived, or null when no response arrived at all. */
|
|
165
147
|
readonly status: number | null;
|
|
166
148
|
|
|
@@ -174,7 +156,7 @@ export function emitErrorsModule() {
|
|
|
174
156
|
const classes = Object.entries(FRAMEWORK_ERROR_CLASSES).map(([name, summary]) => {
|
|
175
157
|
const codes = ERROR_CODES.filter((code) => FRAMEWORK_ERROR_CLASS_OF[code] === name);
|
|
176
158
|
return (`\n/** ${summary} Thrown for ${codes.join(", ")}. */\n` +
|
|
177
|
-
`export class ${name} extends
|
|
159
|
+
`export class ${name} extends AvError<${codes.map((code) => JSON.stringify(code)).join(" | ")}> {\n` +
|
|
178
160
|
`\toverride readonly name: string = ${JSON.stringify(name)};\n}\n`);
|
|
179
161
|
});
|
|
180
162
|
const table = ERROR_CODES.map((code) => `\t${code}: ${FRAMEWORK_ERROR_CLASS_OF[code]},\n`).join("");
|
|
@@ -184,21 +166,21 @@ export function emitErrorsModule() {
|
|
|
184
166
|
"\n/** The outcome codes of the protocol, in allocation order. */\n" +
|
|
185
167
|
`export const OPERATION_CODES = ${codeArray(OPERATION_CODES)};\n` +
|
|
186
168
|
"\n/** An outcome code. */\n" +
|
|
187
|
-
"export type
|
|
169
|
+
"export type AvOperationCode = (typeof OPERATION_CODES)[number];\n" +
|
|
188
170
|
"\n/** An outcome code that throws: every code outside A1xxx. */\n" +
|
|
189
|
-
`export type
|
|
171
|
+
`export type AvOperationErrorCode = Exclude<AvOperationCode, \`A1\${string}\`>;\n` +
|
|
190
172
|
"\n/** The validation codes of the protocol, in allocation order. */\n" +
|
|
191
173
|
`export const VALIDATION_CODES = ${codeArray(VALIDATION_CODES)};\n` +
|
|
192
174
|
"\n/** A validation code: what one issue in a cause is about. */\n" +
|
|
193
|
-
"export type
|
|
175
|
+
"export type AvValidationCode = (typeof VALIDATION_CODES)[number];\n" +
|
|
194
176
|
ERRORS_MODULE_TYPES +
|
|
195
177
|
classes.join("") +
|
|
196
178
|
ERRORS_MODULE_TAIL +
|
|
197
179
|
"\nconst ERROR_CLASS_OF: {\n" +
|
|
198
|
-
"\treadonly [C in
|
|
180
|
+
"\treadonly [C in AvOperationErrorCode]: new (code: C, cause: AvCause) => AvError;\n" +
|
|
199
181
|
`} = {\n${table}};\n` +
|
|
200
|
-
"\n/** The
|
|
201
|
-
"export function frameworkErrorOf<C extends
|
|
182
|
+
"\n/** The AvError subclass a failure envelope's code throws. */\n" +
|
|
183
|
+
"export function frameworkErrorOf<C extends AvOperationErrorCode>(code: C, cause: AvCause): AvError {\n" +
|
|
202
184
|
"\tconst ErrorClass = ERROR_CLASS_OF[code];\n" +
|
|
203
185
|
"\treturn new ErrorClass(code, cause);\n" +
|
|
204
186
|
"}\n",
|
|
@@ -227,26 +209,26 @@ const IDENTITY_HEADERS = (() => {
|
|
|
227
209
|
})();
|
|
228
210
|
function transportModuleSource(from) {
|
|
229
211
|
return `import { CLIENT_CONTRACT_HASH, PROTOCOL_VERSION } from ${from("../metadata")};
|
|
230
|
-
import { decodeResult, encodeArguments, serializeWireBody } from ${from("./codec")};
|
|
212
|
+
import { decodeResult, encodeArguments, readArgument, serializeWireBody } from ${from("./codec")};
|
|
231
213
|
import { DECODE_TABLE } from ${from("./descriptor")};
|
|
232
214
|
import {
|
|
233
|
-
type
|
|
215
|
+
type AvCause,
|
|
234
216
|
frameworkErrorOf,
|
|
235
217
|
OPERATION_CODES,
|
|
236
|
-
type
|
|
237
|
-
type
|
|
238
|
-
|
|
218
|
+
type AvOperationCode,
|
|
219
|
+
type AvOperationErrorCode,
|
|
220
|
+
AvTransportError,
|
|
239
221
|
VALIDATION_CODES,
|
|
240
|
-
type
|
|
241
|
-
type
|
|
222
|
+
type AvValidationCode,
|
|
223
|
+
type AvValidationIssue,
|
|
242
224
|
} from ${from("./errors")};
|
|
243
225
|
|
|
244
226
|
/**
|
|
245
227
|
* The transport of Aventara protocol version 1: one POST per operation,
|
|
246
228
|
* sent with the protocol version and ClientContract hash, its response
|
|
247
|
-
* read by the protocol's rule
|
|
248
|
-
* code throws its
|
|
249
|
-
* envelope throws
|
|
229
|
+
* read by the protocol's rule: A1xxx resolves with the envelope's data, every other
|
|
230
|
+
* code throws its AvError subclass, and anything that is not a framework
|
|
231
|
+
* envelope throws AvTransportError. No request is ever retried.
|
|
250
232
|
*
|
|
251
233
|
* # A stale client
|
|
252
234
|
*
|
|
@@ -254,9 +236,9 @@ import {
|
|
|
254
236
|
* regenerate. The server checks the identity this transport sends before it
|
|
255
237
|
* routes, so a request under a stale hash, to an operation the deployment still
|
|
256
238
|
* advertises or to one it no longer does, is answered 409 A2005, which throws
|
|
257
|
-
*
|
|
239
|
+
* AvContractMismatchError naming the remedy. A response that is not a framework
|
|
258
240
|
* envelope, such as a host's own 404 for a path it does not mount, is a
|
|
259
|
-
*
|
|
241
|
+
* AvTransportError and names no remedy, because nothing in it says this client is
|
|
260
242
|
* stale.
|
|
261
243
|
*/
|
|
262
244
|
|
|
@@ -270,7 +252,7 @@ const FRAMEWORK_HEADERS: Readonly<Record<string, string>> = {
|
|
|
270
252
|
[WIRE_PREFIX + ${JSON.stringify(IDENTITY_HEADERS.contractHash)}]: CLIENT_CONTRACT_HASH,
|
|
271
253
|
};
|
|
272
254
|
|
|
273
|
-
/** The request id header:
|
|
255
|
+
/** The request id header: AvCallOptions.requestId sends it. */
|
|
274
256
|
const REQUEST_ID_HEADER = WIRE_PREFIX + ${JSON.stringify(IDENTITY_HEADERS.requestId)};
|
|
275
257
|
|
|
276
258
|
/** What the transport reads of an AbortSignal, where the consumer's lib declares none. */
|
|
@@ -292,16 +274,16 @@ export interface FetchResponse {
|
|
|
292
274
|
text(): Promise<string>;
|
|
293
275
|
}
|
|
294
276
|
|
|
295
|
-
/** The
|
|
277
|
+
/** The AvFetch API as far as the transport calls it, where the consumer's lib declares none. */
|
|
296
278
|
export type StructuralFetch = (url: string, init: FetchInit) => Promise<FetchResponse>;
|
|
297
279
|
|
|
298
280
|
/**
|
|
299
281
|
* The fetch this client calls: the consumer's own platform fetch type when
|
|
300
|
-
* their lib declares one
|
|
282
|
+
* their lib declares one (DOM's, Node's), so the platform fetch is accepted
|
|
301
283
|
* without a cast, and the structural fetch above where it declares none. Read
|
|
302
284
|
* through typeof globalThis, so this module names neither lib.
|
|
303
285
|
*/
|
|
304
|
-
export type
|
|
286
|
+
export type AvFetch = typeof globalThis extends { readonly fetch: infer F } ? F : StructuralFetch;
|
|
305
287
|
|
|
306
288
|
/** The consumer's AbortSignal type when their lib declares one, else the structural signal. */
|
|
307
289
|
export type PlatformSignal = typeof globalThis extends {
|
|
@@ -314,11 +296,11 @@ export type PlatformSignal = typeof globalThis extends {
|
|
|
314
296
|
export interface TransportConnection {
|
|
315
297
|
/** The framework entrypoint, an absolute URL without a trailing slash. */
|
|
316
298
|
readonly entrypoint: string;
|
|
317
|
-
readonly fetch:
|
|
299
|
+
readonly fetch: AvFetch;
|
|
318
300
|
}
|
|
319
301
|
|
|
320
302
|
/** Per-call options. Never serialized into the operation body. */
|
|
321
|
-
export interface
|
|
303
|
+
export interface AvCallOptions {
|
|
322
304
|
/** Aborts the request; the call rejects with what fetch rejected with. */
|
|
323
305
|
readonly signal?: PlatformSignal;
|
|
324
306
|
/** Extra request headers. The framework's own headers cannot be set here. */
|
|
@@ -330,18 +312,18 @@ export interface CallOptions {
|
|
|
330
312
|
readonly requestId?: string;
|
|
331
313
|
}
|
|
332
314
|
|
|
333
|
-
type SuccessCode = Exclude<
|
|
315
|
+
type SuccessCode = Exclude<AvOperationCode, AvOperationErrorCode>;
|
|
334
316
|
|
|
335
317
|
/** A framework envelope, read and checked, told apart by \`outcome\`. */
|
|
336
318
|
type Envelope =
|
|
337
319
|
| { readonly outcome: "data"; readonly code: SuccessCode; readonly data: unknown }
|
|
338
|
-
| { readonly outcome: "failure"; readonly code:
|
|
320
|
+
| { readonly outcome: "failure"; readonly code: AvOperationErrorCode; readonly cause: AvCause };
|
|
339
321
|
|
|
340
322
|
/**
|
|
341
323
|
* Runs one operation: POSTs its arguments and settles its envelope.
|
|
342
324
|
*
|
|
343
|
-
* @returns the envelope's data when the code is A1xxx
|
|
344
|
-
* @throws the
|
|
325
|
+
* @returns the envelope's data when the code is A1xxx; null for A1001.
|
|
326
|
+
* @throws the AvError subclass of any other code; AvTransportError when no
|
|
345
327
|
* framework envelope arrived; TypeError, before anything is sent, when the
|
|
346
328
|
* arguments are not a JSON-safe object or a request header is the framework's.
|
|
347
329
|
* A caller's abort rejects with what fetch rejected with.
|
|
@@ -352,7 +334,7 @@ export async function execute(
|
|
|
352
334
|
family: string,
|
|
353
335
|
variant: string,
|
|
354
336
|
args: Readonly<Record<string, unknown>>,
|
|
355
|
-
options:
|
|
337
|
+
options: AvCallOptions = {},
|
|
356
338
|
): Promise<unknown> {
|
|
357
339
|
const operation = JSON.stringify(resource) + " " + family + "." + variant;
|
|
358
340
|
const headers = requestHeaders(options);
|
|
@@ -368,13 +350,13 @@ export async function execute(
|
|
|
368
350
|
* by its own operation's Resource (\`resources\`, in plan order).
|
|
369
351
|
*
|
|
370
352
|
* @throws as {@link execute} does; a rolled-back plan throws its failing
|
|
371
|
-
* operation's
|
|
353
|
+
* operation's AvError subclass, its cause naming the operation.
|
|
372
354
|
*/
|
|
373
355
|
export async function executePlan(
|
|
374
356
|
connection: TransportConnection,
|
|
375
357
|
plan: { readonly operations: readonly unknown[] },
|
|
376
358
|
resources: readonly string[],
|
|
377
|
-
options:
|
|
359
|
+
options: AvCallOptions = {},
|
|
378
360
|
): Promise<unknown[]> {
|
|
379
361
|
const operation = "the transaction";
|
|
380
362
|
const headers = requestHeaders(options);
|
|
@@ -395,7 +377,7 @@ async function post(
|
|
|
395
377
|
operation: string,
|
|
396
378
|
headers: Readonly<Record<string, string>>,
|
|
397
379
|
body: string,
|
|
398
|
-
options:
|
|
380
|
+
options: AvCallOptions,
|
|
399
381
|
): Promise<{ readonly status: number; readonly data: unknown }> {
|
|
400
382
|
const url = connection.entrypoint + path;
|
|
401
383
|
const init: FetchInit =
|
|
@@ -412,7 +394,7 @@ async function post(
|
|
|
412
394
|
if (options.signal?.aborted === true) {
|
|
413
395
|
throw error;
|
|
414
396
|
}
|
|
415
|
-
throw new
|
|
397
|
+
throw new AvTransportError("The request for " + operation + " failed before any response arrived.", null);
|
|
416
398
|
}
|
|
417
399
|
let text: string;
|
|
418
400
|
try {
|
|
@@ -421,7 +403,7 @@ async function post(
|
|
|
421
403
|
if (options.signal?.aborted === true) {
|
|
422
404
|
throw error;
|
|
423
405
|
}
|
|
424
|
-
throw new
|
|
406
|
+
throw new AvTransportError(
|
|
425
407
|
"The response to " + operation + " (HTTP " + response.status + ") could not be read.",
|
|
426
408
|
response.status,
|
|
427
409
|
);
|
|
@@ -430,14 +412,14 @@ async function post(
|
|
|
430
412
|
}
|
|
431
413
|
|
|
432
414
|
/**
|
|
433
|
-
* The decoded data, or
|
|
434
|
-
* otherwise valid envelope
|
|
415
|
+
* The decoded data, or an AvTransportError when a scalar in \`data\` does not
|
|
416
|
+
* decode inside an otherwise valid envelope.
|
|
435
417
|
*/
|
|
436
418
|
function decoded<T>(operation: string, status: number, decode: () => T): T {
|
|
437
419
|
try {
|
|
438
420
|
return decode();
|
|
439
421
|
} catch (error) {
|
|
440
|
-
throw new
|
|
422
|
+
throw new AvTransportError(
|
|
441
423
|
"The response to " + operation + " (HTTP " + status + ") holds a value this client cannot read: " +
|
|
442
424
|
(error instanceof Error ? error.message : String(error)),
|
|
443
425
|
status,
|
|
@@ -448,14 +430,14 @@ function decoded<T>(operation: string, status: number, decode: () => T): T {
|
|
|
448
430
|
/**
|
|
449
431
|
* Settles one response by the protocol's rule: the envelope's data, still in wire form,
|
|
450
432
|
* 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
|
-
*
|
|
433
|
+
* A scalar in \`data\` that fails to decode, inside an otherwise valid envelope, throws AvTransportError.
|
|
434
|
+
* It is no AvError, because the server reported no failure: the response is one
|
|
435
|
+
* this client cannot read, which is what AvTransportError means.
|
|
454
436
|
*/
|
|
455
437
|
function settle(operation: string, status: number, text: string): unknown {
|
|
456
438
|
const envelope = envelopeOf(parseJson(text));
|
|
457
439
|
if (envelope === undefined) {
|
|
458
|
-
throw new
|
|
440
|
+
throw new AvTransportError(
|
|
459
441
|
"The response to " + operation + " (HTTP " + status + ") is not an Aventara response envelope.",
|
|
460
442
|
status,
|
|
461
443
|
);
|
|
@@ -466,7 +448,7 @@ function settle(operation: string, status: number, text: string): unknown {
|
|
|
466
448
|
return envelope.data;
|
|
467
449
|
}
|
|
468
450
|
|
|
469
|
-
/** The parsed body, or undefined
|
|
451
|
+
/** The parsed body, or undefined (which no envelope is) when it is not JSON. */
|
|
470
452
|
function parseJson(text: string): unknown {
|
|
471
453
|
try {
|
|
472
454
|
return JSON.parse(text);
|
|
@@ -479,15 +461,15 @@ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
|
479
461
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
480
462
|
}
|
|
481
463
|
|
|
482
|
-
function isOperationCode(value: unknown): value is
|
|
464
|
+
function isOperationCode(value: unknown): value is AvOperationCode {
|
|
483
465
|
return OPERATION_CODES.some((code) => code === value);
|
|
484
466
|
}
|
|
485
467
|
|
|
486
|
-
function isValidationCode(value: unknown): value is
|
|
468
|
+
function isValidationCode(value: unknown): value is AvValidationCode {
|
|
487
469
|
return VALIDATION_CODES.some((code) => code === value);
|
|
488
470
|
}
|
|
489
471
|
|
|
490
|
-
function isSuccessCode(code:
|
|
472
|
+
function isSuccessCode(code: AvOperationCode): code is SuccessCode {
|
|
491
473
|
return code.startsWith("A1");
|
|
492
474
|
}
|
|
493
475
|
|
|
@@ -511,9 +493,9 @@ function envelopeOf(body: unknown): Envelope | undefined {
|
|
|
511
493
|
/**
|
|
512
494
|
* The cause rebuilt from the members the protocol defines, or undefined when one of them
|
|
513
495
|
* is malformed. Every other member is dropped, so whatever else a server put in
|
|
514
|
-
* its cause
|
|
496
|
+
* its cause (a stack, SQL, a file path) is never carried into a thrown error.
|
|
515
497
|
*/
|
|
516
|
-
function causeOf(value: unknown):
|
|
498
|
+
function causeOf(value: unknown): AvCause | undefined {
|
|
517
499
|
if (!isRecord(value)) {
|
|
518
500
|
return undefined;
|
|
519
501
|
}
|
|
@@ -523,12 +505,12 @@ function causeOf(value: unknown): Cause | undefined {
|
|
|
523
505
|
if (typeof message !== "string") {
|
|
524
506
|
return undefined;
|
|
525
507
|
}
|
|
526
|
-
let cause:
|
|
508
|
+
let cause: AvCause = { message };
|
|
527
509
|
if (issues !== undefined) {
|
|
528
510
|
if (!Array.isArray(issues)) {
|
|
529
511
|
return undefined;
|
|
530
512
|
}
|
|
531
|
-
const rebuilt:
|
|
513
|
+
const rebuilt: AvValidationIssue[] = [];
|
|
532
514
|
for (const issue of issues) {
|
|
533
515
|
const copy = issueOf(issue);
|
|
534
516
|
if (copy === undefined) {
|
|
@@ -547,18 +529,30 @@ function causeOf(value: unknown): Cause | undefined {
|
|
|
547
529
|
return Object.freeze(cause);
|
|
548
530
|
}
|
|
549
531
|
|
|
550
|
-
|
|
532
|
+
/**
|
|
533
|
+
* One issue rebuilt from the members the protocol defines (its path, code and
|
|
534
|
+
* message, and a failed check's name and parameters), or undefined when one of
|
|
535
|
+
* them is malformed.
|
|
536
|
+
*/
|
|
537
|
+
function issueOf(value: unknown): AvValidationIssue | undefined {
|
|
551
538
|
if (!isRecord(value)) {
|
|
552
539
|
return undefined;
|
|
553
540
|
}
|
|
554
541
|
const path = value["path"];
|
|
555
542
|
const code = value["code"];
|
|
556
543
|
const message = value["message"];
|
|
544
|
+
const check = value["check"];
|
|
545
|
+
const params = value["params"];
|
|
557
546
|
if (!isValidationCode(code) || typeof message !== "string") {
|
|
558
547
|
return undefined;
|
|
559
548
|
}
|
|
549
|
+
const checked = check === undefined && params === undefined;
|
|
550
|
+
if (!checked && (typeof check !== "string" || !isRecord(params))) {
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
const failed = checked ? {} : { check: check as string, params: frozen(params) as Readonly<Record<string, unknown>> };
|
|
560
554
|
if (path === undefined) {
|
|
561
|
-
return Object.freeze({ code, message });
|
|
555
|
+
return Object.freeze({ code, message, ...failed });
|
|
562
556
|
}
|
|
563
557
|
if (!Array.isArray(path)) {
|
|
564
558
|
return undefined;
|
|
@@ -570,12 +564,23 @@ function issueOf(value: unknown): ValidationIssue | undefined {
|
|
|
570
564
|
}
|
|
571
565
|
segments.push(segment);
|
|
572
566
|
}
|
|
573
|
-
return Object.freeze({ path: Object.freeze(segments), code, message });
|
|
567
|
+
return Object.freeze({ path: Object.freeze(segments), code, message, ...failed });
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** A parsed JSON value, frozen at every depth. */
|
|
571
|
+
function frozen(value: unknown): unknown {
|
|
572
|
+
if (typeof value === "object" && value !== null) {
|
|
573
|
+
for (const member of Object.values(value)) {
|
|
574
|
+
frozen(member);
|
|
575
|
+
}
|
|
576
|
+
Object.freeze(value);
|
|
577
|
+
}
|
|
578
|
+
return value;
|
|
574
579
|
}
|
|
575
580
|
|
|
576
581
|
/** The arguments, refused unless they are an object: the body is always one. */
|
|
577
582
|
function argumentObject(args: unknown): unknown {
|
|
578
|
-
if (!isRecord(args)) {
|
|
583
|
+
if (!readArgument([], () => isRecord(args))) {
|
|
579
584
|
throw new TypeError("An operation's arguments must be an object; send {} when there are none.");
|
|
580
585
|
}
|
|
581
586
|
return args;
|
|
@@ -586,7 +591,7 @@ function argumentObject(args: unknown): unknown {
|
|
|
586
591
|
* the framework's; then requestId, which overwrites any request id header the
|
|
587
592
|
* caller set.
|
|
588
593
|
*/
|
|
589
|
-
function requestHeaders(options:
|
|
594
|
+
function requestHeaders(options: AvCallOptions): Record<string, string> {
|
|
590
595
|
const headers: Record<string, string> = { ...FRAMEWORK_HEADERS };
|
|
591
596
|
const owned = new Set(Object.keys(FRAMEWORK_HEADERS).map((name) => name.toLowerCase()));
|
|
592
597
|
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.
|