elysia-openapi-codegen 0.1.9 → 0.2.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/README.md +1 -1
- package/dist/index.js +77 -32
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -43,7 +43,8 @@ ${props.join(`
|
|
|
43
43
|
}`;
|
|
44
44
|
}
|
|
45
45
|
if (schema.anyOf || schema.oneOf) {
|
|
46
|
-
|
|
46
|
+
const members = [...new Set((schema.anyOf || schema.oneOf || []).map(resolveType))];
|
|
47
|
+
return members.join(" | ");
|
|
47
48
|
}
|
|
48
49
|
if (schema.allOf) {
|
|
49
50
|
return schema.allOf.map(resolveType).join(" & ");
|
|
@@ -63,11 +64,11 @@ function getPathGroup(pathUrl) {
|
|
|
63
64
|
const segment = pathUrl.split("/").filter(Boolean)[0] || "root";
|
|
64
65
|
return toSafeIdentifier(segment);
|
|
65
66
|
}
|
|
66
|
-
function generateGroups(spec) {
|
|
67
|
+
function generateGroups(spec, hooksMode) {
|
|
67
68
|
const groups = {};
|
|
68
69
|
const getGroup = (name) => {
|
|
69
70
|
if (!groups[name])
|
|
70
|
-
groups[name] = { types: [],
|
|
71
|
+
groups[name] = { types: [], functions: [] };
|
|
71
72
|
return groups[name];
|
|
72
73
|
};
|
|
73
74
|
if (spec.components?.schemas) {
|
|
@@ -120,6 +121,7 @@ ${paramProps.join(`
|
|
|
120
121
|
for (const p of pathParamsList) {
|
|
121
122
|
urlPath = urlPath.replace(`{${p.name}}`, `\${params.${p.name}}`);
|
|
122
123
|
}
|
|
124
|
+
urlPath = urlPath.replace(/\/+$/, "");
|
|
123
125
|
const qsLines = (indent) => queryParamsList.map((p) => `${indent}if (params?.${p.name} !== undefined) _qs.set('${p.name}', String(params.${p.name}));`).join(`
|
|
124
126
|
`);
|
|
125
127
|
if (!hasResponse) {
|
|
@@ -127,19 +129,24 @@ ${paramProps.join(`
|
|
|
127
129
|
${qsLines(" ")}
|
|
128
130
|
const _qstr = _qs.toString();
|
|
129
131
|
return \`\${baseUrl}${urlPath}\${_qstr ? '?' + _qstr : ''}\`;` : ` return \`\${baseUrl}${urlPath}\`;`;
|
|
130
|
-
g.
|
|
132
|
+
g.functions.push(`
|
|
131
133
|
export const ${opId}Url = (params${hasParams ? "" : "?"}: ${paramsType}): string => {
|
|
132
134
|
${body}
|
|
133
135
|
};`);
|
|
134
136
|
continue;
|
|
135
137
|
}
|
|
136
|
-
|
|
138
|
+
if (hooksMode) {
|
|
139
|
+
const fetchStatement = queryParamsList.length > 0 ? ` const _qs = new URLSearchParams();
|
|
137
140
|
${qsLines(" ")}
|
|
138
141
|
const _qstr = _qs.toString();
|
|
139
142
|
const res = await fetch(\`\${baseUrl}${urlPath}\${_qstr ? '?' + _qstr : ''}\`);` : ` const res = await fetch(\`\${baseUrl}${urlPath}\`);`;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
const guardParams = [
|
|
144
|
+
...pathParamsList,
|
|
145
|
+
...queryParamsList.filter((p) => p.required)
|
|
146
|
+
];
|
|
147
|
+
const enabledGuard = guardParams.length > 0 ? `
|
|
148
|
+
enabled: ${guardParams.map((p) => `params.${p.name} != null`).join(" && ")},` : "";
|
|
149
|
+
g.functions.push(`
|
|
143
150
|
export const use${capitalize(opId)} = (
|
|
144
151
|
params${hasParams ? "" : "?"}: ${paramsType},
|
|
145
152
|
options?: Omit<UseQueryOptions<${responseType}>, 'queryKey' | 'queryFn'>
|
|
@@ -154,6 +161,18 @@ ${fetchStatement}
|
|
|
154
161
|
...options,
|
|
155
162
|
});
|
|
156
163
|
};`);
|
|
164
|
+
} else {
|
|
165
|
+
const fetchStatement = queryParamsList.length > 0 ? ` const _qs = new URLSearchParams();
|
|
166
|
+
${qsLines(" ")}
|
|
167
|
+
const _qstr = _qs.toString();
|
|
168
|
+
const res = await fetch(\`\${baseUrl}${urlPath}\${_qstr ? '?' + _qstr : ''}\`);` : ` const res = await fetch(\`\${baseUrl}${urlPath}\`);`;
|
|
169
|
+
g.functions.push(`
|
|
170
|
+
export async function ${opId}(params${hasParams ? "" : "?"}: ${paramsType}): Promise<${responseType}> {
|
|
171
|
+
${fetchStatement}
|
|
172
|
+
if (!res.ok) throw new Error(\`\${res.status}: \${await res.text()}\`);
|
|
173
|
+
return res.json();
|
|
174
|
+
}`);
|
|
175
|
+
}
|
|
157
176
|
} else {
|
|
158
177
|
const pathParamsList = params.filter((p) => p.in === "path");
|
|
159
178
|
const queryParamsList = params.filter((p) => p.in === "query");
|
|
@@ -164,41 +183,43 @@ ${fetchStatement}
|
|
|
164
183
|
for (const p of pathParamsList) {
|
|
165
184
|
urlPath = urlPath.replace(`{${p.name}}`, `\${${p.name}}`);
|
|
166
185
|
}
|
|
186
|
+
urlPath = urlPath.replace(/\/+$/, "");
|
|
167
187
|
const mutQsCode = hasQueryParams ? `
|
|
168
|
-
|
|
169
|
-
${queryParamsList.map((p) => `
|
|
188
|
+
const _qs = new URLSearchParams();
|
|
189
|
+
${queryParamsList.map((p) => ` if (${p.name} !== undefined) _qs.set('${p.name}', String(${p.name}));`).join(`
|
|
170
190
|
`)}
|
|
171
|
-
|
|
191
|
+
const _qstr = _qs.toString();` : "";
|
|
172
192
|
const fetchUrlExpr = hasQueryParams ? `\`\${baseUrl}${urlPath}\${_qstr ? '?' + _qstr : ''}\`` : `\`\${baseUrl}${urlPath}\``;
|
|
173
193
|
const allParamNames = [
|
|
174
194
|
...pathParamsList.map((p) => p.name),
|
|
175
195
|
...queryParamsList.map((p) => p.name)
|
|
176
196
|
];
|
|
177
197
|
let inputType;
|
|
178
|
-
let
|
|
198
|
+
let destructureArg;
|
|
179
199
|
if (hasBody && hasAnyParams) {
|
|
180
200
|
inputType = `${capitalize(opId)}Params & ${capitalize(opId)}Body`;
|
|
181
|
-
|
|
201
|
+
destructureArg = `{ ${allParamNames.join(", ")}, ...body }`;
|
|
182
202
|
} else if (hasBody) {
|
|
183
203
|
inputType = bodyType;
|
|
184
|
-
|
|
204
|
+
destructureArg = "body";
|
|
185
205
|
} else if (hasAnyParams) {
|
|
186
206
|
inputType = paramsType;
|
|
187
|
-
|
|
207
|
+
destructureArg = `{ ${allParamNames.join(", ")} }`;
|
|
188
208
|
} else {
|
|
189
209
|
inputType = "void";
|
|
190
|
-
|
|
210
|
+
destructureArg = "";
|
|
191
211
|
}
|
|
192
212
|
const fetchBodyLines = hasBody ? useFormData ? `
|
|
193
213
|
body: Object.entries(body as Record<string, unknown>).reduce((f, [k, v]) => { f.append(k, v as any); return f; }, new FormData()),` : `
|
|
194
214
|
headers: { 'Content-Type': 'application/json' },
|
|
195
215
|
body: JSON.stringify(body),` : "";
|
|
196
|
-
|
|
216
|
+
if (hooksMode) {
|
|
217
|
+
g.functions.push(`
|
|
197
218
|
export const use${capitalize(opId)} = (
|
|
198
219
|
options?: UseMutationOptions<${responseType}, Error, ${inputType}>
|
|
199
220
|
) => {
|
|
200
221
|
return useMutation<${responseType}, Error, ${inputType}>({
|
|
201
|
-
mutationFn: async (${
|
|
222
|
+
mutationFn: async (${destructureArg}) => {${mutQsCode.replace(/^ /gm, " ")}
|
|
202
223
|
const res = await fetch(${fetchUrlExpr}, {
|
|
203
224
|
method: '${method.toUpperCase()}',${fetchBodyLines}
|
|
204
225
|
});
|
|
@@ -208,6 +229,26 @@ export const use${capitalize(opId)} = (
|
|
|
208
229
|
...options,
|
|
209
230
|
});
|
|
210
231
|
};`);
|
|
232
|
+
} else {
|
|
233
|
+
const argDecl = inputType === "void" ? "" : `input: ${inputType}`;
|
|
234
|
+
const destructure = inputType === "void" ? "" : hasBody && hasAnyParams ? ` const { ${allParamNames.join(", ")}, ...body } = input;
|
|
235
|
+
` : hasAnyParams ? ` const { ${allParamNames.join(", ")} } = input;
|
|
236
|
+
` : ` const body = input;
|
|
237
|
+
`;
|
|
238
|
+
const fetchBodyLinesApi = hasBody ? useFormData ? `
|
|
239
|
+
body: Object.entries(body as Record<string, unknown>).reduce((f, [k, v]) => { f.append(k, v as any); return f; }, new FormData()),` : `
|
|
240
|
+
headers: { 'Content-Type': 'application/json' },
|
|
241
|
+
body: JSON.stringify(body),` : "";
|
|
242
|
+
g.functions.push(`
|
|
243
|
+
export async function ${opId}(${argDecl}): Promise<${responseType}> {
|
|
244
|
+
${destructure}${mutQsCode.replace(/^\n/, "")}
|
|
245
|
+
const res = await fetch(${fetchUrlExpr}, {
|
|
246
|
+
method: '${method.toUpperCase()}',${fetchBodyLinesApi}
|
|
247
|
+
});
|
|
248
|
+
if (!res.ok) throw new Error(\`\${res.status}: \${await res.text()}\`);
|
|
249
|
+
return res.json();
|
|
250
|
+
}`);
|
|
251
|
+
}
|
|
211
252
|
}
|
|
212
253
|
}
|
|
213
254
|
}
|
|
@@ -215,7 +256,7 @@ export const use${capitalize(opId)} = (
|
|
|
215
256
|
}
|
|
216
257
|
function parseArgs() {
|
|
217
258
|
const args = process.argv.slice(2);
|
|
218
|
-
const config = { input: "", output: "" };
|
|
259
|
+
const config = { input: "", output: "", hooks: false };
|
|
219
260
|
for (let i = 0;i < args.length; i++) {
|
|
220
261
|
const arg = args[i];
|
|
221
262
|
if (!arg)
|
|
@@ -234,6 +275,8 @@ function parseArgs() {
|
|
|
234
275
|
process.exit(1);
|
|
235
276
|
}
|
|
236
277
|
config.output = value;
|
|
278
|
+
} else if (arg === "--hooks" || arg === "-hooks") {
|
|
279
|
+
config.hooks = true;
|
|
237
280
|
} else if (!arg.startsWith("-")) {
|
|
238
281
|
if (!config.input)
|
|
239
282
|
config.input = arg;
|
|
@@ -246,19 +289,21 @@ function parseArgs() {
|
|
|
246
289
|
function showHelp() {
|
|
247
290
|
console.log(`
|
|
248
291
|
Elysia OpenAPI Code Generator
|
|
249
|
-
Generate React Query hooks
|
|
292
|
+
Generate TypeScript API functions or React Query hooks from OpenAPI specifications.
|
|
250
293
|
|
|
251
294
|
Usage:
|
|
252
|
-
elysia-codegen -i <source> -o <output>
|
|
253
|
-
elysia-codegen --input <source> --output <output>
|
|
295
|
+
elysia-codegen -i <source> -o <output> [--hooks]
|
|
296
|
+
elysia-codegen --input <source> --output <output> [--hooks]
|
|
254
297
|
elysia-codegen <source> <output>
|
|
255
298
|
|
|
256
299
|
Arguments:
|
|
257
300
|
-i, --input <source> OpenAPI spec source (URL or file path)
|
|
258
301
|
-o, --output <output> Output directory for generated files
|
|
302
|
+
--hooks, -hooks Generate React Query hooks instead of plain async functions
|
|
259
303
|
|
|
260
304
|
Examples:
|
|
261
305
|
elysia-codegen -i https://api.example.com/openapi.json -o ./src/api
|
|
306
|
+
elysia-codegen -i https://api.example.com/openapi.json -o ./src/api --hooks
|
|
262
307
|
elysia-codegen ./openapi.json ./generated
|
|
263
308
|
`);
|
|
264
309
|
}
|
|
@@ -268,17 +313,17 @@ async function main() {
|
|
|
268
313
|
showHelp();
|
|
269
314
|
process.exit(0);
|
|
270
315
|
}
|
|
271
|
-
const { input, output } = parseArgs();
|
|
316
|
+
const { input, output, hooks } = parseArgs();
|
|
272
317
|
if (!input || !output) {
|
|
273
318
|
console.error(`Error: Both input source and output directory are required.
|
|
274
319
|
`);
|
|
275
320
|
showHelp();
|
|
276
321
|
process.exit(1);
|
|
277
322
|
}
|
|
278
|
-
console.log(
|
|
323
|
+
console.log(`Fetching OpenAPI spec... (mode: ${hooks ? "hooks" : "api"})`);
|
|
279
324
|
try {
|
|
280
325
|
const spec = await fetchSpec(input);
|
|
281
|
-
const groups = generateGroups(spec);
|
|
326
|
+
const groups = generateGroups(spec, hooks);
|
|
282
327
|
const serverPath = spec.servers?.[0]?.url || "";
|
|
283
328
|
const baseUrl = input.startsWith("http://") || input.startsWith("https://") ? new URL(input).origin + serverPath : serverPath;
|
|
284
329
|
if (!fs.existsSync(output)) {
|
|
@@ -289,27 +334,27 @@ async function main() {
|
|
|
289
334
|
export let baseUrl = '${baseUrl}';
|
|
290
335
|
`);
|
|
291
336
|
const groupNames = [];
|
|
292
|
-
for (const [groupName, { types,
|
|
293
|
-
if (types.length === 0 &&
|
|
337
|
+
for (const [groupName, { types, functions }] of Object.entries(groups)) {
|
|
338
|
+
if (types.length === 0 && functions.length === 0)
|
|
294
339
|
continue;
|
|
295
|
-
const
|
|
340
|
+
const hasFunctions = functions.length > 0;
|
|
296
341
|
const sections = [`/* eslint-disable */`, `import { baseUrl } from './base';`];
|
|
297
|
-
if (
|
|
342
|
+
if (hasFunctions && hooks) {
|
|
298
343
|
sections.push(`import { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';`);
|
|
299
344
|
}
|
|
300
345
|
if (types.length > 0)
|
|
301
346
|
sections.push(types.join(`
|
|
302
347
|
|
|
303
348
|
`));
|
|
304
|
-
if (
|
|
305
|
-
sections.push(
|
|
349
|
+
if (functions.length > 0)
|
|
350
|
+
sections.push(functions.join(`
|
|
306
351
|
`));
|
|
307
352
|
fs.writeFileSync(path.join(output, `${groupName}.ts`), sections.join(`
|
|
308
353
|
|
|
309
354
|
`) + `
|
|
310
355
|
`);
|
|
311
356
|
groupNames.push(groupName);
|
|
312
|
-
console.log(` ↳ ${groupName}.ts (${types.length} types, ${
|
|
357
|
+
console.log(` ↳ ${groupName}.ts (${types.length} types, ${functions.length} ${hooks ? "hooks" : "functions"})`);
|
|
313
358
|
}
|
|
314
359
|
const indexContent = ["base", ...groupNames].map((f) => `export * from './${f}';`).join(`
|
|
315
360
|
`);
|