@aventara/client 1.0.0-rc.0 → 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 +504 -0
- package/README.md +159 -141
- package/dist/cli/command.parser.d.ts +5 -5
- package/dist/cli/command.parser.js +13 -13
- package/dist/cli/generation-failure.renderer.js +24 -24
- package/dist/config/client-config.interface.d.ts +7 -7
- package/dist/config/config.loader.d.ts +10 -4
- package/dist/config/config.loader.js +16 -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 +61 -40
- 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/derivation.emitter.js +1 -0
- 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 +21 -0
- package/dist/emit/field-check.emitter.js +592 -0
- package/dist/emit/json-shape.emitter.d.ts +2 -2
- package/dist/emit/name.deriver.d.ts +6 -6
- package/dist/emit/name.deriver.js +14 -14
- 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 +166 -112
- 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 +2 -2
- package/dist/generate.js +2 -2
- 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.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/output/output.validator.d.ts +3 -4
- package/dist/output/output.validator.js +2 -2
- package/dist/output/output.writer.d.ts +5 -5
- package/dist/output/output.writer.js +12 -12
- package/package.json +4 -4
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AvContractClient } from "@aventara/core";
|
|
2
2
|
import type { ClientEntrypoint } from "../config/client-config.interface.js";
|
|
3
3
|
import type { EmittedModule } from "./emitted-tree.interface.js";
|
|
4
4
|
import { type ClientModuleStyle } from "./module-style.interface.js";
|
|
5
5
|
export declare function emitDecimalModule(): EmittedModule;
|
|
6
|
-
export declare function emitMetadataModule(protocol:
|
|
6
|
+
export declare function emitMetadataModule(protocol: AvContractClient["protocol"], entrypoint: ClientEntrypoint): EmittedModule;
|
|
7
7
|
export declare const FRAMEWORK_ERROR_CLASS_NAMES: readonly string[];
|
|
8
8
|
export declare function errorsModuleExports(): readonly string[];
|
|
9
9
|
export declare function emitErrorsModule(): EmittedModule;
|
|
@@ -1,4 +1,4 @@
|
|
|
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
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, readArgument, serializeWireBody } from ${from("./codec")};
|
|
212
|
+
import { decodeResult, encodeArguments, readArgument, serializeWireBody, unsentValues } 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,19 +312,65 @@ 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 };
|
|
321
|
+
|
|
322
|
+
/** What a refusal of an operation's arguments says. */
|
|
323
|
+
const REFUSAL_MESSAGE = "Operation arguments failed framework validation.";
|
|
324
|
+
|
|
325
|
+
/** What an undefined value the request would leave out is told. */
|
|
326
|
+
const UNSENT_MESSAGE =
|
|
327
|
+
"Value must not be undefined: sent as JSON it would be left out, and the request would reach more rows.";
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Refuses arguments that hold an undefined value whose leaving out would reach more
|
|
331
|
+
* rows than the caller wrote (in a filter, a unique selector or a cursor, or a where,
|
|
332
|
+
* cursor, limit or offset left undefined), before anything is sent: JSON would drop
|
|
333
|
+
* it. Throws the AvValidationError (A2004) the server answers when it is handed such
|
|
334
|
+
* a value: one issue at each, with the server's code, its path under \`at\`
|
|
335
|
+
* (["arguments"] for a call, ["operations", i, "args"] for a plan step, whose index
|
|
336
|
+
* the cause names).
|
|
337
|
+
*/
|
|
338
|
+
export function refuseUnsentValues(
|
|
339
|
+
resource: string,
|
|
340
|
+
family: string,
|
|
341
|
+
variant: string,
|
|
342
|
+
args: unknown,
|
|
343
|
+
at: readonly (string | number)[],
|
|
344
|
+
operation?: number,
|
|
345
|
+
): void {
|
|
346
|
+
const unsent = unsentValues(DECODE_TABLE, resource, family, variant, args);
|
|
347
|
+
if (unsent.length === 0) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const issues: readonly AvValidationIssue[] = Object.freeze(
|
|
351
|
+
unsent.map(({ path, code }) =>
|
|
352
|
+
Object.freeze({ path: Object.freeze([...at, ...path]), code, message: UNSENT_MESSAGE }),
|
|
353
|
+
),
|
|
354
|
+
);
|
|
355
|
+
throw frameworkErrorOf(
|
|
356
|
+
"A2004",
|
|
357
|
+
Object.freeze(
|
|
358
|
+
operation === undefined ? { message: REFUSAL_MESSAGE, issues } : { message: REFUSAL_MESSAGE, issues, operation },
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
}
|
|
339
362
|
|
|
340
363
|
/**
|
|
341
364
|
* Runs one operation: POSTs its arguments and settles its envelope.
|
|
342
365
|
*
|
|
343
|
-
* @
|
|
344
|
-
*
|
|
345
|
-
*
|
|
366
|
+
* @param check the client's own checks of the arguments, run before anything is sent
|
|
367
|
+
* (the ClientContract's field validators, when it has any).
|
|
368
|
+
* @returns the envelope's data when the code is A1xxx; null for A1001.
|
|
369
|
+
* @throws the AvError subclass of any other code; AvTransportError when no
|
|
370
|
+
* framework envelope arrived; AvValidationError (A2004), before anything is sent,
|
|
371
|
+
* when the arguments hold an undefined value JSON would leave out to widen the
|
|
372
|
+
* request (see refuseUnsentValues), or when
|
|
373
|
+
* \`check\` refuses the arguments; TypeError, before anything is sent, when the
|
|
346
374
|
* arguments are not a JSON-safe object or a request header is the framework's.
|
|
347
375
|
* A caller's abort rejects with what fetch rejected with.
|
|
348
376
|
*/
|
|
@@ -352,8 +380,11 @@ export async function execute(
|
|
|
352
380
|
family: string,
|
|
353
381
|
variant: string,
|
|
354
382
|
args: Readonly<Record<string, unknown>>,
|
|
355
|
-
options:
|
|
383
|
+
options: AvCallOptions = {},
|
|
384
|
+
check?: (resource: string, family: string, args: unknown) => void,
|
|
356
385
|
): Promise<unknown> {
|
|
386
|
+
refuseUnsentValues(resource, family, variant, args, ["arguments"]);
|
|
387
|
+
check?.(resource, family, args);
|
|
357
388
|
const operation = JSON.stringify(resource) + " " + family + "." + variant;
|
|
358
389
|
const headers = requestHeaders(options);
|
|
359
390
|
const body = serializeWireBody(encodeArguments(argumentObject(args)));
|
|
@@ -368,13 +399,13 @@ export async function execute(
|
|
|
368
399
|
* by its own operation's Resource (\`resources\`, in plan order).
|
|
369
400
|
*
|
|
370
401
|
* @throws as {@link execute} does; a rolled-back plan throws its failing
|
|
371
|
-
* operation's
|
|
402
|
+
* operation's AvError subclass, its cause naming the operation.
|
|
372
403
|
*/
|
|
373
404
|
export async function executePlan(
|
|
374
405
|
connection: TransportConnection,
|
|
375
406
|
plan: { readonly operations: readonly unknown[] },
|
|
376
407
|
resources: readonly string[],
|
|
377
|
-
options:
|
|
408
|
+
options: AvCallOptions = {},
|
|
378
409
|
): Promise<unknown[]> {
|
|
379
410
|
const operation = "the transaction";
|
|
380
411
|
const headers = requestHeaders(options);
|
|
@@ -395,7 +426,7 @@ async function post(
|
|
|
395
426
|
operation: string,
|
|
396
427
|
headers: Readonly<Record<string, string>>,
|
|
397
428
|
body: string,
|
|
398
|
-
options:
|
|
429
|
+
options: AvCallOptions,
|
|
399
430
|
): Promise<{ readonly status: number; readonly data: unknown }> {
|
|
400
431
|
const url = connection.entrypoint + path;
|
|
401
432
|
const init: FetchInit =
|
|
@@ -412,7 +443,7 @@ async function post(
|
|
|
412
443
|
if (options.signal?.aborted === true) {
|
|
413
444
|
throw error;
|
|
414
445
|
}
|
|
415
|
-
throw new
|
|
446
|
+
throw new AvTransportError("The request for " + operation + " failed before any response arrived.", null);
|
|
416
447
|
}
|
|
417
448
|
let text: string;
|
|
418
449
|
try {
|
|
@@ -421,7 +452,7 @@ async function post(
|
|
|
421
452
|
if (options.signal?.aborted === true) {
|
|
422
453
|
throw error;
|
|
423
454
|
}
|
|
424
|
-
throw new
|
|
455
|
+
throw new AvTransportError(
|
|
425
456
|
"The response to " + operation + " (HTTP " + response.status + ") could not be read.",
|
|
426
457
|
response.status,
|
|
427
458
|
);
|
|
@@ -430,14 +461,14 @@ async function post(
|
|
|
430
461
|
}
|
|
431
462
|
|
|
432
463
|
/**
|
|
433
|
-
* The decoded data, or
|
|
434
|
-
* otherwise valid envelope
|
|
464
|
+
* The decoded data, or an AvTransportError when a scalar in \`data\` does not
|
|
465
|
+
* decode inside an otherwise valid envelope.
|
|
435
466
|
*/
|
|
436
467
|
function decoded<T>(operation: string, status: number, decode: () => T): T {
|
|
437
468
|
try {
|
|
438
469
|
return decode();
|
|
439
470
|
} catch (error) {
|
|
440
|
-
throw new
|
|
471
|
+
throw new AvTransportError(
|
|
441
472
|
"The response to " + operation + " (HTTP " + status + ") holds a value this client cannot read: " +
|
|
442
473
|
(error instanceof Error ? error.message : String(error)),
|
|
443
474
|
status,
|
|
@@ -448,14 +479,14 @@ function decoded<T>(operation: string, status: number, decode: () => T): T {
|
|
|
448
479
|
/**
|
|
449
480
|
* Settles one response by the protocol's rule: the envelope's data, still in wire form,
|
|
450
481
|
* 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
|
-
*
|
|
482
|
+
* A scalar in \`data\` that fails to decode, inside an otherwise valid envelope, throws AvTransportError.
|
|
483
|
+
* It is no AvError, because the server reported no failure: the response is one
|
|
484
|
+
* this client cannot read, which is what AvTransportError means.
|
|
454
485
|
*/
|
|
455
486
|
function settle(operation: string, status: number, text: string): unknown {
|
|
456
487
|
const envelope = envelopeOf(parseJson(text));
|
|
457
488
|
if (envelope === undefined) {
|
|
458
|
-
throw new
|
|
489
|
+
throw new AvTransportError(
|
|
459
490
|
"The response to " + operation + " (HTTP " + status + ") is not an Aventara response envelope.",
|
|
460
491
|
status,
|
|
461
492
|
);
|
|
@@ -466,7 +497,7 @@ function settle(operation: string, status: number, text: string): unknown {
|
|
|
466
497
|
return envelope.data;
|
|
467
498
|
}
|
|
468
499
|
|
|
469
|
-
/** The parsed body, or undefined
|
|
500
|
+
/** The parsed body, or undefined (which no envelope is) when it is not JSON. */
|
|
470
501
|
function parseJson(text: string): unknown {
|
|
471
502
|
try {
|
|
472
503
|
return JSON.parse(text);
|
|
@@ -479,15 +510,15 @@ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
|
479
510
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
480
511
|
}
|
|
481
512
|
|
|
482
|
-
function isOperationCode(value: unknown): value is
|
|
513
|
+
function isOperationCode(value: unknown): value is AvOperationCode {
|
|
483
514
|
return OPERATION_CODES.some((code) => code === value);
|
|
484
515
|
}
|
|
485
516
|
|
|
486
|
-
function isValidationCode(value: unknown): value is
|
|
517
|
+
function isValidationCode(value: unknown): value is AvValidationCode {
|
|
487
518
|
return VALIDATION_CODES.some((code) => code === value);
|
|
488
519
|
}
|
|
489
520
|
|
|
490
|
-
function isSuccessCode(code:
|
|
521
|
+
function isSuccessCode(code: AvOperationCode): code is SuccessCode {
|
|
491
522
|
return code.startsWith("A1");
|
|
492
523
|
}
|
|
493
524
|
|
|
@@ -511,9 +542,9 @@ function envelopeOf(body: unknown): Envelope | undefined {
|
|
|
511
542
|
/**
|
|
512
543
|
* The cause rebuilt from the members the protocol defines, or undefined when one of them
|
|
513
544
|
* is malformed. Every other member is dropped, so whatever else a server put in
|
|
514
|
-
* its cause
|
|
545
|
+
* its cause (a stack, SQL, a file path) is never carried into a thrown error.
|
|
515
546
|
*/
|
|
516
|
-
function causeOf(value: unknown):
|
|
547
|
+
function causeOf(value: unknown): AvCause | undefined {
|
|
517
548
|
if (!isRecord(value)) {
|
|
518
549
|
return undefined;
|
|
519
550
|
}
|
|
@@ -523,12 +554,12 @@ function causeOf(value: unknown): Cause | undefined {
|
|
|
523
554
|
if (typeof message !== "string") {
|
|
524
555
|
return undefined;
|
|
525
556
|
}
|
|
526
|
-
let cause:
|
|
557
|
+
let cause: AvCause = { message };
|
|
527
558
|
if (issues !== undefined) {
|
|
528
559
|
if (!Array.isArray(issues)) {
|
|
529
560
|
return undefined;
|
|
530
561
|
}
|
|
531
|
-
const rebuilt:
|
|
562
|
+
const rebuilt: AvValidationIssue[] = [];
|
|
532
563
|
for (const issue of issues) {
|
|
533
564
|
const copy = issueOf(issue);
|
|
534
565
|
if (copy === undefined) {
|
|
@@ -547,18 +578,30 @@ function causeOf(value: unknown): Cause | undefined {
|
|
|
547
578
|
return Object.freeze(cause);
|
|
548
579
|
}
|
|
549
580
|
|
|
550
|
-
|
|
581
|
+
/**
|
|
582
|
+
* One issue rebuilt from the members the protocol defines (its path, code and
|
|
583
|
+
* message, and a failed check's name and parameters), or undefined when one of
|
|
584
|
+
* them is malformed.
|
|
585
|
+
*/
|
|
586
|
+
function issueOf(value: unknown): AvValidationIssue | undefined {
|
|
551
587
|
if (!isRecord(value)) {
|
|
552
588
|
return undefined;
|
|
553
589
|
}
|
|
554
590
|
const path = value["path"];
|
|
555
591
|
const code = value["code"];
|
|
556
592
|
const message = value["message"];
|
|
593
|
+
const check = value["check"];
|
|
594
|
+
const params = value["params"];
|
|
557
595
|
if (!isValidationCode(code) || typeof message !== "string") {
|
|
558
596
|
return undefined;
|
|
559
597
|
}
|
|
598
|
+
const checked = check === undefined && params === undefined;
|
|
599
|
+
if (!checked && (typeof check !== "string" || !isRecord(params))) {
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
const failed = checked ? {} : { check: check as string, params: frozen(params) as Readonly<Record<string, unknown>> };
|
|
560
603
|
if (path === undefined) {
|
|
561
|
-
return Object.freeze({ code, message });
|
|
604
|
+
return Object.freeze({ code, message, ...failed });
|
|
562
605
|
}
|
|
563
606
|
if (!Array.isArray(path)) {
|
|
564
607
|
return undefined;
|
|
@@ -570,7 +613,18 @@ function issueOf(value: unknown): ValidationIssue | undefined {
|
|
|
570
613
|
}
|
|
571
614
|
segments.push(segment);
|
|
572
615
|
}
|
|
573
|
-
return Object.freeze({ path: Object.freeze(segments), code, message });
|
|
616
|
+
return Object.freeze({ path: Object.freeze(segments), code, message, ...failed });
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** A parsed JSON value, frozen at every depth. */
|
|
620
|
+
function frozen(value: unknown): unknown {
|
|
621
|
+
if (typeof value === "object" && value !== null) {
|
|
622
|
+
for (const member of Object.values(value)) {
|
|
623
|
+
frozen(member);
|
|
624
|
+
}
|
|
625
|
+
Object.freeze(value);
|
|
626
|
+
}
|
|
627
|
+
return value;
|
|
574
628
|
}
|
|
575
629
|
|
|
576
630
|
/** The arguments, refused unless they are an object: the body is always one. */
|
|
@@ -586,7 +640,7 @@ function argumentObject(args: unknown): unknown {
|
|
|
586
640
|
* the framework's; then requestId, which overwrites any request id header the
|
|
587
641
|
* caller set.
|
|
588
642
|
*/
|
|
589
|
-
function requestHeaders(options:
|
|
643
|
+
function requestHeaders(options: AvCallOptions): Record<string, string> {
|
|
590
644
|
const headers: Record<string, string> = { ...FRAMEWORK_HEADERS };
|
|
591
645
|
const owned = new Set(Object.keys(FRAMEWORK_HEADERS).map((name) => name.toLowerCase()));
|
|
592
646
|
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.
|