@distilled.cloud/core 1.0.0-rc.2 → 1.0.0-rc.4
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/lib/category.d.ts +4 -4
- package/lib/category.js +4 -4
- package/lib/codegen/cli.d.ts +48 -0
- package/lib/codegen/cli.d.ts.map +1 -1
- package/lib/codegen/cli.js +50 -10
- package/lib/codegen/cli.js.map +1 -1
- package/lib/codegen/emit.d.ts +2 -2
- package/lib/codegen/emit.js +4 -4
- package/lib/codegen/emit.js.map +1 -1
- package/lib/codegen/generator.d.ts.map +1 -1
- package/lib/codegen/generator.js +80 -14
- package/lib/codegen/generator.js.map +1 -1
- package/lib/codegen/graphql.d.ts +201 -0
- package/lib/codegen/graphql.d.ts.map +1 -0
- package/lib/codegen/graphql.js +789 -0
- package/lib/codegen/graphql.js.map +1 -0
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +17 -13
- package/lib/errors.js.map +1 -1
- package/lib/pagination.d.ts +43 -3
- package/lib/pagination.d.ts.map +1 -1
- package/lib/pagination.js +84 -5
- package/lib/pagination.js.map +1 -1
- package/lib/protocol-http.d.ts.map +1 -1
- package/lib/protocol-http.js +12 -3
- package/lib/protocol-http.js.map +1 -1
- package/lib/schema.js +1 -1
- package/lib/schema.js.map +1 -1
- package/package.json +4 -4
- package/src/category.ts +4 -4
- package/src/codegen/cli.ts +110 -16
- package/src/codegen/emit.ts +4 -4
- package/src/codegen/generator.ts +83 -16
- package/src/codegen/graphql.ts +1195 -0
- package/src/errors.ts +20 -25
- package/src/pagination.ts +125 -7
- package/src/protocol-http.ts +11 -3
- package/src/schema.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@distilled.cloud/core",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/alchemy-run/distilled",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"build": "tsc -b"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@effect/platform-bun": ">=4.0.0-beta.
|
|
34
|
+
"@effect/platform-bun": ">=4.0.0-beta.104 || >=4.0.0",
|
|
35
35
|
"@types/bun": "latest",
|
|
36
36
|
"@types/node": "latest",
|
|
37
|
-
"effect": ">=4.0.0-beta.
|
|
37
|
+
"effect": ">=4.0.0-beta.104 || >=4.0.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"effect": ">=4.0.0-beta.
|
|
40
|
+
"effect": ">=4.0.0-beta.104 || >=4.0.0"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/category.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* ```ts
|
|
10
10
|
* import * as Category from "@distilled.cloud/core/category";
|
|
11
11
|
*
|
|
12
|
-
* export class Unauthorized extends Schema.
|
|
12
|
+
* export class Unauthorized extends Schema.TaggedError<Unauthorized>()(
|
|
13
13
|
* "Unauthorized",
|
|
14
14
|
* { message: Schema.String },
|
|
15
15
|
* ).pipe(Category.withAuthError) {}
|
|
@@ -92,7 +92,7 @@ export interface RetryableInfo {
|
|
|
92
92
|
*
|
|
93
93
|
* @example
|
|
94
94
|
* ```ts
|
|
95
|
-
* export class MyError extends Schema.
|
|
95
|
+
* export class MyError extends Schema.TaggedError<MyError>()(
|
|
96
96
|
* "MyError",
|
|
97
97
|
* { message: Schema.String },
|
|
98
98
|
* ).pipe(Category.withCategory(Category.AuthError)) {}
|
|
@@ -123,13 +123,13 @@ export const withCategory =
|
|
|
123
123
|
* @example
|
|
124
124
|
* ```ts
|
|
125
125
|
* // Standard retryable error
|
|
126
|
-
* export class TransientError extends Schema.
|
|
126
|
+
* export class TransientError extends Schema.TaggedError<TransientError>()(
|
|
127
127
|
* "TransientError",
|
|
128
128
|
* { message: Schema.String },
|
|
129
129
|
* ).pipe(Category.withRetryable()) {}
|
|
130
130
|
*
|
|
131
131
|
* // Throttling error (uses longer backoff)
|
|
132
|
-
* export class RateLimitError extends Schema.
|
|
132
|
+
* export class RateLimitError extends Schema.TaggedError<RateLimitError>()(
|
|
133
133
|
* "RateLimitError",
|
|
134
134
|
* { message: Schema.String },
|
|
135
135
|
* ).pipe(Category.withRetryable({ throttling: true })) {}
|
package/src/codegen/cli.ts
CHANGED
|
@@ -37,6 +37,52 @@ export interface GeneratorCliOptions {
|
|
|
37
37
|
readonly outDir?: string;
|
|
38
38
|
/** Model files to skip (e.g. a shared protocols model). */
|
|
39
39
|
readonly excludeModel?: (file: string) => boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Where the models are, when they aren't a flat directory of `<resource>.json`.
|
|
42
|
+
* AWS vendors Amazon's own repo, whose models sit at
|
|
43
|
+
* `models/<service>/service/<version>/<service>-<version>.json`.
|
|
44
|
+
*
|
|
45
|
+
* Returns the model files to compile; manual specs are merged in after,
|
|
46
|
+
* as usual.
|
|
47
|
+
*/
|
|
48
|
+
readonly discoverModels?: (ctx: {
|
|
49
|
+
readonly smithyDir: string;
|
|
50
|
+
}) => Effect.Effect<
|
|
51
|
+
ReadonlyArray<{ readonly file: string; readonly dir: string }>,
|
|
52
|
+
never,
|
|
53
|
+
FileSystem.FileSystem | Path.Path
|
|
54
|
+
>;
|
|
55
|
+
/**
|
|
56
|
+
* Side effects to run before generating — e.g. AWS copies `partitions.json`
|
|
57
|
+
* out of the smithy submodule, runtime data its endpoint resolver reads.
|
|
58
|
+
*/
|
|
59
|
+
readonly prepare?: (ctx: {
|
|
60
|
+
readonly root: string;
|
|
61
|
+
readonly outDir: string;
|
|
62
|
+
}) => Effect.Effect<void, never, FileSystem.FileSystem | Path.Path>;
|
|
63
|
+
/**
|
|
64
|
+
* The output module's name, when it isn't the model's filename. AWS names
|
|
65
|
+
* modules after the service's `aws.api#service` sdkId (`amazon-s3.json` →
|
|
66
|
+
* `s3.ts`), so the public surface reads `AWS.S3.getObject`.
|
|
67
|
+
*/
|
|
68
|
+
readonly resourceName?: (ctx: {
|
|
69
|
+
readonly model: any;
|
|
70
|
+
readonly file: string;
|
|
71
|
+
}) => string;
|
|
72
|
+
/** Barrel export name for a resource. Default: the resource name itself. */
|
|
73
|
+
readonly barrelExportName?: (resource: string) => string;
|
|
74
|
+
/**
|
|
75
|
+
* The closing pass over the output directory. Defaults to formatting;
|
|
76
|
+
* AWS lint-fixes first (see `codegen/format.ts`).
|
|
77
|
+
*/
|
|
78
|
+
readonly finalize?: (dir: string) => Effect.Effect<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Keep generating after a model fails, instead of stopping at the first.
|
|
81
|
+
* AWS compiles 430 services in one run, and one broken model shouldn't
|
|
82
|
+
* hide the state of the other 429. The run still FAILS at the end — the
|
|
83
|
+
* failures are reported together rather than swallowed.
|
|
84
|
+
*/
|
|
85
|
+
readonly continueOnModelError?: boolean;
|
|
40
86
|
/** Directory of hand-authored models merged after the generated ones. */
|
|
41
87
|
readonly manualSpecsDir?: string;
|
|
42
88
|
/** RFC-6902 patch chain root. Default `patches`; `false` disables. */
|
|
@@ -82,14 +128,19 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
82
128
|
yield* Console.log(` Smithy: ${smithyDir}`);
|
|
83
129
|
yield* Console.log(` Output: ${outDir}`);
|
|
84
130
|
|
|
131
|
+
yield* options.prepare?.({ root, outDir }) ?? Effect.void;
|
|
132
|
+
|
|
85
133
|
// Generated models plus optional manual-specs (hand-authored models
|
|
86
134
|
// for APIs the provider's spec source doesn't cover). A manual model
|
|
87
135
|
// must not shadow a generated one.
|
|
88
|
-
const generated =
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
136
|
+
const generated = options.discoverModels
|
|
137
|
+
? yield* options.discoverModels({ smithyDir })
|
|
138
|
+
: (yield* fs.readDirectory(smithyDir))
|
|
139
|
+
.filter(
|
|
140
|
+
(f) =>
|
|
141
|
+
f.endsWith(".json") && !(options.excludeModel?.(f) ?? false),
|
|
142
|
+
)
|
|
143
|
+
.map((f) => ({ file: f, dir: smithyDir }));
|
|
93
144
|
const manualDir = options.manualSpecsDir
|
|
94
145
|
? path.resolve(root, options.manualSpecsDir)
|
|
95
146
|
: undefined;
|
|
@@ -115,6 +166,7 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
115
166
|
yield* fs.makeDirectory(outDir, { recursive: true });
|
|
116
167
|
|
|
117
168
|
const written: string[] = [];
|
|
169
|
+
const failedModels: string[] = [];
|
|
118
170
|
let totalOps = 0;
|
|
119
171
|
let totalPatches = 0;
|
|
120
172
|
let staleOps = 0;
|
|
@@ -140,12 +192,15 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
140
192
|
}
|
|
141
193
|
|
|
142
194
|
for (const { file, dir } of entries) {
|
|
143
|
-
const resource = file.replace(/\.json$/, "");
|
|
144
|
-
if (config.resource && resource !== config.resource) continue;
|
|
145
|
-
|
|
146
195
|
const model = JSON.parse(
|
|
147
196
|
yield* fs.readFileString(path.join(dir, file)),
|
|
148
197
|
);
|
|
198
|
+
// The module's name — the model's filename unless the provider
|
|
199
|
+
// derives it from the model itself (AWS: the service's sdkId).
|
|
200
|
+
const resource =
|
|
201
|
+
options.resourceName?.({ model, file }) ??
|
|
202
|
+
file.replace(/\.json$/, "");
|
|
203
|
+
if (config.resource && resource !== config.resource) continue;
|
|
149
204
|
|
|
150
205
|
// Apply the RFC-6902 patch chain before generating. Hand-written
|
|
151
206
|
// *.manual.json patches apply after the generated ones — they
|
|
@@ -189,10 +244,22 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
189
244
|
if (note) yield* Console.log(` ${note}`);
|
|
190
245
|
}
|
|
191
246
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
247
|
+
// `generateService` and the provider spec are synchronous and
|
|
248
|
+
// throw; with continueOnModelError the throw is recorded and the
|
|
249
|
+
// run moves on, so one broken model doesn't hide the state of
|
|
250
|
+
// every model after it. The run still fails at the end.
|
|
251
|
+
let generated;
|
|
252
|
+
try {
|
|
253
|
+
generated = generateService(model, options.spec(model));
|
|
254
|
+
} catch (e) {
|
|
255
|
+
if (!options.continueOnModelError) throw e;
|
|
256
|
+
failedModels.push(
|
|
257
|
+
`${resource}: ${e instanceof Error ? e.message : String(e)}`,
|
|
258
|
+
);
|
|
259
|
+
yield* Console.error(`❌ ${resource}`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const { code, operations } = generated;
|
|
196
263
|
if (operations === 0) continue;
|
|
197
264
|
|
|
198
265
|
yield* fs.writeFileString(path.join(outDir, `${resource}.ts`), code);
|
|
@@ -224,11 +291,22 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
224
291
|
// resource without removing anything.
|
|
225
292
|
const barrelPath = path.join(outDir, "index.ts");
|
|
226
293
|
const filtered = config.resource !== "";
|
|
227
|
-
|
|
294
|
+
// Sorted, not in generation order: a provider that discovers models
|
|
295
|
+
// in some other order (AWS walks Amazon's directory tree) would
|
|
296
|
+
// otherwise reshuffle the barrel on every run.
|
|
297
|
+
let resources = [...written].sort((a, b) =>
|
|
298
|
+
`${a}.json`.localeCompare(`${b}.json`),
|
|
299
|
+
);
|
|
228
300
|
if (filtered && (yield* fs.exists(barrelPath))) {
|
|
301
|
+
// Recover the RESOURCE from each export line's path, not its name:
|
|
302
|
+
// the two differ when barrelExportName renames (AWS exports
|
|
303
|
+
// `S3` from `./s3.ts`).
|
|
229
304
|
const existing = (yield* fs.readFileString(barrelPath))
|
|
230
305
|
.split("\n")
|
|
231
|
-
.map(
|
|
306
|
+
.map(
|
|
307
|
+
(line) =>
|
|
308
|
+
/^export \* as \S+ from "\.\/(.+)\.ts";/.exec(line)?.[1],
|
|
309
|
+
)
|
|
232
310
|
.filter((name): name is string => name !== undefined);
|
|
233
311
|
// Ordered exactly as a full run orders it: by MODEL FILENAME, so
|
|
234
312
|
// the `.json` takes part in the collation (`ai_gateway.json` sorts
|
|
@@ -243,11 +321,14 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
243
321
|
barrelPath,
|
|
244
322
|
barrel(
|
|
245
323
|
`// AUTO-GENERATED by scripts/generate.ts. Do not edit.\n`,
|
|
246
|
-
resources.map((r) => ({
|
|
324
|
+
resources.map((r) => ({
|
|
325
|
+
name: options.barrelExportName?.(r) ?? r,
|
|
326
|
+
path: `./${r}.ts`,
|
|
327
|
+
})),
|
|
247
328
|
),
|
|
248
329
|
);
|
|
249
330
|
|
|
250
|
-
yield* formatGenerated(outDir);
|
|
331
|
+
yield* (options.finalize ?? formatGenerated)(outDir);
|
|
251
332
|
|
|
252
333
|
yield* Console.log(
|
|
253
334
|
`\n✅ Generated ${totalOps} operations across ${written.length} resource modules` +
|
|
@@ -256,6 +337,19 @@ export const runGeneratorCli = (options: GeneratorCliOptions): void => {
|
|
|
256
337
|
: "."),
|
|
257
338
|
);
|
|
258
339
|
yield* Console.log(` ${path.join(outDir, "index.ts")}`);
|
|
340
|
+
|
|
341
|
+
// Models that failed under continueOnModelError. Reported together,
|
|
342
|
+
// at the end, and the run FAILS — a generate that couldn't produce
|
|
343
|
+
// a module must not exit 0 with the failure buried in the log.
|
|
344
|
+
if (failedModels.length) {
|
|
345
|
+
yield* Console.error(
|
|
346
|
+
`\n❌ ${failedModels.length} model(s) failed to generate:`,
|
|
347
|
+
);
|
|
348
|
+
for (const f of failedModels) yield* Console.error(` ${f}`);
|
|
349
|
+
return yield* Effect.die(
|
|
350
|
+
new Error(`${failedModels.length} model(s) failed to generate`),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
259
353
|
}),
|
|
260
354
|
).pipe(Command.withDescription(options.description));
|
|
261
355
|
|
package/src/codegen/emit.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* .annotate({ identifier: "X" })
|
|
10
10
|
* as any as S.Schema<X>; // no inference needed
|
|
11
11
|
*
|
|
12
|
-
* plus closed-alias string-union enums, `S.
|
|
12
|
+
* plus closed-alias string-union enums, `S.TaggedError` error classes, and
|
|
13
13
|
* `API.make(() => ({ … }))` operation consts. The helpers here own those
|
|
14
14
|
* shared skeletons; providers own the content strings (member pipes, trait
|
|
15
15
|
* calls, config fields). Emitted output is normalized by oxfmt afterwards,
|
|
@@ -120,7 +120,7 @@ export interface ErrorClassOptions {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
* `export class X extends /*@__PURE__*/ S.
|
|
123
|
+
* `export class X extends /*@__PURE__*/ S.TaggedError<X>()("X", { … }) {}`
|
|
124
124
|
*
|
|
125
125
|
* The PURE markers are what make an unused error class droppable. A class
|
|
126
126
|
* whose heritage clause is an unannotated call can never be tree-shaken —
|
|
@@ -130,12 +130,12 @@ export interface ErrorClassOptions {
|
|
|
130
130
|
*
|
|
131
131
|
* `wrap` needs its own marker as well as the inner one: a pure call's
|
|
132
132
|
* ARGUMENTS are still evaluated, so annotating only
|
|
133
|
-
* `T.applyErrorMatchers(S.
|
|
133
|
+
* `T.applyErrorMatchers(S.TaggedError…(…), […])` leaves the inner call
|
|
134
134
|
* holding the class alive. Verified against esbuild in both directions.
|
|
135
135
|
*/
|
|
136
136
|
export const errorClass = (o: ErrorClassOptions): string => {
|
|
137
137
|
const annotations = o.annotations ? `,\n${o.annotations}` : "";
|
|
138
|
-
const cls = `${PURE}S.
|
|
138
|
+
const cls = `${PURE}S.TaggedError<${o.name}>()(${q(o.tag ?? o.name)}, {\n${o.fields.join("\n")}\n}${annotations})${o.pipes ?? ""}`;
|
|
139
139
|
const body = o.wrap ? `${PURE}${o.wrap(cls)}` : cls;
|
|
140
140
|
return `export class ${o.name} extends ${body} {}\n`;
|
|
141
141
|
};
|
package/src/codegen/generator.ts
CHANGED
|
@@ -487,8 +487,27 @@ export const generateService = (
|
|
|
487
487
|
const order = topoOrder(shapes, reachable, shapeDeps);
|
|
488
488
|
const indexOf = orderIndex(order);
|
|
489
489
|
|
|
490
|
-
|
|
491
|
-
|
|
490
|
+
/**
|
|
491
|
+
* Shape id → the id whose emitted body it shares.
|
|
492
|
+
*
|
|
493
|
+
* Structural aliasing (see the struct emission below) has to cascade: a
|
|
494
|
+
* parent only matches another parent if their MEMBERS render identically,
|
|
495
|
+
* and a member renders as its target's name. So every reference resolves
|
|
496
|
+
* through this map first — once `…ItemGroupRuleGroup` is aliased onto a
|
|
497
|
+
* canonical shape, the parents referencing it start rendering the same
|
|
498
|
+
* text and collapse in turn.
|
|
499
|
+
*
|
|
500
|
+
* Filled in topological order, so a canonical target is always a shape
|
|
501
|
+
* that was already emitted — a back-reference, never a forward one.
|
|
502
|
+
*/
|
|
503
|
+
const canonicalId = new Map<string, string>();
|
|
504
|
+
const canon = (target: string): string => canonicalId.get(target) ?? target;
|
|
505
|
+
|
|
506
|
+
const rawRef = makeSchemaRef(prelude, indexOf);
|
|
507
|
+
const rawTsRef = makeTsRef(tsPrelude);
|
|
508
|
+
const ref = (target: string, selfIdx: number) =>
|
|
509
|
+
rawRef(canon(target), selfIdx);
|
|
510
|
+
const tsRef = (target: string) => rawTsRef(canon(target));
|
|
492
511
|
|
|
493
512
|
// Direction classification for enum openness. Enum ALIASES are emitted
|
|
494
513
|
// CLOSED (exhaustively matchable on reads); request-reachable shapes
|
|
@@ -529,7 +548,8 @@ export const generateService = (
|
|
|
529
548
|
* pure response/error shapes stay the plain closed alias, and so do
|
|
530
549
|
* union-arm discriminant literals.
|
|
531
550
|
*/
|
|
532
|
-
const tsRefAt = (
|
|
551
|
+
const tsRefAt = (rawTarget: string, ownerId: string): string => {
|
|
552
|
+
const target = canon(rawTarget);
|
|
533
553
|
const base = tsRef(target);
|
|
534
554
|
if (!requestReachable.has(ownerId)) return base;
|
|
535
555
|
if (discriminantEnums.has(target)) return base;
|
|
@@ -731,6 +751,13 @@ export const generateService = (
|
|
|
731
751
|
|
|
732
752
|
// 4. Error classes from the operations' errors lists.
|
|
733
753
|
const out: string[] = [];
|
|
754
|
+
// Emitted struct body → the name that owns it, for structural aliasing.
|
|
755
|
+
// Keyed on the emitted TEXT, so two shapes collapse only when what they
|
|
756
|
+
// would emit is byte-identical — including member names, targets, docs and
|
|
757
|
+
// pipes. References are already resolved to names at this point, so a
|
|
758
|
+
// difference anywhere in the tree shows up as different text.
|
|
759
|
+
const structBodies = new Map<string, { id: string; name: string }>();
|
|
760
|
+
let aliased = 0;
|
|
734
761
|
// Set when any error carries CATEGORY_TRAIT, so the header only imports
|
|
735
762
|
// the category module when something actually uses it.
|
|
736
763
|
let usesCategories = false;
|
|
@@ -866,7 +893,6 @@ export const generateService = (
|
|
|
866
893
|
fields.push(...inject.interfaceLines);
|
|
867
894
|
members.push(inject.structLine);
|
|
868
895
|
}
|
|
869
|
-
out.push(interfaceDecl(name, fields));
|
|
870
896
|
const struct = members.length
|
|
871
897
|
? `S.Struct({\n${members.join("\n")}\n})`
|
|
872
898
|
: `S.Struct({})`;
|
|
@@ -884,15 +910,44 @@ export const generateService = (
|
|
|
884
910
|
: []),
|
|
885
911
|
];
|
|
886
912
|
const tail = pipes.map((p) => `.pipe(${p})`).join("");
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
913
|
+
|
|
914
|
+
// Shapes whose emitted body is byte-identical are one shape wearing
|
|
915
|
+
// many names. The docs pipelines generate a fresh copy of every nested
|
|
916
|
+
// shape per operation, so cloudflare's zero_trust carries 374 copies of
|
|
917
|
+
// `{ group?: unknown }` — one per operation x application type x
|
|
918
|
+
// include/exclude/require — and 72% of its 16,800 structures are
|
|
919
|
+
// redundant that way.
|
|
920
|
+
//
|
|
921
|
+
// Emit the body once and alias the rest to it. The public name is kept,
|
|
922
|
+
// so nothing about the surface changes; only the duplicate bodies go.
|
|
923
|
+
// Operation I/O is excluded: those carry the operation's Http trait and
|
|
924
|
+
// must never be merged onto one another.
|
|
925
|
+
const bodyKey = `${JSON.stringify(fields)}|${struct}${tail}`;
|
|
926
|
+
const canonical = structCtx.isOpIo
|
|
927
|
+
? undefined
|
|
928
|
+
: structBodies.get(bodyKey);
|
|
929
|
+
if (canonical !== undefined) {
|
|
930
|
+
out.push(`export type ${name} = ${canonical.name};`);
|
|
931
|
+
out.push(`export const ${name} = ${canonical.name};\n`);
|
|
932
|
+
// Later shapes referencing this one now render the canonical name,
|
|
933
|
+
// which is what lets their bodies collapse too.
|
|
934
|
+
canonicalId.set(id, canonical.id);
|
|
935
|
+
aliased++;
|
|
936
|
+
} else {
|
|
937
|
+
if (!structCtx.isOpIo) {
|
|
938
|
+
structBodies.set(bodyKey, { id, name });
|
|
939
|
+
}
|
|
940
|
+
out.push(interfaceDecl(name, fields));
|
|
941
|
+
out.push(
|
|
942
|
+
suspendConst({
|
|
943
|
+
name,
|
|
944
|
+
pure,
|
|
945
|
+
multiline: true,
|
|
946
|
+
annotateIdentifier: true,
|
|
947
|
+
expr: `${struct}${tail}`,
|
|
948
|
+
}),
|
|
949
|
+
);
|
|
950
|
+
}
|
|
896
951
|
} else if (d.type === "list") {
|
|
897
952
|
out.push(`export type ${name} = Array<${tsRefAt(d.member.target, id)}>;`);
|
|
898
953
|
out.push(
|
|
@@ -975,14 +1030,26 @@ export const generateService = (
|
|
|
975
1030
|
// No items path: `.items()` is a page passthrough at runtime, so an
|
|
976
1031
|
// item IS a whole response.
|
|
977
1032
|
if (!itemsPath) return tsRef(outputId);
|
|
978
|
-
let
|
|
1033
|
+
let id: string = outputId;
|
|
1034
|
+
// Whether the path crossed a list on the way down (`"edges.node"` on a
|
|
1035
|
+
// Relay connection). When it did, the path itself is what fans out, so
|
|
1036
|
+
// the value it lands on IS one item — it needn't be a list again.
|
|
1037
|
+
let fannedOut = false;
|
|
979
1038
|
for (const segment of itemsPath.split(".")) {
|
|
1039
|
+
let def: any = shapes[id];
|
|
1040
|
+
if (def?.type === "list") {
|
|
1041
|
+
id = def.member.target as string;
|
|
1042
|
+
def = shapes[id];
|
|
1043
|
+
fannedOut = true;
|
|
1044
|
+
}
|
|
980
1045
|
if (def?.type !== "structure") return undefined;
|
|
981
1046
|
const info = memberInfos(def).find((m) => m.tsName === segment);
|
|
982
1047
|
if (!info) return undefined;
|
|
983
|
-
|
|
1048
|
+
id = info.target;
|
|
984
1049
|
}
|
|
985
|
-
|
|
1050
|
+
const last: any = shapes[id];
|
|
1051
|
+
if (last?.type === "list") return tsRef(last.member.target);
|
|
1052
|
+
return fannedOut ? tsRef(id) : undefined;
|
|
986
1053
|
};
|
|
987
1054
|
|
|
988
1055
|
const emitOperation =
|