react-query-lightbase-codegen 1.6.4 → 2.0.1
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/dist/config/loadConfig.d.ts +2 -0
- package/dist/config/loadConfig.js +24 -0
- package/dist/generator/clientGenerator.d.ts +13 -0
- package/dist/generator/clientGenerator.js +199 -0
- package/dist/generator/instanceGenerator.d.ts +1 -0
- package/dist/generator/instanceGenerator.js +21 -0
- package/dist/generator/reactQueryGenerator.d.ts +2 -0
- package/dist/generator/reactQueryGenerator.js +82 -0
- package/dist/generator/schemaGenerator.d.ts +5 -0
- package/dist/generator/schemaGenerator.js +114 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +106 -4
- package/dist/types/config.d.ts +11 -0
- package/dist/types/config.js +2 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +31 -20
- package/dist/utils.js +56 -179
- package/package.json +54 -82
- package/readme.md +94 -0
- package/src/config/loadConfig.ts +25 -0
- package/src/generator/clientGenerator.ts +236 -0
- package/src/generator/instanceGenerator.ts +20 -0
- package/src/generator/reactQueryGenerator.ts +94 -0
- package/src/generator/schemaGenerator.ts +138 -0
- package/src/index.ts +78 -3
- package/src/types/config.ts +12 -0
- package/src/types.ts +5 -0
- package/src/utils.ts +55 -213
- package/README.md +0 -77
- package/dist/convertSwaggerFile.d.ts +0 -5
- package/dist/convertSwaggerFile.js +0 -26
- package/dist/convertSwaggerFile.js.map +0 -1
- package/dist/generateHooks.d.ts +0 -18
- package/dist/generateHooks.js +0 -448
- package/dist/generateHooks.js.map +0 -1
- package/dist/generateImports.d.ts +0 -6
- package/dist/generateImports.js +0 -21
- package/dist/generateImports.js.map +0 -1
- package/dist/generateSchemas.d.ts +0 -7
- package/dist/generateSchemas.js +0 -100
- package/dist/generateSchemas.js.map +0 -1
- package/dist/importSpecs.d.ts +0 -10
- package/dist/importSpecs.js +0 -127
- package/dist/importSpecs.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/src/convertSwaggerFile.ts +0 -25
- package/src/generateHooks.ts +0 -540
- package/src/generateImports.ts +0 -32
- package/src/generateSchemas.ts +0 -117
- package/src/importSpecs.ts +0 -168
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
2
|
+
import type { OpenAPIConfig } from "../types/config";
|
|
3
|
+
import { camelCase, pascalCase, sanitizePropertyName, sanitizeTypeName, specTitle } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface OperationInfo {
|
|
6
|
+
method: string;
|
|
7
|
+
path: string;
|
|
8
|
+
operationId: string;
|
|
9
|
+
summary?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
parameters?: OpenAPIV3.ParameterObject[];
|
|
12
|
+
requestBody?: OpenAPIV3.RequestBodyObject;
|
|
13
|
+
responses: OpenAPIV3.ResponsesObject;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveSchema(
|
|
17
|
+
schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject | undefined,
|
|
18
|
+
spec: OpenAPIV3.Document
|
|
19
|
+
): OpenAPIV3.SchemaObject | undefined {
|
|
20
|
+
if (!schema) return undefined;
|
|
21
|
+
if ("$ref" in schema) {
|
|
22
|
+
const index = schema.$ref.split("/").pop();
|
|
23
|
+
return spec.components?.schemas?.[index as string] as OpenAPIV3.SchemaObject;
|
|
24
|
+
}
|
|
25
|
+
return schema;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document): string {
|
|
29
|
+
const { method, path, operationId, summary, description, parameters, requestBody, responses } = operation;
|
|
30
|
+
// Generate JSDoc
|
|
31
|
+
const jsDocLines = ["/**"];
|
|
32
|
+
if (summary) jsDocLines.push(` * ${summary}`);
|
|
33
|
+
if (description) jsDocLines.push(` * ${description}`);
|
|
34
|
+
|
|
35
|
+
// Add parameter descriptions
|
|
36
|
+
parameters?.forEach((param) => {
|
|
37
|
+
const desc = param.description ? ` - ${param.description}` : "";
|
|
38
|
+
jsDocLines.push(
|
|
39
|
+
` * @param ${param.in === "path" ? "params." : param.in === "query" ? "query." : ""}${param.name}${desc}`
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (requestBody && "description" in requestBody) {
|
|
44
|
+
jsDocLines.push(` * @param data - ${requestBody.description}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Add return type description
|
|
48
|
+
const responseDetails = Object.entries(responses).find(([code]) => code.startsWith("2"));
|
|
49
|
+
if (responseDetails) {
|
|
50
|
+
const [code, response] = responseDetails;
|
|
51
|
+
const responseObj = response as OpenAPIV3.ResponseObject;
|
|
52
|
+
const desc = "description" in responseObj ? responseObj.description : "";
|
|
53
|
+
const contentType = responseObj.content?.["application/json"]?.schema;
|
|
54
|
+
const typeName = `${operationId}Response${code}`;
|
|
55
|
+
|
|
56
|
+
if (contentType) {
|
|
57
|
+
if (desc) {
|
|
58
|
+
jsDocLines.push(` * @returns ${desc}`);
|
|
59
|
+
}
|
|
60
|
+
jsDocLines.push(` * @see ${typeName}`);
|
|
61
|
+
} else if (desc) {
|
|
62
|
+
jsDocLines.push(` * @returns ${desc}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
jsDocLines.push(" */");
|
|
67
|
+
|
|
68
|
+
const urlParams = parameters?.filter((p) => p.in === "path") || [];
|
|
69
|
+
const queryParams = parameters?.filter((p) => p.in === "query") || [];
|
|
70
|
+
|
|
71
|
+
const isFormData = requestBody && "content" in requestBody && requestBody.content?.["multipart/form-data"];
|
|
72
|
+
|
|
73
|
+
const formDataSchema = isFormData
|
|
74
|
+
? resolveSchema(requestBody.content["multipart/form-data"].schema, spec)
|
|
75
|
+
: undefined;
|
|
76
|
+
|
|
77
|
+
const requestBodySchema = requestBody?.content?.["application/json"]?.schema
|
|
78
|
+
? resolveSchema(requestBody.content["application/json"].schema, spec)
|
|
79
|
+
: undefined;
|
|
80
|
+
|
|
81
|
+
// Build data type parts
|
|
82
|
+
const dataProps: string[] = [];
|
|
83
|
+
|
|
84
|
+
// Add path and query parameters
|
|
85
|
+
urlParams.forEach((p) => {
|
|
86
|
+
const safeName = sanitizePropertyName(p.name);
|
|
87
|
+
dataProps.push(`${safeName}: ${getTypeFromParam(p)}`);
|
|
88
|
+
});
|
|
89
|
+
queryParams.forEach((p) => {
|
|
90
|
+
const safeName = sanitizePropertyName(p.name);
|
|
91
|
+
dataProps.push(`${safeName}${p.required ? "" : "?"}: ${getTypeFromParam(p)}`);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Add request body type if it exists
|
|
95
|
+
const hasData = (parameters && parameters.length > 0) || operation.requestBody;
|
|
96
|
+
|
|
97
|
+
let dataType = "undefined";
|
|
98
|
+
const namedType = operationId;
|
|
99
|
+
if (hasData) {
|
|
100
|
+
if (requestBody && dataProps.length > 0) {
|
|
101
|
+
dataType = `T.${namedType}Request & { ${dataProps.join("; ")} }`;
|
|
102
|
+
} else if (requestBody) {
|
|
103
|
+
dataType = `T.${namedType}Request`;
|
|
104
|
+
} else if (dataProps.length > 0) {
|
|
105
|
+
dataType = `{ ${dataProps.join("; ")} }`;
|
|
106
|
+
} else {
|
|
107
|
+
dataType = "Record<string, never>";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Get response type from 2xx response
|
|
112
|
+
const successResponse = Object.entries(responses).find(([code]) => code.startsWith("2"));
|
|
113
|
+
const responseType = successResponse ? `T.${`${namedType}Response${successResponse[0]}`}` : "any";
|
|
114
|
+
|
|
115
|
+
const urlWithParams = urlParams.length > 0 ? path.replace(/{(\w+)}/g, "${data.$1}") : path;
|
|
116
|
+
|
|
117
|
+
const methodBody = [
|
|
118
|
+
"const apiClient = getApiClient();",
|
|
119
|
+
`const url = \`${urlWithParams}\`;`,
|
|
120
|
+
queryParams.length > 0
|
|
121
|
+
? `const queryData = {
|
|
122
|
+
${queryParams.map((p) => `["${p.name}"]: data["${p.name}"]`).join(",\n ")}
|
|
123
|
+
};`
|
|
124
|
+
: "",
|
|
125
|
+
requestBodySchema?.properties
|
|
126
|
+
? `const bodyData = {
|
|
127
|
+
${Object.entries(requestBodySchema.properties)
|
|
128
|
+
.map(([key]) => `${key}: data.${key}`)
|
|
129
|
+
.join(",\n ")}
|
|
130
|
+
};`
|
|
131
|
+
: "",
|
|
132
|
+
formDataSchema?.properties
|
|
133
|
+
? `const formData = new FormData();
|
|
134
|
+
${Object.entries(formDataSchema.properties)
|
|
135
|
+
.map(([key, prop]) => {
|
|
136
|
+
const schemaProperty = prop as OpenAPIV3.SchemaObject;
|
|
137
|
+
const isBinary = schemaProperty.format === "binary";
|
|
138
|
+
return formDataSchema?.required?.includes(key)
|
|
139
|
+
? `formData.append("${key}", ${isBinary ? "" : "String("}${queryParams.length > 0 ? "bodyData" : "data"}.${key}${isBinary ? "" : ")"});`
|
|
140
|
+
: `if (${queryParams.length > 0 ? "bodyData" : "data"}.${key} != null) {
|
|
141
|
+
formData.append("${key}", ${isBinary ? "" : "String("}${queryParams.length > 0 ? "bodyData" : "data"}.${key}${isBinary ? "" : ")"});
|
|
142
|
+
}`;
|
|
143
|
+
})
|
|
144
|
+
.join("\n ")}`
|
|
145
|
+
: "",
|
|
146
|
+
`return apiClient.${method}<${responseType}>(url, {
|
|
147
|
+
${queryParams.length > 0 ? "params: queryData," : ""}
|
|
148
|
+
${requestBody ? `data: ${isFormData ? "formData" : "bodyData"},` : ""}
|
|
149
|
+
${isFormData ? `config: { headers: { 'Content-Type': 'multipart/form-data', ...config?.headers }, ...config },` : "...config"}
|
|
150
|
+
});`,
|
|
151
|
+
]
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.join("\n ");
|
|
154
|
+
|
|
155
|
+
return `
|
|
156
|
+
${jsDocLines.join("\n ")}
|
|
157
|
+
export function ${camelCase(operationId)}(data${hasData ? `: ${dataType}` : "?: undefined"}, config?: AxiosRequestConfig): Promise<AxiosResponse<${responseType}>> {
|
|
158
|
+
${methodBody}
|
|
159
|
+
}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getTypeFromParam(param: OpenAPIV3.ParameterObject): string {
|
|
163
|
+
if ("schema" in param) {
|
|
164
|
+
const schema = param.schema as OpenAPIV3.SchemaObject;
|
|
165
|
+
switch (schema.type) {
|
|
166
|
+
case "string":
|
|
167
|
+
return "string";
|
|
168
|
+
case "integer":
|
|
169
|
+
case "number":
|
|
170
|
+
return "number";
|
|
171
|
+
case "boolean":
|
|
172
|
+
return "boolean";
|
|
173
|
+
case "array":
|
|
174
|
+
return "Array<any>"; // You might want to make this more specific
|
|
175
|
+
default:
|
|
176
|
+
return "any";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return "any";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function generateApiClient(spec: OpenAPIV3.Document, config: OpenAPIConfig): string {
|
|
183
|
+
const operations: OperationInfo[] = [];
|
|
184
|
+
|
|
185
|
+
const resolveParameters = (
|
|
186
|
+
parameters: (OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject)[]
|
|
187
|
+
): OpenAPIV3.ParameterObject[] => {
|
|
188
|
+
return parameters.map((p) => {
|
|
189
|
+
if ("$ref" in p) {
|
|
190
|
+
const index = p.$ref.split("/").pop();
|
|
191
|
+
return spec.components?.schemas?.[index as string] as OpenAPIV3.ParameterObject;
|
|
192
|
+
}
|
|
193
|
+
return p;
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const resolveRequestBody = (
|
|
198
|
+
requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject | undefined
|
|
199
|
+
): OpenAPIV3.RequestBodyObject | undefined => {
|
|
200
|
+
if (!requestBody) return undefined;
|
|
201
|
+
if ("$ref" in requestBody) {
|
|
202
|
+
const index = requestBody.$ref.split("/").pop();
|
|
203
|
+
return spec.components?.schemas?.[index as string] as OpenAPIV3.RequestBodyObject;
|
|
204
|
+
}
|
|
205
|
+
return requestBody;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Collect all operations
|
|
209
|
+
Object.entries(spec.paths || {}).forEach(([path, pathItem]) => {
|
|
210
|
+
if (!pathItem) return;
|
|
211
|
+
["get", "post", "put", "delete", "patch"].forEach((method) => {
|
|
212
|
+
const operation = pathItem[method as keyof OpenAPIV3.PathItemObject] as OpenAPIV3.OperationObject;
|
|
213
|
+
if (!operation) return;
|
|
214
|
+
operations.push({
|
|
215
|
+
method: method,
|
|
216
|
+
path,
|
|
217
|
+
operationId: pascalCase(
|
|
218
|
+
`${method}_${sanitizeTypeName(operation.operationId || `${path.replace(/\W+/g, "_")}`)}`
|
|
219
|
+
),
|
|
220
|
+
summary: operation.summary,
|
|
221
|
+
description: operation.description,
|
|
222
|
+
parameters: resolveParameters([...(pathItem.parameters || []), ...(operation.parameters || [])]),
|
|
223
|
+
requestBody: resolveRequestBody(operation.requestBody),
|
|
224
|
+
responses: operation.responses,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const title = specTitle(spec);
|
|
230
|
+
|
|
231
|
+
return `import type { AxiosResponse, AxiosRequestConfig } from 'axios';
|
|
232
|
+
import { getApiClient } from './apiClient';
|
|
233
|
+
import type * as T from './${title}.schema';
|
|
234
|
+
|
|
235
|
+
${operations.map((op) => generateAxiosMethod(op, spec)).join("\n\n")}`;
|
|
236
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
2
|
+
|
|
3
|
+
export function generateInstance(): string {
|
|
4
|
+
return `import type { AxiosInstance } from 'axios';
|
|
5
|
+
|
|
6
|
+
let apiClient: AxiosInstance;
|
|
7
|
+
|
|
8
|
+
export function setApiClient(instance: AxiosInstance): void {
|
|
9
|
+
apiClient = instance;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getApiClient(): AxiosInstance {
|
|
13
|
+
if (!apiClient) {
|
|
14
|
+
throw new Error('API client not configured. Call setApiClient with an axios instance first.');
|
|
15
|
+
}
|
|
16
|
+
return apiClient;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { apiClient };`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
2
|
+
import { camelCase, pascalCase, sanitizeTypeName, specTitle } from "../utils";
|
|
3
|
+
import type { OperationInfo } from "./clientGenerator";
|
|
4
|
+
|
|
5
|
+
function generateQueryOptions(operation: OperationInfo, spec: OpenAPIV3.Document): string {
|
|
6
|
+
const { operationId, parameters, requestBody } = operation;
|
|
7
|
+
|
|
8
|
+
const hasData = (parameters && parameters.length > 0) || operation.requestBody;
|
|
9
|
+
|
|
10
|
+
// Helper to get required fields from a schema
|
|
11
|
+
const getRequiredFields = (
|
|
12
|
+
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
|
|
13
|
+
context: { schemas: { [key: string]: OpenAPIV3.SchemaObject } }
|
|
14
|
+
): string[] => {
|
|
15
|
+
if ("$ref" in schema) {
|
|
16
|
+
const refType = schema.$ref.split("/").pop();
|
|
17
|
+
const refSchema = context.schemas[refType as string];
|
|
18
|
+
return refSchema?.required?.map((p) => `'${p}'`) || [];
|
|
19
|
+
}
|
|
20
|
+
return schema.required?.map((p) => `'${p}'`) || [];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Get required parameter names from both parameters and request body
|
|
24
|
+
const requiredParams = [
|
|
25
|
+
...(parameters?.filter((p) => p.required).map((p) => `'${p.name}'`) || []),
|
|
26
|
+
...(requestBody && "content" in requestBody && requestBody.content?.["application/json"]?.schema
|
|
27
|
+
? getRequiredFields(requestBody.content["application/json"].schema, {
|
|
28
|
+
schemas: (spec.components?.schemas as { [key: string]: OpenAPIV3.SchemaObject }) || {},
|
|
29
|
+
})
|
|
30
|
+
: []),
|
|
31
|
+
...(requestBody && "content" in requestBody && requestBody.content?.["multipart/form-data"]?.schema
|
|
32
|
+
? getRequiredFields(requestBody.content["multipart/form-data"].schema, {
|
|
33
|
+
schemas: (spec.components?.schemas as { [key: string]: OpenAPIV3.SchemaObject }) || {},
|
|
34
|
+
})
|
|
35
|
+
: []),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const namedQuery = camelCase(operationId);
|
|
39
|
+
|
|
40
|
+
return `
|
|
41
|
+
export const ${namedQuery}QueryOptions = (
|
|
42
|
+
${hasData ? `params: Partial<Parameters<typeof apiClient.${namedQuery}>[0]>, config?: Partial<Parameters<typeof apiClient.${namedQuery}>[1]>` : `_: undefined, config?: Partial<Parameters<typeof apiClient.${namedQuery}>[1]>`}
|
|
43
|
+
) => {
|
|
44
|
+
const enabled = ${hasData ? `hasDefinedProps(params, ${requiredParams.join(", ")})` : "true"};
|
|
45
|
+
return queryOptions({
|
|
46
|
+
queryKey: ['${camelCase(operationId)}', ${hasData ? "params" : "undefined"}],
|
|
47
|
+
queryFn: enabled ? async () => {
|
|
48
|
+
const response = await apiClient.${namedQuery}(${hasData ? "params" : "undefined"}, config);
|
|
49
|
+
return response.data;
|
|
50
|
+
} : skipToken,
|
|
51
|
+
});
|
|
52
|
+
};`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function generateReactQuery(spec: OpenAPIV3.Document): string {
|
|
56
|
+
const operations: OperationInfo[] = [];
|
|
57
|
+
|
|
58
|
+
// Collect operations (same as in clientGenerator)
|
|
59
|
+
Object.entries(spec.paths || {}).forEach(([path, pathItem]) => {
|
|
60
|
+
if (!pathItem) return;
|
|
61
|
+
|
|
62
|
+
["get", "post", "put", "delete", "patch"].forEach((method) => {
|
|
63
|
+
const operation = pathItem[method as keyof OpenAPIV3.PathItemObject] as OpenAPIV3.OperationObject;
|
|
64
|
+
if (!operation) return;
|
|
65
|
+
operations.push({
|
|
66
|
+
method: method.toUpperCase(),
|
|
67
|
+
path,
|
|
68
|
+
operationId: pascalCase(
|
|
69
|
+
`${method}_${sanitizeTypeName(operation.operationId || `${path.replace(/\W+/g, "_")}`)}`
|
|
70
|
+
),
|
|
71
|
+
summary: operation.summary,
|
|
72
|
+
description: operation.description,
|
|
73
|
+
parameters: [
|
|
74
|
+
...(pathItem.parameters || []),
|
|
75
|
+
...(operation.parameters || []),
|
|
76
|
+
] as OpenAPIV3.ParameterObject[],
|
|
77
|
+
requestBody: operation.requestBody as OpenAPIV3.RequestBodyObject,
|
|
78
|
+
responses: operation.responses,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return `import { queryOptions, skipToken } from '@tanstack/react-query';
|
|
84
|
+
import * as apiClient from './${specTitle(spec)}.client';
|
|
85
|
+
const hasDefinedProps = <T extends { [P in K]?: any }, K extends PropertyKey>(
|
|
86
|
+
obj: T,
|
|
87
|
+
...keys: K[]
|
|
88
|
+
): obj is T & { [P in K]-?: Exclude<T[P], undefined> } => {
|
|
89
|
+
return keys.every((k) => obj[k] !== undefined);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
${operations.map((op) => generateQueryOptions(op, spec)).join("\n\n")}
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
2
|
+
import { pascalCase, sanitizePropertyName, sanitizeTypeName } from "../utils";
|
|
3
|
+
|
|
4
|
+
interface SchemaContext {
|
|
5
|
+
schemas: { [key: string]: OpenAPIV3.SchemaObject };
|
|
6
|
+
generatedTypes: Set<string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts OpenAPI schema type to TypeScript type
|
|
11
|
+
*/
|
|
12
|
+
function getTypeFromSchema(
|
|
13
|
+
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
|
|
14
|
+
context: SchemaContext
|
|
15
|
+
): string {
|
|
16
|
+
if (!schema) return "any";
|
|
17
|
+
|
|
18
|
+
if ("$ref" in schema) {
|
|
19
|
+
const refType = schema.$ref.split("/").pop();
|
|
20
|
+
return sanitizeTypeName(refType as string);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Handle enum types properly
|
|
24
|
+
if (schema.enum) {
|
|
25
|
+
return schema.enum.map((e) => (typeof e === "string" ? `'${e}'` : e)).join(" | ");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
switch (schema.type) {
|
|
29
|
+
case "string":
|
|
30
|
+
return "string";
|
|
31
|
+
case "number":
|
|
32
|
+
case "integer":
|
|
33
|
+
return "number";
|
|
34
|
+
case "boolean":
|
|
35
|
+
return "boolean";
|
|
36
|
+
case "array": {
|
|
37
|
+
const itemType = getTypeFromSchema(schema.items, context);
|
|
38
|
+
return `Array<${itemType}>`;
|
|
39
|
+
}
|
|
40
|
+
case "object":
|
|
41
|
+
if (schema.properties) {
|
|
42
|
+
const properties = Object.entries(schema.properties)
|
|
43
|
+
.map(([key, prop]) => {
|
|
44
|
+
const isRequired = schema.required?.includes(key);
|
|
45
|
+
const propertyType = getTypeFromSchema(prop, context);
|
|
46
|
+
const safeName = sanitizePropertyName(key);
|
|
47
|
+
return ` ${safeName}${isRequired ? "" : "?"}: ${propertyType};`;
|
|
48
|
+
})
|
|
49
|
+
.join("\n");
|
|
50
|
+
return `{\n${properties}\n}`;
|
|
51
|
+
}
|
|
52
|
+
if (schema.additionalProperties) {
|
|
53
|
+
const valueType =
|
|
54
|
+
typeof schema.additionalProperties === "boolean"
|
|
55
|
+
? "any"
|
|
56
|
+
: getTypeFromSchema(schema.additionalProperties, context);
|
|
57
|
+
return `Record<string, ${valueType}>`;
|
|
58
|
+
}
|
|
59
|
+
return "Record<string, any>";
|
|
60
|
+
default:
|
|
61
|
+
return "any";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function generateTypeDefinition(
|
|
66
|
+
name: string,
|
|
67
|
+
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
|
|
68
|
+
context: SchemaContext
|
|
69
|
+
): string {
|
|
70
|
+
const description = !("$ref" in schema) && schema.description ? `/**\n * ${schema.description}\n */\n` : "";
|
|
71
|
+
const typeValue = getTypeFromSchema(schema, context);
|
|
72
|
+
|
|
73
|
+
// Use 'type' for primitives, unions, and simple types
|
|
74
|
+
// Use 'interface' only for complex objects with properties
|
|
75
|
+
const isInterface = !("$ref" in schema) && schema.type === "object" && schema.properties;
|
|
76
|
+
|
|
77
|
+
return isInterface
|
|
78
|
+
? `${description}export interface ${name} ${typeValue}\n\n`
|
|
79
|
+
: `${description}export type ${name} = ${typeValue}\n\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generates TypeScript interface definitions from OpenAPI schemas
|
|
84
|
+
*/
|
|
85
|
+
export function generateTypeDefinitions(spec: OpenAPIV3.Document): string {
|
|
86
|
+
const context: SchemaContext = {
|
|
87
|
+
schemas: (spec.components?.schemas as { [key: string]: OpenAPIV3.SchemaObject }) || {},
|
|
88
|
+
generatedTypes: new Set(),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
let output = "/* Generated TypeScript Definitions */\n\n";
|
|
92
|
+
|
|
93
|
+
// Generate types for all schema definitions
|
|
94
|
+
for (const [name, schema] of Object.entries(context.schemas)) {
|
|
95
|
+
if (context.generatedTypes.has(name)) continue;
|
|
96
|
+
output += generateTypeDefinition(name, schema, context);
|
|
97
|
+
context.generatedTypes.add(name);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Generate request/response types
|
|
101
|
+
if (spec.paths) {
|
|
102
|
+
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
103
|
+
for (const [method, operation] of Object.entries(pathItem as OpenAPIV3.PathItemObject)) {
|
|
104
|
+
if (method === "$ref") continue;
|
|
105
|
+
|
|
106
|
+
const operationObject = operation as OpenAPIV3.OperationObject;
|
|
107
|
+
if (!operationObject) continue;
|
|
108
|
+
const operationId = pascalCase(
|
|
109
|
+
`${method}_${sanitizeTypeName(operationObject.operationId || `${path.replace(/\W+/g, "_")}`)}`
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Generate request body type
|
|
113
|
+
if (operationObject.requestBody) {
|
|
114
|
+
const content = (operationObject.requestBody as OpenAPIV3.RequestBodyObject).content;
|
|
115
|
+
const jsonContent = content["application/json"] || content["multipart/form-data"];
|
|
116
|
+
if (jsonContent?.schema) {
|
|
117
|
+
const typeName = sanitizeTypeName(`${operationId}Request`);
|
|
118
|
+
output += generateTypeDefinition(typeName, jsonContent.schema as OpenAPIV3.SchemaObject, context);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Generate response types
|
|
123
|
+
if (operationObject.responses) {
|
|
124
|
+
for (const [code, response] of Object.entries(operationObject.responses)) {
|
|
125
|
+
const responseObj = response as OpenAPIV3.ResponseObject;
|
|
126
|
+
const content = responseObj.content?.["application/json"];
|
|
127
|
+
if (content?.schema) {
|
|
128
|
+
const typeName = sanitizeTypeName(`${operationId}Response${code}`);
|
|
129
|
+
output += generateTypeDefinition(typeName, content.schema as OpenAPIV3.SchemaObject, context);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return output;
|
|
138
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
5
|
+
import * as yaml from "yaml";
|
|
6
|
+
import { generateApiClient } from "./generator/clientGenerator";
|
|
7
|
+
import { generateInstance } from "./generator/instanceGenerator";
|
|
8
|
+
import { generateReactQuery } from "./generator/reactQueryGenerator";
|
|
9
|
+
import { generateTypeDefinitions } from "./generator/schemaGenerator";
|
|
10
|
+
import type { OpenAPIConfig } from "./types/config";
|
|
11
|
+
import { specTitle } from "./utils";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Loads the OpenAPI specification from either a URL or local file
|
|
15
|
+
*/
|
|
16
|
+
async function loadOpenAPISpec(specSource: string): Promise<OpenAPIV3.Document> {
|
|
17
|
+
try {
|
|
18
|
+
if (specSource.startsWith("http")) {
|
|
19
|
+
const response = await axios.get(specSource);
|
|
20
|
+
// Check if response is YAML by looking for common YAML indicators
|
|
21
|
+
const isYaml =
|
|
22
|
+
typeof response.data === "string" &&
|
|
23
|
+
(response.data.trim().startsWith("openapi:") || response.data.trim().startsWith("swagger:"));
|
|
24
|
+
|
|
25
|
+
return isYaml ? yaml.parse(response.data) : response.data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const content = await readFile(specSource, "utf-8");
|
|
29
|
+
// Handle both JSON and YAML formats
|
|
30
|
+
return specSource.endsWith(".json") ? JSON.parse(content) : yaml.parse(content);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof Error) {
|
|
33
|
+
throw new Error(`Failed to load OpenAPI spec: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
throw new Error("Failed to load OpenAPI spec: Unknown error");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Main function to generate the API client
|
|
41
|
+
*/
|
|
42
|
+
export async function codegenerate(config: OpenAPIConfig): Promise<void> {
|
|
43
|
+
try {
|
|
44
|
+
// Load specs
|
|
45
|
+
const specs = Array.isArray(config.specSource)
|
|
46
|
+
? await Promise.all(config.specSource.map(loadOpenAPISpec))
|
|
47
|
+
: [await loadOpenAPISpec(config.specSource)];
|
|
48
|
+
|
|
49
|
+
// Create export directory if it doesn't exist
|
|
50
|
+
await mkdir(config.exportDir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
// Generate and write type definitions
|
|
53
|
+
const instance = generateInstance();
|
|
54
|
+
await writeFile(resolve(config.exportDir, "apiClient.ts"), instance, "utf-8");
|
|
55
|
+
|
|
56
|
+
// Generate for each spec...
|
|
57
|
+
for (const spec of specs) {
|
|
58
|
+
const title = specTitle(spec);
|
|
59
|
+
|
|
60
|
+
// Generate and write type definitions
|
|
61
|
+
const typeDefinitions = generateTypeDefinitions(spec);
|
|
62
|
+
await writeFile(resolve(config.exportDir, `${title}.schema.ts`), typeDefinitions, "utf-8");
|
|
63
|
+
|
|
64
|
+
// Generate and write API client
|
|
65
|
+
const clientCode = generateApiClient(spec, config);
|
|
66
|
+
await writeFile(resolve(config.exportDir, `${title}.client.ts`), clientCode, "utf-8");
|
|
67
|
+
|
|
68
|
+
// Generate and write React Query options
|
|
69
|
+
const queryCode = generateReactQuery(spec);
|
|
70
|
+
await writeFile(resolve(config.exportDir, `${title}.queryOptions.ts`), queryCode, "utf-8");
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof Error) {
|
|
74
|
+
throw new Error(`Failed to generate API client: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
throw new Error("Failed to generate API client: Unknown error");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface OpenAPIConfig {
|
|
2
|
+
/**
|
|
3
|
+
* OpenAPI specification source - can be a URL or local file path
|
|
4
|
+
* Supports JSON, YAML, or YML formats
|
|
5
|
+
*/
|
|
6
|
+
specSource: string | string[];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Directory where generated files will be exported
|
|
10
|
+
*/
|
|
11
|
+
exportDir: string;
|
|
12
|
+
}
|