Skip to content

Commit 63537d3

Browse files
maxholmanclaude
andcommitted
fix: file-level import type in main.ts; drop | undefined from optional query property types
- Main client file emits `import type { … }` for type-only imports instead of per-name `import { type X, type Y }` — matches consumer convention. - Optional query/header params now emit `name?: T` instead of `name?: T | undefined`; optionality is conveyed by `?:` alone. Pairs with rest-client's CommandQuery constraint (`UndefinedOnPartialDeep<JsonObject>`) which allows undefined values across the wire boundary. - Adds regression tests covering import shape, optional query types, and AllInputs membership. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 35dbf4f commit 63537d3

31 files changed

Lines changed: 15385 additions & 14871 deletions

__tests__/__snapshots__/nullables.test.ts.snap

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ export const nullConstSchema = inputNullConstSchema;
2323
`;
2424

2525
exports[`header parameters 1`] = `
26-
"export type UploadStatus = "pending" | "complete";
26+
"import type { UndefinedOnPartialDeep } from "type-fest";
27+
28+
export type UploadStatus = "pending" | "complete";
2729
export type UploadDataCommandHeader = {
2830
"content-type": "application/json" | "text/csv" | "application/xml";
2931
"content-length": \`\${number}\`;
@@ -33,11 +35,13 @@ export type UploadDataCommandParams = {
3335
uploadId: string;
3436
};
3537
export type UploadDataCommandInput = UploadDataCommandParams;
38+
export type InputUploadDataCommandResponse = UndefinedOnPartialDeep<UploadStatus>;
3639
"
3740
`;
3841
3942
exports[`header parameters 2`] = `
4043
"import { Command } from "@block65/rest-client";
44+
import type { UndefinedOnPartialDeep } from "type-fest";
4145
import type { UploadDataCommandHeader, UploadDataCommandInput, UploadStatus } from "./types.js";
4246
4347
/**
@@ -53,10 +57,10 @@ function encodePath(strings: TemplateStringsArray, ...values: string[]): string
5357
* UploadDataCommand
5458
*
5559
*/
56-
export class UploadDataCommand extends Command<UploadDataCommandInput, UploadStatus, never, UploadDataCommandHeader> {
60+
export class UploadDataCommand extends Command<UndefinedOnPartialDeep<UploadDataCommandInput>, UploadStatus, never, UploadDataCommandHeader> {
5761
public override method = "post" as const;
5862
59-
constructor(input: UploadDataCommandInput, headers: UploadDataCommandHeader) {
63+
constructor(input: UndefinedOnPartialDeep<UploadDataCommandInput>, headers: UploadDataCommandHeader) {
6064
const {uploadId } = input;
6165
super(encodePath\`/uploads/\${uploadId}\`, undefined, undefined, headers);
6266
}
@@ -137,16 +141,19 @@ export const nullableImageSchema = v.union([v.pipe(v.string(), v.trim()), v.null
137141
`;
138142
139143
exports[`query and header integer params coerce strings to numbers 1`] = `
140-
"export type Dummy = string;
144+
"import type { UndefinedOnPartialDeep } from "type-fest";
145+
146+
export type Dummy = string;
141147
export type ExpireTime = number;
142148
export type ListFilesCommandQuery = {
143149
exp: \`\${number}\`;
144-
limit?: \`\${number}\` | undefined;
150+
limit?: \`\${number}\`;
145151
};
146152
export type ListFilesCommandHeader = {
147153
"x-rate-limit": \`\${number}\`;
148154
};
149155
export type ListFilesCommandInput = ListFilesCommandQuery;
156+
export type InputListFilesCommandResponse = UndefinedOnPartialDeep<Dummy>;
150157
"
151158
`;
152159
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import type { oas31 } from "openapi3-ts";
2+
import { expect, test } from "vitest";
3+
import { processOpenApiDocument } from "../lib/process-document.ts";
4+
5+
const respOk = {
6+
"200": {
7+
description: "OK",
8+
content: { "application/json": { schema: { type: "string" } } },
9+
},
10+
} as const;
11+
12+
test("main.ts emits file-level `import type` for type-only imports", async () => {
13+
const schema: oas31.OpenAPIObject = {
14+
openapi: "3.1.0",
15+
info: { title: "Test", version: "1.0.0" },
16+
paths: {
17+
"/one": {
18+
post: {
19+
operationId: "oneCommand",
20+
requestBody: {
21+
content: {
22+
"application/json": {
23+
schema: {
24+
type: "object",
25+
properties: { x: { type: "string" } },
26+
},
27+
},
28+
},
29+
},
30+
responses: respOk,
31+
},
32+
},
33+
},
34+
};
35+
36+
const result = await processOpenApiDocument("/tmp/whatever", schema);
37+
const mainText = result.mainFile.getText();
38+
39+
expect(mainText).toMatch(
40+
/import type \{[^}]*OneCommandInput[^}]*\}\s+from\s+"\.\/types\.js"/,
41+
);
42+
expect(mainText).toMatch(
43+
/import type \{[^}]*UndefinedOnPartialDeep[^}]*\}\s+from\s+"type-fest"/,
44+
);
45+
expect(mainText).not.toMatch(/import \{[^}]*type\s+OneCommandInput/);
46+
expect(mainText).not.toMatch(/import \{[^}]*type\s+UndefinedOnPartialDeep[^}]*\}\s+from\s+"type-fest"/);
47+
});
48+
49+
test("optional query params do not carry `| undefined` in their property type", async () => {
50+
const schema: oas31.OpenAPIObject = {
51+
openapi: "3.1.0",
52+
info: { title: "Test", version: "1.0.0" },
53+
paths: {
54+
"/files": {
55+
get: {
56+
operationId: "listFilesCommand",
57+
parameters: [
58+
{
59+
name: "purpose",
60+
in: "query",
61+
required: false,
62+
schema: { type: "string" },
63+
},
64+
{
65+
name: "limit",
66+
in: "query",
67+
required: false,
68+
schema: { type: "integer", minimum: 1, maximum: 100 },
69+
},
70+
],
71+
responses: respOk,
72+
},
73+
},
74+
},
75+
};
76+
77+
const result = await processOpenApiDocument("/tmp/whatever", schema);
78+
const typesText = result.typesFile.getText();
79+
const queryBlock =
80+
typesText.match(/export type ListFilesCommandQuery = \{[\s\S]*?\};/)?.[0] ??
81+
"";
82+
83+
expect(queryBlock).toContain("purpose?: string");
84+
expect(queryBlock).toContain("limit?: `${number}`");
85+
expect(queryBlock).not.toContain("undefined");
86+
});
87+
88+
test("AllInputs union includes every command's Input (no silent drops)", async () => {
89+
const schema: oas31.OpenAPIObject = {
90+
openapi: "3.1.0",
91+
info: { title: "Test", version: "1.0.0" },
92+
paths: {
93+
"/with-body": {
94+
post: {
95+
operationId: "withBodyCommand",
96+
requestBody: {
97+
content: {
98+
"application/json": {
99+
schema: {
100+
type: "object",
101+
properties: { x: { type: "string" } },
102+
},
103+
},
104+
},
105+
},
106+
responses: respOk,
107+
},
108+
},
109+
"/empty-a": {
110+
get: { operationId: "alphaCommand", responses: respOk },
111+
},
112+
"/empty-b": {
113+
get: { operationId: "betaCommand", responses: respOk },
114+
},
115+
"/empty-c": {
116+
get: { operationId: "gammaCommand", responses: respOk },
117+
},
118+
"/with-query": {
119+
get: {
120+
operationId: "withQueryCommand",
121+
parameters: [
122+
{
123+
name: "q",
124+
in: "query",
125+
required: false,
126+
schema: { type: "string" },
127+
},
128+
],
129+
responses: respOk,
130+
},
131+
},
132+
},
133+
};
134+
135+
const result = await processOpenApiDocument("/tmp/whatever", schema);
136+
const mainText = result.mainFile.getText();
137+
const commandsText = result.commandsFile.getText();
138+
139+
const allInputsBlock =
140+
mainText.match(/type AllInputs =[\s\S]*?;/)?.[0] ?? "";
141+
142+
const commandNames = [
143+
...commandsText.matchAll(/^export class (\w+Command) extends Command</gm),
144+
].map((m) => m[1] ?? "");
145+
146+
expect(commandNames.length).toBeGreaterThan(0);
147+
const missing = commandNames.filter(
148+
(name) => !allInputsBlock.includes(`${name}Input`),
149+
);
150+
expect(missing).toEqual([]);
151+
});

__tests__/fixtures/docker/commands-validated.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* WARN: Do not edit directly.
55
*
6-
* Generated on 2026-05-11T06:13:31.281Z
6+
* Generated on 2026-05-18T09:28:16.280Z
77
*
88
*/
99
/** eslint-disable max-classes */

0 commit comments

Comments
 (0)