@distilled.cloud/core 1.0.0-rc.12 → 1.0.0-rc.13
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/api.d.ts +4 -4
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +1 -1
- package/lib/api.js.map +1 -1
- package/lib/category.js +1 -1
- package/lib/category.js.map +1 -1
- package/lib/codegen/boolean-string-enums.d.ts +8 -8
- package/lib/codegen/boolean-string-enums.d.ts.map +1 -1
- package/lib/codegen/boolean-string-enums.js +84 -27
- package/lib/codegen/boolean-string-enums.js.map +1 -1
- package/lib/codegen/boolean-string-enums.test.js +130 -0
- package/lib/codegen/boolean-string-enums.test.js.map +1 -1
- package/lib/codegen/cli.js +2 -2
- package/lib/codegen/cli.js.map +1 -1
- package/lib/codegen/generator.d.ts +20 -3
- package/lib/codegen/generator.d.ts.map +1 -1
- package/lib/codegen/generator.js +36 -9
- package/lib/codegen/generator.js.map +1 -1
- package/lib/codegen/generator.test.d.ts +2 -0
- package/lib/codegen/generator.test.d.ts.map +1 -0
- package/lib/codegen/generator.test.js +126 -0
- package/lib/codegen/generator.test.js.map +1 -0
- package/lib/codegen/graphql-client.d.ts +7 -1
- package/lib/codegen/graphql-client.d.ts.map +1 -1
- package/lib/codegen/graphql-client.js +262 -125
- package/lib/codegen/graphql-client.js.map +1 -1
- package/lib/codegen/graphql-client.test.js +104 -236
- package/lib/codegen/graphql-client.test.js.map +1 -1
- package/lib/codegen/openapi-binary.test.d.ts +2 -0
- package/lib/codegen/openapi-binary.test.d.ts.map +1 -0
- package/lib/codegen/openapi-binary.test.js +67 -0
- package/lib/codegen/openapi-binary.test.js.map +1 -0
- package/lib/codegen/openapi.d.ts +7 -1
- package/lib/codegen/openapi.d.ts.map +1 -1
- package/lib/codegen/openapi.js +15 -4
- package/lib/codegen/openapi.js.map +1 -1
- package/lib/codegen/patches.d.ts +16 -1
- package/lib/codegen/patches.d.ts.map +1 -1
- package/lib/codegen/patches.js +70 -4
- package/lib/codegen/patches.js.map +1 -1
- package/lib/codegen/patches.test.js +48 -2
- package/lib/codegen/patches.test.js.map +1 -1
- package/lib/codegen/untagged-unions.test.d.ts +2 -0
- package/lib/codegen/untagged-unions.test.d.ts.map +1 -0
- package/lib/codegen/untagged-unions.test.js +80 -0
- package/lib/codegen/untagged-unions.test.js.map +1 -0
- package/lib/errors.d.ts +2 -1
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +5 -2
- package/lib/errors.js.map +1 -1
- package/lib/fixtures/xml/upstream-validator.json +397 -0
- package/lib/graphql.d.ts +296 -205
- package/lib/graphql.d.ts.map +1 -1
- package/lib/graphql.js +968 -631
- package/lib/graphql.js.map +1 -1
- package/lib/graphql.test.js +134 -763
- package/lib/graphql.test.js.map +1 -1
- package/lib/graphql.types.d.ts +10 -0
- package/lib/graphql.types.d.ts.map +1 -1
- package/lib/graphql.types.js +4 -44
- package/lib/graphql.types.js.map +1 -1
- package/lib/index.d.ts +27 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +27 -0
- package/lib/index.js.map +1 -0
- package/lib/pagination.d.ts +2 -1
- package/lib/pagination.d.ts.map +1 -1
- package/lib/pagination.js +24 -16
- package/lib/pagination.js.map +1 -1
- package/lib/pagination.test.d.ts +2 -0
- package/lib/pagination.test.d.ts.map +1 -0
- package/lib/pagination.test.js +134 -0
- package/lib/pagination.test.js.map +1 -0
- package/lib/protocol-http.d.ts +12 -17
- package/lib/protocol-http.d.ts.map +1 -1
- package/lib/protocol-http.js +100 -45
- package/lib/protocol-http.js.map +1 -1
- package/lib/protocol-http.test.js +130 -0
- package/lib/protocol-http.test.js.map +1 -1
- package/lib/protocol-rest.d.ts +18 -1
- package/lib/protocol-rest.d.ts.map +1 -1
- package/lib/protocol-rest.js +24 -4
- package/lib/protocol-rest.js.map +1 -1
- package/lib/query.d.ts +29 -0
- package/lib/query.d.ts.map +1 -0
- package/lib/query.js +27 -0
- package/lib/query.js.map +1 -0
- package/lib/response-validation.d.ts +67 -0
- package/lib/response-validation.d.ts.map +1 -0
- package/lib/response-validation.js +76 -0
- package/lib/response-validation.js.map +1 -0
- package/lib/response-validation.test.d.ts +2 -0
- package/lib/response-validation.test.d.ts.map +1 -0
- package/lib/response-validation.test.js +67 -0
- package/lib/response-validation.test.js.map +1 -0
- package/lib/testing.d.ts +39 -0
- package/lib/testing.d.ts.map +1 -0
- package/lib/testing.js +41 -0
- package/lib/testing.js.map +1 -0
- package/lib/trait.d.ts +29 -9
- package/lib/trait.d.ts.map +1 -1
- package/lib/trait.js +8 -0
- package/lib/trait.js.map +1 -1
- package/lib/xml.d.ts +44 -0
- package/lib/xml.d.ts.map +1 -0
- package/lib/xml.js +315 -0
- package/lib/xml.js.map +1 -0
- package/lib/xml.test.d.ts +2 -0
- package/lib/xml.test.d.ts.map +1 -0
- package/lib/xml.test.js +211 -0
- package/lib/xml.test.js.map +1 -0
- package/package.json +4 -4
- package/src/api.ts +4 -4
- package/src/category.ts +1 -1
- package/src/codegen/boolean-string-enums.test.ts +147 -0
- package/src/codegen/boolean-string-enums.ts +84 -27
- package/src/codegen/cli.ts +2 -2
- package/src/codegen/generator.test.ts +151 -0
- package/src/codegen/generator.ts +70 -12
- package/src/codegen/graphql-client.test.ts +116 -283
- package/src/codegen/graphql-client.ts +326 -169
- package/src/codegen/openapi-binary.test.ts +98 -0
- package/src/codegen/openapi.ts +26 -4
- package/src/codegen/patches.test.ts +66 -1
- package/src/codegen/patches.ts +84 -4
- package/src/codegen/untagged-unions.test.ts +89 -0
- package/src/errors.ts +5 -2
- package/src/fixtures/xml/upstream-validator.json +397 -0
- package/src/graphql.test.ts +174 -948
- package/src/graphql.ts +1592 -1167
- package/src/graphql.types.ts +92 -163
- package/src/index.ts +26 -0
- package/src/pagination.test.ts +191 -0
- package/src/pagination.ts +35 -25
- package/src/protocol-http.test.ts +163 -0
- package/src/protocol-http.ts +122 -46
- package/src/protocol-rest.ts +62 -13
- package/src/query.ts +39 -0
- package/src/response-validation.test.ts +95 -0
- package/src/response-validation.ts +116 -0
- package/src/testing.ts +78 -0
- package/src/trait.ts +39 -8
- package/src/xml.test.ts +269 -0
- package/src/xml.ts +339 -0
- package/lib/graphql.fixture.d.ts +0 -249
- package/lib/graphql.fixture.d.ts.map +0 -1
- package/lib/graphql.fixture.js +0 -240
- package/lib/graphql.fixture.js.map +0 -1
- package/src/graphql.fixture.ts +0 -371
|
@@ -206,114 +206,236 @@ export interface GenerateGraphQLClientOptions {
|
|
|
206
206
|
readonly requirementsImport: string;
|
|
207
207
|
/** Optional legacy-name aliases, e.g. createProject -> projectCreate. */
|
|
208
208
|
readonly operationAliases?: Readonly<Record<string, string>>;
|
|
209
|
+
/** Export name for the root object (`Railway`, `Eas`, …). Default `"Sdk"`. */
|
|
210
|
+
readonly sdkName?: string;
|
|
209
211
|
}
|
|
210
212
|
|
|
211
|
-
|
|
213
|
+
const peelRef = (
|
|
214
|
+
ref: string,
|
|
215
|
+
): { name: string; list: boolean; nonNull: boolean } => {
|
|
216
|
+
let text = ref;
|
|
217
|
+
let nonNull = false;
|
|
218
|
+
let list = false;
|
|
219
|
+
if (text.endsWith("!")) {
|
|
220
|
+
nonNull = true;
|
|
221
|
+
text = text.slice(0, -1);
|
|
222
|
+
}
|
|
223
|
+
if (text.startsWith("[") && text.endsWith("]")) {
|
|
224
|
+
list = true;
|
|
225
|
+
text = text.slice(1, -1);
|
|
226
|
+
if (text.endsWith("!")) text = text.slice(0, -1);
|
|
227
|
+
}
|
|
228
|
+
return { name: text, list, nonNull };
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Emit a Query-algebra SDK: TypeMeta + TS types + named roots
|
|
233
|
+
* (`me`, `project`, …). Combinators stay in `@distilled.cloud/core/query`;
|
|
234
|
+
* this file only describes the schema as lazy lenses.
|
|
235
|
+
*/
|
|
212
236
|
export const generateGraphQLClient = (
|
|
213
237
|
model: GraphQLModel,
|
|
214
238
|
options: GenerateGraphQLClientOptions,
|
|
215
239
|
): string => {
|
|
216
240
|
validateGraphQLModel(model);
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
241
|
+
const sdkName = options.sdkName ?? "Sdk";
|
|
242
|
+
const skipRoots = new Set(
|
|
243
|
+
[model.queryType, model.mutationType, model.subscriptionType].filter(
|
|
244
|
+
(name): name is string => typeof name === "string",
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
const tsNamed = (name: string): string => {
|
|
248
|
+
const type = model.types[name];
|
|
249
|
+
if (!type) return "unknown";
|
|
250
|
+
if (type.kind === "SCALAR") return type.scalar ?? "unknown";
|
|
251
|
+
if (type.kind === "ENUM")
|
|
252
|
+
return (
|
|
253
|
+
(type.enumValues ?? [])
|
|
254
|
+
.map((value) => JSON.stringify(value))
|
|
255
|
+
.join(" | ") || "string"
|
|
256
|
+
);
|
|
257
|
+
if (type.kind === "UNION")
|
|
258
|
+
return (
|
|
259
|
+
(type.possibleTypes ?? []).filter(Boolean).join(" | ") || "unknown"
|
|
260
|
+
);
|
|
261
|
+
return name;
|
|
220
262
|
};
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
263
|
+
/** Relay `edges { node }` → node type name, else undefined. */
|
|
264
|
+
const connectionNode = (typeName: string): string | undefined => {
|
|
265
|
+
const type = model.types[typeName];
|
|
266
|
+
const edges = type?.fields?.edges;
|
|
267
|
+
if (!edges) return undefined;
|
|
268
|
+
const edgeRef = peelRef(edges.type);
|
|
269
|
+
if (!edgeRef.list) return undefined;
|
|
270
|
+
const node = model.types[edgeRef.name]?.fields?.node;
|
|
271
|
+
if (!node) return undefined;
|
|
272
|
+
const nodeName = peelRef(node.type).name;
|
|
273
|
+
const kind = model.types[nodeName]?.kind;
|
|
274
|
+
if (kind !== "OBJECT" && kind !== "INTERFACE" && kind !== "UNION") {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
return nodeName;
|
|
228
278
|
};
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
"
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
"
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
279
|
+
/** Input type: nullable GraphQL inputs (no `!`) accept `null`. */
|
|
280
|
+
const tsArg = (ref: string): string => {
|
|
281
|
+
const nonNull = ref.endsWith("!");
|
|
282
|
+
const inner = nonNull ? ref.slice(0, -1) : ref;
|
|
283
|
+
const ts =
|
|
284
|
+
inner.startsWith("[") && inner.endsWith("]")
|
|
285
|
+
? `ReadonlyArray<${tsArg(inner.slice(1, -1))}>`
|
|
286
|
+
: tsNamed(inner);
|
|
287
|
+
return nonNull ? ts : `${ts} | null`;
|
|
288
|
+
};
|
|
289
|
+
/** Response type: nullable GraphQL types (no `!`) include `null`. */
|
|
290
|
+
const tsOutput = (ref: string): string => {
|
|
291
|
+
const nonNull = ref.endsWith("!");
|
|
292
|
+
const inner = nonNull ? ref.slice(0, -1) : ref;
|
|
293
|
+
let ts: string;
|
|
294
|
+
if (inner.startsWith("[") && inner.endsWith("]")) {
|
|
295
|
+
ts = `ReadonlyArray<${tsOutput(inner.slice(1, -1))}>`;
|
|
296
|
+
} else {
|
|
297
|
+
const node = connectionNode(inner);
|
|
298
|
+
ts = node ? `ReadonlyArray<${tsNamed(node)}>` : tsNamed(inner);
|
|
299
|
+
}
|
|
300
|
+
return nonNull ? ts : `${ts} | null`;
|
|
301
|
+
};
|
|
302
|
+
const isObjectLike = (name: string): boolean => {
|
|
303
|
+
const kind = model.types[name]?.kind;
|
|
304
|
+
return kind === "OBJECT" || kind === "INTERFACE";
|
|
305
|
+
};
|
|
306
|
+
const fieldMeta = (fieldName: string, field: GraphQLField): string => {
|
|
307
|
+
const { name, list } = peelRef(field.type);
|
|
308
|
+
const argTypes = Object.keys(field.args).length
|
|
309
|
+
? `{ ${Object.entries(field.args)
|
|
310
|
+
.map(([argName, arg]) => `${argName}: ${JSON.stringify(arg.type)}`)
|
|
311
|
+
.join(", ")} }`
|
|
312
|
+
: undefined;
|
|
313
|
+
const node = !list ? connectionNode(name) : undefined;
|
|
314
|
+
if (node) {
|
|
315
|
+
return argTypes
|
|
316
|
+
? `connectionField(${JSON.stringify(fieldName)}, ${node}, ${argTypes})`
|
|
317
|
+
: `connectionField(${JSON.stringify(fieldName)}, ${node})`;
|
|
318
|
+
}
|
|
319
|
+
if (list && isObjectLike(name)) {
|
|
320
|
+
return argTypes
|
|
321
|
+
? `listField(${JSON.stringify(fieldName)}, ${name}, ${argTypes})`
|
|
322
|
+
: `listField(${JSON.stringify(fieldName)}, ${name})`;
|
|
323
|
+
}
|
|
324
|
+
if (isObjectLike(name) || model.types[name]?.kind === "UNION") {
|
|
325
|
+
return `objectField(${JSON.stringify(fieldName)}, ${name})`;
|
|
326
|
+
}
|
|
327
|
+
return `scalarField(${JSON.stringify(fieldName)})`;
|
|
328
|
+
};
|
|
329
|
+
// Errors: one tagged class per declared error, a runtime spec per class,
|
|
330
|
+
// and per-root error unions (field errors ∪ global errors).
|
|
331
|
+
const errorTags = Object.keys(model.errors).sort();
|
|
332
|
+
const globalErrorType = `${sdkName}GlobalError`;
|
|
333
|
+
const reserved = new Set([
|
|
334
|
+
...Object.keys(model.types),
|
|
335
|
+
sdkName,
|
|
336
|
+
`${sdkName}Error`,
|
|
337
|
+
globalErrorType,
|
|
271
338
|
]);
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
339
|
+
for (const tag of errorTags) {
|
|
340
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(tag))
|
|
341
|
+
throw new Error(`GraphQL error tag ${tag} is not a valid identifier`);
|
|
342
|
+
for (const name of [tag, `${tag}Spec`])
|
|
343
|
+
if (reserved.has(name))
|
|
344
|
+
throw new Error(`GraphQL error ${tag} collides with ${name}`);
|
|
345
|
+
}
|
|
346
|
+
const globalErrors = [...model.globalErrors].sort();
|
|
347
|
+
const rootErrors = (field: GraphQLField) => {
|
|
348
|
+
const fieldOnly = [...new Set(field.errors)]
|
|
349
|
+
.filter((tag) => !globalErrors.includes(tag))
|
|
350
|
+
.sort();
|
|
351
|
+
const type = union([...fieldOnly, globalErrorType]);
|
|
352
|
+
const specs = fieldOnly.length
|
|
353
|
+
? `[${fieldOnly.map((tag) => `${tag}Spec`).join(", ")}, ...globalErrors]`
|
|
354
|
+
: "globalErrors";
|
|
355
|
+
return { type, specs };
|
|
283
356
|
};
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
357
|
+
const rootFn = (
|
|
358
|
+
kind: "query" | "mutation",
|
|
359
|
+
fieldName: string,
|
|
360
|
+
field: GraphQLField,
|
|
361
|
+
): string => {
|
|
362
|
+
const errors = rootErrors(field);
|
|
363
|
+
const { name, list } = peelRef(field.type);
|
|
364
|
+
const node = !list ? connectionNode(name) : undefined;
|
|
365
|
+
const resultTs = tsOutput(field.type);
|
|
366
|
+
const argEntries = Object.entries(field.args);
|
|
367
|
+
const argTypes =
|
|
368
|
+
argEntries.length > 0
|
|
369
|
+
? `{ ${argEntries.map(([argName, arg]) => `${argName}: ${JSON.stringify(arg.type)}`).join(", ")} }`
|
|
370
|
+
: undefined;
|
|
371
|
+
const required = argEntries.filter(([, arg]) => arg.type.endsWith("!"));
|
|
372
|
+
const optional = argEntries.filter(([, arg]) => !arg.type.endsWith("!"));
|
|
373
|
+
const argFields = [
|
|
374
|
+
...required.map(
|
|
375
|
+
([argName, arg]) => `readonly ${argName}: ${tsArg(arg.type)}`,
|
|
376
|
+
),
|
|
377
|
+
...optional.map(
|
|
378
|
+
([argName, arg]) => `readonly ${argName}?: ${tsArg(arg.type)}`,
|
|
379
|
+
),
|
|
380
|
+
].join("; ");
|
|
381
|
+
const sig =
|
|
382
|
+
argEntries.length === 0
|
|
383
|
+
? ""
|
|
384
|
+
: required.length === 0
|
|
385
|
+
? `args?: { ${argFields} }`
|
|
386
|
+
: `args: { ${argFields} }`;
|
|
387
|
+
const argsExpr = argEntries.length === 0 ? ", undefined" : ", args";
|
|
388
|
+
const argTypesExpr = argTypes ? `, ${argTypes}` : ", undefined";
|
|
389
|
+
const rest = `${argsExpr}${argTypesExpr}, ${errors.specs}`;
|
|
390
|
+
const result = `Query<${resultTs}, ${errors.type}>`;
|
|
391
|
+
const call = (helper: string, target: string) =>
|
|
392
|
+
`${helper}(${JSON.stringify(kind)}, ${JSON.stringify(fieldName)}, ${target}${rest})`;
|
|
393
|
+
const body = node
|
|
394
|
+
? call("rootConnection", node)
|
|
395
|
+
: isObjectLike(name)
|
|
396
|
+
? call(list ? "rootList" : "root", name)
|
|
397
|
+
: call("rootLeaf", String(list));
|
|
398
|
+
return ` ${fieldName}: (${sig}): ${result} =>\n ${body},`;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const objects: string[] = [];
|
|
402
|
+
const enums: string[] = [];
|
|
403
|
+
const inputs: string[] = [];
|
|
404
|
+
const unions: string[] = [];
|
|
405
|
+
for (const [name, type] of Object.entries(model.types)) {
|
|
406
|
+
if (name.startsWith("__") || skipRoots.has(name)) continue;
|
|
407
|
+
if (type.kind === "OBJECT" || type.kind === "INTERFACE") objects.push(name);
|
|
408
|
+
else if (type.kind === "ENUM") enums.push(name);
|
|
409
|
+
else if (type.kind === "INPUT_OBJECT") inputs.push(name);
|
|
410
|
+
else if (type.kind === "UNION") unions.push(name);
|
|
411
|
+
}
|
|
412
|
+
objects.sort();
|
|
413
|
+
enums.sort();
|
|
414
|
+
inputs.sort();
|
|
415
|
+
unions.sort();
|
|
416
|
+
|
|
290
417
|
const lines: string[] = [
|
|
291
418
|
"// Generated by @distilled.cloud/core/codegen/graphql-client. DO NOT EDIT.",
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
`
|
|
296
|
-
`
|
|
297
|
-
`
|
|
298
|
-
|
|
419
|
+
`import * as S from "effect/Schema";`,
|
|
420
|
+
`import * as Category from "@distilled.cloud/core/category";`,
|
|
421
|
+
`import {`,
|
|
422
|
+
` connectionField,`,
|
|
423
|
+
` errorFields,`,
|
|
424
|
+
` errorSpec,`,
|
|
425
|
+
` listField,`,
|
|
426
|
+
` objectField,`,
|
|
427
|
+
` root,`,
|
|
428
|
+
` rootConnection,`,
|
|
429
|
+
` rootLeaf,`,
|
|
430
|
+
` rootList,`,
|
|
431
|
+
` scalarField,`,
|
|
432
|
+
` type ErrorSpec,`,
|
|
433
|
+
` type Query,`,
|
|
434
|
+
` type TypeMeta,`,
|
|
435
|
+
`} from "@distilled.cloud/core/graphql";`,
|
|
299
436
|
"",
|
|
300
|
-
"export type Scalars = {",
|
|
301
437
|
];
|
|
302
|
-
|
|
303
|
-
if (type.kind === "SCALAR" || type.kind === "ENUM")
|
|
304
|
-
lines.push(
|
|
305
|
-
`${literal(name)}: ${type.kind === "SCALAR" ? (type.scalar ?? "unknown") : union(type.enumValues!.map(literal))};`,
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
lines.push("}", "export type Inputs = {");
|
|
309
|
-
for (const [name, type] of Object.entries(model.types)) {
|
|
310
|
-
if (type.kind === "INPUT_OBJECT")
|
|
311
|
-
lines.push(
|
|
312
|
-
documentation(type.description),
|
|
313
|
-
`${literal(name)}: ${argsType(type.inputFields ?? {})};`,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
lines.push("}");
|
|
438
|
+
|
|
317
439
|
const categories: Record<string, string> = {
|
|
318
440
|
auth: "withAuthError",
|
|
319
441
|
badRequest: "withBadRequestError",
|
|
@@ -323,97 +445,132 @@ export const generateGraphQLClient = (
|
|
|
323
445
|
server: "withServerError",
|
|
324
446
|
conflict: "withConflictError",
|
|
325
447
|
};
|
|
326
|
-
for (const
|
|
327
|
-
const
|
|
448
|
+
for (const tag of errorTags) {
|
|
449
|
+
const error = model.errors[tag]!;
|
|
450
|
+
if (error.category && !categories[error.category])
|
|
451
|
+
throw new Error(
|
|
452
|
+
`GraphQL error ${tag}: unknown category ${error.category}`,
|
|
453
|
+
);
|
|
328
454
|
const pipes = [
|
|
329
|
-
error.category
|
|
330
|
-
? categories[error.category] && `Category.${categories[error.category]}`
|
|
331
|
-
: undefined,
|
|
455
|
+
error.category ? `Category.${categories[error.category]}` : undefined,
|
|
332
456
|
error.retryable
|
|
333
457
|
? `Category.withRetryable(${error.category === "throttling" ? "{ throttling: true }" : ""})`
|
|
334
458
|
: undefined,
|
|
335
459
|
].filter(Boolean);
|
|
460
|
+
const options = [
|
|
461
|
+
error.retryable ? "retryable: true" : undefined,
|
|
462
|
+
globalErrors.includes(tag) ? "global: true" : undefined,
|
|
463
|
+
].filter(Boolean);
|
|
336
464
|
lines.push(
|
|
337
|
-
documentation(error.description)
|
|
338
|
-
|
|
465
|
+
documentation(error.description) +
|
|
466
|
+
`export class ${tag} extends S.TaggedError<${tag}>()(${literal(tag)}, errorFields)${pipes.length ? `.pipe(${pipes.join(", ")})` : ""} {}`,
|
|
467
|
+
`const ${tag}Spec: ErrorSpec<${tag}> = errorSpec(${tag}, ${literal(tag)}, ${JSON.stringify(error.matchers)}${options.length ? `, { ${options.join(", ")} }` : ""});`,
|
|
468
|
+
"",
|
|
339
469
|
);
|
|
340
470
|
}
|
|
341
|
-
lines.push(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
471
|
+
lines.push(
|
|
472
|
+
`/** Errors every ${sdkName} root field can return. */`,
|
|
473
|
+
`export type ${globalErrorType} = ${union(globalErrors)};`,
|
|
474
|
+
`const globalErrors: ReadonlyArray<ErrorSpec> = [${globalErrors.map((tag) => `${tag}Spec`).join(", ")}];`,
|
|
475
|
+
"",
|
|
476
|
+
`/** Every typed error the ${sdkName} SDK declares. */`,
|
|
477
|
+
`export type ${sdkName}Error = ${union(errorTags)};`,
|
|
478
|
+
"",
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
for (const name of enums) {
|
|
482
|
+
const type = model.types[name]!;
|
|
483
|
+
lines.push(
|
|
484
|
+
documentation(type.description) +
|
|
485
|
+
`export type ${name} = ${union((type.enumValues ?? []).map(literal))};`,
|
|
486
|
+
"",
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
for (const name of inputs) {
|
|
491
|
+
const type = model.types[name]!;
|
|
492
|
+
const fields = Object.entries(type.inputFields ?? {}).map(
|
|
493
|
+
([fieldName, field]) =>
|
|
494
|
+
` readonly ${fieldName}${field.type.endsWith("!") ? "" : "?"}: ${tsArg(field.type)};`,
|
|
495
|
+
);
|
|
496
|
+
lines.push(
|
|
497
|
+
documentation(type.description) + `export interface ${name} {`,
|
|
498
|
+
...fields,
|
|
499
|
+
"}",
|
|
500
|
+
"",
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
for (const name of unions) {
|
|
505
|
+
const type = model.types[name]!;
|
|
506
|
+
lines.push(
|
|
507
|
+
documentation(type.description) +
|
|
508
|
+
`export type ${name} = ${(type.possibleTypes ?? []).join(" | ") || "unknown"};`,
|
|
509
|
+
"",
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
for (const name of objects) {
|
|
514
|
+
const type = model.types[name]!;
|
|
515
|
+
const fields = Object.entries(type.fields ?? {}).map(
|
|
516
|
+
([fieldName, field]) =>
|
|
517
|
+
` readonly ${fieldName}: ${tsOutput(field.type)};`,
|
|
518
|
+
);
|
|
519
|
+
lines.push(
|
|
520
|
+
documentation(type.description) + `export interface ${name} {`,
|
|
521
|
+
...fields,
|
|
522
|
+
"}",
|
|
523
|
+
"",
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
for (const name of [...objects, ...unions]) {
|
|
528
|
+
lines.push(
|
|
529
|
+
`export const ${name}: TypeMeta = { name: ${JSON.stringify(name)}, fields: {} };`,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
lines.push("");
|
|
533
|
+
|
|
534
|
+
for (const name of objects) {
|
|
535
|
+
const type = model.types[name]!;
|
|
536
|
+
lines.push(`Object.assign(${name}.fields, {`);
|
|
347
537
|
for (const [fieldName, field] of Object.entries(type.fields ?? {})) {
|
|
348
|
-
lines.push(
|
|
349
|
-
documentation(field.description),
|
|
350
|
-
`${literal(fieldName)}: G.Field<${argsType(field.args)}, ${literal(field.type)}, ${union(field.errors.map(literal))}>;`,
|
|
351
|
-
);
|
|
538
|
+
lines.push(` ${fieldName}: ${fieldMeta(fieldName, field)},`);
|
|
352
539
|
}
|
|
353
|
-
lines.push("};");
|
|
540
|
+
lines.push("});", "");
|
|
354
541
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
);
|
|
542
|
+
for (const name of unions) {
|
|
543
|
+
lines.push(
|
|
544
|
+
`Object.assign(${name}.fields, { __typename: scalarField("__typename") });`,
|
|
545
|
+
"",
|
|
546
|
+
);
|
|
361
547
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
"};",
|
|
371
|
-
);
|
|
372
|
-
lines.push(
|
|
373
|
-
"export type Selection<Name extends keyof Types> = G.Selection<Schema, Name>;",
|
|
374
|
-
"export type Result<Name extends string, Select> = G.Result<Schema, Name, Select>;",
|
|
375
|
-
"export type SelectedErrors<Name extends keyof Types, Select> = G.Errors<Schema, Name, Select>;",
|
|
376
|
-
);
|
|
377
|
-
lines.push(`export const schema: G.GraphQLModel = ${JSON.stringify(model)};`);
|
|
378
|
-
lines.push(
|
|
379
|
-
`export const errorClasses = { ${[...errorNames].map(([tag, name]) => `${literal(tag)}: ${name}`).join(", ")} };`,
|
|
380
|
-
);
|
|
381
|
-
lines.push(
|
|
382
|
-
`export const client = G.makeClient<Schema, ${options.requirementsType}>(schema, ${options.transportName ?? "transport"}, errorClasses);`,
|
|
383
|
-
"export const query = client.query;",
|
|
384
|
-
"export const mutation = client.mutation;",
|
|
385
|
-
"export const report = client.report;",
|
|
386
|
-
);
|
|
387
|
-
const operationNames = new Map<string, string>();
|
|
388
|
-
for (const [kind, name] of [
|
|
389
|
-
["query", model.queryType],
|
|
390
|
-
["mutation", model.mutationType],
|
|
391
|
-
] as const) {
|
|
392
|
-
for (const field of Object.keys(model.types[name ?? ""]?.fields ?? {})) {
|
|
393
|
-
const exportName = allocate(
|
|
394
|
-
field,
|
|
395
|
-
`${kind}${field[0]!.toUpperCase()}${field.slice(1)}`,
|
|
396
|
-
);
|
|
397
|
-
if (!operationNames.has(field)) operationNames.set(field, exportName);
|
|
398
|
-
lines.push(
|
|
399
|
-
`export const ${exportName} = client.operation(${literal(kind)}, ${literal(field)});`,
|
|
400
|
-
);
|
|
548
|
+
|
|
549
|
+
lines.push(`export const ${sdkName} = {`);
|
|
550
|
+
const queryNames = new Set<string>();
|
|
551
|
+
const queryType = model.types[model.queryType];
|
|
552
|
+
if (queryType?.fields) {
|
|
553
|
+
for (const [fieldName, field] of Object.entries(queryType.fields)) {
|
|
554
|
+
queryNames.add(fieldName);
|
|
555
|
+
lines.push(rootFn("query", fieldName, field));
|
|
401
556
|
}
|
|
402
557
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
558
|
+
if (model.mutationType) {
|
|
559
|
+
const mutationType = model.types[model.mutationType];
|
|
560
|
+
if (mutationType?.fields) {
|
|
561
|
+
for (const [fieldName, field] of Object.entries(mutationType.fields)) {
|
|
562
|
+
const exportName = queryNames.has(fieldName)
|
|
563
|
+
? `${fieldName}Mutation`
|
|
564
|
+
: fieldName;
|
|
565
|
+
const line = rootFn("mutation", fieldName, field);
|
|
566
|
+
lines.push(
|
|
567
|
+
exportName === fieldName
|
|
568
|
+
? line
|
|
569
|
+
: line.replace(` ${fieldName}:`, ` ${exportName}:`),
|
|
570
|
+
);
|
|
571
|
+
}
|
|
416
572
|
}
|
|
417
573
|
}
|
|
574
|
+
lines.push("};", "");
|
|
418
575
|
return lines.join("\n") + "\n";
|
|
419
576
|
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { convertOpenApiToSmithy } from "./openapi.ts";
|
|
3
|
+
|
|
4
|
+
const model = (
|
|
5
|
+
schema: unknown,
|
|
6
|
+
mediaType = "multipart/form-data",
|
|
7
|
+
responseSchema: unknown = { type: "string", format: "binary" },
|
|
8
|
+
components: Record<string, unknown> = {},
|
|
9
|
+
options: { binaryTypes?: boolean } = { binaryTypes: true },
|
|
10
|
+
) =>
|
|
11
|
+
convertOpenApiToSmithy(
|
|
12
|
+
{
|
|
13
|
+
openapi: "3.0.3",
|
|
14
|
+
info: { title: "Binary", version: "1" },
|
|
15
|
+
components,
|
|
16
|
+
paths: {
|
|
17
|
+
"/files": {
|
|
18
|
+
post: {
|
|
19
|
+
operationId: "uploadFile",
|
|
20
|
+
requestBody: { content: { [mediaType]: { schema } } },
|
|
21
|
+
responses: {
|
|
22
|
+
"200": {
|
|
23
|
+
content: {
|
|
24
|
+
"application/octet-stream": {
|
|
25
|
+
schema: responseSchema,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{ namespace: "com.example.binary", serviceName: "Binary", ...options },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
describe("OpenAPI binary contracts", () => {
|
|
38
|
+
for (const options of [{}, { binaryTypes: false }]) {
|
|
39
|
+
test(`binary conversion preserves existing contracts with ${JSON.stringify(options)}`, () => {
|
|
40
|
+
const result = model(
|
|
41
|
+
{
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
zip: { type: "string", format: "binary" },
|
|
45
|
+
archive: { $ref: "#/components/schemas/Archive" },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
"multipart/form-data",
|
|
49
|
+
{ $ref: "#/components/schemas/Archive" },
|
|
50
|
+
{ schemas: { Archive: { type: "string", format: "binary" } } },
|
|
51
|
+
options,
|
|
52
|
+
);
|
|
53
|
+
const request = result.shapes["com.example.binary#UploadFileRequest"];
|
|
54
|
+
expect(request.members.zip.target).toBe("smithy.api#String");
|
|
55
|
+
expect(request.members.archive.target).toBe("smithy.api#String");
|
|
56
|
+
expect(result.shapes["com.example.binary#UploadFile"].output.target).toBe(
|
|
57
|
+
"smithy.api#Unit",
|
|
58
|
+
);
|
|
59
|
+
expect(
|
|
60
|
+
result.shapes["com.example.binary#UploadFileResponse"],
|
|
61
|
+
).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test("component-referenced binary responses retain the original reference's nullability", () => {
|
|
66
|
+
const result = model(
|
|
67
|
+
{ type: "object" },
|
|
68
|
+
"application/json",
|
|
69
|
+
{ $ref: "#/components/schemas/Archive", nullable: true },
|
|
70
|
+
{ schemas: { Archive: { type: "string", format: "binary" } } },
|
|
71
|
+
);
|
|
72
|
+
const body =
|
|
73
|
+
result.shapes["com.example.binary#UploadFileResponse"].members.body;
|
|
74
|
+
expect(body.target).toBe("smithy.api#Blob");
|
|
75
|
+
expect(body.traits["com.distilled.openapi#rawResponse"]).toEqual({});
|
|
76
|
+
expect(body.traits["com.distilled.openapi#nullable"]).toEqual({});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("multipart binary fields become blobs, not text or base64", () => {
|
|
80
|
+
const result = model({
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
zip: { type: "string", format: "binary" },
|
|
84
|
+
encoded: { type: "string", format: "byte" },
|
|
85
|
+
environment: { type: "string" },
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const request = result.shapes["com.example.binary#UploadFileRequest"];
|
|
89
|
+
expect(request.members.zip.target).toBe("smithy.api#Blob");
|
|
90
|
+
expect(request.members.encoded.target).toBe("smithy.api#String");
|
|
91
|
+
expect(request.members.environment.target).toBe("smithy.api#String");
|
|
92
|
+
const response = result.shapes["com.example.binary#UploadFileResponse"];
|
|
93
|
+
expect(response.members.body.target).toBe("smithy.api#Blob");
|
|
94
|
+
expect(
|
|
95
|
+
response.members.body.traits["com.distilled.openapi#rawResponse"],
|
|
96
|
+
).toEqual({});
|
|
97
|
+
});
|
|
98
|
+
});
|