@aventara/client 0.1.0-pilot.4 → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +679 -0
- package/README.md +168 -132
- package/dist/cli/command.parser.d.ts +6 -4
- package/dist/cli/command.parser.js +22 -11
- package/dist/cli/generate.command.d.ts +6 -1
- package/dist/cli/generate.command.js +12 -1
- package/dist/cli/generation-failure.renderer.js +24 -24
- package/dist/cli/step.reporter.d.ts +31 -0
- package/dist/cli/step.reporter.js +87 -0
- package/dist/cli.js +2 -0
- package/dist/config/client-config.interface.d.ts +7 -7
- package/dist/config/config.loader.d.ts +4 -4
- package/dist/config/config.loader.js +7 -7
- package/dist/config/config.resolver.d.ts +7 -7
- package/dist/config/config.resolver.js +12 -12
- package/dist/config/env.cascade.d.ts +2 -2
- package/dist/config/env.cascade.js +4 -4
- package/dist/config/module-style.resolver.d.ts +1 -1
- package/dist/config/module-style.resolver.js +2 -2
- package/dist/config/tsconfig.locator.d.ts +1 -1
- package/dist/config/tsconfig.locator.js +2 -2
- package/dist/contract/contract.acceptance.d.ts +4 -4
- package/dist/contract/contract.acceptance.js +4 -4
- package/dist/contract/contract.fetcher.d.ts +3 -3
- package/dist/contract/contract.fetcher.js +6 -6
- package/dist/contract/contract.loader.d.ts +6 -6
- package/dist/contract/contract.loader.js +2 -2
- package/dist/emit/client-surface.emitter.d.ts +2 -2
- package/dist/emit/client-surface.emitter.js +77 -43
- package/dist/emit/client-tree.emitter.d.ts +3 -3
- package/dist/emit/client-tree.emitter.js +8 -5
- package/dist/emit/contract-carrier.emitter.d.ts +3 -3
- package/dist/emit/contract-carrier.emitter.js +1 -1
- package/dist/emit/derivation.emitter.js +1 -0
- package/dist/emit/descriptor.emitter.d.ts +2 -2
- package/dist/emit/descriptor.emitter.js +2 -2
- package/dist/emit/emitted-tree.interface.d.ts +1 -1
- package/dist/emit/enum.emitter.d.ts +2 -2
- package/dist/emit/field-check.emitter.d.ts +21 -0
- package/dist/emit/field-check.emitter.js +603 -0
- package/dist/emit/json-shape.emitter.d.ts +16 -0
- package/dist/emit/json-shape.emitter.js +88 -0
- package/dist/emit/name.deriver.d.ts +6 -6
- package/dist/emit/name.deriver.js +15 -15
- package/dist/emit/named-type.emitter.d.ts +2 -2
- package/dist/emit/runtime.emitter.d.ts +2 -2
- package/dist/emit/runtime.emitter.js +117 -112
- package/dist/emit/scalar.codec.d.ts +1 -1
- package/dist/emit/scalar.codec.js +78 -35
- package/dist/emit/transaction.emitter.js +131 -83
- package/dist/generate.d.ts +11 -3
- package/dist/generate.js +40 -26
- package/dist/index.d.ts +1 -1
- package/dist/init/client-init.errors.d.ts +4 -4
- package/dist/init/client-init.errors.js +4 -4
- package/dist/init/client-init.orchestrator.js +37 -22
- package/dist/init/client-init.questions.d.ts +5 -5
- package/dist/init/client-init.questions.js +10 -10
- package/dist/init/client-project.inspector.d.ts +2 -2
- package/dist/init/client-project.inspector.js +3 -3
- package/dist/init/command.runner.d.ts +2 -0
- package/dist/init/command.runner.js +4 -1
- package/dist/output/output.writer.d.ts +5 -5
- package/dist/output/output.writer.js +12 -12
- package/package.json +6 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BUILT_IN_SCALARS } from "@aventara/core";
|
|
1
|
+
import { BUILT_IN_SCALARS } from "@aventara/core/internal";
|
|
2
2
|
import { AvProtocol } from "@aventara/core/protocol";
|
|
3
3
|
import { moduleSpecifierWriter, } from "./module-style.interface.js";
|
|
4
4
|
export function wireGrammarLiteral(scalar) {
|
|
@@ -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
|
|
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 {
|
|
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
|
|
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:
|
|
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):
|
|
97
|
+
function decodeDecimal(wire: unknown): AvDecimal {
|
|
98
98
|
if (typeof wire === "string") {
|
|
99
99
|
try {
|
|
100
|
-
return new
|
|
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
|
|
199
|
-
return "
|
|
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";
|
|
@@ -288,7 +288,7 @@ function writeWireJson(value: unknown, path: Path, ancestors: Set<object>): stri
|
|
|
288
288
|
return text;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
function codecOf(scalar:
|
|
291
|
+
function codecOf(scalar: AvBuiltInScalar): ScalarCodec {
|
|
292
292
|
if (!Object.hasOwn(SCALAR_CODECS, scalar)) {
|
|
293
293
|
throw new TypeError('"' + String(scalar) + '" is not a built-in scalar of this protocol version.');
|
|
294
294
|
}
|
|
@@ -296,21 +296,21 @@ function codecOf(scalar: BuiltInScalar): ScalarCodec {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
/** The JSON wire value of a scalar's runtime value; null passes through. */
|
|
299
|
-
export function encodeScalar(scalar:
|
|
299
|
+
export function encodeScalar(scalar: AvBuiltInScalar, value: unknown): unknown {
|
|
300
300
|
const codec = codecOf(scalar);
|
|
301
301
|
return value === null ? null : codec.encode(value);
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
/** The runtime value of a scalar's JSON wire value; null passes through. */
|
|
305
|
-
export function decodeScalar(scalar:
|
|
305
|
+
export function decodeScalar(scalar: AvBuiltInScalar, wire: unknown): unknown {
|
|
306
306
|
const codec = codecOf(scalar);
|
|
307
307
|
return wire === null ? null : codec.decode(wire);
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
/**
|
|
311
311
|
* The JSON text of a request body whose scalars are already encoded. Every
|
|
312
|
-
* undefined property is stripped, at every depth
|
|
313
|
-
* omitted, never sent
|
|
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.
|
|
314
314
|
* Anything else that is not JSON-safe is refused rather than coerced: it means a
|
|
315
315
|
* value escaped its scalar codec.
|
|
316
316
|
*/
|
|
@@ -321,8 +321,8 @@ export function serializeWireBody(body: unknown): string {
|
|
|
321
321
|
/**
|
|
322
322
|
* An operation's arguments with every scalar in its wire form, read off each
|
|
323
323
|
* value's runtime class: a
|
|
324
|
-
* bigint, a Date,
|
|
325
|
-
* filter operand, a data value, a cursor
|
|
324
|
+
* bigint, a Date, an AvDecimal or a Uint8Array is encoded wherever it stands (a
|
|
325
|
+
* filter operand, a data value, a cursor), and plain objects and arrays are
|
|
326
326
|
* walked. Every other value is left for serializeWireBody, which refuses what is
|
|
327
327
|
* not JSON-safe. A json value cannot hold one of the four classes (the types
|
|
328
328
|
* exclude them), so nothing inside one is ever re-read as a scalar.
|
|
@@ -331,31 +331,74 @@ export function encodeArguments(value: unknown): unknown {
|
|
|
331
331
|
return encodeValue(value, [], new Set());
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
334
|
+
/**
|
|
335
|
+
* 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 such as an Immer draft after produce, a throwing getter or trap) is refused at its
|
|
338
|
+
* path with a TypeError before anything is sent, never with the engine's own error.
|
|
339
|
+
*/
|
|
340
|
+
export function readArgument<T>(path: Path, read: () => T): T {
|
|
341
|
+
try {
|
|
342
|
+
return read();
|
|
343
|
+
} catch {
|
|
344
|
+
throw new TypeError(
|
|
345
|
+
"An operation's arguments must be readable plain data, and the value at " + pointerOf(path) + " could not be read.",
|
|
346
|
+
);
|
|
337
347
|
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** How encodeValue treats an object: a scalar's runtime class, a container, or neither. */
|
|
351
|
+
function objectKind(value: object): "datetime" | "decimal" | "bytes" | "container" | "other" {
|
|
338
352
|
if (value instanceof Date) {
|
|
339
|
-
return
|
|
353
|
+
return "datetime";
|
|
340
354
|
}
|
|
341
|
-
if (value instanceof
|
|
342
|
-
return
|
|
355
|
+
if (value instanceof AvDecimal) {
|
|
356
|
+
return "decimal";
|
|
343
357
|
}
|
|
344
358
|
if (value instanceof Uint8Array) {
|
|
345
|
-
return
|
|
359
|
+
return "bytes";
|
|
346
360
|
}
|
|
347
|
-
|
|
361
|
+
return isJsonContainer(value) ? "container" : "other";
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function encodeValue(value: unknown, path: Path, ancestors: Set<object>): unknown {
|
|
365
|
+
if (typeof value === "bigint") {
|
|
366
|
+
return encodeScalar("bigint", value);
|
|
367
|
+
}
|
|
368
|
+
if (typeof value !== "object" || value === null) {
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
const kind = readArgument(path, () => objectKind(value));
|
|
372
|
+
if (kind === "other") {
|
|
348
373
|
return value;
|
|
349
374
|
}
|
|
375
|
+
if (kind !== "container") {
|
|
376
|
+
return encodeScalar(kind, value);
|
|
377
|
+
}
|
|
350
378
|
if (ancestors.has(value)) {
|
|
351
379
|
return refuseBody(path, "a cycle");
|
|
352
380
|
}
|
|
353
381
|
ancestors.add(value);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
382
|
+
let copy: unknown;
|
|
383
|
+
if (readArgument(path, () => Array.isArray(value))) {
|
|
384
|
+
const items = value as readonly unknown[];
|
|
385
|
+
const length = readArgument(path, () => items.length);
|
|
386
|
+
const list: unknown[] = [];
|
|
387
|
+
for (let index = 0; index < length; index += 1) {
|
|
388
|
+
const itemPath = [...path, index];
|
|
389
|
+
list.push(encodeValue(readArgument(itemPath, () => items[index]), itemPath, ancestors));
|
|
390
|
+
}
|
|
391
|
+
copy = list;
|
|
392
|
+
} else {
|
|
393
|
+
const record = value as Readonly<Record<string, unknown>>;
|
|
394
|
+
const members: [string, unknown][] = [];
|
|
395
|
+
for (const key of readArgument(path, () => Object.keys(record))) {
|
|
396
|
+
const memberPath = [...path, key];
|
|
397
|
+
members.push([key, encodeValue(readArgument(memberPath, () => record[key]), memberPath, ancestors)]);
|
|
398
|
+
}
|
|
399
|
+
// fromEntries defines each key, so "__proto__" stays an own key.
|
|
400
|
+
copy = Object.fromEntries(members);
|
|
401
|
+
}
|
|
359
402
|
ancestors.delete(value);
|
|
360
403
|
return copy;
|
|
361
404
|
}
|
|
@@ -370,13 +413,13 @@ export type DecodeTable = {
|
|
|
370
413
|
* table: a number (a count), null, and the data of a Resource the table
|
|
371
414
|
* does not name pass through; a list is a list of
|
|
372
415
|
* records, anything else is one record of \`resource\`. In a record, a field the
|
|
373
|
-
* table names is revived
|
|
416
|
+
* table names is revived (each item of a list field), a relation recurses with
|
|
374
417
|
* its target, and every other key passes through untouched: json is never
|
|
375
418
|
* revived. A to-many relation's value is a list, \`{ data, count }\` or
|
|
376
|
-
* \`{ count }
|
|
419
|
+
* \`{ count }\`, the three shapes the derivation gives it.
|
|
377
420
|
*
|
|
378
|
-
* @throws TypeError when a value is not the wire form its field's scalar takes
|
|
379
|
-
* which the transport answers as
|
|
421
|
+
* @throws TypeError when a value is not the wire form its field's scalar takes,
|
|
422
|
+
* which the transport answers as an AvTransportError.
|
|
380
423
|
*/
|
|
381
424
|
export function decodeResult(table: DecodeTable, resource: string, value: unknown): unknown {
|
|
382
425
|
// A Resource the table does not name has nothing to revive: its data passes.
|
|
@@ -434,9 +477,9 @@ export function emitScalarCodecModule(style) {
|
|
|
434
477
|
path: "runtime/codec.ts",
|
|
435
478
|
source: codecModuleHead(moduleSpecifierWriter(style)) +
|
|
436
479
|
"\n/** The built-in scalars of the protocol. */\n" +
|
|
437
|
-
`export type
|
|
480
|
+
`export type AvBuiltInScalar =\n${union};\n` +
|
|
438
481
|
CODEC_MODULE_BODY +
|
|
439
|
-
"\nconst SCALAR_CODECS: { readonly [S in
|
|
482
|
+
"\nconst SCALAR_CODECS: { readonly [S in AvBuiltInScalar]: ScalarCodec } = {\n" +
|
|
440
483
|
`${table}\n};\n`,
|
|
441
484
|
};
|
|
442
485
|
}
|
|
@@ -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
|
|
28
|
-
* lone surrogate, a non-finite number
|
|
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 {
|
|
@@ -209,25 +209,29 @@ function base64Url(bytes: Uint8Array): string {
|
|
|
209
209
|
}
|
|
210
210
|
`;
|
|
211
211
|
function transactionModuleSource(from) {
|
|
212
|
-
return `import { encodeArguments, serializeWireBody } from ${from("./codec")};
|
|
213
|
-
import { type
|
|
212
|
+
return `import { encodeArguments, readArgument, serializeWireBody } from ${from("./codec")};
|
|
213
|
+
import { type AvCause, type AvError, frameworkErrorOf } from ${from("./errors")};
|
|
214
214
|
import { fingerprintOf } from ${from("./fingerprint")};
|
|
215
|
-
import { type
|
|
215
|
+
import { type AvCallOptions, executePlan, 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
|
|
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
|
|
227
|
+
* never throws into app code, and running the handle refuses it instead.
|
|
228
|
+
*/
|
|
226
229
|
interface DeferredRecord {
|
|
227
230
|
readonly resource: string;
|
|
228
231
|
readonly family: string;
|
|
229
232
|
readonly variant: string;
|
|
230
233
|
readonly args: unknown;
|
|
234
|
+
readonly unreadable?: TypeError;
|
|
231
235
|
}
|
|
232
236
|
|
|
233
237
|
/** What a \`$ref\` placeholder stands for until the plan is assembled. */
|
|
@@ -236,8 +240,6 @@ interface Placeholder {
|
|
|
236
240
|
readonly field: string;
|
|
237
241
|
}
|
|
238
242
|
|
|
239
|
-
type MutableRecord = Record<string, unknown>;
|
|
240
|
-
|
|
241
243
|
/** One wire node of a transaction plan, its arguments in wire form. */
|
|
242
244
|
interface PlanNode {
|
|
243
245
|
readonly resource: string;
|
|
@@ -247,10 +249,11 @@ interface PlanNode {
|
|
|
247
249
|
readonly fingerprint: string | null;
|
|
248
250
|
}
|
|
249
251
|
|
|
250
|
-
/** One
|
|
251
|
-
interface
|
|
252
|
+
/** One placeholder an argument tree holds: where, the object itself, and its meaning. */
|
|
253
|
+
interface PlaceholderSite {
|
|
252
254
|
readonly path: readonly (string | number)[];
|
|
253
|
-
readonly
|
|
255
|
+
readonly object: object;
|
|
256
|
+
readonly placeholder: Placeholder;
|
|
254
257
|
}
|
|
255
258
|
|
|
256
259
|
const HANDLES = new WeakMap<object, DeferredRecord>();
|
|
@@ -264,45 +267,73 @@ function isPlainRecord(value: unknown): value is Readonly<Record<string, unknown
|
|
|
264
267
|
return prototype === Object.prototype || prototype === null;
|
|
265
268
|
}
|
|
266
269
|
|
|
267
|
-
|
|
268
|
-
function referenceKeys(family: string): readonly string[] {
|
|
269
|
-
return family === "create" || family === "update" ? ["data"] : family === "upsert" ? ["create", "update"] : [];
|
|
270
|
-
}
|
|
270
|
+
const KEEP: unique symbol = Symbol("keep");
|
|
271
271
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Rebuilds an argument tree: every plain record and array is copied, every other
|
|
274
|
+
* value kept, and \`leaf\` may replace any value first (a placeholder, by identity).
|
|
275
|
+
* A placeholder is bound wherever the arguments hold one; the server answers a
|
|
276
|
+
* position that takes no reference. A container met again on its own ancestry is
|
|
277
|
+
* kept as it is; the codec refuses it.
|
|
278
|
+
*/
|
|
279
|
+
function rebuild(
|
|
280
|
+
value: unknown,
|
|
281
|
+
path: readonly (string | number)[],
|
|
282
|
+
leaf: (value: unknown, path: readonly (string | number)[]) => unknown,
|
|
283
|
+
ancestors: Set<object> = new Set(),
|
|
284
|
+
): unknown {
|
|
285
|
+
const replaced = leaf(value, path);
|
|
286
|
+
if (replaced !== KEEP || typeof value !== "object" || value === null) {
|
|
287
|
+
return replaced === KEEP ? value : replaced;
|
|
288
|
+
}
|
|
289
|
+
const array = readArgument(path, () => Array.isArray(value));
|
|
290
|
+
if ((!array && !readArgument(path, () => isPlainRecord(value))) || ancestors.has(value)) {
|
|
291
|
+
return value;
|
|
275
292
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
293
|
+
ancestors.add(value);
|
|
294
|
+
let copy: unknown;
|
|
295
|
+
if (array) {
|
|
296
|
+
const items = value as readonly unknown[];
|
|
297
|
+
const length = readArgument(path, () => items.length);
|
|
298
|
+
const list: unknown[] = [];
|
|
299
|
+
for (let index = 0; index < length; index += 1) {
|
|
300
|
+
const itemPath = [...path, index];
|
|
301
|
+
list.push(rebuild(readArgument(itemPath, () => items[index]), itemPath, leaf, ancestors));
|
|
280
302
|
}
|
|
281
|
-
|
|
282
|
-
}
|
|
303
|
+
copy = list;
|
|
304
|
+
} else {
|
|
305
|
+
const record = value as Readonly<Record<string, unknown>>;
|
|
306
|
+
const members: [string, unknown][] = [];
|
|
307
|
+
for (const key of readArgument(path, () => Object.keys(record))) {
|
|
308
|
+
const memberPath = [...path, key];
|
|
309
|
+
members.push([key, rebuild(readArgument(memberPath, () => record[key]), memberPath, leaf, ancestors)]);
|
|
310
|
+
}
|
|
311
|
+
copy = Object.fromEntries(members);
|
|
312
|
+
}
|
|
313
|
+
ancestors.delete(value);
|
|
314
|
+
return copy;
|
|
283
315
|
}
|
|
284
316
|
|
|
285
|
-
/** \`args\` with
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const value = args[key];
|
|
293
|
-
if (Array.isArray(value)) {
|
|
294
|
-
result[key] = value.map((entry: unknown, index) => (isPlainRecord(entry) ? rebuild({ path: [key, index], record: entry }) : entry));
|
|
295
|
-
} else if (isPlainRecord(value)) {
|
|
296
|
-
result[key] = rebuild({ path: [key], record: value });
|
|
317
|
+
/** Every placeholder \`args\` holds, with its path inside the arguments. */
|
|
318
|
+
function placeholderSites(args: unknown): readonly PlaceholderSite[] {
|
|
319
|
+
const sites: PlaceholderSite[] = [];
|
|
320
|
+
rebuild(args, [], (value, path) => {
|
|
321
|
+
const placeholder = typeof value === "object" && value !== null ? PLACEHOLDERS.get(value) : undefined;
|
|
322
|
+
if (placeholder === undefined) {
|
|
323
|
+
return KEEP;
|
|
297
324
|
}
|
|
298
|
-
|
|
299
|
-
|
|
325
|
+
sites.push({ path, object: value as object, placeholder });
|
|
326
|
+
return value;
|
|
327
|
+
});
|
|
328
|
+
return sites;
|
|
300
329
|
}
|
|
301
330
|
|
|
302
331
|
/**
|
|
303
|
-
* Defers one operation into a handle: nothing is sent
|
|
304
|
-
*
|
|
305
|
-
* a reference
|
|
332
|
+
* Defers one operation into a handle: nothing is sent, and nothing is thrown.
|
|
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.
|
|
335
|
+
* Arguments that cannot be read are kept as their refusal, which running the
|
|
336
|
+
* handle rejects with.
|
|
306
337
|
*/
|
|
307
338
|
export function deferOperation(resource: string, family: string, variant: string, args: unknown): object {
|
|
308
339
|
const handle: object = Object.freeze({
|
|
@@ -312,37 +343,55 @@ export function deferOperation(resource: string, family: string, variant: string
|
|
|
312
343
|
return placeholder;
|
|
313
344
|
},
|
|
314
345
|
});
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
346
|
+
let record: DeferredRecord;
|
|
347
|
+
try {
|
|
348
|
+
record = {
|
|
349
|
+
resource,
|
|
350
|
+
family,
|
|
351
|
+
variant,
|
|
352
|
+
args: rebuild(args, [], (value) => (typeof value === "object" && value !== null && PLACEHOLDERS.has(value) ? value : KEEP)),
|
|
353
|
+
};
|
|
354
|
+
} catch (error) {
|
|
355
|
+
// Only readArgument throws here: an argument that could not be read.
|
|
356
|
+
record = { resource, family, variant, args: {}, unreadable: error as TypeError };
|
|
357
|
+
}
|
|
358
|
+
HANDLES.set(handle, record);
|
|
321
359
|
return handle;
|
|
322
360
|
}
|
|
323
361
|
|
|
324
362
|
/** What core answers in process for a plan entry it cannot run (A2004 / V1001). */
|
|
325
|
-
function entryRefusal(operation: number, message: string):
|
|
326
|
-
const cause:
|
|
363
|
+
function entryRefusal(operation: number, message: string): AvError {
|
|
364
|
+
const cause: AvCause = {
|
|
327
365
|
message: "Transaction plan failed framework validation.",
|
|
328
|
-
issues: [{ path: ["operation
|
|
366
|
+
issues: [{ path: ["operations", operation], code: "V1001", message }],
|
|
329
367
|
operation,
|
|
330
368
|
};
|
|
331
369
|
return frameworkErrorOf("A2004", cause);
|
|
332
370
|
}
|
|
333
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
|
+
|
|
334
380
|
/**
|
|
335
381
|
* Binds \`steps\` into the plan, sends it once, and resolves one decoded result per
|
|
336
382
|
* step. Refused before anything is sent, with what core answers in
|
|
337
383
|
* process: a list that is not one, an entry that is not a handle of this
|
|
338
|
-
* generated client, one handle twice (
|
|
339
|
-
* ["
|
|
340
|
-
* V1010 at its \`$ref\`)
|
|
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 /
|
|
386
|
+
* V1010 at its \`$ref\`); a handle whose arguments could not be read when it was
|
|
387
|
+
* built (the TypeError naming where); and, when \`check\` is given, a step whose
|
|
388
|
+
* values break a field check (AvValidationError A2004, as the server answers it).
|
|
341
389
|
*/
|
|
342
390
|
export async function runTransaction(
|
|
343
391
|
connection: TransportConnection,
|
|
344
392
|
steps: unknown,
|
|
345
|
-
options:
|
|
393
|
+
options: AvCallOptions = {},
|
|
394
|
+
check?: PlanCheck,
|
|
346
395
|
): Promise<unknown[]> {
|
|
347
396
|
if (!Array.isArray(steps)) {
|
|
348
397
|
throw frameworkErrorOf("A2004", {
|
|
@@ -364,29 +413,31 @@ export async function runTransaction(
|
|
|
364
413
|
records.push(record);
|
|
365
414
|
}
|
|
366
415
|
for (const [index, record] of records.entries()) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
416
|
+
if (record.unreadable !== undefined) {
|
|
417
|
+
throw record.unreadable;
|
|
418
|
+
}
|
|
419
|
+
for (const { path, placeholder } of placeholderSites(record.args)) {
|
|
420
|
+
if (!positions.has(placeholder.source)) {
|
|
421
|
+
throw frameworkErrorOf("A2007", {
|
|
422
|
+
message: "Operation arguments failed framework validation.",
|
|
423
|
+
issues: [
|
|
424
|
+
{
|
|
425
|
+
path: ["operations", index, "args", ...path, "$ref"],
|
|
426
|
+
code: "V1010",
|
|
427
|
+
message: "Transaction reference names an operation that is not in this transaction.",
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
operation: index,
|
|
431
|
+
});
|
|
383
432
|
}
|
|
384
433
|
}
|
|
385
434
|
}
|
|
386
435
|
|
|
436
|
+
check?.(records);
|
|
437
|
+
|
|
387
438
|
// Bound lazily: a node's references need their source's
|
|
388
439
|
// fingerprint, so each node is built once, on first need, wherever its source
|
|
389
|
-
// stands in the list
|
|
440
|
+
// stands in the list. The server, not this client, refuses a later one.
|
|
390
441
|
const nodes = new Map<number, PlanNode>();
|
|
391
442
|
const nodeAt = (index: number): PlanNode => {
|
|
392
443
|
const built = nodes.get(index);
|
|
@@ -394,17 +445,14 @@ export async function runTransaction(
|
|
|
394
445
|
return built;
|
|
395
446
|
}
|
|
396
447
|
const record = records[index] as DeferredRecord;
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
return copy;
|
|
407
|
-
});
|
|
448
|
+
const references = new Map<object, unknown>();
|
|
449
|
+
for (const { object, placeholder } of placeholderSites(record.args)) {
|
|
450
|
+
const operation = positions.get(placeholder.source) as number;
|
|
451
|
+
references.set(object, { $ref: { operation, fingerprint: nodeAt(operation).fingerprint, path: [placeholder.field] } });
|
|
452
|
+
}
|
|
453
|
+
const bound = rebuild(record.args, [], (value) =>
|
|
454
|
+
typeof value === "object" && value !== null && references.has(value) ? references.get(value) : KEEP,
|
|
455
|
+
);
|
|
408
456
|
const args: unknown = JSON.parse(serializeWireBody(encodeArguments(bound)));
|
|
409
457
|
const node: PlanNode = {
|
|
410
458
|
resource: record.resource,
|
package/dist/generate.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AbsolutePath } from "./config/client-config.interface.js";
|
|
2
2
|
import { type ProcessEnvInput } from "./config/env.cascade.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type ContractFetch } from "./contract/contract.fetcher.js";
|
|
4
4
|
import { type EmittedFilePath } from "./emit/emitted-tree.interface.js";
|
|
5
5
|
import type { OutputCheck, TypeScriptResolver } from "./output/output.validator.js";
|
|
6
6
|
export interface ClientGenerationInput {
|
|
@@ -17,7 +17,15 @@ export interface ClientGenerationInput {
|
|
|
17
17
|
readonly overrideForeign?: boolean;
|
|
18
18
|
readonly fetch?: ContractFetch;
|
|
19
19
|
readonly resolveTypeScript?: TypeScriptResolver;
|
|
20
|
+
/**
|
|
21
|
+
* Runs each of the run's four phases — the config, the contract, the
|
|
22
|
+
* emission, the write — under a title, so whoever owns the terminal can show
|
|
23
|
+
* them (`cli/step.reporter.ts`); each phase simply runs when omitted.
|
|
24
|
+
*/
|
|
25
|
+
readonly step?: GenerationStep;
|
|
20
26
|
}
|
|
27
|
+
/** Runs one phase of a generation under its title, resolving to what it returns. */
|
|
28
|
+
export type GenerationStep = <T>(title: string, body: () => Promise<T> | T) => Promise<T>;
|
|
21
29
|
export interface ClientGenerated {
|
|
22
30
|
readonly kind: "generated";
|
|
23
31
|
/** The shared directory the client was generated into. */
|
|
@@ -65,8 +73,8 @@ export type ClientGenerationOutcome = ClientGenerated | ClientUpToDate | Foreign
|
|
|
65
73
|
* generator did not produce stands in its way and `overrideForeign` is not set,
|
|
66
74
|
* says what it is and writes nothing.
|
|
67
75
|
*
|
|
68
|
-
* @throws a refusal (`
|
|
69
|
-
* `
|
|
76
|
+
* @throws a refusal (`AvEnvFileError`, `AvClientConfigError`, `AvContractTransportError`,
|
|
77
|
+
* `AvContractProtocolError`, `AvGeneratedNameError`, `AvOutputWriteError`) whose
|
|
70
78
|
* message is the whole diagnosis; the previous output is intact. Anything else
|
|
71
79
|
* is a defect.
|
|
72
80
|
*/
|