@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.
- package/CHANGELOG.md +651 -0
- package/README.md +195 -141
- package/dist/cli/command.parser.d.ts +7 -6
- package/dist/cli/command.parser.js +13 -13
- package/dist/cli/generation-failure.renderer.d.ts +2 -1
- package/dist/cli/generation-failure.renderer.js +24 -24
- package/dist/cli/generation-success.renderer.d.ts +2 -1
- package/dist/cli/step.reporter.d.ts +2 -1
- package/dist/cli/terminal.prompter.d.ts +2 -1
- package/dist/config/client-config.interface.d.ts +24 -7
- package/dist/config/config.loader.d.ts +12 -7
- package/dist/config/config.loader.js +26 -8
- package/dist/config/config.resolver.d.ts +16 -9
- package/dist/config/config.resolver.js +40 -12
- package/dist/config/env.cascade.d.ts +4 -8
- package/dist/config/env.cascade.js +7 -7
- package/dist/config/module-style.resolver.d.ts +4 -7
- package/dist/config/module-style.resolver.js +4 -4
- package/dist/config/tsconfig.locator.d.ts +3 -2
- package/dist/config/tsconfig.locator.js +16 -4
- package/dist/contract/contract.acceptance.d.ts +4 -5
- package/dist/contract/contract.acceptance.js +5 -5
- package/dist/contract/contract.fetcher.d.ts +5 -4
- package/dist/contract/contract.fetcher.js +6 -6
- package/dist/contract/contract.loader.d.ts +9 -8
- package/dist/contract/contract.loader.js +2 -2
- package/dist/emit/access.emitter.d.ts +12 -0
- package/dist/emit/access.emitter.js +263 -0
- package/dist/emit/banner.emitter.d.ts +6 -1
- package/dist/emit/banner.emitter.js +12 -8
- package/dist/emit/client-surface.emitter.d.ts +2 -2
- package/dist/emit/client-surface.emitter.js +136 -41
- package/dist/emit/client-tree.emitter.d.ts +11 -4
- package/dist/emit/client-tree.emitter.js +51 -17
- package/dist/emit/contract-carrier.emitter.d.ts +3 -3
- package/dist/emit/derivation.emitter.d.ts +3 -3
- package/dist/emit/derivation.emitter.js +7 -3
- package/dist/emit/descriptor.emitter.d.ts +2 -2
- package/dist/emit/descriptor.emitter.js +1 -1
- package/dist/emit/emitted-tree.interface.d.ts +1 -1
- package/dist/emit/enum.emitter.d.ts +2 -2
- package/dist/emit/field-check.emitter.d.ts +22 -0
- package/dist/emit/field-check.emitter.js +592 -0
- package/dist/emit/json-shape.emitter.d.ts +2 -2
- package/dist/emit/module-specifier.scanner.d.ts +2 -1
- package/dist/emit/name.deriver.d.ts +8 -9
- package/dist/emit/name.deriver.js +18 -15
- package/dist/emit/named-type.emitter.d.ts +2 -2
- package/dist/emit/named-type.emitter.js +12 -2
- package/dist/emit/runtime.emitter.d.ts +6 -5
- package/dist/emit/runtime.emitter.js +215 -116
- package/dist/emit/scalar.codec.d.ts +1 -1
- package/dist/emit/scalar.codec.js +246 -29
- package/dist/emit/transaction.emitter.js +30 -14
- package/dist/generate.d.ts +5 -4
- package/dist/generate.js +3 -3
- package/dist/hydration/hydration-tag.format.d.ts +11 -0
- package/dist/hydration/hydration-tag.format.js +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/init/client-init.errors.d.ts +4 -4
- package/dist/init/client-init.errors.js +4 -4
- package/dist/init/client-init.orchestrator.js +7 -7
- package/dist/init/client-init.planner.d.ts +3 -2
- package/dist/init/client-init.questions.d.ts +5 -16
- package/dist/init/client-init.questions.js +12 -12
- package/dist/init/client-project.inspector.d.ts +2 -2
- package/dist/init/client-project.inspector.js +3 -3
- package/dist/internal.d.ts +25 -0
- package/dist/internal.js +11 -0
- package/dist/output/output.validator.d.ts +3 -4
- package/dist/output/output.validator.js +2 -2
- package/dist/output/output.writer.d.ts +8 -7
- package/dist/output/output.writer.js +12 -12
- package/package.json +9 -5
|
@@ -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";
|
|
@@ -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
|
-
|
|
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)
|
|
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
|
|
275
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
313
|
-
* omitted, never sent
|
|
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,
|
|
325
|
-
* filter operand, a data value, a cursor
|
|
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
|
|
337
|
-
* Proxy
|
|
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
|
|
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
|
|
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 }
|
|
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
|
|
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
|
|
697
|
+
`export type AvBuiltInScalar =\n${union};\n` +
|
|
481
698
|
CODEC_MODULE_BODY +
|
|
482
|
-
"\nconst SCALAR_CODECS: { readonly [S in
|
|
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
|
|
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 {
|
|
@@ -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
|
|
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, 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
|
|
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
|
|
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
|
|
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):
|
|
364
|
-
const 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 (
|
|
377
|
-
* ["operations", i]: the entry itself); a reference to a handle not in the list (
|
|
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:
|
|
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
|
|
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);
|
package/dist/generate.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type ProcessEnvInput } from "./config/env.cascade.js";
|
|
|
3
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
|
+
interface ClientGenerationInput {
|
|
7
7
|
/** The project directory: where `framework.client.ts` and the `.env` files are. */
|
|
8
8
|
readonly directory: string;
|
|
9
9
|
/** A snapshot of the process environment. Never mutated. */
|
|
@@ -25,7 +25,7 @@ export interface ClientGenerationInput {
|
|
|
25
25
|
readonly step?: GenerationStep;
|
|
26
26
|
}
|
|
27
27
|
/** Runs one phase of a generation under its title, resolving to what it returns. */
|
|
28
|
-
|
|
28
|
+
type GenerationStep = <T>(title: string, body: () => Promise<T> | T) => Promise<T>;
|
|
29
29
|
export interface ClientGenerated {
|
|
30
30
|
readonly kind: "generated";
|
|
31
31
|
/** The shared directory the client was generated into. */
|
|
@@ -73,9 +73,10 @@ 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 (`
|
|
77
|
-
* `
|
|
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
|
*/
|
|
81
81
|
export declare function generateClient(input: ClientGenerationInput): Promise<ClientGenerationOutcome>;
|
|
82
|
+
export {};
|
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 {
|
|
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 {
|
|
@@ -51,7 +51,7 @@ export async function generateClient(input) {
|
|
|
51
51
|
const emission = emitClientTree(contract, config.entrypoint, {
|
|
52
52
|
importFileExtension: style.importFileExtension,
|
|
53
53
|
moduleFormat: style.moduleFormat,
|
|
54
|
-
});
|
|
54
|
+
}, undefined, config.accessOrigins);
|
|
55
55
|
return {
|
|
56
56
|
emission,
|
|
57
57
|
upToDate: notModified &&
|
|
@@ -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
|
|
90
|
+
throw error instanceof AvOutputWriteError
|
|
91
91
|
? error.precededBy(emission.warnings)
|
|
92
92
|
: precedeDefect(error, emission.warnings);
|
|
93
93
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The tagged form a dehydrated value takes: `{ [tagKey]: <tag>, [valueKey]: <wire form> }`. */
|
|
2
|
+
export declare const HYDRATION_FORMAT: {
|
|
3
|
+
/** The key a tag is under. */
|
|
4
|
+
readonly tagKey: "$av";
|
|
5
|
+
/** The key the tagged value's wire form is under. */
|
|
6
|
+
readonly valueKey: "v";
|
|
7
|
+
/** The tag of a plain object that holds `tagKey` as data. */
|
|
8
|
+
readonly objectTag: "object";
|
|
9
|
+
/** The scalars a value is tagged as: those whose runtime values JSON does not carry. */
|
|
10
|
+
readonly scalarTags: readonly ["bigint", "bytes", "datetime", "decimal"];
|
|
11
|
+
};
|
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 {
|
|
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
|
|
4
|
-
readonly name = "
|
|
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
|
|
8
|
-
readonly name = "
|
|
7
|
+
export declare class AvClientInitStepError extends Error {
|
|
8
|
+
readonly name = "AvClientInitStepError";
|
|
9
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export class
|
|
2
|
-
name = "
|
|
1
|
+
export class AvClientInitNotConfirmedError extends Error {
|
|
2
|
+
name = "AvClientInitNotConfirmedError";
|
|
3
3
|
}
|
|
4
|
-
export class
|
|
5
|
-
name = "
|
|
4
|
+
export class AvClientInitStepError extends Error {
|
|
5
|
+
name = "AvClientInitStepError";
|
|
6
6
|
}
|