@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +651 -0
  2. package/README.md +195 -141
  3. package/dist/cli/command.parser.d.ts +7 -6
  4. package/dist/cli/command.parser.js +13 -13
  5. package/dist/cli/generation-failure.renderer.d.ts +2 -1
  6. package/dist/cli/generation-failure.renderer.js +24 -24
  7. package/dist/cli/generation-success.renderer.d.ts +2 -1
  8. package/dist/cli/step.reporter.d.ts +2 -1
  9. package/dist/cli/terminal.prompter.d.ts +2 -1
  10. package/dist/config/client-config.interface.d.ts +24 -7
  11. package/dist/config/config.loader.d.ts +12 -7
  12. package/dist/config/config.loader.js +26 -8
  13. package/dist/config/config.resolver.d.ts +16 -9
  14. package/dist/config/config.resolver.js +40 -12
  15. package/dist/config/env.cascade.d.ts +4 -8
  16. package/dist/config/env.cascade.js +7 -7
  17. package/dist/config/module-style.resolver.d.ts +4 -7
  18. package/dist/config/module-style.resolver.js +4 -4
  19. package/dist/config/tsconfig.locator.d.ts +3 -2
  20. package/dist/config/tsconfig.locator.js +16 -4
  21. package/dist/contract/contract.acceptance.d.ts +4 -5
  22. package/dist/contract/contract.acceptance.js +5 -5
  23. package/dist/contract/contract.fetcher.d.ts +5 -4
  24. package/dist/contract/contract.fetcher.js +6 -6
  25. package/dist/contract/contract.loader.d.ts +9 -8
  26. package/dist/contract/contract.loader.js +2 -2
  27. package/dist/emit/access.emitter.d.ts +12 -0
  28. package/dist/emit/access.emitter.js +263 -0
  29. package/dist/emit/banner.emitter.d.ts +6 -1
  30. package/dist/emit/banner.emitter.js +12 -8
  31. package/dist/emit/client-surface.emitter.d.ts +2 -2
  32. package/dist/emit/client-surface.emitter.js +136 -41
  33. package/dist/emit/client-tree.emitter.d.ts +11 -4
  34. package/dist/emit/client-tree.emitter.js +51 -17
  35. package/dist/emit/contract-carrier.emitter.d.ts +3 -3
  36. package/dist/emit/derivation.emitter.d.ts +3 -3
  37. package/dist/emit/derivation.emitter.js +7 -3
  38. package/dist/emit/descriptor.emitter.d.ts +2 -2
  39. package/dist/emit/descriptor.emitter.js +1 -1
  40. package/dist/emit/emitted-tree.interface.d.ts +1 -1
  41. package/dist/emit/enum.emitter.d.ts +2 -2
  42. package/dist/emit/field-check.emitter.d.ts +22 -0
  43. package/dist/emit/field-check.emitter.js +592 -0
  44. package/dist/emit/json-shape.emitter.d.ts +2 -2
  45. package/dist/emit/module-specifier.scanner.d.ts +2 -1
  46. package/dist/emit/name.deriver.d.ts +8 -9
  47. package/dist/emit/name.deriver.js +18 -15
  48. package/dist/emit/named-type.emitter.d.ts +2 -2
  49. package/dist/emit/named-type.emitter.js +12 -2
  50. package/dist/emit/runtime.emitter.d.ts +6 -5
  51. package/dist/emit/runtime.emitter.js +215 -116
  52. package/dist/emit/scalar.codec.d.ts +1 -1
  53. package/dist/emit/scalar.codec.js +246 -29
  54. package/dist/emit/transaction.emitter.js +30 -14
  55. package/dist/generate.d.ts +5 -4
  56. package/dist/generate.js +3 -3
  57. package/dist/hydration/hydration-tag.format.d.ts +11 -0
  58. package/dist/hydration/hydration-tag.format.js +6 -0
  59. package/dist/index.d.ts +1 -1
  60. package/dist/init/client-init.errors.d.ts +4 -4
  61. package/dist/init/client-init.errors.js +4 -4
  62. package/dist/init/client-init.orchestrator.js +7 -7
  63. package/dist/init/client-init.planner.d.ts +3 -2
  64. package/dist/init/client-init.questions.d.ts +5 -16
  65. package/dist/init/client-init.questions.js +12 -12
  66. package/dist/init/client-project.inspector.d.ts +2 -2
  67. package/dist/init/client-project.inspector.js +3 -3
  68. package/dist/internal.d.ts +25 -0
  69. package/dist/internal.js +11 -0
  70. package/dist/output/output.validator.d.ts +3 -4
  71. package/dist/output/output.validator.js +2 -2
  72. package/dist/output/output.writer.d.ts +8 -7
  73. package/dist/output/output.writer.js +12 -12
  74. 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 Decimal {
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 a Decimal. */
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
- AuthError: "The caller is not authenticated, or is not permitted to run the operation.",
56
- ConflictError: "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
- ContractMismatchError: "This client was generated against a ClientContract other than the one the server serves. Regenerate it with `avclient generate`.",
58
- InternalError: "The server failed. Nothing in its cause is actionable; the diagnostics are in the server's logs.",
59
- NotFoundError: "A strict unique target does not exist.",
60
- ProtocolError: "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
- ValidationError: "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
- };
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
- "FrameworkError",
94
- "TransportError",
95
- "type Cause",
96
- "type OperationCode",
97
- "type ValidationCode",
98
- "type ValidationIssue",
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 FrameworkError subclass of its
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
- * TransportError. Branch on the class or on \`code\`, never on the message.
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 ValidationIssue {
97
+ export interface AvValidationIssue {
119
98
  readonly path?: readonly (string | number)[];
120
- readonly code: ValidationCode;
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 Cause {
115
+ export interface AvCause {
130
116
  readonly message: string;
131
- readonly issues?: readonly ValidationIssue[];
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 FrameworkError to
124
+ * always thrown as the subclass of its code's class; catch AvError to
139
125
  * catch them all.
140
126
  */
141
- export class FrameworkError<C extends OperationErrorCode = OperationErrorCode> extends Error {
142
- override readonly name: string = "FrameworkError";
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: Cause;
132
+ override readonly cause: AvCause;
147
133
 
148
- constructor(code: C, cause: 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 — a proxy's
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 TransportError extends Error {
163
- override readonly name: string = "TransportError";
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 classes = Object.entries(FRAMEWORK_ERROR_CLASSES).map(([name, summary]) => {
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 FrameworkError<${codes.map((code) => JSON.stringify(code)).join(" | ")}> {\n` +
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 OperationCode = (typeof OPERATION_CODES)[number];\n" +
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 OperationErrorCode = Exclude<OperationCode, \`A1\${string}\`>;\n` +
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 ValidationCode = (typeof VALIDATION_CODES)[number];\n" +
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 OperationErrorCode]: new (code: C, cause: Cause) => FrameworkError;\n" +
188
+ "\treadonly [C in AvOperationErrorCode]: new (code: C, cause: AvCause) => AvError;\n" +
199
189
  `} = {\n${table}};\n` +
200
- "\n/** The FrameworkError subclass a failure envelope's code throws. */\n" +
201
- "export function frameworkErrorOf<C extends OperationErrorCode>(code: C, cause: Cause): FrameworkError {\n" +
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 Cause,
260
+ type AvCause,
234
261
  frameworkErrorOf,
235
262
  OPERATION_CODES,
236
- type OperationCode,
237
- type OperationErrorCode,
238
- TransportError,
263
+ type AvOperationCode,
264
+ type AvOperationErrorCode,
265
+ AvTransportError,
239
266
  VALIDATION_CODES,
240
- type ValidationCode,
241
- type ValidationIssue,
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 — A1xxx resolves with the envelope's data, every other
248
- * code throws its FrameworkError subclass, and anything that is not a framework
249
- * envelope throws TransportError. No request is ever retried.
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
- * ContractMismatchError naming the remedy. A response that is not a framework
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
- * TransportError and names no remedy, because nothing in it says this client is
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: CallOptions.requestId sends it. */
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 Fetch API as far as the transport calls it, where the consumer's lib declares none. */
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 — DOM's, Node's — so the platform fetch is accepted
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 Fetch = typeof globalThis extends { readonly fetch: infer F } ? F : StructuralFetch;
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: Fetch;
344
+ readonly fetch: AvFetch;
318
345
  }
319
346
 
320
347
  /** Per-call options. Never serialized into the operation body. */
321
- export interface CallOptions {
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<OperationCode, OperationErrorCode>;
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: OperationErrorCode; readonly cause: Cause };
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
- * @returns the envelope's data when the code is A1xxx — null for A1001.
344
- * @throws the FrameworkError subclass of any other code; TransportError when no
345
- * framework envelope arrived; TypeError, before anything is sent, when the
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: CallOptions = {},
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 FrameworkError subclass, its cause naming the operation.
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: CallOptions = {},
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: CallOptions,
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 TransportError("The request for " + operation + " failed before any response arrived.", null);
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 TransportError(
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 — a scalar in \`data\` that does not decode, inside an
434
- * otherwise valid envelope — a TransportError.
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 TransportError(
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 TransportError
452
- * — not a FrameworkError, because the server reported no failure; the response is
453
- * one this client cannot read, which is what TransportError means.
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 TransportError(
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 — which no envelope is — when it is not JSON. */
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 OperationCode {
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 ValidationCode {
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: OperationCode): code is SuccessCode {
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 — a stack, SQL, a file path — is never carried into a thrown error.
590
+ * its cause (a stack, SQL, a file path) is never carried into a thrown error.
515
591
  */
516
- function causeOf(value: unknown): Cause | undefined {
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: Cause = { message };
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: ValidationIssue[] = [];
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
- function issueOf(value: unknown): ValidationIssue | undefined {
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: CallOptions): Record<string, string> {
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 `BuiltInScalar` union and the codec table in core's
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.