@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +504 -0
  2. package/README.md +159 -141
  3. package/dist/cli/command.parser.d.ts +5 -5
  4. package/dist/cli/command.parser.js +13 -13
  5. package/dist/cli/generation-failure.renderer.js +24 -24
  6. package/dist/config/client-config.interface.d.ts +7 -7
  7. package/dist/config/config.loader.d.ts +10 -4
  8. package/dist/config/config.loader.js +16 -7
  9. package/dist/config/config.resolver.d.ts +7 -7
  10. package/dist/config/config.resolver.js +12 -12
  11. package/dist/config/env.cascade.d.ts +2 -2
  12. package/dist/config/env.cascade.js +4 -4
  13. package/dist/config/module-style.resolver.d.ts +1 -1
  14. package/dist/config/module-style.resolver.js +2 -2
  15. package/dist/config/tsconfig.locator.d.ts +1 -1
  16. package/dist/config/tsconfig.locator.js +2 -2
  17. package/dist/contract/contract.acceptance.d.ts +4 -4
  18. package/dist/contract/contract.acceptance.js +4 -4
  19. package/dist/contract/contract.fetcher.d.ts +3 -3
  20. package/dist/contract/contract.fetcher.js +6 -6
  21. package/dist/contract/contract.loader.d.ts +6 -6
  22. package/dist/contract/contract.loader.js +2 -2
  23. package/dist/emit/client-surface.emitter.d.ts +2 -2
  24. package/dist/emit/client-surface.emitter.js +61 -40
  25. package/dist/emit/client-tree.emitter.d.ts +3 -3
  26. package/dist/emit/client-tree.emitter.js +8 -5
  27. package/dist/emit/contract-carrier.emitter.d.ts +3 -3
  28. package/dist/emit/derivation.emitter.js +1 -0
  29. package/dist/emit/descriptor.emitter.d.ts +2 -2
  30. package/dist/emit/descriptor.emitter.js +1 -1
  31. package/dist/emit/emitted-tree.interface.d.ts +1 -1
  32. package/dist/emit/enum.emitter.d.ts +2 -2
  33. package/dist/emit/field-check.emitter.d.ts +21 -0
  34. package/dist/emit/field-check.emitter.js +592 -0
  35. package/dist/emit/json-shape.emitter.d.ts +2 -2
  36. package/dist/emit/name.deriver.d.ts +6 -6
  37. package/dist/emit/name.deriver.js +14 -14
  38. package/dist/emit/named-type.emitter.d.ts +2 -2
  39. package/dist/emit/runtime.emitter.d.ts +2 -2
  40. package/dist/emit/runtime.emitter.js +166 -112
  41. package/dist/emit/scalar.codec.d.ts +1 -1
  42. package/dist/emit/scalar.codec.js +246 -29
  43. package/dist/emit/transaction.emitter.js +30 -14
  44. package/dist/generate.d.ts +2 -2
  45. package/dist/generate.js +2 -2
  46. package/dist/index.d.ts +1 -1
  47. package/dist/init/client-init.errors.d.ts +4 -4
  48. package/dist/init/client-init.errors.js +4 -4
  49. package/dist/init/client-init.orchestrator.js +7 -7
  50. package/dist/init/client-init.questions.d.ts +5 -5
  51. package/dist/init/client-init.questions.js +10 -10
  52. package/dist/init/client-project.inspector.d.ts +2 -2
  53. package/dist/init/client-project.inspector.js +3 -3
  54. package/dist/output/output.validator.d.ts +3 -4
  55. package/dist/output/output.validator.js +2 -2
  56. package/dist/output/output.writer.d.ts +5 -5
  57. package/dist/output/output.writer.js +12 -12
  58. package/package.json +4 -4
@@ -29,7 +29,7 @@ const SCALAR_CODEC_SOURCES = {
29
29
  : refuse("bigint", "wire", "a base-10 integer string")`,
30
30
  },
31
31
  decimal: {
32
- encode: `(value) => (value instanceof Decimal ? value.toString() : refuse("decimal", "runtime", "a Decimal"))`,
32
+ encode: `(value) => (value instanceof AvDecimal ? value.toString() : refuse("decimal", "runtime", "an AvDecimal"))`,
33
33
  decode: "decodeDecimal",
34
34
  },
35
35
  datetime: {
@@ -48,13 +48,13 @@ const SCALAR_CODEC_SOURCES = {
48
48
  },
49
49
  };
50
50
  function codecModuleHead(from) {
51
- return `import { Decimal } from ${from("./decimal")};
51
+ return `import { AvDecimal } from ${from("./decimal")};
52
52
  import type { FieldDecoding } from ${from("./descriptor")};
53
53
 
54
54
  /**
55
55
  * The scalar codecs of Aventara protocol version 1: one per built-in
56
56
  * scalar, converting between the generated runtime value and the JSON wire value.
57
- * A value is never wrapped per field — the Contract names the scalar, so the
57
+ * A value is never wrapped per field: the Contract names the scalar, so the
58
58
  * caller says which scalar a value is.
59
59
  *
60
60
  * null is explicit in both directions and passes through; whether a field admits
@@ -82,7 +82,7 @@ const DATETIME_WIRE = ${wireGrammarLiteral("datetime")};
82
82
  const BASE64_WIRE = ${wireGrammarLiteral("bytes")};
83
83
  const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
84
84
 
85
- function refuse(scalar: BuiltInScalar, side: Side, expected: string): never {
85
+ function refuse(scalar: AvBuiltInScalar, side: Side, expected: string): never {
86
86
  throw new TypeError('A "' + scalar + '" ' + side + " value must be " + expected + ".");
87
87
  }
88
88
 
@@ -94,10 +94,10 @@ function isFiniteNumber(value: unknown): value is number {
94
94
  return typeof value === "number" && Number.isFinite(value);
95
95
  }
96
96
 
97
- function decodeDecimal(wire: unknown): Decimal {
97
+ function decodeDecimal(wire: unknown): AvDecimal {
98
98
  if (typeof wire === "string") {
99
99
  try {
100
- return new Decimal(wire);
100
+ return new AvDecimal(wire);
101
101
  } catch {
102
102
  // Refused below, naming the scalar.
103
103
  }
@@ -195,8 +195,8 @@ function kindOf(value: unknown): string {
195
195
  if (value instanceof Uint8Array) {
196
196
  return "a Uint8Array";
197
197
  }
198
- if (value instanceof Decimal) {
199
- return "a Decimal";
198
+ if (value instanceof AvDecimal) {
199
+ return "an AvDecimal";
200
200
  }
201
201
  if (typeof value === "object" && value !== null && isPlainRecord(value)) {
202
202
  return "a symbol-keyed property";
@@ -231,13 +231,18 @@ function copyJson(side: Side, value: unknown, path: Path, ancestors: Set<object>
231
231
  if (Array.isArray(value)) {
232
232
  const items: unknown[] = [];
233
233
  for (let index = 0; index < value.length; index += 1) {
234
- items.push(copyJson(side, value[index], [...path, index], ancestors));
234
+ // An undefined element (or a hole) is null, as JSON writes it (rc.2).
235
+ const item = value[index];
236
+ items.push(item === undefined ? null : copyJson(side, item, [...path, index], ancestors));
235
237
  }
236
238
  copy = items;
237
239
  } else {
238
- // fromEntries defines each key, so "__proto__" stays an own key.
240
+ // fromEntries defines each key, so "__proto__" stays an own key. A key holding
241
+ // undefined is left out, as JSON leaves it out (rc.2).
239
242
  copy = Object.fromEntries(
240
- Object.entries(value).map(([key, member]) => [key, copyJson(side, member, [...path, key], ancestors)]),
243
+ Object.entries(value)
244
+ .filter(([, member]) => member !== undefined)
245
+ .map(([key, member]) => [key, copyJson(side, member, [...path, key], ancestors)]),
241
246
  );
242
247
  }
243
248
  ancestors.delete(value);
@@ -271,8 +276,10 @@ function writeWireJson(value: unknown, path: Path, ancestors: Set<object>): stri
271
276
  if (Array.isArray(value)) {
272
277
  const items: string[] = [];
273
278
  for (let index = 0; index < value.length; index += 1) {
274
- // An undefined element is refused, not stripped: JSON would make it null.
275
- items.push(writeWireJson(value[index], [...path, index], ancestors));
279
+ // An undefined element (or a hole) is null, as JSON writes it (rc.2): the
280
+ // server reads the same value from an in-process caller.
281
+ const item = value[index];
282
+ items.push(item === undefined ? "null" : writeWireJson(item, [...path, index], ancestors));
276
283
  }
277
284
  text = "[" + items.join(",") + "]";
278
285
  } else {
@@ -288,7 +295,7 @@ function writeWireJson(value: unknown, path: Path, ancestors: Set<object>): stri
288
295
  return text;
289
296
  }
290
297
 
291
- function codecOf(scalar: BuiltInScalar): ScalarCodec {
298
+ function codecOf(scalar: AvBuiltInScalar): ScalarCodec {
292
299
  if (!Object.hasOwn(SCALAR_CODECS, scalar)) {
293
300
  throw new TypeError('"' + String(scalar) + '" is not a built-in scalar of this protocol version.');
294
301
  }
@@ -296,21 +303,22 @@ function codecOf(scalar: BuiltInScalar): ScalarCodec {
296
303
  }
297
304
 
298
305
  /** The JSON wire value of a scalar's runtime value; null passes through. */
299
- export function encodeScalar(scalar: BuiltInScalar, value: unknown): unknown {
306
+ export function encodeScalar(scalar: AvBuiltInScalar, value: unknown): unknown {
300
307
  const codec = codecOf(scalar);
301
308
  return value === null ? null : codec.encode(value);
302
309
  }
303
310
 
304
311
  /** The runtime value of a scalar's JSON wire value; null passes through. */
305
- export function decodeScalar(scalar: BuiltInScalar, wire: unknown): unknown {
312
+ export function decodeScalar(scalar: AvBuiltInScalar, wire: unknown): unknown {
306
313
  const codec = codecOf(scalar);
307
314
  return wire === null ? null : codec.decode(wire);
308
315
  }
309
316
 
310
317
  /**
311
318
  * The JSON text of a request body whose scalars are already encoded. Every
312
- * undefined property is stripped, at every depth — an optional property is
313
- * omitted, never sent — and null is kept as the explicit value it is.
319
+ * undefined property is stripped, at every depth (an optional property is
320
+ * omitted, never sent), an undefined array element is null, as JSON.stringify
321
+ * writes both, and null is kept as the explicit value it is.
314
322
  * Anything else that is not JSON-safe is refused rather than coerced: it means a
315
323
  * value escaped its scalar codec.
316
324
  */
@@ -321,8 +329,8 @@ export function serializeWireBody(body: unknown): string {
321
329
  /**
322
330
  * An operation's arguments with every scalar in its wire form, read off each
323
331
  * value's runtime class: a
324
- * bigint, a Date, a Decimal or a Uint8Array is encoded wherever it stands — a
325
- * filter operand, a data value, a cursor — and plain objects and arrays are
332
+ * bigint, a Date, an AvDecimal or a Uint8Array is encoded wherever it stands (a
333
+ * filter operand, a data value, a cursor), and plain objects and arrays are
326
334
  * walked. Every other value is left for serializeWireBody, which refuses what is
327
335
  * not JSON-safe. A json value cannot hold one of the four classes (the types
328
336
  * exclude them), so nothing inside one is ever re-read as a scalar.
@@ -333,8 +341,8 @@ export function encodeArguments(value: unknown): unknown {
333
341
 
334
342
  /**
335
343
  * One read of a caller's arguments. Every native read on them happens through it,
336
- * here and in the transaction builder, so a value that cannot be read — a revoked
337
- * Proxy (an Immer draft after produce), a throwing getter or trap — is refused at its
344
+ * here and in the transaction builder, so a value that cannot be read (a revoked
345
+ * Proxy such as an Immer draft after produce, a throwing getter or trap) is refused at its
338
346
  * path with a TypeError before anything is sent, never with the engine's own error.
339
347
  */
340
348
  export function readArgument<T>(path: Path, read: () => T): T {
@@ -347,12 +355,221 @@ export function readArgument<T>(path: Path, read: () => T): T {
347
355
  }
348
356
  }
349
357
 
358
+ /** Where each relation directive holds the records its target writes; its other members select. */
359
+ export const RECORD_SITES: { readonly [directive: string]: readonly (string | undefined)[] } = {
360
+ $create: [undefined],
361
+ $connect: [],
362
+ $connectOrCreate: ["create"],
363
+ $disconnect: [],
364
+ $set: [],
365
+ $update: ["data"],
366
+ $delete: [],
367
+ $upsert: ["create", "update"],
368
+ };
369
+
370
+ /** The members of an operation's arguments that hold its write records. */
371
+ export function writeKeysOf(family: string): readonly string[] {
372
+ return family === "create" || family === "update" ? ["data"] : family === "upsert" ? ["create", "update"] : [];
373
+ }
374
+
375
+ /** The code an unsent value is refused with: the server's for that place. */
376
+ export type UnsentCode = "V1001" | "V1007" | "V1009";
377
+
378
+ /** One value a request would leave out: where it is, and the server's code for it. */
379
+ export interface UnsentValue {
380
+ readonly path: Path;
381
+ readonly code: UnsentCode;
382
+ }
383
+
384
+ /**
385
+ * Every undefined value an operation's arguments hold where leaving it out would
386
+ * reach more rows than the caller wrote, in argument order, each with the code the
387
+ * server answers it with:
388
+ *
389
+ * - inside a filter, a unique selector or a cursor, at any depth: a where (AND, OR,
390
+ * NOT, a relation filter, an operator object, an in list), a cursor, the where and
391
+ * cursor of a relation that select or include reads, and a nested write's
392
+ * selectors (what $connect, $disconnect, $set and $delete name, and the where of
393
+ * $update, $upsert and $connectOrCreate) (V1001);
394
+ * - a where, cursor, limit or offset that is itself undefined, as a call's or a
395
+ * relation's argument (V1001; a unique variant's where V1007, a cursor V1009),
396
+ * and the where of $update (V1001), $upsert or $connectOrCreate (V1007).
397
+ *
398
+ * JSON drops an undefined value, so such a value is never sent. An undefined field
399
+ * of a write record is not sent, which is what leaving it out means; a projection,
400
+ * an order or a directive left undefined is left out as well.
401
+ */
402
+ export function unsentValues(
403
+ table: DecodeTable,
404
+ resource: string,
405
+ family: string,
406
+ variant: string,
407
+ args: unknown,
408
+ ): UnsentValue[] {
409
+ const found: UnsentValue[] = [];
410
+ const ancestors = new Set<object>();
411
+
412
+ const listAt = (value: unknown, path: Path): readonly unknown[] | undefined =>
413
+ typeof value === "object" && value !== null && readArgument(path, () => Array.isArray(value))
414
+ ? (value as readonly unknown[])
415
+ : undefined;
416
+ const recordAt = (value: unknown, path: Path): Readonly<Record<string, unknown>> | undefined =>
417
+ typeof value === "object" && value !== null && readArgument(path, () => !Array.isArray(value) && isPlainRecord(value))
418
+ ? (value as Readonly<Record<string, unknown>>)
419
+ : undefined;
420
+
421
+ // Visits each member of a container in order (each entry of a list, each key of a
422
+ // record), undefined ones only when asked; a container met again on its own
423
+ // ancestry is not walked again, and the encoder refuses it.
424
+ const each = (
425
+ container: readonly unknown[] | Readonly<Record<string, unknown>>,
426
+ path: Path,
427
+ withUndefined: boolean,
428
+ visit: (key: string | number, value: unknown, at: Path) => void,
429
+ ): void => {
430
+ if (ancestors.has(container)) {
431
+ return;
432
+ }
433
+ ancestors.add(container);
434
+ const keys: readonly (string | number)[] = Array.isArray(container)
435
+ ? Array.from({ length: readArgument(path, () => container.length) }, (_, index) => index)
436
+ : readArgument(path, () => Object.keys(container));
437
+ for (const key of keys) {
438
+ const at = [...path, key];
439
+ const value = readArgument(at, () => (container as Readonly<Record<string | number, unknown>>)[key]);
440
+ if (withUndefined || value !== undefined) {
441
+ visit(key, value, at);
442
+ }
443
+ }
444
+ ancestors.delete(container);
445
+ };
446
+ // One entry, or each entry of a list.
447
+ const entries = (value: unknown, path: Path, visit: (entry: unknown, at: Path) => void): void => {
448
+ const list = listAt(value, path);
449
+ if (list === undefined) {
450
+ visit(value, path);
451
+ } else {
452
+ each(list, path, true, (_index, entry, at) => visit(entry, at));
453
+ }
454
+ };
455
+
456
+ const filter = (value: unknown, path: Path): void => {
457
+ if (value === undefined) {
458
+ found.push({ path, code: "V1001" });
459
+ return;
460
+ }
461
+ const container = listAt(value, path) ?? recordAt(value, path);
462
+ if (container !== undefined) {
463
+ each(container, path, true, (_key, member, at) => filter(member, at));
464
+ }
465
+ };
466
+ // A read's own arguments (a call's, or a relation's under select or include):
467
+ // its where and cursor are filters, its limit and offset bound what it returns.
468
+ const read = (key: string | number, value: unknown, at: Path, whereCode: UnsentCode, cursorCode: UnsentCode): void => {
469
+ if (key === "where" || key === "cursor") {
470
+ if (value === undefined) {
471
+ found.push({ path: at, code: key === "where" ? whereCode : cursorCode });
472
+ } else {
473
+ filter(value, at);
474
+ }
475
+ } else if (key === "limit" || key === "offset") {
476
+ if (value === undefined) {
477
+ found.push({ path: at, code: "V1001" });
478
+ }
479
+ } else if ((key === "select" || key === "include") && value !== undefined) {
480
+ projection(value, at);
481
+ }
482
+ };
483
+ const projection = (value: unknown, path: Path): void => {
484
+ const list = listAt(value, path);
485
+ if (list === undefined) {
486
+ return;
487
+ }
488
+ each(list, path, false, (_index, entry, at) => {
489
+ const relations = recordAt(entry, at);
490
+ if (relations === undefined) {
491
+ return;
492
+ }
493
+ each(relations, at, false, (_relation, nested, nestedAt) => {
494
+ const args = recordAt(nested, nestedAt);
495
+ if (args !== undefined) {
496
+ each(args, nestedAt, true, (key, member, memberAt) => read(key, member, memberAt, "V1001", "V1009"));
497
+ }
498
+ });
499
+ });
500
+ };
501
+ const writeRecord = (target: string, value: unknown, path: Path): void => {
502
+ const record = recordAt(value, path);
503
+ if (record === undefined) {
504
+ return;
505
+ }
506
+ const fields = Object.hasOwn(table, target) ? table[target] : undefined;
507
+ each(record, path, false, (key, member, at) => {
508
+ const decoding = fields !== undefined && Object.hasOwn(fields, key) ? fields[key as string] : undefined;
509
+ if (typeof decoding === "object") {
510
+ relationWrite(decoding.relation, member, at);
511
+ }
512
+ });
513
+ };
514
+ const relationWrite = (target: string, value: unknown, path: Path): void => {
515
+ const directives = recordAt(value, path);
516
+ if (directives === undefined) {
517
+ return;
518
+ }
519
+ each(directives, path, false, (directive, operand, at) => {
520
+ const sites = Object.hasOwn(RECORD_SITES, directive) ? RECORD_SITES[directive as string] : undefined;
521
+ if (sites === undefined) {
522
+ return;
523
+ }
524
+ if (sites.length === 0) {
525
+ filter(operand, at);
526
+ return;
527
+ }
528
+ entries(operand, at, (entry, entryAt) => {
529
+ if (sites.includes(undefined)) {
530
+ writeRecord(target, entry, entryAt);
531
+ return;
532
+ }
533
+ const parts = recordAt(entry, entryAt);
534
+ if (parts !== undefined) {
535
+ each(parts, entryAt, true, (key, member, memberAt) => {
536
+ if (sites.includes(key as string)) {
537
+ writeRecord(target, member, memberAt);
538
+ } else if (member !== undefined) {
539
+ filter(member, memberAt);
540
+ } else if (key === "where") {
541
+ found.push({ path: memberAt, code: directive === "$update" ? "V1001" : "V1007" });
542
+ }
543
+ });
544
+ }
545
+ });
546
+ });
547
+ };
548
+
549
+ const root = recordAt(args, []);
550
+ if (root !== undefined) {
551
+ const writeKeys = writeKeysOf(family);
552
+ const whereCode: UnsentCode = variant === "unique" ? "V1007" : "V1001";
553
+ const cursorCode: UnsentCode = family === "find" && variant === "many" ? "V1009" : "V1001";
554
+ each(root, [], true, (key, value, at) => {
555
+ if (writeKeys.includes(key as string)) {
556
+ if (value !== undefined) {
557
+ entries(value, at, (entry, entryAt) => writeRecord(resource, entry, entryAt));
558
+ }
559
+ } else {
560
+ read(key, value, at, whereCode, cursorCode);
561
+ }
562
+ });
563
+ }
564
+ return found;
565
+ }
566
+
350
567
  /** How encodeValue treats an object: a scalar's runtime class, a container, or neither. */
351
568
  function objectKind(value: object): "datetime" | "decimal" | "bytes" | "container" | "other" {
352
569
  if (value instanceof Date) {
353
570
  return "datetime";
354
571
  }
355
- if (value instanceof Decimal) {
572
+ if (value instanceof AvDecimal) {
356
573
  return "decimal";
357
574
  }
358
575
  if (value instanceof Uint8Array) {
@@ -413,13 +630,13 @@ export type DecodeTable = {
413
630
  * table: a number (a count), null, and the data of a Resource the table
414
631
  * does not name pass through; a list is a list of
415
632
  * records, anything else is one record of \`resource\`. In a record, a field the
416
- * table names is revived — each item of a list field — a relation recurses with
633
+ * table names is revived (each item of a list field), a relation recurses with
417
634
  * its target, and every other key passes through untouched: json is never
418
635
  * revived. A to-many relation's value is a list, \`{ data, count }\` or
419
- * \`{ count }\` — the three shapes the derivation gives it.
636
+ * \`{ count }\`, the three shapes the derivation gives it.
420
637
  *
421
- * @throws TypeError when a value is not the wire form its field's scalar takes —
422
- * which the transport answers as a TransportError.
638
+ * @throws TypeError when a value is not the wire form its field's scalar takes,
639
+ * which the transport answers as an AvTransportError.
423
640
  */
424
641
  export function decodeResult(table: DecodeTable, resource: string, value: unknown): unknown {
425
642
  // A Resource the table does not name has nothing to revive: its data passes.
@@ -477,9 +694,9 @@ export function emitScalarCodecModule(style) {
477
694
  path: "runtime/codec.ts",
478
695
  source: codecModuleHead(moduleSpecifierWriter(style)) +
479
696
  "\n/** The built-in scalars of the protocol. */\n" +
480
- `export type BuiltInScalar =\n${union};\n` +
697
+ `export type AvBuiltInScalar =\n${union};\n` +
481
698
  CODEC_MODULE_BODY +
482
- "\nconst SCALAR_CODECS: { readonly [S in BuiltInScalar]: ScalarCodec } = {\n" +
699
+ "\nconst SCALAR_CODECS: { readonly [S in AvBuiltInScalar]: ScalarCodec } = {\n" +
483
700
  `${table}\n};\n`,
484
701
  };
485
702
  }
@@ -24,8 +24,8 @@ export interface FingerprintNode {
24
24
  }
25
25
 
26
26
  /**
27
- * The node's fingerprint, or null when its arguments are not canonical JSON — a
28
- * lone surrogate, a non-finite number — exactly when core cannot compute one
27
+ * The node's fingerprint, or null when its arguments are not canonical JSON (a
28
+ * lone surrogate, a non-finite number), exactly when core cannot compute one
29
29
  * either; the server then answers the operation itself.
30
30
  */
31
31
  export function fingerprintOf(node: FingerprintNode): string | null {
@@ -210,20 +210,20 @@ function base64Url(bytes: Uint8Array): string {
210
210
  `;
211
211
  function transactionModuleSource(from) {
212
212
  return `import { encodeArguments, readArgument, serializeWireBody } from ${from("./codec")};
213
- import { type Cause, type FrameworkError, frameworkErrorOf } from ${from("./errors")};
213
+ import { type AvCause, type AvError, frameworkErrorOf } from ${from("./errors")};
214
214
  import { fingerprintOf } from ${from("./fingerprint")};
215
- import { type CallOptions, executePlan, type TransportConnection } from ${from("./transport")};
215
+ import { type AvCallOptions, executePlan, refuseUnsentValues, type TransportConnection } from ${from("./transport")};
216
216
 
217
217
  /**
218
218
  * The deferred transaction builder and runner: \`avClient.tx\` defers an operation
219
- * into a handle — pure data, no request — and \`avClient.transaction\` binds a
219
+ * into a handle (pure data, no request), and \`avClient.transaction\` binds a
220
220
  * list of handles into one transaction plan, sends it once, and resolves one
221
221
  * result per handle in list order. A handle has no connection, so any client of
222
222
  * this generated tree may run it.
223
223
  */
224
224
 
225
225
  /**
226
- * What a handle defers — or why its arguments could not be read: the builder
226
+ * What a handle defers, or why its arguments could not be read: the builder
227
227
  * never throws into app code, and running the handle refuses it instead.
228
228
  */
229
229
  interface DeferredRecord {
@@ -331,7 +331,7 @@ function placeholderSites(args: unknown): readonly PlaceholderSite[] {
331
331
  /**
332
332
  * Defers one operation into a handle: nothing is sent, and nothing is thrown.
333
333
  * Its arguments are copied now, so a later change to the caller's data cannot
334
- * move a reference — and a handle can reference only handles built before it.
334
+ * move a reference, and a handle can reference only handles built before it.
335
335
  * Arguments that cannot be read are kept as their refusal, which running the
336
336
  * handle rejects with.
337
337
  */
@@ -360,8 +360,8 @@ export function deferOperation(resource: string, family: string, variant: string
360
360
  }
361
361
 
362
362
  /** What core answers in process for a plan entry it cannot run (A2004 / V1001). */
363
- function entryRefusal(operation: number, message: string): FrameworkError {
364
- const cause: Cause = {
363
+ function entryRefusal(operation: number, message: string): AvError {
364
+ const cause: AvCause = {
365
365
  message: "Transaction plan failed framework validation.",
366
366
  issues: [{ path: ["operations", operation], code: "V1001", message }],
367
367
  operation,
@@ -369,19 +369,32 @@ function entryRefusal(operation: number, message: string): FrameworkError {
369
369
  return frameworkErrorOf("A2004", cause);
370
370
  }
371
371
 
372
+ /**
373
+ * The checks a plan's steps run before it is sent (the ClientContract's field
374
+ * validators, when it has any). Throws the AvError the server would answer.
375
+ */
376
+ export type PlanCheck = (
377
+ steps: readonly { readonly resource: string; readonly family: string; readonly args: unknown }[],
378
+ ) => void;
379
+
372
380
  /**
373
381
  * Binds \`steps\` into the plan, sends it once, and resolves one decoded result per
374
382
  * step. Refused before anything is sent, with what core answers in
375
383
  * process: a list that is not one, an entry that is not a handle of this
376
- * generated client, one handle twice (ValidationError A2004 / V1001 at
377
- * ["operations", i]: the entry itself); a reference to a handle not in the list (ValidationError A2007 /
384
+ * generated client, one handle twice (AvValidationError A2004 / V1001 at
385
+ * ["operations", i]: the entry itself); a reference to a handle not in the list (AvValidationError A2007 /
378
386
  * V1010 at its \`$ref\`); a handle whose arguments could not be read when it was
379
- * built (the TypeError naming where).
387
+ * built (the TypeError naming where); a step holding an undefined value JSON would
388
+ * leave out to widen it (AvValidationError A2004, as refuseUnsentValues states it,
389
+ * the step named);
390
+ * and, when \`check\` is given, a step whose values break a field check
391
+ * (AvValidationError A2004, as the server answers it).
380
392
  */
381
393
  export async function runTransaction(
382
394
  connection: TransportConnection,
383
395
  steps: unknown,
384
- options: CallOptions = {},
396
+ options: AvCallOptions = {},
397
+ check?: PlanCheck,
385
398
  ): Promise<unknown[]> {
386
399
  if (!Array.isArray(steps)) {
387
400
  throw frameworkErrorOf("A2004", {
@@ -421,11 +434,14 @@ export async function runTransaction(
421
434
  });
422
435
  }
423
436
  }
437
+ refuseUnsentValues(record.resource, record.family, record.variant, record.args, ["operations", index, "args"], index);
424
438
  }
425
439
 
440
+ check?.(records);
441
+
426
442
  // Bound lazily: a node's references need their source's
427
443
  // fingerprint, so each node is built once, on first need, wherever its source
428
- // stands in the list — the server, not this client, refuses a later one.
444
+ // stands in the list. The server, not this client, refuses a later one.
429
445
  const nodes = new Map<number, PlanNode>();
430
446
  const nodeAt = (index: number): PlanNode => {
431
447
  const built = nodes.get(index);
@@ -73,8 +73,8 @@ export type ClientGenerationOutcome = ClientGenerated | ClientUpToDate | Foreign
73
73
  * generator did not produce stands in its way and `overrideForeign` is not set,
74
74
  * says what it is and writes nothing.
75
75
  *
76
- * @throws a refusal (`EnvFileError`, `ClientConfigError`, `ContractTransportError`,
77
- * `ContractProtocolError`, `GeneratedNameError`, `OutputWriteError`) whose
76
+ * @throws a refusal (`AvEnvFileError`, `AvClientConfigError`, `AvContractTransportError`,
77
+ * `AvContractProtocolError`, `AvGeneratedNameError`, `AvOutputWriteError`) whose
78
78
  * message is the whole diagnosis; the previous output is intact. Anything else
79
79
  * is a defect.
80
80
  */
package/dist/generate.js CHANGED
@@ -9,7 +9,7 @@ import { loadClientContractSince } from "./contract/contract.loader.js";
9
9
  import { emitClientTree } from "./emit/client-tree.emitter.js";
10
10
  import { parseContractCarrier } from "./emit/contract-carrier.emitter.js";
11
11
  import { CLIENT_ENTRY_FILE, CONTRACT_CARRIER_MODULE, GENERATED_DIRECTORY, } from "./emit/emitted-tree.interface.js";
12
- import { findForeignOutputContent, OutputWriteError, ownedOutputMatches, precedeDefect, writeClientOutput, } from "./output/output.writer.js";
12
+ import { AvOutputWriteError, findForeignOutputContent, ownedOutputMatches, precedeDefect, writeClientOutput, } from "./output/output.writer.js";
13
13
  const runPhase = async (_title, body) => await body();
14
14
  async function storedContract(generateAt) {
15
15
  try {
@@ -87,7 +87,7 @@ export async function generateClient(input) {
87
87
  const into = path.relative(input.directory, config.generateAt) || ".";
88
88
  const write = (overrideForeign) => step(`Checking and writing the client into ${into}`, () => writeInto(overrideForeign));
89
89
  const foreign = await findForeignOutputContent(config.generateAt).catch((error) => {
90
- throw error instanceof OutputWriteError
90
+ throw error instanceof AvOutputWriteError
91
91
  ? error.precededBy(emission.warnings)
92
92
  : precedeDefect(error, emission.warnings);
93
93
  });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** The version of `@aventara/client`. */
2
2
  export declare const AVENTARA_CLIENT_GENERATOR_VERSION: string;
3
- export type { ClientConfigInput, ConfigValue, EnvReference, } from "./config/client-config.interface.js";
3
+ export type { AvClientConfigInput, AvClientConfigValue, AvEnvReference, } from "./config/client-config.interface.js";
4
4
  export { defineClientConfig, env } from "./config/config.resolver.js";
@@ -1,9 +1,9 @@
1
1
  /** `avclient init`'s refusals after the questions: each one sentence, exit 1. */
2
2
  /** Nobody confirmed replacing existing content. A refusal: nothing was touched. */
3
- export declare class ClientInitNotConfirmedError extends Error {
4
- readonly name = "ClientInitNotConfirmedError";
3
+ export declare class AvClientInitNotConfirmedError extends Error {
4
+ readonly name = "AvClientInitNotConfirmedError";
5
5
  }
6
6
  /** A step after the write failed. Reported with what to run; the files are kept. */
7
- export declare class ClientInitStepError extends Error {
8
- readonly name = "ClientInitStepError";
7
+ export declare class AvClientInitStepError extends Error {
8
+ readonly name = "AvClientInitStepError";
9
9
  }
@@ -1,6 +1,6 @@
1
- export class ClientInitNotConfirmedError extends Error {
2
- name = "ClientInitNotConfirmedError";
1
+ export class AvClientInitNotConfirmedError extends Error {
2
+ name = "AvClientInitNotConfirmedError";
3
3
  }
4
- export class ClientInitStepError extends Error {
5
- name = "ClientInitStepError";
4
+ export class AvClientInitStepError extends Error {
5
+ name = "AvClientInitStepError";
6
6
  }
@@ -4,9 +4,9 @@ import { GENERATION_STEPS, runGenerate, } from "../cli/generate.command.js";
4
4
  import { createStepReporter } from "../cli/step.reporter.js";
5
5
  import { CLIENT_CONFIG_FILE } from "../config/config.loader.js";
6
6
  import { resolveModuleStyle } from "../config/module-style.resolver.js";
7
- import { ContractTransportError } from "../contract/contract.fetcher.js";
7
+ import { AvContractTransportError } from "../contract/contract.fetcher.js";
8
8
  import { CLIENT_ENTRY_FILE } from "../emit/emitted-tree.interface.js";
9
- import { ClientInitNotConfirmedError, ClientInitStepError, } from "./client-init.errors.js";
9
+ import { AvClientInitNotConfirmedError, AvClientInitStepError, } from "./client-init.errors.js";
10
10
  import { GENERATE_SCRIPT, planClientInit } from "./client-init.planner.js";
11
11
  import { resolveClientInitAnswers } from "./client-init.questions.js";
12
12
  import { inspectClientProject } from "./client-project.inspector.js";
@@ -40,11 +40,11 @@ export async function runClientInit(command, io) {
40
40
  const them = plan.conflicts.length === 1 ? "it" : "them";
41
41
  const has = plan.conflicts.length === 1 ? "has" : "have";
42
42
  if (!io.interactive) {
43
- throw new ClientInitNotConfirmedError(`not initializing, and nothing was touched: ${plan.conflicts.join(", ")} already ${has} other content and nobody can be asked (stdin is not a terminal); run \`avclient init --yes\` to replace ${them}.`);
43
+ throw new AvClientInitNotConfirmedError(`not initializing, and nothing was touched: ${plan.conflicts.join(", ")} already ${has} other content and nobody can be asked (stdin is not a terminal); run \`avclient init --yes\` to replace ${them}.`);
44
44
  }
45
45
  io.stderr(`avclient: warning: ${plan.conflicts.join(", ")} already ${has} other content, and initializing will replace ${them}.\n`);
46
46
  if (!(await io.confirm("avclient: replace them and initialize? [y/N] "))) {
47
- throw new ClientInitNotConfirmedError(`cancelled, and nothing was touched; run again and answer yes to replace ${them}.`);
47
+ throw new AvClientInitNotConfirmedError(`cancelled, and nothing was touched; run again and answer yes to replace ${them}.`);
48
48
  }
49
49
  }
50
50
  const writes = plan.conflicts.length === 0 ? plan.keeping : plan.replacing;
@@ -80,7 +80,7 @@ export async function runClientInit(command, io) {
80
80
  onOutput: progress.output,
81
81
  });
82
82
  if (installed.code !== 0) {
83
- throw new ClientInitStepError(`\`${install}\` exited with code ${installed.code} after ${project.configFile ?? CLIENT_CONFIG_FILE} was written; fix what it reports and run \`${install}\` again:\n${installed.stderr.trimEnd()}`);
83
+ throw new AvClientInitStepError(`\`${install}\` exited with code ${installed.code} after ${project.configFile ?? CLIENT_CONFIG_FILE} was written; fix what it reports and run \`${install}\` again:\n${installed.stderr.trimEnd()}`);
84
84
  }
85
85
  });
86
86
  if (command.skipGenerate) {
@@ -92,8 +92,8 @@ export async function runClientInit(command, io) {
92
92
  return await runGenerate(io, command.yes, progress);
93
93
  }
94
94
  catch (error) {
95
- if (error instanceof ContractTransportError) {
96
- throw new ClientInitStepError(`the project is set up, but the server did not answer (${error.message.replace(/\.$/, "")}); once it is running, run \`avclient generate\`.`);
95
+ if (error instanceof AvContractTransportError) {
96
+ throw new AvClientInitStepError(`the project is set up, but the server did not answer (${error.message.replace(/\.$/, "")}); once it is running, run \`avclient generate\`.`);
97
97
  }
98
98
  throw error;
99
99
  }
@@ -8,12 +8,12 @@ export type ClientInitAnswers = {
8
8
  };
9
9
  type QuestionId = "entrypoint" | "envVar" | "generateAt" | "packageManager";
10
10
  /** An answer the question cannot take. A refusal: one sentence. */
11
- export declare class ClientInitAnswerError extends Error {
12
- readonly name = "ClientInitAnswerError";
11
+ export declare class AvClientInitAnswerError extends Error {
12
+ readonly name = "AvClientInitAnswerError";
13
13
  }
14
14
  /** Nobody can answer and some questions are unanswered. A refusal: one sentence. */
15
- export declare class ClientInitUnansweredError extends Error {
16
- readonly name = "ClientInitUnansweredError";
15
+ export declare class AvClientInitUnansweredError extends Error {
16
+ readonly name = "AvClientInitUnansweredError";
17
17
  }
18
18
  export type ClientInitQuestion = {
19
19
  readonly id: QuestionId;
@@ -21,7 +21,7 @@ export type ClientInitQuestion = {
21
21
  readonly placeholder: string;
22
22
  readonly label: string;
23
23
  readonly defaultFor: (detectedPackageManager: PackageManager) => string;
24
- /** @throws ClientInitAnswerError */
24
+ /** @throws AvClientInitAnswerError */
25
25
  readonly parse: (raw: string) => string;
26
26
  };
27
27
  export declare const DEFAULT_ENV_VAR = "AVENTARA_API_URL";