@aventara/client 0.1.0-pilot.4 → 1.0.0-rc.0
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 +222 -0
- package/README.md +21 -3
- package/dist/cli/command.parser.d.ts +3 -1
- package/dist/cli/command.parser.js +12 -1
- package/dist/cli/generate.command.d.ts +6 -1
- package/dist/cli/generate.command.js +12 -1
- 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/contract/contract.acceptance.d.ts +1 -1
- package/dist/contract/contract.acceptance.js +1 -1
- package/dist/emit/client-surface.emitter.js +17 -6
- package/dist/emit/contract-carrier.emitter.d.ts +1 -1
- package/dist/emit/contract-carrier.emitter.js +1 -1
- package/dist/emit/descriptor.emitter.d.ts +1 -1
- package/dist/emit/descriptor.emitter.js +1 -1
- 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 +1 -1
- package/dist/emit/name.deriver.js +1 -1
- package/dist/emit/runtime.emitter.d.ts +1 -1
- package/dist/emit/runtime.emitter.js +3 -3
- package/dist/emit/scalar.codec.js +56 -13
- package/dist/emit/transaction.emitter.js +109 -73
- package/dist/generate.d.ts +9 -1
- package/dist/generate.js +38 -24
- package/dist/init/client-init.orchestrator.js +31 -16
- package/dist/init/command.runner.d.ts +2 -0
- package/dist/init/command.runner.js +4 -1
- package/package.json +6 -5
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ClientContract } from "@aventara/core";
|
|
2
|
+
type Schema = Readonly<Record<string, unknown>>;
|
|
3
|
+
/**
|
|
4
|
+
* The TypeScript type a Json Shape schema describes, as source text.
|
|
5
|
+
*
|
|
6
|
+
* @throws Error on a schema outside the accepted forms — unreachable for a
|
|
7
|
+
* ClientContract that passed the structure check.
|
|
8
|
+
*/
|
|
9
|
+
export declare function jsonSchemaType(schema: Schema): string;
|
|
10
|
+
/**
|
|
11
|
+
* The type `client.ts` gives its derivation in place of the carrier when the
|
|
12
|
+
* contract shapes a Json field — the carrier with each shaped field's type — or
|
|
13
|
+
* `undefined` when it shapes none.
|
|
14
|
+
*/
|
|
15
|
+
export declare function shapedContractType(contract: ClientContract): string | undefined;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const SCALAR_TYPES = {
|
|
2
|
+
string: "string",
|
|
3
|
+
number: "number",
|
|
4
|
+
integer: "number",
|
|
5
|
+
boolean: "boolean",
|
|
6
|
+
null: "null",
|
|
7
|
+
};
|
|
8
|
+
const plain = (text) => ({ text, union: false });
|
|
9
|
+
function unionOf(members) {
|
|
10
|
+
return members.length === 1
|
|
11
|
+
? members[0]
|
|
12
|
+
: { text: members.map((member) => member.text).join(" | "), union: true };
|
|
13
|
+
}
|
|
14
|
+
function operand(type) {
|
|
15
|
+
return type.union ? `(${type.text})` : type.text;
|
|
16
|
+
}
|
|
17
|
+
function emitted(schema) {
|
|
18
|
+
if (Object.hasOwn(schema, "const")) {
|
|
19
|
+
return plain(JSON.stringify(schema.const));
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(schema.enum)) {
|
|
22
|
+
return unionOf(schema.enum.map((value) => plain(JSON.stringify(value))));
|
|
23
|
+
}
|
|
24
|
+
for (const keyword of ["anyOf", "oneOf"]) {
|
|
25
|
+
const options = schema[keyword];
|
|
26
|
+
if (Array.isArray(options)) {
|
|
27
|
+
return unionOf(options.map((option) => emitted(option)));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const type = schema.type;
|
|
31
|
+
if (typeof type === "string" && Object.hasOwn(SCALAR_TYPES, type)) {
|
|
32
|
+
return plain(SCALAR_TYPES[type]);
|
|
33
|
+
}
|
|
34
|
+
if (type === "array") {
|
|
35
|
+
if (Array.isArray(schema.prefixItems)) {
|
|
36
|
+
return plain(`[${schema.prefixItems.map((item) => emitted(item).text).join(", ")}]`);
|
|
37
|
+
}
|
|
38
|
+
return plain(`${operand(emitted(schema.items))}[]`);
|
|
39
|
+
}
|
|
40
|
+
if (type === "object") {
|
|
41
|
+
const properties = schema.properties;
|
|
42
|
+
if (properties === undefined) {
|
|
43
|
+
return plain(`{ [key: string]: ${emitted(schema.additionalProperties).text} }`);
|
|
44
|
+
}
|
|
45
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
46
|
+
const members = Object.keys(properties).map((key) => {
|
|
47
|
+
const member = emitted(properties[key]).text;
|
|
48
|
+
return required.has(key)
|
|
49
|
+
? `${JSON.stringify(key)}: ${member}`
|
|
50
|
+
: `${JSON.stringify(key)}?: ${member} | undefined`;
|
|
51
|
+
});
|
|
52
|
+
return plain(members.length === 0 ? "{}" : `{ ${members.join("; ")} }`);
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`a Json Shape schema outside the accepted forms reached the emitter: ${JSON.stringify(schema)}`);
|
|
55
|
+
}
|
|
56
|
+
export function jsonSchemaType(schema) {
|
|
57
|
+
return emitted(schema).text;
|
|
58
|
+
}
|
|
59
|
+
export function shapedContractType(contract) {
|
|
60
|
+
const order = (keys) => [...keys].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
61
|
+
const resources = order(Object.keys(contract.resources)).flatMap((resourceName) => {
|
|
62
|
+
const fields = contract.resources[resourceName]?.fields ?? {};
|
|
63
|
+
const shaped = order(Object.keys(fields)).flatMap((fieldName) => {
|
|
64
|
+
const field = fields[fieldName];
|
|
65
|
+
return field?.kind === "scalar" && field.json !== undefined
|
|
66
|
+
? [
|
|
67
|
+
`\t\t\t\treadonly ${JSON.stringify(fieldName)}: { readonly json: JsonFieldType<${jsonSchemaType(field.json)}> };\n`,
|
|
68
|
+
]
|
|
69
|
+
: [];
|
|
70
|
+
});
|
|
71
|
+
return shaped.length === 0
|
|
72
|
+
? []
|
|
73
|
+
: [
|
|
74
|
+
`\t\treadonly ${JSON.stringify(resourceName)}: {\n` +
|
|
75
|
+
"\t\t\treadonly fields: {\n" +
|
|
76
|
+
shaped.join("") +
|
|
77
|
+
"\t\t\t};\n" +
|
|
78
|
+
"\t\t};\n",
|
|
79
|
+
];
|
|
80
|
+
});
|
|
81
|
+
return resources.length === 0
|
|
82
|
+
? undefined
|
|
83
|
+
: "ClientContractShape & {\n" +
|
|
84
|
+
"\treadonly resources: {\n" +
|
|
85
|
+
resources.join("") +
|
|
86
|
+
"\t};\n" +
|
|
87
|
+
"}";
|
|
88
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ClientContract } from "@aventara/core";
|
|
2
2
|
import type { ClientEntrypoint } from "../config/client-config.interface.js";
|
|
3
3
|
import type { EmittedModule } from "./emitted-tree.interface.js";
|
|
4
4
|
import { type ClientModuleStyle } from "./module-style.interface.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OPERATION_CODES, VALIDATION_CODES
|
|
1
|
+
import { OPERATION_CODES, VALIDATION_CODES } from "@aventara/core/internal";
|
|
2
2
|
import { AvProtocol } from "@aventara/core/protocol";
|
|
3
3
|
import { entrypointHref } from "../config/config.resolver.js";
|
|
4
4
|
import { moduleSpecifierWriter, } from "./module-style.interface.js";
|
|
@@ -227,7 +227,7 @@ const IDENTITY_HEADERS = (() => {
|
|
|
227
227
|
})();
|
|
228
228
|
function transportModuleSource(from) {
|
|
229
229
|
return `import { CLIENT_CONTRACT_HASH, PROTOCOL_VERSION } from ${from("../metadata")};
|
|
230
|
-
import { decodeResult, encodeArguments, serializeWireBody } from ${from("./codec")};
|
|
230
|
+
import { decodeResult, encodeArguments, readArgument, serializeWireBody } from ${from("./codec")};
|
|
231
231
|
import { DECODE_TABLE } from ${from("./descriptor")};
|
|
232
232
|
import {
|
|
233
233
|
type Cause,
|
|
@@ -575,7 +575,7 @@ function issueOf(value: unknown): ValidationIssue | undefined {
|
|
|
575
575
|
|
|
576
576
|
/** The arguments, refused unless they are an object: the body is always one. */
|
|
577
577
|
function argumentObject(args: unknown): unknown {
|
|
578
|
-
if (!isRecord(args)) {
|
|
578
|
+
if (!readArgument([], () => isRecord(args))) {
|
|
579
579
|
throw new TypeError("An operation's arguments must be an object; send {} when there are none.");
|
|
580
580
|
}
|
|
581
581
|
return args;
|
|
@@ -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) {
|
|
@@ -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 (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
355
|
if (value instanceof Decimal) {
|
|
342
|
-
return
|
|
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
|
}
|
|
@@ -209,7 +209,7 @@ function base64Url(bytes: Uint8Array): string {
|
|
|
209
209
|
}
|
|
210
210
|
`;
|
|
211
211
|
function transactionModuleSource(from) {
|
|
212
|
-
return `import { encodeArguments, serializeWireBody } from ${from("./codec")};
|
|
212
|
+
return `import { encodeArguments, readArgument, serializeWireBody } from ${from("./codec")};
|
|
213
213
|
import { type Cause, type FrameworkError, frameworkErrorOf } from ${from("./errors")};
|
|
214
214
|
import { fingerprintOf } from ${from("./fingerprint")};
|
|
215
215
|
import { type CallOptions, executePlan, type TransportConnection } from ${from("./transport")};
|
|
@@ -222,12 +222,16 @@ import { type CallOptions, executePlan, type TransportConnection } from ${from("
|
|
|
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;
|
|
275
288
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
289
|
+
const array = readArgument(path, () => Array.isArray(value));
|
|
290
|
+
if ((!array && !readArgument(path, () => isPlainRecord(value))) || ancestors.has(value)) {
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
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 — and a handle can reference only handles built before it.
|
|
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,12 +343,19 @@ 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
|
|
|
@@ -325,7 +363,7 @@ export function deferOperation(resource: string, family: string, variant: string
|
|
|
325
363
|
function entryRefusal(operation: number, message: string): FrameworkError {
|
|
326
364
|
const cause: Cause = {
|
|
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);
|
|
@@ -336,8 +374,9 @@ function entryRefusal(operation: number, message: string): FrameworkError {
|
|
|
336
374
|
* step. Refused before anything is sent, with what core answers in
|
|
337
375
|
* process: a list that is not one, an entry that is not a handle of this
|
|
338
376
|
* generated client, one handle twice (ValidationError A2004 / V1001 at
|
|
339
|
-
* ["
|
|
340
|
-
* V1010 at its \`$ref\`)
|
|
377
|
+
* ["operations", i]: the entry itself); a reference to a handle not in the list (ValidationError A2007 /
|
|
378
|
+
* V1010 at its \`$ref\`); a handle whose arguments could not be read when it was
|
|
379
|
+
* built (the TypeError naming where).
|
|
341
380
|
*/
|
|
342
381
|
export async function runTransaction(
|
|
343
382
|
connection: TransportConnection,
|
|
@@ -364,22 +403,22 @@ export async function runTransaction(
|
|
|
364
403
|
records.push(record);
|
|
365
404
|
}
|
|
366
405
|
for (const [index, record] of records.entries()) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
406
|
+
if (record.unreadable !== undefined) {
|
|
407
|
+
throw record.unreadable;
|
|
408
|
+
}
|
|
409
|
+
for (const { path, placeholder } of placeholderSites(record.args)) {
|
|
410
|
+
if (!positions.has(placeholder.source)) {
|
|
411
|
+
throw frameworkErrorOf("A2007", {
|
|
412
|
+
message: "Operation arguments failed framework validation.",
|
|
413
|
+
issues: [
|
|
414
|
+
{
|
|
415
|
+
path: ["operations", index, "args", ...path, "$ref"],
|
|
416
|
+
code: "V1010",
|
|
417
|
+
message: "Transaction reference names an operation that is not in this transaction.",
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
operation: index,
|
|
421
|
+
});
|
|
383
422
|
}
|
|
384
423
|
}
|
|
385
424
|
}
|
|
@@ -394,17 +433,14 @@ export async function runTransaction(
|
|
|
394
433
|
return built;
|
|
395
434
|
}
|
|
396
435
|
const record = records[index] as DeferredRecord;
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
return copy;
|
|
407
|
-
});
|
|
436
|
+
const references = new Map<object, unknown>();
|
|
437
|
+
for (const { object, placeholder } of placeholderSites(record.args)) {
|
|
438
|
+
const operation = positions.get(placeholder.source) as number;
|
|
439
|
+
references.set(object, { $ref: { operation, fingerprint: nodeAt(operation).fingerprint, path: [placeholder.field] } });
|
|
440
|
+
}
|
|
441
|
+
const bound = rebuild(record.args, [], (value) =>
|
|
442
|
+
typeof value === "object" && value !== null && references.has(value) ? references.get(value) : KEEP,
|
|
443
|
+
);
|
|
408
444
|
const args: unknown = JSON.parse(serializeWireBody(encodeArguments(bound)));
|
|
409
445
|
const node: PlanNode = {
|
|
410
446
|
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. */
|
package/dist/generate.js
CHANGED
|
@@ -4,11 +4,13 @@ import { loadClientConfigFile } from "./config/config.loader.js";
|
|
|
4
4
|
import { resolveClientConfig } from "./config/config.resolver.js";
|
|
5
5
|
import { resolveEnvCascade, } from "./config/env.cascade.js";
|
|
6
6
|
import { resolveModuleStyle } from "./config/module-style.resolver.js";
|
|
7
|
+
import { contractUrlOf, displayUrl, } from "./contract/contract.fetcher.js";
|
|
7
8
|
import { loadClientContractSince } from "./contract/contract.loader.js";
|
|
8
9
|
import { emitClientTree } from "./emit/client-tree.emitter.js";
|
|
9
10
|
import { parseContractCarrier } from "./emit/contract-carrier.emitter.js";
|
|
10
11
|
import { CLIENT_ENTRY_FILE, CONTRACT_CARRIER_MODULE, GENERATED_DIRECTORY, } from "./emit/emitted-tree.interface.js";
|
|
11
12
|
import { findForeignOutputContent, OutputWriteError, ownedOutputMatches, precedeDefect, writeClientOutput, } from "./output/output.writer.js";
|
|
13
|
+
const runPhase = async (_title, body) => await body();
|
|
12
14
|
async function storedContract(generateAt) {
|
|
13
15
|
try {
|
|
14
16
|
if ((await findForeignOutputContent(generateAt)).length > 0) {
|
|
@@ -21,32 +23,42 @@ async function storedContract(generateAt) {
|
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
export async function generateClient(input) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
const step = input.step ?? runPhase;
|
|
27
|
+
const { config, style } = await step("Loading the client config", async () => {
|
|
28
|
+
const cascade = resolveEnvCascade({
|
|
29
|
+
directory: input.directory,
|
|
30
|
+
processEnv: input.processEnv,
|
|
31
|
+
});
|
|
32
|
+
const loaded = await loadClientConfigFile(input.directory);
|
|
33
|
+
const config = resolveClientConfig({
|
|
34
|
+
config: loaded.config,
|
|
35
|
+
cascade,
|
|
36
|
+
configDirectory: path.dirname(loaded.file),
|
|
37
|
+
});
|
|
38
|
+
const style = resolveModuleStyle({
|
|
39
|
+
generateAt: config.generateAt,
|
|
40
|
+
entryFile: path.join(config.generateAt, CLIENT_ENTRY_FILE),
|
|
41
|
+
...(config.tsconfigFile === undefined
|
|
42
|
+
? {}
|
|
43
|
+
: { tsconfigFile: config.tsconfigFile }),
|
|
44
|
+
});
|
|
45
|
+
return { config, style };
|
|
40
46
|
});
|
|
41
|
-
const { contract, notModified } = await loadClientContractSince(input.fetch === undefined
|
|
47
|
+
const { contract, notModified } = await step(`Fetching the ClientContract (${displayUrl(contractUrlOf(config.entrypoint))})`, async () => loadClientContractSince(input.fetch === undefined
|
|
42
48
|
? { entrypoint: config.entrypoint }
|
|
43
|
-
: { entrypoint: config.entrypoint, fetch: input.fetch }, await storedContract(config.generateAt));
|
|
44
|
-
const emission =
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
: { entrypoint: config.entrypoint, fetch: input.fetch }, await storedContract(config.generateAt)));
|
|
50
|
+
const { emission, upToDate } = await step("Emitting the client", async () => {
|
|
51
|
+
const emission = emitClientTree(contract, config.entrypoint, {
|
|
52
|
+
importFileExtension: style.importFileExtension,
|
|
53
|
+
moduleFormat: style.moduleFormat,
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
emission,
|
|
57
|
+
upToDate: notModified &&
|
|
58
|
+
(await ownedOutputMatches(config.generateAt, emission.tree)),
|
|
59
|
+
};
|
|
47
60
|
});
|
|
48
|
-
if (
|
|
49
|
-
(await ownedOutputMatches(config.generateAt, emission.tree))) {
|
|
61
|
+
if (upToDate) {
|
|
50
62
|
return {
|
|
51
63
|
kind: "up-to-date",
|
|
52
64
|
generateAt: config.generateAt,
|
|
@@ -54,7 +66,7 @@ export async function generateClient(input) {
|
|
|
54
66
|
warnings: emission.warnings,
|
|
55
67
|
};
|
|
56
68
|
}
|
|
57
|
-
const
|
|
69
|
+
const writeInto = async (overrideForeign) => {
|
|
58
70
|
const written = await writeClientOutput(input.resolveTypeScript === undefined
|
|
59
71
|
? { emission, generateAt: config.generateAt, overrideForeign }
|
|
60
72
|
: {
|
|
@@ -72,6 +84,8 @@ export async function generateClient(input) {
|
|
|
72
84
|
warnings: written.warnings,
|
|
73
85
|
};
|
|
74
86
|
};
|
|
87
|
+
const into = path.relative(input.directory, config.generateAt) || ".";
|
|
88
|
+
const write = (overrideForeign) => step(`Checking and writing the client into ${into}`, () => writeInto(overrideForeign));
|
|
75
89
|
const foreign = await findForeignOutputContent(config.generateAt).catch((error) => {
|
|
76
90
|
throw error instanceof OutputWriteError
|
|
77
91
|
? error.precededBy(emission.warnings)
|