chowbea-axios 1.0.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/LICENSE +22 -0
- package/README.md +162 -0
- package/bin/dev.js +13 -0
- package/bin/run.js +10 -0
- package/dist/commands/diff.d.ts +31 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +215 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/fetch.d.ts +28 -0
- package/dist/commands/fetch.d.ts.map +1 -0
- package/dist/commands/fetch.js +223 -0
- package/dist/commands/fetch.js.map +1 -0
- package/dist/commands/generate.d.ts +26 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +187 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +92 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +738 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/status.d.ts +38 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +233 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/validate.d.ts +27 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +209 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/watch.d.ts +34 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +202 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +151 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +336 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/errors.d.ts +77 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +144 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/fetcher.d.ts +115 -0
- package/dist/lib/fetcher.d.ts.map +1 -0
- package/dist/lib/fetcher.js +237 -0
- package/dist/lib/fetcher.js.map +1 -0
- package/dist/lib/generator.d.ts +96 -0
- package/dist/lib/generator.d.ts.map +1 -0
- package/dist/lib/generator.js +1575 -0
- package/dist/lib/generator.js.map +1 -0
- package/dist/lib/logger.d.ts +63 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +183 -0
- package/dist/lib/logger.js.map +1 -0
- package/oclif.manifest.json +556 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core generation logic for TypeScript types and operations.
|
|
3
|
+
* Migrated from scripts/generate-operations.js with atomic writes and rollback.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { access, copyFile, readFile, rename, unlink, writeFile, } from "node:fs/promises";
|
|
7
|
+
import { GenerationError } from "./errors.js";
|
|
8
|
+
/**
|
|
9
|
+
* Extracts path parameter names from an OpenAPI path template.
|
|
10
|
+
* Example: "/api/users/{id}/posts/{postId}" -> ["id", "postId"]
|
|
11
|
+
*/
|
|
12
|
+
function extractPathParams(pathTemplate) {
|
|
13
|
+
const matches = pathTemplate.matchAll(/\{([^}]+)\}/g);
|
|
14
|
+
return [...matches].map((match) => match[1]);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Converts a path parameter name to camelCase.
|
|
18
|
+
* Example: "user_id" -> "userId", "user-id" -> "userId"
|
|
19
|
+
*/
|
|
20
|
+
function toCamelCase(str) {
|
|
21
|
+
return str.replace(/[-_]([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generates TypeScript type for path parameters.
|
|
25
|
+
*/
|
|
26
|
+
function generatePathParamsType(pathParams) {
|
|
27
|
+
if (pathParams.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
const params = pathParams
|
|
30
|
+
.map((param) => `${toCamelCase(param)}: string | number`)
|
|
31
|
+
.join(", ");
|
|
32
|
+
return `{ ${params} }`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Generates a single operation function.
|
|
36
|
+
*/
|
|
37
|
+
function generateOperationFunction(operation) {
|
|
38
|
+
const { operationId, method, path: pathTemplate, pathParams, hasRequestBody, summary, description, } = operation;
|
|
39
|
+
const httpMethod = method.toLowerCase();
|
|
40
|
+
// Build parameter list
|
|
41
|
+
const params = [];
|
|
42
|
+
// Path parameters
|
|
43
|
+
if (pathParams.length > 0) {
|
|
44
|
+
const pathParamsType = generatePathParamsType(pathParams);
|
|
45
|
+
params.push(`pathParams: ${pathParamsType}`);
|
|
46
|
+
}
|
|
47
|
+
// Request body for POST/PUT/PATCH
|
|
48
|
+
if (hasRequestBody) {
|
|
49
|
+
params.push(`data: RequestBody<"${pathTemplate}", "${httpMethod}">`);
|
|
50
|
+
}
|
|
51
|
+
// Config parameter (always last and optional)
|
|
52
|
+
params.push(`config?: RequestConfig<"${pathTemplate}", "${httpMethod}">`);
|
|
53
|
+
// Generate JSDoc comment
|
|
54
|
+
const jsdoc = [];
|
|
55
|
+
jsdoc.push(" /**");
|
|
56
|
+
if (summary) {
|
|
57
|
+
jsdoc.push(` * ${summary}`);
|
|
58
|
+
}
|
|
59
|
+
if (description && description !== summary) {
|
|
60
|
+
jsdoc.push(` * ${description}`);
|
|
61
|
+
}
|
|
62
|
+
jsdoc.push(" * ");
|
|
63
|
+
jsdoc.push(` * @operationId ${operationId}`);
|
|
64
|
+
jsdoc.push(` * @method ${method.toUpperCase()}`);
|
|
65
|
+
jsdoc.push(` * @path ${pathTemplate}`);
|
|
66
|
+
jsdoc.push(" */");
|
|
67
|
+
// Generate function with explicit return type - uses Result<T> for consistent error handling
|
|
68
|
+
const functionParams = params.join(", ");
|
|
69
|
+
const returnType = `Promise<Result<ResponseData<"${pathTemplate}", "${httpMethod}">>>`;
|
|
70
|
+
// Build the apiClient call (apiClient methods already return Result<T>)
|
|
71
|
+
let apiCall;
|
|
72
|
+
if (hasRequestBody) {
|
|
73
|
+
// POST/PUT/PATCH with body
|
|
74
|
+
if (pathParams.length > 0) {
|
|
75
|
+
apiCall = `apiClient.${httpMethod}("${pathTemplate}", data, pathParams, config)`;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
apiCall = `apiClient.${httpMethod}("${pathTemplate}", data, config)`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// GET/DELETE without body
|
|
83
|
+
// PATCH without body still needs undefined as data parameter
|
|
84
|
+
if (httpMethod === "patch") {
|
|
85
|
+
if (pathParams.length > 0) {
|
|
86
|
+
apiCall = `apiClient.${httpMethod}("${pathTemplate}", undefined, pathParams, config)`;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
apiCall = `apiClient.${httpMethod}("${pathTemplate}", undefined, config)`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (pathParams.length > 0) {
|
|
93
|
+
apiCall = `apiClient.${httpMethod}("${pathTemplate}", pathParams, config)`;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
apiCall = `apiClient.${httpMethod}("${pathTemplate}", config)`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return `${jsdoc.join("\n")}
|
|
100
|
+
${operationId}: (${functionParams}): ${returnType} => ${apiCall},\n`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Parses the OpenAPI spec and extracts all operations with operationIds.
|
|
104
|
+
*/
|
|
105
|
+
function parseOperations(spec, logger) {
|
|
106
|
+
const operations = [];
|
|
107
|
+
if (typeof spec !== "object" || spec === null) {
|
|
108
|
+
return operations;
|
|
109
|
+
}
|
|
110
|
+
const specObj = spec;
|
|
111
|
+
const paths = specObj.paths;
|
|
112
|
+
if (!paths) {
|
|
113
|
+
return operations;
|
|
114
|
+
}
|
|
115
|
+
// Iterate through all paths
|
|
116
|
+
for (const [pathTemplate, pathItem] of Object.entries(paths)) {
|
|
117
|
+
// Iterate through all HTTP methods
|
|
118
|
+
for (const method of ["get", "post", "put", "delete", "patch"]) {
|
|
119
|
+
const operation = pathItem[method];
|
|
120
|
+
if (!operation)
|
|
121
|
+
continue;
|
|
122
|
+
// Skip operations without operationId
|
|
123
|
+
if (!operation.operationId || typeof operation.operationId !== "string") {
|
|
124
|
+
logger.warn({ method: method.toUpperCase(), path: pathTemplate }, "Skipping operation without operationId");
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Extract path parameters
|
|
128
|
+
const pathParams = extractPathParams(pathTemplate);
|
|
129
|
+
// Check for request body
|
|
130
|
+
const hasRequestBody = Boolean(operation.requestBody);
|
|
131
|
+
// Check for query parameters
|
|
132
|
+
const parameters = operation.parameters;
|
|
133
|
+
const hasQueryParams = parameters?.some((param) => param.in === "query") ?? false;
|
|
134
|
+
operations.push({
|
|
135
|
+
operationId: operation.operationId,
|
|
136
|
+
method,
|
|
137
|
+
path: pathTemplate,
|
|
138
|
+
pathParams,
|
|
139
|
+
hasRequestBody,
|
|
140
|
+
hasQueryParams,
|
|
141
|
+
summary: operation.summary ?? "",
|
|
142
|
+
description: operation.description ?? "",
|
|
143
|
+
});
|
|
144
|
+
logger.debug({ operationId: operation.operationId }, "Found operation");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return operations;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Generates the TypeScript file content with all operations.
|
|
151
|
+
*/
|
|
152
|
+
function generateOperationsFileContent(operations) {
|
|
153
|
+
const header = `/**
|
|
154
|
+
* Auto-generated API operations from OpenAPI spec.
|
|
155
|
+
*
|
|
156
|
+
* This file is automatically generated by chowbea-axios CLI.
|
|
157
|
+
* DO NOT EDIT MANUALLY - your changes will be overwritten.
|
|
158
|
+
*
|
|
159
|
+
* Last generated: ${new Date().toISOString()}
|
|
160
|
+
* Total operations: ${operations.length}
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
/* ~ =================================== ~ */
|
|
164
|
+
/* -- This file provides semantic operation-based API functions -- */
|
|
165
|
+
/* -- Use apiClient.op.operationName() instead of raw paths -- */
|
|
166
|
+
/* ~ =================================== ~ */
|
|
167
|
+
|
|
168
|
+
import type { paths } from "./api.types"
|
|
169
|
+
import type { AxiosRequestConfig } from "axios"
|
|
170
|
+
import type { Result } from "../api.error"
|
|
171
|
+
|
|
172
|
+
/* ~ =================================== ~ */
|
|
173
|
+
/* -- Type Helpers -- */
|
|
174
|
+
/* ~ =================================== ~ */
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extracts the operation definition for a given path and method.
|
|
178
|
+
*/
|
|
179
|
+
type Operation<
|
|
180
|
+
P extends keyof paths,
|
|
181
|
+
M extends "get" | "post" | "put" | "delete" | "patch"
|
|
182
|
+
> = paths[P][M]
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Maps OpenAPI form-data field types to their runtime equivalents.
|
|
186
|
+
* Uses field names to intelligently detect file upload fields vs regular string fields.
|
|
187
|
+
*
|
|
188
|
+
* File field patterns: images, files, attachments, uploads, documents, photos, videos, media
|
|
189
|
+
* Regular string fields: All other string/string[] fields remain unchanged
|
|
190
|
+
*/
|
|
191
|
+
type MapFormDataTypes<T> = T extends Record<string, unknown>
|
|
192
|
+
? {
|
|
193
|
+
[K in keyof T]:
|
|
194
|
+
// Check if field name suggests it's a file upload field
|
|
195
|
+
K extends \`\${string}image\${string}\` | \`\${string}file\${string}\` | \`\${string}attachment\${string}\` |
|
|
196
|
+
\`\${string}upload\${string}\` | \`\${string}document\${string}\` | \`\${string}photo\${string}\` |
|
|
197
|
+
\`\${string}video\${string}\` | \`\${string}media\${string}\`
|
|
198
|
+
? T[K] extends string[]
|
|
199
|
+
? File[] // File upload fields become File[]
|
|
200
|
+
: T[K] extends string
|
|
201
|
+
? File | Blob // Single file becomes File or Blob
|
|
202
|
+
: T[K]
|
|
203
|
+
: T[K]; // Non-file fields keep their original type
|
|
204
|
+
}
|
|
205
|
+
: T;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Extracts the request body type for a given path and method directly from OpenAPI spec.
|
|
209
|
+
* Handles both JSON (application/json) and FormData (multipart/form-data) content types.
|
|
210
|
+
* For form-data, maps string/string[] types to File/File[] for file upload fields.
|
|
211
|
+
* Converts Record<string, never> (from generic object schemas) to Record<string, unknown>.
|
|
212
|
+
*/
|
|
213
|
+
type RequestBody<
|
|
214
|
+
P extends keyof paths,
|
|
215
|
+
M extends "get" | "post" | "put" | "delete" | "patch"
|
|
216
|
+
> =
|
|
217
|
+
Operation<P, M> extends { requestBody: { content: { "application/json": infer T } } }
|
|
218
|
+
? T extends Record<string, never>
|
|
219
|
+
? Record<string, unknown>
|
|
220
|
+
: T
|
|
221
|
+
: Operation<P, M> extends { requestBody?: { content: { "application/json": infer T } } }
|
|
222
|
+
? T extends Record<string, never>
|
|
223
|
+
? Record<string, unknown>
|
|
224
|
+
: T
|
|
225
|
+
: Operation<P, M> extends { requestBody: { content: { "multipart/form-data": infer T } } }
|
|
226
|
+
? MapFormDataTypes<T>
|
|
227
|
+
: Operation<P, M> extends { requestBody?: { content: { "multipart/form-data": infer T } } }
|
|
228
|
+
? MapFormDataTypes<T>
|
|
229
|
+
: never
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Extracts the response data type for a given path and method from OpenAPI spec.
|
|
233
|
+
* Defaults to 200 status code response.
|
|
234
|
+
*/
|
|
235
|
+
type ResponseData<
|
|
236
|
+
P extends keyof paths,
|
|
237
|
+
M extends "get" | "post" | "put" | "delete" | "patch"
|
|
238
|
+
> = Operation<P, M> extends {
|
|
239
|
+
responses: { 200: { content: { "application/json": infer T } } }
|
|
240
|
+
}
|
|
241
|
+
? T
|
|
242
|
+
: Operation<P, M> extends {
|
|
243
|
+
responses: { 201: { content: { "application/json": infer T } } }
|
|
244
|
+
}
|
|
245
|
+
? T
|
|
246
|
+
: unknown
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Extracts query parameters for a given path and method from OpenAPI spec.
|
|
250
|
+
*/
|
|
251
|
+
type QueryParams<
|
|
252
|
+
P extends keyof paths,
|
|
253
|
+
M extends "get" | "post" | "put" | "delete" | "patch"
|
|
254
|
+
> = Operation<P, M> extends { parameters: { query?: infer Q } }
|
|
255
|
+
? Q extends Record<string, unknown>
|
|
256
|
+
? Q
|
|
257
|
+
: never
|
|
258
|
+
: never
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Request config with typed query parameters extracted from OpenAPI spec.
|
|
262
|
+
*/
|
|
263
|
+
type RequestConfig<
|
|
264
|
+
P extends keyof paths,
|
|
265
|
+
M extends "get" | "post" | "put" | "delete" | "patch"
|
|
266
|
+
> = Omit<AxiosRequestConfig, "params"> & {
|
|
267
|
+
params?: QueryParams<P, M>
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* ~ =================================== ~ */
|
|
271
|
+
/* -- Generated Operations -- */
|
|
272
|
+
/* ~ =================================== ~ */
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Collection of all API operations extracted from the OpenAPI spec.
|
|
276
|
+
* Each operation is a typed function that wraps the underlying apiClient methods.
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* \`\`\`typescript
|
|
280
|
+
* // Using operation-based API
|
|
281
|
+
* await apiClient.op.getUserById({ id: "123" })
|
|
282
|
+
*
|
|
283
|
+
* // With query parameters
|
|
284
|
+
* await apiClient.op.listUsers({ params: { limit: 10, offset: 0 } })
|
|
285
|
+
*
|
|
286
|
+
* // With request body
|
|
287
|
+
* await apiClient.op.createUser({ name: "John", email: "john@example.com" })
|
|
288
|
+
* \`\`\`
|
|
289
|
+
*/
|
|
290
|
+
export const createOperations = (apiClient: any) => ({
|
|
291
|
+
`;
|
|
292
|
+
const operationFunctions = operations
|
|
293
|
+
.map((op) => generateOperationFunction(op))
|
|
294
|
+
.join("\n");
|
|
295
|
+
const footer = `}) as const
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Type representing all available API operations.
|
|
299
|
+
* This type is inferred from the createOperations return value for proper TypeScript support.
|
|
300
|
+
*/
|
|
301
|
+
export type ApiOperations = ReturnType<typeof createOperations>
|
|
302
|
+
`;
|
|
303
|
+
return header + operationFunctions + footer;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Runs openapi-typescript to generate base types.
|
|
307
|
+
* Uses pnpm dlx to avoid requiring it as a direct dependency.
|
|
308
|
+
*/
|
|
309
|
+
async function generateTypes(specPath, typesPath, logger) {
|
|
310
|
+
logger.info({ specPath, typesPath }, "Generating TypeScript types...");
|
|
311
|
+
const result = spawnSync("pnpm", ["dlx", "openapi-typescript", specPath, "--output", typesPath], {
|
|
312
|
+
stdio: "pipe",
|
|
313
|
+
cwd: process.cwd(),
|
|
314
|
+
});
|
|
315
|
+
if (result.error) {
|
|
316
|
+
throw new GenerationError("openapi-typescript", `Failed to spawn: ${result.error.message}`);
|
|
317
|
+
}
|
|
318
|
+
if (result.status !== 0) {
|
|
319
|
+
const stderr = result.stderr?.toString() ?? "Unknown error";
|
|
320
|
+
throw new GenerationError("openapi-typescript", `Exited with code ${result.status}: ${stderr}`);
|
|
321
|
+
}
|
|
322
|
+
logger.info("TypeScript types generated successfully");
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Atomic write - writes to temp file then renames.
|
|
326
|
+
* This ensures the file is never in a partial state.
|
|
327
|
+
*/
|
|
328
|
+
async function atomicWrite(filePath, content) {
|
|
329
|
+
const tempPath = `${filePath}.tmp.${Date.now()}`;
|
|
330
|
+
try {
|
|
331
|
+
await writeFile(tempPath, content, "utf8");
|
|
332
|
+
await rename(tempPath, filePath);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
// Clean up temp file if rename fails
|
|
336
|
+
try {
|
|
337
|
+
await unlink(tempPath);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// Ignore cleanup errors
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Creates a backup of existing generated files for rollback.
|
|
347
|
+
*/
|
|
348
|
+
async function createBackup(paths) {
|
|
349
|
+
const timestamp = Date.now();
|
|
350
|
+
let typesBackup = null;
|
|
351
|
+
let operationsBackup = null;
|
|
352
|
+
try {
|
|
353
|
+
const typesBackupPath = `${paths.types}.backup.${timestamp}`;
|
|
354
|
+
await copyFile(paths.types, typesBackupPath);
|
|
355
|
+
typesBackup = typesBackupPath;
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// No existing types file to backup
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const operationsBackupPath = `${paths.operations}.backup.${timestamp}`;
|
|
362
|
+
await copyFile(paths.operations, operationsBackupPath);
|
|
363
|
+
operationsBackup = operationsBackupPath;
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// No existing operations file to backup
|
|
367
|
+
}
|
|
368
|
+
return { typesBackup, operationsBackup };
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Restores files from backup on generation failure.
|
|
372
|
+
*/
|
|
373
|
+
async function restoreFromBackup(backups, paths) {
|
|
374
|
+
if (backups.typesBackup) {
|
|
375
|
+
try {
|
|
376
|
+
await rename(backups.typesBackup, paths.types);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// Ignore restore errors
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (backups.operationsBackup) {
|
|
383
|
+
try {
|
|
384
|
+
await rename(backups.operationsBackup, paths.operations);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Ignore restore errors
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Cleans up backup files after successful generation.
|
|
393
|
+
*/
|
|
394
|
+
async function cleanupBackups(backups) {
|
|
395
|
+
if (backups.typesBackup) {
|
|
396
|
+
try {
|
|
397
|
+
await unlink(backups.typesBackup);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Ignore cleanup errors
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (backups.operationsBackup) {
|
|
404
|
+
try {
|
|
405
|
+
await unlink(backups.operationsBackup);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// Ignore cleanup errors
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Checks if a file exists using access check (more efficient than reading).
|
|
414
|
+
*/
|
|
415
|
+
async function fileExists(filePath) {
|
|
416
|
+
try {
|
|
417
|
+
await access(filePath);
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/* ~ =================================== ~ */
|
|
425
|
+
/* -- Client File Templates -- */
|
|
426
|
+
/* ~ =================================== ~ */
|
|
427
|
+
/**
|
|
428
|
+
* Generates the api.helpers.ts file content.
|
|
429
|
+
* Contains all utility types for extracting request/response types from OpenAPI schema.
|
|
430
|
+
*/
|
|
431
|
+
export function generateHelpersFileContent() {
|
|
432
|
+
return `/**
|
|
433
|
+
* Type utilities for extracting request/response types from OpenAPI schema.
|
|
434
|
+
*
|
|
435
|
+
* This file is generated once by chowbea-axios CLI.
|
|
436
|
+
* You can safely modify this file - it will NOT be overwritten.
|
|
437
|
+
*
|
|
438
|
+
* Generated: ${new Date().toISOString()}
|
|
439
|
+
*/
|
|
440
|
+
|
|
441
|
+
import type { paths, components, operations } from "./_generated/api.types";
|
|
442
|
+
|
|
443
|
+
/* ~ =================================== ~ */
|
|
444
|
+
/* -- Base Types -- */
|
|
445
|
+
/* ~ =================================== ~ */
|
|
446
|
+
|
|
447
|
+
/** All path templates defined by the OpenAPI paths map. */
|
|
448
|
+
type Paths = keyof paths;
|
|
449
|
+
|
|
450
|
+
/** HTTP methods supported by the client. */
|
|
451
|
+
type HttpMethod = "get" | "post" | "put" | "delete" | "patch";
|
|
452
|
+
|
|
453
|
+
/** Resolves the OpenAPI operation schema for a given path and method. */
|
|
454
|
+
type Operation<P extends Paths, M extends HttpMethod> = paths[P][M];
|
|
455
|
+
|
|
456
|
+
/* ~ =================================== ~ */
|
|
457
|
+
/* -- Path Parameter Extraction -- */
|
|
458
|
+
/* ~ =================================== ~ */
|
|
459
|
+
|
|
460
|
+
/** Extracts placeholder parameter names from an OpenAPI-style path template. */
|
|
461
|
+
type ExtractPathParamNames<T extends string> =
|
|
462
|
+
T extends \`\${string}{\${infer P}}\${infer R}\`
|
|
463
|
+
? P | ExtractPathParamNames<R>
|
|
464
|
+
: never;
|
|
465
|
+
|
|
466
|
+
/** Maps extracted path parameter names to a simple serializable value type. */
|
|
467
|
+
type PathParams<P extends Paths> = ExtractPathParamNames<P & string> extends never
|
|
468
|
+
? never
|
|
469
|
+
: Record<ExtractPathParamNames<P & string>, string | number | boolean>;
|
|
470
|
+
|
|
471
|
+
/* ~ =================================== ~ */
|
|
472
|
+
/* -- Query Parameter Extraction -- */
|
|
473
|
+
/* ~ =================================== ~ */
|
|
474
|
+
|
|
475
|
+
/** Extracts query parameter types from the OpenAPI operation schema. */
|
|
476
|
+
type QueryParams<P extends Paths, M extends HttpMethod> = Operation<P, M> extends {
|
|
477
|
+
parameters: { query?: infer Q };
|
|
478
|
+
}
|
|
479
|
+
? Q extends Record<string, unknown>
|
|
480
|
+
? Q
|
|
481
|
+
: never
|
|
482
|
+
: never;
|
|
483
|
+
|
|
484
|
+
/* ~ =================================== ~ */
|
|
485
|
+
/* -- Request Body Extraction -- */
|
|
486
|
+
/* ~ =================================== ~ */
|
|
487
|
+
|
|
488
|
+
/** Maps OpenAPI form-data field types to their runtime equivalents. */
|
|
489
|
+
type MapFormDataTypes<T> = T extends Record<string, unknown>
|
|
490
|
+
? {
|
|
491
|
+
[K in keyof T]: K extends
|
|
492
|
+
| \`\${string}image\${string}\`
|
|
493
|
+
| \`\${string}file\${string}\`
|
|
494
|
+
| \`\${string}attachment\${string}\`
|
|
495
|
+
| \`\${string}upload\${string}\`
|
|
496
|
+
| \`\${string}document\${string}\`
|
|
497
|
+
| \`\${string}photo\${string}\`
|
|
498
|
+
| \`\${string}video\${string}\`
|
|
499
|
+
| \`\${string}media\${string}\`
|
|
500
|
+
? T[K] extends string[]
|
|
501
|
+
? File[]
|
|
502
|
+
: T[K] extends string
|
|
503
|
+
? File | Blob
|
|
504
|
+
: T[K]
|
|
505
|
+
: T[K];
|
|
506
|
+
}
|
|
507
|
+
: T;
|
|
508
|
+
|
|
509
|
+
/** Infers the request body type for a given path/method pair from OpenAPI. */
|
|
510
|
+
type RequestBody<P extends Paths, M extends HttpMethod> = Operation<P, M> extends {
|
|
511
|
+
requestBody: { content: { "application/json": infer T } };
|
|
512
|
+
}
|
|
513
|
+
? T extends Record<string, never>
|
|
514
|
+
? Record<string, unknown>
|
|
515
|
+
: T
|
|
516
|
+
: Operation<P, M> extends { requestBody?: { content: { "application/json": infer T } } }
|
|
517
|
+
? T extends Record<string, never>
|
|
518
|
+
? Record<string, unknown>
|
|
519
|
+
: T
|
|
520
|
+
: Operation<P, M> extends { requestBody: { content: { "multipart/form-data": infer T } } }
|
|
521
|
+
? MapFormDataTypes<T>
|
|
522
|
+
: Operation<P, M> extends { requestBody?: { content: { "multipart/form-data": infer T } } }
|
|
523
|
+
? MapFormDataTypes<T>
|
|
524
|
+
: never;
|
|
525
|
+
|
|
526
|
+
/* ~ =================================== ~ */
|
|
527
|
+
/* -- Response Data Extraction -- */
|
|
528
|
+
/* ~ =================================== ~ */
|
|
529
|
+
|
|
530
|
+
/** Extracts all available status codes from an operation's responses. */
|
|
531
|
+
type AvailableStatusCodes<P extends Paths, M extends HttpMethod> = Operation<P, M> extends {
|
|
532
|
+
responses: infer R;
|
|
533
|
+
}
|
|
534
|
+
? R extends Record<string, unknown>
|
|
535
|
+
? keyof R & number
|
|
536
|
+
: never
|
|
537
|
+
: never;
|
|
538
|
+
|
|
539
|
+
/** Infers the JSON response body type for a given status code from OpenAPI. */
|
|
540
|
+
type ResponseData<
|
|
541
|
+
P extends Paths,
|
|
542
|
+
M extends HttpMethod,
|
|
543
|
+
Status extends AvailableStatusCodes<P, M> = 200 extends AvailableStatusCodes<P, M>
|
|
544
|
+
? 200
|
|
545
|
+
: AvailableStatusCodes<P, M>,
|
|
546
|
+
> = Operation<P, M> extends {
|
|
547
|
+
responses: { [K in Status]: { content: { "application/json": infer T } } };
|
|
548
|
+
}
|
|
549
|
+
? T
|
|
550
|
+
: never;
|
|
551
|
+
|
|
552
|
+
/* ~ =================================== ~ */
|
|
553
|
+
/* -- Intellisense Helpers -- */
|
|
554
|
+
/* ~ =================================== ~ */
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Forces TypeScript to expand and display the full type structure.
|
|
558
|
+
* Improves intellisense by showing actual type properties instead of type references.
|
|
559
|
+
*/
|
|
560
|
+
type Expand<T> = T extends (...args: infer A) => infer R
|
|
561
|
+
? (...args: Expand<A>) => Expand<R>
|
|
562
|
+
: T extends object
|
|
563
|
+
? T extends infer O
|
|
564
|
+
? { [K in keyof O]: O[K] }
|
|
565
|
+
: never
|
|
566
|
+
: T;
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Recursively expands nested types for better intellisense.
|
|
570
|
+
* Expands all levels of nested objects to show full type structure.
|
|
571
|
+
*/
|
|
572
|
+
type ExpandRecursively<T> = T extends (...args: infer A) => infer R
|
|
573
|
+
? (...args: ExpandRecursively<A>) => ExpandRecursively<R>
|
|
574
|
+
: T extends object
|
|
575
|
+
? T extends infer O
|
|
576
|
+
? { [K in keyof O]: ExpandRecursively<O[K]> }
|
|
577
|
+
: never
|
|
578
|
+
: T;
|
|
579
|
+
|
|
580
|
+
/* ~ =================================== ~ */
|
|
581
|
+
/* -- Path-Based API Type Helpers -- */
|
|
582
|
+
/* ~ =================================== ~ */
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Extract request body type for a given path and method.
|
|
586
|
+
* @example type CreateUserInput = ApiRequestBody<"/api/users", "post">
|
|
587
|
+
*/
|
|
588
|
+
export type ApiRequestBody<P extends Paths, M extends HttpMethod> = ExpandRecursively<
|
|
589
|
+
RequestBody<P, M>
|
|
590
|
+
>;
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Extract response data type for a given path, method, and status code.
|
|
594
|
+
* @example type UserResponse = ApiResponseData<"/api/users/{id}", "get">
|
|
595
|
+
* @example type CreatedResponse = ApiResponseData<"/api/users", "post", 201>
|
|
596
|
+
*/
|
|
597
|
+
export type ApiResponseData<
|
|
598
|
+
P extends Paths,
|
|
599
|
+
M extends HttpMethod,
|
|
600
|
+
Status extends AvailableStatusCodes<P, M> = 200 extends AvailableStatusCodes<P, M>
|
|
601
|
+
? 200
|
|
602
|
+
: AvailableStatusCodes<P, M>,
|
|
603
|
+
> = ExpandRecursively<ResponseData<P, M, Status>>;
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Extract path parameters for a given path.
|
|
607
|
+
* @example type UserPathParams = ApiPathParams<"/api/users/{id}">
|
|
608
|
+
*/
|
|
609
|
+
export type ApiPathParams<P extends Paths> = ExpandRecursively<PathParams<P>>;
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Extract query parameters for a given path and method.
|
|
613
|
+
* @example type ListUsersQuery = ApiQueryParams<"/api/users", "get">
|
|
614
|
+
*/
|
|
615
|
+
export type ApiQueryParams<P extends Paths, M extends HttpMethod> = ExpandRecursively<
|
|
616
|
+
QueryParams<P, M>
|
|
617
|
+
>;
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Get all available status codes for a given path and method.
|
|
621
|
+
* @example type UserStatusCodes = ApiStatusCodes<"/api/users/{id}", "get">
|
|
622
|
+
*/
|
|
623
|
+
export type ApiStatusCodes<P extends Paths, M extends HttpMethod> = AvailableStatusCodes<P, M>;
|
|
624
|
+
|
|
625
|
+
/* ~ =================================== ~ */
|
|
626
|
+
/* -- Operation-Based API Type Helpers -- */
|
|
627
|
+
/* ~ =================================== ~ */
|
|
628
|
+
|
|
629
|
+
/** Extracts all available status codes from an operation's responses by operation ID. */
|
|
630
|
+
type OperationStatusCodes<OpId extends keyof operations> = operations[OpId] extends {
|
|
631
|
+
responses: infer R;
|
|
632
|
+
}
|
|
633
|
+
? R extends Record<string, unknown>
|
|
634
|
+
? keyof R & number
|
|
635
|
+
: never
|
|
636
|
+
: never;
|
|
637
|
+
|
|
638
|
+
/** Determines the default positive status code for an operation. */
|
|
639
|
+
type OperationPositiveStatus<OpId extends keyof operations> =
|
|
640
|
+
200 extends OperationStatusCodes<OpId>
|
|
641
|
+
? 200
|
|
642
|
+
: 201 extends OperationStatusCodes<OpId>
|
|
643
|
+
? 201
|
|
644
|
+
: 202 extends OperationStatusCodes<OpId>
|
|
645
|
+
? 202
|
|
646
|
+
: 204 extends OperationStatusCodes<OpId>
|
|
647
|
+
? 204
|
|
648
|
+
: OperationStatusCodes<OpId>;
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Extract request body type by operation ID.
|
|
652
|
+
* @example type CreateUserInput = ServerRequestBody<"createUser">
|
|
653
|
+
*/
|
|
654
|
+
export type ServerRequestBody<OpId extends keyof operations> = ExpandRecursively<
|
|
655
|
+
operations[OpId] extends { requestBody: { content: { "application/json": infer T } } }
|
|
656
|
+
? T extends Record<string, never>
|
|
657
|
+
? Record<string, unknown>
|
|
658
|
+
: T
|
|
659
|
+
: operations[OpId] extends { requestBody?: { content: { "application/json": infer T } } }
|
|
660
|
+
? T extends Record<string, never>
|
|
661
|
+
? Record<string, unknown>
|
|
662
|
+
: T
|
|
663
|
+
: never
|
|
664
|
+
>;
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Extract request parameters (path and query) by operation ID.
|
|
668
|
+
* @example type GetUserParams = ServerRequestParams<"getUserById">
|
|
669
|
+
*/
|
|
670
|
+
export type ServerRequestParams<OpId extends keyof operations> = ExpandRecursively<
|
|
671
|
+
operations[OpId] extends { parameters: infer P }
|
|
672
|
+
? P extends { path?: infer Path; query?: infer Query }
|
|
673
|
+
? (Path extends Record<string, unknown> ? { path: Path } : Record<string, never>) &
|
|
674
|
+
(Query extends Record<string, unknown> ? { query?: Query } : Record<string, never>)
|
|
675
|
+
: P extends { path?: infer Path }
|
|
676
|
+
? Path extends Record<string, unknown>
|
|
677
|
+
? { path: Path }
|
|
678
|
+
: never
|
|
679
|
+
: P extends { query?: infer Query }
|
|
680
|
+
? Query extends Record<string, unknown>
|
|
681
|
+
? { query?: Query }
|
|
682
|
+
: never
|
|
683
|
+
: never
|
|
684
|
+
: never
|
|
685
|
+
>;
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Extract response type by operation ID with optional status code.
|
|
689
|
+
* Defaults to the positive status code (200, 201, 202, or 204).
|
|
690
|
+
* @example type UserResponse = ServerResponseType<"getUserById">
|
|
691
|
+
* @example type NotFoundResponse = ServerResponseType<"getUserById", 404>
|
|
692
|
+
*/
|
|
693
|
+
export type ServerResponseType<
|
|
694
|
+
OpId extends keyof operations,
|
|
695
|
+
Status extends OperationStatusCodes<OpId> = OperationPositiveStatus<OpId>,
|
|
696
|
+
> = ExpandRecursively<
|
|
697
|
+
operations[OpId] extends {
|
|
698
|
+
responses: { [K in Status]: { content: { "application/json": infer T } } };
|
|
699
|
+
}
|
|
700
|
+
? T
|
|
701
|
+
: never
|
|
702
|
+
>;
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Extract model/schema type from OpenAPI components.
|
|
706
|
+
* @example type User = ServerModel<"UserContract">
|
|
707
|
+
* @example type Meeting = ServerModel<"MeetingContract">
|
|
708
|
+
*/
|
|
709
|
+
export type ServerModel<ModelName extends keyof components["schemas"]> = ExpandRecursively<
|
|
710
|
+
components["schemas"][ModelName]
|
|
711
|
+
>;
|
|
712
|
+
|
|
713
|
+
/* ~ =================================== ~ */
|
|
714
|
+
/* -- Re-exports for Convenience -- */
|
|
715
|
+
/* ~ =================================== ~ */
|
|
716
|
+
|
|
717
|
+
export type { Paths, HttpMethod, Expand, ExpandRecursively };
|
|
718
|
+
`;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Generates the api.instance.ts file content.
|
|
722
|
+
*/
|
|
723
|
+
export function generateInstanceFileContent(config) {
|
|
724
|
+
return `/**
|
|
725
|
+
* Axios instance with authentication interceptor.
|
|
726
|
+
*
|
|
727
|
+
* This file is generated once by chowbea-axios CLI.
|
|
728
|
+
* You can safely modify this file - it will NOT be overwritten.
|
|
729
|
+
*
|
|
730
|
+
* Generated: ${new Date().toISOString()}
|
|
731
|
+
*/
|
|
732
|
+
|
|
733
|
+
import axios from "axios";
|
|
734
|
+
|
|
735
|
+
/** localStorage key for auth token */
|
|
736
|
+
export const tokenKey = "${config.token_key}";
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Shared Axios instance configured with the API base URL.
|
|
740
|
+
*/
|
|
741
|
+
export const axiosInstance = axios.create({
|
|
742
|
+
baseURL: import.meta.env.${config.base_url_env},
|
|
743
|
+
withCredentials: ${config.with_credentials},
|
|
744
|
+
timeout: ${config.timeout},
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Request interceptor that automatically attaches the auth token.
|
|
749
|
+
* Reads the token from localStorage and adds it to the Authorization header.
|
|
750
|
+
*/
|
|
751
|
+
axiosInstance.interceptors.request.use(
|
|
752
|
+
(config) => {
|
|
753
|
+
// Check if code is running in browser environment
|
|
754
|
+
if (typeof window !== "undefined") {
|
|
755
|
+
const tokenObject = localStorage.getItem(tokenKey);
|
|
756
|
+
|
|
757
|
+
if (tokenObject) {
|
|
758
|
+
try {
|
|
759
|
+
// Handle both { state: { token } } and plain token string
|
|
760
|
+
const parsed = JSON.parse(tokenObject);
|
|
761
|
+
const token = parsed.state?.token || parsed.token || parsed;
|
|
762
|
+
if (typeof token === "string") {
|
|
763
|
+
config.headers.Authorization = \`Bearer \${token}\`;
|
|
764
|
+
}
|
|
765
|
+
} catch {
|
|
766
|
+
// If not JSON, use as-is
|
|
767
|
+
config.headers.Authorization = \`Bearer \${tokenObject}\`;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return config;
|
|
773
|
+
},
|
|
774
|
+
(error) => Promise.reject(error)
|
|
775
|
+
);
|
|
776
|
+
`;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Generates the api.error.ts file content.
|
|
780
|
+
*/
|
|
781
|
+
export function generateErrorFileContent() {
|
|
782
|
+
return `/**
|
|
783
|
+
* Result-based error handling for API calls.
|
|
784
|
+
*
|
|
785
|
+
* This file is generated once by chowbea-axios CLI.
|
|
786
|
+
* You can safely modify this file - it will NOT be overwritten.
|
|
787
|
+
*
|
|
788
|
+
* Generated: ${new Date().toISOString()}
|
|
789
|
+
*/
|
|
790
|
+
|
|
791
|
+
import { AxiosError, type AxiosResponse } from "axios";
|
|
792
|
+
|
|
793
|
+
/* ~ =================================== ~ */
|
|
794
|
+
/* -- Types -- */
|
|
795
|
+
/* ~ =================================== ~ */
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Request context for debugging - what request caused the error.
|
|
799
|
+
*/
|
|
800
|
+
export interface RequestContext {
|
|
801
|
+
/** HTTP method (GET, POST, etc.) */
|
|
802
|
+
method: string;
|
|
803
|
+
/** URL that was called */
|
|
804
|
+
url: string;
|
|
805
|
+
/** Base URL from axios config */
|
|
806
|
+
baseURL?: string;
|
|
807
|
+
/** Query parameters */
|
|
808
|
+
params?: unknown;
|
|
809
|
+
/** Request body (sensitive fields redacted) */
|
|
810
|
+
data?: unknown;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Normalized API error with extracted message and metadata.
|
|
815
|
+
*/
|
|
816
|
+
export interface ApiError {
|
|
817
|
+
/** Human-readable error message */
|
|
818
|
+
message: string;
|
|
819
|
+
/** Error code (NETWORK_ERROR, VALIDATION_ERROR, etc.) */
|
|
820
|
+
code: string;
|
|
821
|
+
/** HTTP status code (null for network errors) */
|
|
822
|
+
status: number | null;
|
|
823
|
+
/** What request caused this error */
|
|
824
|
+
request: RequestContext;
|
|
825
|
+
/** Original error response body for debugging */
|
|
826
|
+
details?: unknown;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Result type - API calls return this instead of throwing.
|
|
831
|
+
* Success: { data: T, error: null }
|
|
832
|
+
* Failure: { data: null, error: ApiError }
|
|
833
|
+
*/
|
|
834
|
+
export type Result<T> =
|
|
835
|
+
| { data: T; error: null }
|
|
836
|
+
| { data: null; error: ApiError };
|
|
837
|
+
|
|
838
|
+
/* ~ =================================== ~ */
|
|
839
|
+
/* -- Error Normalization -- */
|
|
840
|
+
/* ~ =================================== ~ */
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Normalizes error messages from various API response formats.
|
|
844
|
+
* Handles common patterns from different backend frameworks.
|
|
845
|
+
*/
|
|
846
|
+
export function normalizeErrorMessage(error: unknown): string {
|
|
847
|
+
if (!error || typeof error !== "object") {
|
|
848
|
+
return "An unexpected error occurred";
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const e = error as Record<string, unknown>;
|
|
852
|
+
|
|
853
|
+
// Common: { message: "..." }
|
|
854
|
+
if (typeof e.message === "string") return e.message;
|
|
855
|
+
|
|
856
|
+
// .NET: { error: "..." } or { error: { message: "..." } }
|
|
857
|
+
if (e.error) {
|
|
858
|
+
if (typeof e.error === "string") return e.error;
|
|
859
|
+
if (typeof (e.error as Record<string, unknown>)?.message === "string") {
|
|
860
|
+
return (e.error as Record<string, unknown>).message as string;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Validation: { errors: [...] } or { errors: { field: [...] } }
|
|
865
|
+
if (e.errors) {
|
|
866
|
+
if (Array.isArray(e.errors)) {
|
|
867
|
+
const first = e.errors[0];
|
|
868
|
+
if (typeof first === "string") return first;
|
|
869
|
+
if (typeof first?.message === "string") return first.message;
|
|
870
|
+
} else if (typeof e.errors === "object") {
|
|
871
|
+
const firstField = Object.values(e.errors)[0];
|
|
872
|
+
if (Array.isArray(firstField) && firstField.length > 0) {
|
|
873
|
+
return String(firstField[0]);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// FastAPI: { detail: "..." } or { detail: [...] }
|
|
879
|
+
if (typeof e.detail === "string") return e.detail;
|
|
880
|
+
if (Array.isArray(e.detail) && e.detail[0]?.msg) {
|
|
881
|
+
return e.detail[0].msg;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// ASP.NET Problem Details: { title: "..." }
|
|
885
|
+
if (typeof e.title === "string") return e.title;
|
|
886
|
+
|
|
887
|
+
return "An unexpected error occurred";
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/* ~ =================================== ~ */
|
|
891
|
+
/* -- Request Context Extraction -- */
|
|
892
|
+
/* ~ =================================== ~ */
|
|
893
|
+
|
|
894
|
+
/** Fields that should be redacted from request data */
|
|
895
|
+
const SENSITIVE_FIELDS = [
|
|
896
|
+
"password",
|
|
897
|
+
"token",
|
|
898
|
+
"secret",
|
|
899
|
+
"authorization",
|
|
900
|
+
"apikey",
|
|
901
|
+
"api_key",
|
|
902
|
+
"access_token",
|
|
903
|
+
"refresh_token",
|
|
904
|
+
];
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Redacts sensitive fields from request data for safe logging.
|
|
908
|
+
*/
|
|
909
|
+
function redactSensitive(data: unknown): unknown {
|
|
910
|
+
if (!data || typeof data !== "object") return data;
|
|
911
|
+
|
|
912
|
+
const redacted = { ...(data as Record<string, unknown>) };
|
|
913
|
+
|
|
914
|
+
for (const key of Object.keys(redacted)) {
|
|
915
|
+
if (SENSITIVE_FIELDS.some((s) => key.toLowerCase().includes(s))) {
|
|
916
|
+
redacted[key] = "[REDACTED]";
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return redacted;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Extracts request context from AxiosError for debugging.
|
|
925
|
+
*/
|
|
926
|
+
function extractRequestContext(err: AxiosError): RequestContext {
|
|
927
|
+
const config = err.config;
|
|
928
|
+
|
|
929
|
+
return {
|
|
930
|
+
method: config?.method?.toUpperCase() || "UNKNOWN",
|
|
931
|
+
url: config?.url || "unknown",
|
|
932
|
+
baseURL: config?.baseURL,
|
|
933
|
+
params: config?.params,
|
|
934
|
+
data: redactSensitive(config?.data),
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/* ~ =================================== ~ */
|
|
939
|
+
/* -- Error Creation -- */
|
|
940
|
+
/* ~ =================================== ~ */
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Maps HTTP status codes to error codes.
|
|
944
|
+
*/
|
|
945
|
+
function getErrorCode(status: number): string {
|
|
946
|
+
if (status >= 500) return "SERVER_ERROR";
|
|
947
|
+
switch (status) {
|
|
948
|
+
case 400:
|
|
949
|
+
return "BAD_REQUEST";
|
|
950
|
+
case 401:
|
|
951
|
+
return "UNAUTHORIZED";
|
|
952
|
+
case 403:
|
|
953
|
+
return "FORBIDDEN";
|
|
954
|
+
case 404:
|
|
955
|
+
return "NOT_FOUND";
|
|
956
|
+
case 409:
|
|
957
|
+
return "CONFLICT";
|
|
958
|
+
case 422:
|
|
959
|
+
return "VALIDATION_ERROR";
|
|
960
|
+
case 429:
|
|
961
|
+
return "RATE_LIMITED";
|
|
962
|
+
default:
|
|
963
|
+
return "REQUEST_ERROR";
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Creates an ApiError from any error.
|
|
969
|
+
*/
|
|
970
|
+
export function createApiError(err: unknown): ApiError {
|
|
971
|
+
if (err instanceof AxiosError) {
|
|
972
|
+
const request = extractRequestContext(err);
|
|
973
|
+
|
|
974
|
+
// Network error (no response)
|
|
975
|
+
if (!err.response) {
|
|
976
|
+
return {
|
|
977
|
+
message: err.code === "ECONNABORTED"
|
|
978
|
+
? "Request timed out"
|
|
979
|
+
: "Network error - please check your connection",
|
|
980
|
+
code: err.code === "ECONNABORTED" ? "TIMEOUT" : "NETWORK_ERROR",
|
|
981
|
+
status: null,
|
|
982
|
+
request,
|
|
983
|
+
details: { code: err.code, message: err.message },
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Server responded with error
|
|
988
|
+
const status = err.response.status;
|
|
989
|
+
|
|
990
|
+
return {
|
|
991
|
+
message: normalizeErrorMessage(err.response.data),
|
|
992
|
+
code: getErrorCode(status),
|
|
993
|
+
status,
|
|
994
|
+
request,
|
|
995
|
+
details: err.response.data,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Unknown error
|
|
1000
|
+
return {
|
|
1001
|
+
message: err instanceof Error ? err.message : "An unexpected error occurred",
|
|
1002
|
+
code: "UNKNOWN_ERROR",
|
|
1003
|
+
status: null,
|
|
1004
|
+
request: { method: "UNKNOWN", url: "unknown" },
|
|
1005
|
+
details: err,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/* ~ =================================== ~ */
|
|
1010
|
+
/* -- Safe Request Wrapper -- */
|
|
1011
|
+
/* ~ =================================== ~ */
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Wraps an axios promise and returns a Result instead of throwing.
|
|
1015
|
+
*
|
|
1016
|
+
* @example
|
|
1017
|
+
* \`\`\`typescript
|
|
1018
|
+
* const { data, error } = await safeRequest(axios.get("/users"));
|
|
1019
|
+
* if (error) {
|
|
1020
|
+
* console.error(error.message);
|
|
1021
|
+
* return;
|
|
1022
|
+
* }
|
|
1023
|
+
* console.log(data);
|
|
1024
|
+
* \`\`\`
|
|
1025
|
+
*/
|
|
1026
|
+
export async function safeRequest<T>(
|
|
1027
|
+
promise: Promise<AxiosResponse<T>>
|
|
1028
|
+
): Promise<Result<T>> {
|
|
1029
|
+
try {
|
|
1030
|
+
const response = await promise;
|
|
1031
|
+
return { data: response.data, error: null };
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
return { data: null, error: createApiError(err) };
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Type guard to check if a result is successful.
|
|
1039
|
+
*/
|
|
1040
|
+
export function isSuccess<T>(result: Result<T>): result is { data: T; error: null } {
|
|
1041
|
+
return result.error === null;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Type guard to check if a result is an error.
|
|
1046
|
+
*/
|
|
1047
|
+
export function isError<T>(result: Result<T>): result is { data: null; error: ApiError } {
|
|
1048
|
+
return result.error !== null;
|
|
1049
|
+
}
|
|
1050
|
+
`;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Generates the api.client.ts file content.
|
|
1054
|
+
*/
|
|
1055
|
+
export function generateClientFileContent() {
|
|
1056
|
+
return `/**
|
|
1057
|
+
* Typed HTTP client for API.
|
|
1058
|
+
*
|
|
1059
|
+
* This file is generated once by chowbea-axios CLI.
|
|
1060
|
+
* You can safely modify this file - it will NOT be overwritten.
|
|
1061
|
+
*
|
|
1062
|
+
* Generated: ${new Date().toISOString()}
|
|
1063
|
+
*/
|
|
1064
|
+
|
|
1065
|
+
import type { AxiosRequestConfig, AxiosResponse } from "axios";
|
|
1066
|
+
|
|
1067
|
+
import { axiosInstance } from "./api.instance";
|
|
1068
|
+
import { safeRequest, type Result } from "./api.error";
|
|
1069
|
+
import type { paths, components, operations } from "./_generated/api.types";
|
|
1070
|
+
import { createOperations } from "./_generated/api.operations";
|
|
1071
|
+
|
|
1072
|
+
/* ~ =================================== ~ */
|
|
1073
|
+
/* -- Type Helpers -- */
|
|
1074
|
+
/* ~ =================================== ~ */
|
|
1075
|
+
|
|
1076
|
+
/** All path templates defined by the OpenAPI paths map. */
|
|
1077
|
+
type Paths = keyof paths;
|
|
1078
|
+
|
|
1079
|
+
/** HTTP methods supported by the client. */
|
|
1080
|
+
type HttpMethod = "get" | "post" | "put" | "delete" | "patch";
|
|
1081
|
+
|
|
1082
|
+
/** Resolves the OpenAPI operation schema for a given path and method. */
|
|
1083
|
+
type Operation<P extends Paths, M extends HttpMethod> = paths[P][M];
|
|
1084
|
+
|
|
1085
|
+
/** Extracts placeholder parameter names from an OpenAPI-style path template. */
|
|
1086
|
+
type ExtractPathParamNames<T extends string> =
|
|
1087
|
+
T extends \`\${string}{\${infer P}}\${infer R}\`
|
|
1088
|
+
? P | ExtractPathParamNames<R>
|
|
1089
|
+
: never;
|
|
1090
|
+
|
|
1091
|
+
/** Maps extracted path parameter names to a simple serializable value type. */
|
|
1092
|
+
type PathParams<P extends Paths> = ExtractPathParamNames<
|
|
1093
|
+
P & string
|
|
1094
|
+
> extends never
|
|
1095
|
+
? never
|
|
1096
|
+
: Record<ExtractPathParamNames<P & string>, string | number | boolean>;
|
|
1097
|
+
|
|
1098
|
+
/** Extracts query parameter types from the OpenAPI operation schema. */
|
|
1099
|
+
type QueryParams<P extends Paths, M extends HttpMethod> = Operation<
|
|
1100
|
+
P,
|
|
1101
|
+
M
|
|
1102
|
+
> extends { parameters: { query?: infer Q } }
|
|
1103
|
+
? Q extends Record<string, unknown>
|
|
1104
|
+
? Q
|
|
1105
|
+
: never
|
|
1106
|
+
: never;
|
|
1107
|
+
|
|
1108
|
+
/** Extended Axios config that includes typed query parameters. */
|
|
1109
|
+
type TypedAxiosConfig<P extends Paths, M extends HttpMethod> = Omit<
|
|
1110
|
+
AxiosRequestConfig,
|
|
1111
|
+
"params"
|
|
1112
|
+
> & {
|
|
1113
|
+
params?: QueryParams<P, M>;
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
/** Maps OpenAPI form-data field types to their runtime equivalents. */
|
|
1117
|
+
type MapFormDataTypes<T> = T extends Record<string, unknown>
|
|
1118
|
+
? {
|
|
1119
|
+
[K in keyof T]: K extends
|
|
1120
|
+
| \`\${string}image\${string}\`
|
|
1121
|
+
| \`\${string}file\${string}\`
|
|
1122
|
+
| \`\${string}attachment\${string}\`
|
|
1123
|
+
| \`\${string}upload\${string}\`
|
|
1124
|
+
| \`\${string}document\${string}\`
|
|
1125
|
+
| \`\${string}photo\${string}\`
|
|
1126
|
+
| \`\${string}video\${string}\`
|
|
1127
|
+
| \`\${string}media\${string}\`
|
|
1128
|
+
? T[K] extends string[]
|
|
1129
|
+
? File[]
|
|
1130
|
+
: T[K] extends string
|
|
1131
|
+
? File | Blob
|
|
1132
|
+
: T[K]
|
|
1133
|
+
: T[K];
|
|
1134
|
+
}
|
|
1135
|
+
: T;
|
|
1136
|
+
|
|
1137
|
+
/** Infers the request body type for a given path/method pair. */
|
|
1138
|
+
type RequestBody<P extends Paths, M extends HttpMethod> = Operation<
|
|
1139
|
+
P,
|
|
1140
|
+
M
|
|
1141
|
+
> extends {
|
|
1142
|
+
requestBody: { content: { "application/json": infer T } };
|
|
1143
|
+
}
|
|
1144
|
+
? T extends Record<string, never>
|
|
1145
|
+
? Record<string, unknown>
|
|
1146
|
+
: T
|
|
1147
|
+
: Operation<P, M> extends {
|
|
1148
|
+
requestBody?: { content: { "application/json": infer T } };
|
|
1149
|
+
}
|
|
1150
|
+
? T extends Record<string, never>
|
|
1151
|
+
? Record<string, unknown>
|
|
1152
|
+
: T
|
|
1153
|
+
: Operation<P, M> extends {
|
|
1154
|
+
requestBody: { content: { "multipart/form-data": infer T } };
|
|
1155
|
+
}
|
|
1156
|
+
? MapFormDataTypes<T>
|
|
1157
|
+
: Operation<P, M> extends {
|
|
1158
|
+
requestBody?: { content: { "multipart/form-data": infer T } };
|
|
1159
|
+
}
|
|
1160
|
+
? MapFormDataTypes<T>
|
|
1161
|
+
: never;
|
|
1162
|
+
|
|
1163
|
+
/** Infers the JSON response body type for a given status code. */
|
|
1164
|
+
type ResponseData<
|
|
1165
|
+
P extends Paths,
|
|
1166
|
+
M extends HttpMethod,
|
|
1167
|
+
> = Operation<P, M> extends {
|
|
1168
|
+
responses: { 200: { content: { "application/json": infer T } } };
|
|
1169
|
+
}
|
|
1170
|
+
? T
|
|
1171
|
+
: Operation<P, M> extends {
|
|
1172
|
+
responses: { 201: { content: { "application/json": infer T } } };
|
|
1173
|
+
}
|
|
1174
|
+
? T
|
|
1175
|
+
: unknown;
|
|
1176
|
+
|
|
1177
|
+
/* ~ =================================== ~ */
|
|
1178
|
+
/* -- Utility Functions -- */
|
|
1179
|
+
/* ~ =================================== ~ */
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Replaces {param} placeholders in a path template using provided values.
|
|
1183
|
+
*/
|
|
1184
|
+
function interpolatePath<P extends Paths>(
|
|
1185
|
+
template: P,
|
|
1186
|
+
params?: PathParams<P> | never
|
|
1187
|
+
): string {
|
|
1188
|
+
const pathStr = String(template);
|
|
1189
|
+
if (!params) return pathStr;
|
|
1190
|
+
|
|
1191
|
+
const missing: string[] = [];
|
|
1192
|
+
const result = pathStr.replace(/\\{([^}]+)\\}/g, (match, key: string) => {
|
|
1193
|
+
const value = (params as Record<string, unknown>)[key];
|
|
1194
|
+
if (value === undefined || value === null) {
|
|
1195
|
+
missing.push(key);
|
|
1196
|
+
return match;
|
|
1197
|
+
}
|
|
1198
|
+
return encodeURIComponent(String(value));
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
if (missing.length > 0) {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
\`Missing required path param(s): \${missing.join(", ")} for template: \${pathStr}\`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
return result;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Checks if the request should use multipart/form-data.
|
|
1212
|
+
*/
|
|
1213
|
+
function shouldUseFormData(path: string, data: unknown): boolean {
|
|
1214
|
+
if (data instanceof FormData) return true;
|
|
1215
|
+
const formDataPatterns = [/\\/upload-images$/, /\\/upload$/, /\\/files\\/upload$/];
|
|
1216
|
+
return formDataPatterns.some((pattern) => pattern.test(path));
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* Converts a plain object to FormData for multipart/form-data requests.
|
|
1221
|
+
*/
|
|
1222
|
+
function convertToFormData(data: Record<string, unknown>): FormData {
|
|
1223
|
+
const formData = new FormData();
|
|
1224
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1225
|
+
if (value === undefined || value === null) continue;
|
|
1226
|
+
if (value instanceof File || value instanceof Blob) {
|
|
1227
|
+
formData.append(key, value);
|
|
1228
|
+
} else if (Array.isArray(value)) {
|
|
1229
|
+
for (const item of value) {
|
|
1230
|
+
if (item instanceof File || item instanceof Blob) {
|
|
1231
|
+
formData.append(key, item);
|
|
1232
|
+
} else {
|
|
1233
|
+
formData.append(key, String(item));
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
} else {
|
|
1237
|
+
formData.append(key, String(value));
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return formData;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/* ~ =================================== ~ */
|
|
1244
|
+
/* -- API Client -- */
|
|
1245
|
+
/* ~ =================================== ~ */
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Typed API client with Result-based error handling.
|
|
1249
|
+
* All methods return { data, error } instead of throwing.
|
|
1250
|
+
*/
|
|
1251
|
+
const api = {
|
|
1252
|
+
/**
|
|
1253
|
+
* Sends a GET request to the given OpenAPI path.
|
|
1254
|
+
* Returns Result<T> - never throws.
|
|
1255
|
+
*/
|
|
1256
|
+
get<P extends Paths>(
|
|
1257
|
+
url: P,
|
|
1258
|
+
...args: PathParams<P> extends never
|
|
1259
|
+
? [config?: TypedAxiosConfig<P, "get">]
|
|
1260
|
+
: [pathParams: PathParams<P>, config?: TypedAxiosConfig<P, "get">]
|
|
1261
|
+
): Promise<Result<ResponseData<P, "get">>> {
|
|
1262
|
+
const hasPathParams = String(url).includes("{");
|
|
1263
|
+
const [pathParamsOrConfig, config] = args;
|
|
1264
|
+
|
|
1265
|
+
const pathParams = hasPathParams
|
|
1266
|
+
? (pathParamsOrConfig as PathParams<P>)
|
|
1267
|
+
: undefined;
|
|
1268
|
+
const finalConfig = hasPathParams
|
|
1269
|
+
? (config as TypedAxiosConfig<P, "get"> | undefined)
|
|
1270
|
+
: (pathParamsOrConfig as TypedAxiosConfig<P, "get"> | undefined);
|
|
1271
|
+
|
|
1272
|
+
return safeRequest(
|
|
1273
|
+
axiosInstance.get<ResponseData<P, "get">>(
|
|
1274
|
+
interpolatePath(url, pathParams),
|
|
1275
|
+
finalConfig
|
|
1276
|
+
)
|
|
1277
|
+
);
|
|
1278
|
+
},
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Sends a POST request with a body inferred from the OpenAPI spec.
|
|
1282
|
+
* Returns Result<T> - never throws.
|
|
1283
|
+
*/
|
|
1284
|
+
post<P extends Paths>(
|
|
1285
|
+
url: P,
|
|
1286
|
+
data: RequestBody<P, "post">,
|
|
1287
|
+
...args: PathParams<P> extends never
|
|
1288
|
+
? [config?: TypedAxiosConfig<P, "post">]
|
|
1289
|
+
: [pathParams: PathParams<P>, config?: TypedAxiosConfig<P, "post">]
|
|
1290
|
+
): Promise<Result<ResponseData<P, "post">>> {
|
|
1291
|
+
const hasPathParams = String(url).includes("{");
|
|
1292
|
+
const [pathParamsOrConfig, config] = args;
|
|
1293
|
+
|
|
1294
|
+
const pathParams = hasPathParams
|
|
1295
|
+
? (pathParamsOrConfig as PathParams<P>)
|
|
1296
|
+
: undefined;
|
|
1297
|
+
const finalConfig = hasPathParams
|
|
1298
|
+
? (config as TypedAxiosConfig<P, "post"> | undefined)
|
|
1299
|
+
: (pathParamsOrConfig as TypedAxiosConfig<P, "post"> | undefined);
|
|
1300
|
+
|
|
1301
|
+
const resolvedPath = interpolatePath(url, pathParams);
|
|
1302
|
+
const requestData = shouldUseFormData(resolvedPath, data)
|
|
1303
|
+
? data instanceof FormData
|
|
1304
|
+
? data
|
|
1305
|
+
: convertToFormData(data as Record<string, unknown>)
|
|
1306
|
+
: data;
|
|
1307
|
+
|
|
1308
|
+
return safeRequest(
|
|
1309
|
+
axiosInstance.post<ResponseData<P, "post">>(
|
|
1310
|
+
resolvedPath,
|
|
1311
|
+
requestData,
|
|
1312
|
+
finalConfig
|
|
1313
|
+
)
|
|
1314
|
+
);
|
|
1315
|
+
},
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Sends a PUT request with a JSON body.
|
|
1319
|
+
* Returns Result<T> - never throws.
|
|
1320
|
+
*/
|
|
1321
|
+
put<P extends Paths>(
|
|
1322
|
+
url: P,
|
|
1323
|
+
data: RequestBody<P, "put">,
|
|
1324
|
+
...args: PathParams<P> extends never
|
|
1325
|
+
? [config?: TypedAxiosConfig<P, "put">]
|
|
1326
|
+
: [pathParams: PathParams<P>, config?: TypedAxiosConfig<P, "put">]
|
|
1327
|
+
): Promise<Result<ResponseData<P, "put">>> {
|
|
1328
|
+
const hasPathParams = String(url).includes("{");
|
|
1329
|
+
const [pathParamsOrConfig, config] = args;
|
|
1330
|
+
|
|
1331
|
+
const pathParams = hasPathParams
|
|
1332
|
+
? (pathParamsOrConfig as PathParams<P>)
|
|
1333
|
+
: undefined;
|
|
1334
|
+
const finalConfig = hasPathParams
|
|
1335
|
+
? (config as TypedAxiosConfig<P, "put"> | undefined)
|
|
1336
|
+
: (pathParamsOrConfig as TypedAxiosConfig<P, "put"> | undefined);
|
|
1337
|
+
|
|
1338
|
+
return safeRequest(
|
|
1339
|
+
axiosInstance.put<ResponseData<P, "put">>(
|
|
1340
|
+
interpolatePath(url, pathParams),
|
|
1341
|
+
data,
|
|
1342
|
+
finalConfig
|
|
1343
|
+
)
|
|
1344
|
+
);
|
|
1345
|
+
},
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Sends a DELETE request.
|
|
1349
|
+
* Returns Result<T> - never throws.
|
|
1350
|
+
*/
|
|
1351
|
+
delete<P extends Paths>(
|
|
1352
|
+
url: P,
|
|
1353
|
+
...args: PathParams<P> extends never
|
|
1354
|
+
? [config?: TypedAxiosConfig<P, "delete">]
|
|
1355
|
+
: [pathParams: PathParams<P>, config?: TypedAxiosConfig<P, "delete">]
|
|
1356
|
+
): Promise<Result<ResponseData<P, "delete">>> {
|
|
1357
|
+
const hasPathParams = String(url).includes("{");
|
|
1358
|
+
const [pathParamsOrConfig, config] = args;
|
|
1359
|
+
|
|
1360
|
+
const pathParams = hasPathParams
|
|
1361
|
+
? (pathParamsOrConfig as PathParams<P>)
|
|
1362
|
+
: undefined;
|
|
1363
|
+
const finalConfig = hasPathParams
|
|
1364
|
+
? (config as TypedAxiosConfig<P, "delete"> | undefined)
|
|
1365
|
+
: (pathParamsOrConfig as TypedAxiosConfig<P, "delete"> | undefined);
|
|
1366
|
+
|
|
1367
|
+
return safeRequest(
|
|
1368
|
+
axiosInstance.delete<ResponseData<P, "delete">>(
|
|
1369
|
+
interpolatePath(url, pathParams),
|
|
1370
|
+
finalConfig
|
|
1371
|
+
)
|
|
1372
|
+
);
|
|
1373
|
+
},
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Sends a PATCH request with a JSON body.
|
|
1377
|
+
* Returns Result<T> - never throws.
|
|
1378
|
+
*/
|
|
1379
|
+
patch<P extends Paths>(
|
|
1380
|
+
url: P,
|
|
1381
|
+
data: RequestBody<P, "patch">,
|
|
1382
|
+
...args: PathParams<P> extends never
|
|
1383
|
+
? [config?: TypedAxiosConfig<P, "patch">]
|
|
1384
|
+
: [pathParams: PathParams<P>, config?: TypedAxiosConfig<P, "patch">]
|
|
1385
|
+
): Promise<Result<ResponseData<P, "patch">>> {
|
|
1386
|
+
const hasPathParams = String(url).includes("{");
|
|
1387
|
+
const [pathParamsOrConfig, config] = args;
|
|
1388
|
+
|
|
1389
|
+
const pathParams = hasPathParams
|
|
1390
|
+
? (pathParamsOrConfig as PathParams<P>)
|
|
1391
|
+
: undefined;
|
|
1392
|
+
const finalConfig = hasPathParams
|
|
1393
|
+
? (config as TypedAxiosConfig<P, "patch"> | undefined)
|
|
1394
|
+
: (pathParamsOrConfig as TypedAxiosConfig<P, "patch"> | undefined);
|
|
1395
|
+
|
|
1396
|
+
return safeRequest(
|
|
1397
|
+
axiosInstance.patch<ResponseData<P, "patch">>(
|
|
1398
|
+
interpolatePath(url, pathParams),
|
|
1399
|
+
data,
|
|
1400
|
+
finalConfig
|
|
1401
|
+
)
|
|
1402
|
+
);
|
|
1403
|
+
},
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Operation-based API methods generated from OpenAPI operationIds.
|
|
1407
|
+
* Provides semantic function names instead of raw path endpoints.
|
|
1408
|
+
*/
|
|
1409
|
+
get op() {
|
|
1410
|
+
return createOperations(this);
|
|
1411
|
+
},
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
export { api };
|
|
1415
|
+
export type { Paths, HttpMethod, PathParams, QueryParams, RequestBody, ResponseData };
|
|
1416
|
+
|
|
1417
|
+
// Re-export error types for convenience
|
|
1418
|
+
export type { ApiError, Result, RequestContext } from "./api.error";
|
|
1419
|
+
export { createApiError, safeRequest, isSuccess, isError } from "./api.error";
|
|
1420
|
+
|
|
1421
|
+
// Re-export types for convenience
|
|
1422
|
+
export type { paths, components, operations };
|
|
1423
|
+
`;
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Generates client files if they don't exist.
|
|
1427
|
+
* Returns which files were generated.
|
|
1428
|
+
*/
|
|
1429
|
+
export async function generateClientFiles(options) {
|
|
1430
|
+
const { paths: outputPaths, instanceConfig, logger, force = false } = options;
|
|
1431
|
+
const result = {
|
|
1432
|
+
helpers: false,
|
|
1433
|
+
instance: false,
|
|
1434
|
+
error: false,
|
|
1435
|
+
client: false,
|
|
1436
|
+
};
|
|
1437
|
+
// Generate api.helpers.ts if it doesn't exist
|
|
1438
|
+
const helpersExists = await fileExists(outputPaths.helpers);
|
|
1439
|
+
if (!helpersExists || force) {
|
|
1440
|
+
logger.info({ path: outputPaths.helpers }, helpersExists ? "Regenerating api.helpers.ts" : "Creating api.helpers.ts");
|
|
1441
|
+
const content = generateHelpersFileContent();
|
|
1442
|
+
await atomicWrite(outputPaths.helpers, content);
|
|
1443
|
+
result.helpers = true;
|
|
1444
|
+
}
|
|
1445
|
+
else {
|
|
1446
|
+
logger.debug("api.helpers.ts already exists, skipping");
|
|
1447
|
+
}
|
|
1448
|
+
// Generate api.instance.ts if it doesn't exist
|
|
1449
|
+
const instanceExists = await fileExists(outputPaths.instance);
|
|
1450
|
+
if (!instanceExists || force) {
|
|
1451
|
+
logger.info({ path: outputPaths.instance }, instanceExists
|
|
1452
|
+
? "Regenerating api.instance.ts"
|
|
1453
|
+
: "Creating api.instance.ts");
|
|
1454
|
+
const content = generateInstanceFileContent(instanceConfig);
|
|
1455
|
+
await atomicWrite(outputPaths.instance, content);
|
|
1456
|
+
result.instance = true;
|
|
1457
|
+
}
|
|
1458
|
+
else {
|
|
1459
|
+
logger.debug("api.instance.ts already exists, skipping");
|
|
1460
|
+
}
|
|
1461
|
+
// Generate api.error.ts if it doesn't exist
|
|
1462
|
+
const errorExists = await fileExists(outputPaths.error);
|
|
1463
|
+
if (!errorExists || force) {
|
|
1464
|
+
logger.info({ path: outputPaths.error }, errorExists ? "Regenerating api.error.ts" : "Creating api.error.ts");
|
|
1465
|
+
const content = generateErrorFileContent();
|
|
1466
|
+
await atomicWrite(outputPaths.error, content);
|
|
1467
|
+
result.error = true;
|
|
1468
|
+
}
|
|
1469
|
+
else {
|
|
1470
|
+
logger.debug("api.error.ts already exists, skipping");
|
|
1471
|
+
}
|
|
1472
|
+
// Generate api.client.ts if it doesn't exist
|
|
1473
|
+
const clientExists = await fileExists(outputPaths.client);
|
|
1474
|
+
if (!clientExists || force) {
|
|
1475
|
+
logger.info({ path: outputPaths.client }, clientExists ? "Regenerating api.client.ts" : "Creating api.client.ts");
|
|
1476
|
+
const content = generateClientFileContent();
|
|
1477
|
+
await atomicWrite(outputPaths.client, content);
|
|
1478
|
+
result.client = true;
|
|
1479
|
+
}
|
|
1480
|
+
else {
|
|
1481
|
+
logger.debug("api.client.ts already exists, skipping");
|
|
1482
|
+
}
|
|
1483
|
+
return result;
|
|
1484
|
+
}
|
|
1485
|
+
export async function generate(options) {
|
|
1486
|
+
const { paths: outputPaths, logger, dryRun = false, skipTypes = false, skipOperations = false, } = options;
|
|
1487
|
+
const startTime = Date.now();
|
|
1488
|
+
// Parse spec early for both dry-run and actual generation
|
|
1489
|
+
const specContent = await readFile(outputPaths.spec, "utf8");
|
|
1490
|
+
const spec = JSON.parse(specContent);
|
|
1491
|
+
const operations = parseOperations(spec, logger);
|
|
1492
|
+
if (operations.length === 0) {
|
|
1493
|
+
logger.warn("No operations with operationId found in OpenAPI spec");
|
|
1494
|
+
}
|
|
1495
|
+
else {
|
|
1496
|
+
logger.debug({ count: operations.length }, "Found operations");
|
|
1497
|
+
}
|
|
1498
|
+
// Handle dry-run mode
|
|
1499
|
+
if (dryRun) {
|
|
1500
|
+
logger.info("Dry run mode - no files will be written");
|
|
1501
|
+
const dryRunResult = {
|
|
1502
|
+
files: [],
|
|
1503
|
+
operationCount: operations.length,
|
|
1504
|
+
};
|
|
1505
|
+
// Check types file
|
|
1506
|
+
if (!skipTypes) {
|
|
1507
|
+
const typesExists = await fileExists(outputPaths.types);
|
|
1508
|
+
// We can't easily get line count without running openapi-typescript
|
|
1509
|
+
dryRunResult.files.push({
|
|
1510
|
+
path: outputPaths.types,
|
|
1511
|
+
lines: 0, // Unknown until generated
|
|
1512
|
+
action: typesExists ? "update" : "create",
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
// Generate operations content to get line count
|
|
1516
|
+
if (!skipOperations) {
|
|
1517
|
+
const opsContent = generateOperationsFileContent(operations);
|
|
1518
|
+
const opsExists = await fileExists(outputPaths.operations);
|
|
1519
|
+
dryRunResult.files.push({
|
|
1520
|
+
path: outputPaths.operations,
|
|
1521
|
+
lines: opsContent.split("\n").length,
|
|
1522
|
+
action: opsExists ? "update" : "create",
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
const durationMs = Date.now() - startTime;
|
|
1526
|
+
return {
|
|
1527
|
+
operationCount: operations.length,
|
|
1528
|
+
durationMs,
|
|
1529
|
+
typesGenerated: false,
|
|
1530
|
+
operationsGenerated: false,
|
|
1531
|
+
dryRunResult,
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
// Create backups of existing files
|
|
1535
|
+
const backups = await createBackup(outputPaths);
|
|
1536
|
+
try {
|
|
1537
|
+
let typesGenerated = false;
|
|
1538
|
+
let operationsGenerated = false;
|
|
1539
|
+
// Step 1: Generate TypeScript types from OpenAPI spec
|
|
1540
|
+
if (skipTypes) {
|
|
1541
|
+
logger.info("Skipping types generation (--operations-only)");
|
|
1542
|
+
}
|
|
1543
|
+
else {
|
|
1544
|
+
await generateTypes(outputPaths.spec, outputPaths.types, logger);
|
|
1545
|
+
typesGenerated = true;
|
|
1546
|
+
}
|
|
1547
|
+
// Step 2: Generate operations file
|
|
1548
|
+
if (skipOperations) {
|
|
1549
|
+
logger.info("Skipping operations generation (--types-only)");
|
|
1550
|
+
}
|
|
1551
|
+
else {
|
|
1552
|
+
logger.info("Generating operations file...");
|
|
1553
|
+
const operationsContent = generateOperationsFileContent(operations);
|
|
1554
|
+
await atomicWrite(outputPaths.operations, operationsContent);
|
|
1555
|
+
operationsGenerated = true;
|
|
1556
|
+
}
|
|
1557
|
+
// Clean up backups on success
|
|
1558
|
+
await cleanupBackups(backups);
|
|
1559
|
+
const durationMs = Date.now() - startTime;
|
|
1560
|
+
logger.info({ operationCount: operations.length, durationMs }, "Generation completed successfully");
|
|
1561
|
+
return {
|
|
1562
|
+
operationCount: operations.length,
|
|
1563
|
+
durationMs,
|
|
1564
|
+
typesGenerated,
|
|
1565
|
+
operationsGenerated,
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
// Restore from backups on failure
|
|
1570
|
+
logger.error({ error }, "Generation failed, restoring backups...");
|
|
1571
|
+
await restoreFromBackup(backups, outputPaths);
|
|
1572
|
+
throw error;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
//# sourceMappingURL=generator.js.map
|