elysia-openapi-codegen 0.1.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/README.md +357 -0
- package/index.ts +393 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# Elysia OpenAPI Code Generator
|
|
2
|
+
|
|
3
|
+
Generate fully-typed React Query hooks and TypeScript interfaces from OpenAPI specifications. Perfect for Elysia.js APIs and any OpenAPI 3.x compliant backend.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Type-Safe Hooks**: Automatically generates React Query hooks with full TypeScript support
|
|
8
|
+
- **OpenAPI 3.x Compatible**: Works with any valid OpenAPI specification
|
|
9
|
+
- **Multiple Input Sources**: Fetch specs from URLs or local files
|
|
10
|
+
- **Zero Configuration**: Simple CLI with sensible defaults
|
|
11
|
+
- **React Query Integration**: Generates `useQuery` and `useMutation` hooks ready to use
|
|
12
|
+
- **Flexible Arguments**: Supports both flag-based and positional arguments
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### Using Bun (Recommended)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add -d elysia-openapi-codegen
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Using npm
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install --save-dev elysia-openapi-codegen
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Using yarn
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
yarn add -D elysia-openapi-codegen
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Global Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Bun
|
|
38
|
+
bun add -g elysia-openapi-codegen
|
|
39
|
+
|
|
40
|
+
# npm
|
|
41
|
+
npm install -g elysia-openapi-codegen
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### CLI Flags
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
elysia-codegen -i <source> -o <output>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Arguments:**
|
|
53
|
+
- `-i, --input <source>` - OpenAPI spec source (URL or file path)
|
|
54
|
+
- `-o, --output <output>` - Output directory for generated files
|
|
55
|
+
- `-h, --help` - Show help message
|
|
56
|
+
|
|
57
|
+
### Examples
|
|
58
|
+
|
|
59
|
+
#### Using Flag Arguments
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# From a URL
|
|
63
|
+
elysia-codegen -i https://api.example.com/openapi.json -o ./src/api
|
|
64
|
+
|
|
65
|
+
# From a local file
|
|
66
|
+
elysia-codegen -i ./openapi.json -o ./generated
|
|
67
|
+
|
|
68
|
+
# Using long-form flags
|
|
69
|
+
elysia-codegen --input https://api.example.com/openapi.json --output ./src/api
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Using Positional Arguments
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# From a URL
|
|
76
|
+
elysia-codegen https://api.example.com/openapi.json ./src/api
|
|
77
|
+
|
|
78
|
+
# From a local file
|
|
79
|
+
elysia-codegen ./openapi.json ./generated
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### With Bun
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Run directly with bun
|
|
86
|
+
bun index.ts -i https://api.example.com/openapi.json -o ./src/api
|
|
87
|
+
|
|
88
|
+
# Or using positional arguments
|
|
89
|
+
bun index.ts https://api.example.com/openapi.json ./src/api
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Generated Code Usage
|
|
93
|
+
|
|
94
|
+
The generator creates a single `generated.ts` file containing all types and hooks.
|
|
95
|
+
|
|
96
|
+
### TypeScript Types
|
|
97
|
+
|
|
98
|
+
All request/response types are automatically generated:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import type { User, CreateUserBody, GetUsersResponse } from './generated';
|
|
102
|
+
|
|
103
|
+
// Use types in your components
|
|
104
|
+
const user: User = {
|
|
105
|
+
id: 1,
|
|
106
|
+
name: 'John Doe',
|
|
107
|
+
email: 'john@example.com'
|
|
108
|
+
};
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### React Query Hooks
|
|
112
|
+
|
|
113
|
+
#### Query Hooks (GET requests)
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { useGetUsers, useGetUserById } from './api/generated';
|
|
117
|
+
|
|
118
|
+
function UsersList() {
|
|
119
|
+
// Simple query with no parameters
|
|
120
|
+
const { data, isLoading, error } = useGetUsers();
|
|
121
|
+
|
|
122
|
+
if (isLoading) return <div>Loading...</div>;
|
|
123
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<ul>
|
|
127
|
+
{data?.users.map(user => (
|
|
128
|
+
<li key={user.id}>{user.name}</li>
|
|
129
|
+
))}
|
|
130
|
+
</ul>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function UserProfile({ userId }: { userId: number }) {
|
|
135
|
+
// Query with parameters
|
|
136
|
+
const { data: user } = useGetUserById(
|
|
137
|
+
{ id: userId },
|
|
138
|
+
{
|
|
139
|
+
enabled: !!userId, // React Query options
|
|
140
|
+
staleTime: 5000,
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return <div>{user?.name}</div>;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### Mutation Hooks (POST, PUT, PATCH, DELETE)
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { useCreateUser, useUpdateUser, useDeleteUser } from './api/generated';
|
|
152
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
153
|
+
|
|
154
|
+
function CreateUserForm() {
|
|
155
|
+
const queryClient = useQueryClient();
|
|
156
|
+
|
|
157
|
+
const { mutate, isPending } = useCreateUser({
|
|
158
|
+
onSuccess: () => {
|
|
159
|
+
// Invalidate and refetch
|
|
160
|
+
queryClient.invalidateQueries({ queryKey: ['getUsers'] });
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
const formData = new FormData(e.currentTarget);
|
|
167
|
+
|
|
168
|
+
mutate({
|
|
169
|
+
name: formData.get('name') as string,
|
|
170
|
+
email: formData.get('email') as string,
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<form onSubmit={handleSubmit}>
|
|
176
|
+
<input name="name" required />
|
|
177
|
+
<input name="email" type="email" required />
|
|
178
|
+
<button type="submit" disabled={isPending}>
|
|
179
|
+
{isPending ? 'Creating...' : 'Create User'}
|
|
180
|
+
</button>
|
|
181
|
+
</form>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function UserActions({ userId }: { userId: number }) {
|
|
186
|
+
const queryClient = useQueryClient();
|
|
187
|
+
|
|
188
|
+
const { mutate: updateUser } = useUpdateUser({
|
|
189
|
+
onSuccess: () => {
|
|
190
|
+
queryClient.invalidateQueries({ queryKey: ['getUserById', { id: userId }] });
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const { mutate: deleteUser } = useDeleteUser({
|
|
195
|
+
onSuccess: () => {
|
|
196
|
+
queryClient.invalidateQueries({ queryKey: ['getUsers'] });
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div>
|
|
202
|
+
<button onClick={() => updateUser({ id: userId, name: 'Updated Name' })}>
|
|
203
|
+
Update
|
|
204
|
+
</button>
|
|
205
|
+
<button onClick={() => deleteUser({ id: userId })}>
|
|
206
|
+
Delete
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Advanced Usage
|
|
214
|
+
|
|
215
|
+
#### Custom Query Keys
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { useGetUsers } from './api/generated';
|
|
219
|
+
|
|
220
|
+
function FilteredUsers({ status }: { status: string }) {
|
|
221
|
+
const { data } = useGetUsers(
|
|
222
|
+
{ status },
|
|
223
|
+
{
|
|
224
|
+
queryKey: ['users', status], // Custom query key
|
|
225
|
+
staleTime: 60000,
|
|
226
|
+
refetchOnWindowFocus: false,
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
return <div>{/* Render users */}</div>;
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### Error Handling
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { useCreateUser } from './api/generated';
|
|
238
|
+
|
|
239
|
+
function CreateUserForm() {
|
|
240
|
+
const { mutate, error, isError } = useCreateUser({
|
|
241
|
+
onError: (error) => {
|
|
242
|
+
console.error('Failed to create user:', error);
|
|
243
|
+
// Show toast notification, etc.
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div>
|
|
249
|
+
{isError && <div className="error">{error.message}</div>}
|
|
250
|
+
{/* Form fields */}
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### Optimistic Updates
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { useUpdateUser } from './api/generated';
|
|
260
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
261
|
+
|
|
262
|
+
function UserEditor({ userId }: { userId: number }) {
|
|
263
|
+
const queryClient = useQueryClient();
|
|
264
|
+
|
|
265
|
+
const { mutate } = useUpdateUser({
|
|
266
|
+
onMutate: async (newUser) => {
|
|
267
|
+
// Cancel outgoing refetches
|
|
268
|
+
await queryClient.cancelQueries({ queryKey: ['getUserById', { id: userId }] });
|
|
269
|
+
|
|
270
|
+
// Snapshot the previous value
|
|
271
|
+
const previousUser = queryClient.getQueryData(['getUserById', { id: userId }]);
|
|
272
|
+
|
|
273
|
+
// Optimistically update
|
|
274
|
+
queryClient.setQueryData(['getUserById', { id: userId }], newUser);
|
|
275
|
+
|
|
276
|
+
return { previousUser };
|
|
277
|
+
},
|
|
278
|
+
onError: (err, newUser, context) => {
|
|
279
|
+
// Rollback on error
|
|
280
|
+
queryClient.setQueryData(
|
|
281
|
+
['getUserById', { id: userId }],
|
|
282
|
+
context?.previousUser
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
onSettled: () => {
|
|
286
|
+
queryClient.invalidateQueries({ queryKey: ['getUserById', { id: userId }] });
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return <div>{/* Editor UI */}</div>;
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Requirements
|
|
295
|
+
|
|
296
|
+
- **TypeScript**: ^5.0.0
|
|
297
|
+
- **@tanstack/react-query**: ^5.0.0 (peer dependency for generated code)
|
|
298
|
+
- **React**: ^18.0.0 (peer dependency for generated code)
|
|
299
|
+
|
|
300
|
+
## Project Structure
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
your-project/
|
|
304
|
+
├── src/
|
|
305
|
+
│ ├── api/
|
|
306
|
+
│ │ └── generated.ts # Generated by this tool
|
|
307
|
+
│ ├── components/
|
|
308
|
+
│ │ └── Users.tsx # Your components using the hooks
|
|
309
|
+
│ └── App.tsx
|
|
310
|
+
├── openapi.json # Your OpenAPI spec
|
|
311
|
+
└── package.json
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Development
|
|
315
|
+
|
|
316
|
+
### Setup
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Install dependencies
|
|
320
|
+
bun install
|
|
321
|
+
|
|
322
|
+
# Run the generator locally
|
|
323
|
+
bun index.ts -i ./example/openapi.json -o ./output
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Building
|
|
327
|
+
|
|
328
|
+
This project uses Bun as the runtime. No build step is necessary for development.
|
|
329
|
+
|
|
330
|
+
## How It Works
|
|
331
|
+
|
|
332
|
+
1. **Fetches OpenAPI Spec**: Reads from a URL or local file
|
|
333
|
+
2. **Generates TypeScript Types**: Creates interfaces from schema definitions
|
|
334
|
+
3. **Creates React Query Hooks**: Generates typed hooks for each endpoint
|
|
335
|
+
- GET requests → `useQuery` hooks
|
|
336
|
+
- POST/PUT/PATCH/DELETE → `useMutation` hooks
|
|
337
|
+
4. **Outputs Single File**: All types and hooks in one `generated.ts` file
|
|
338
|
+
|
|
339
|
+
## Limitations
|
|
340
|
+
|
|
341
|
+
- Only supports `application/json` content types
|
|
342
|
+
- Uses the first server URL as the base URL
|
|
343
|
+
- Assumes standard REST conventions for operations
|
|
344
|
+
|
|
345
|
+
## Contributing
|
|
346
|
+
|
|
347
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
348
|
+
|
|
349
|
+
## License
|
|
350
|
+
|
|
351
|
+
MIT
|
|
352
|
+
|
|
353
|
+
## Related Projects
|
|
354
|
+
|
|
355
|
+
- [Elysia](https://elysiajs.com/) - Fast and friendly Bun web framework
|
|
356
|
+
- [TanStack Query](https://tanstack.com/query) - Powerful data synchronization for React
|
|
357
|
+
- [OpenAPI](https://www.openapis.org/) - API specification standard
|
package/index.ts
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Elysia OpenAPI Code Generator
|
|
5
|
+
* Generates typed React Query hooks from OpenAPI specifications.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import https from 'https';
|
|
11
|
+
import http from 'http';
|
|
12
|
+
|
|
13
|
+
interface OpenAPISpec {
|
|
14
|
+
openapi: string;
|
|
15
|
+
info: {
|
|
16
|
+
title: string;
|
|
17
|
+
version: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
servers?: Array<{
|
|
21
|
+
url: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
}>;
|
|
24
|
+
paths: Record<string, OpenAPIPathItem>;
|
|
25
|
+
components?: {
|
|
26
|
+
schemas?: Record<string, OpenAPISchema>;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
};
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface OpenAPIPathItem {
|
|
33
|
+
get?: OpenAPIOperation;
|
|
34
|
+
post?: OpenAPIOperation;
|
|
35
|
+
put?: OpenAPIOperation;
|
|
36
|
+
delete?: OpenAPIOperation;
|
|
37
|
+
patch?: OpenAPIOperation;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface OpenAPIOperation {
|
|
42
|
+
operationId?: string;
|
|
43
|
+
summary?: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
parameters?: OpenAPIParameter[];
|
|
46
|
+
responses?: Record<string, OpenAPIResponse>;
|
|
47
|
+
requestBody?: {
|
|
48
|
+
content: {
|
|
49
|
+
[contentType: string]: {
|
|
50
|
+
schema: OpenAPISchema;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface OpenAPIParameter {
|
|
58
|
+
name: string;
|
|
59
|
+
in: 'query' | 'header' | 'path' | 'cookie';
|
|
60
|
+
description?: string;
|
|
61
|
+
required?: boolean;
|
|
62
|
+
schema?: OpenAPISchema;
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface OpenAPIResponse {
|
|
67
|
+
description: string;
|
|
68
|
+
content?: {
|
|
69
|
+
[contentType: string]: {
|
|
70
|
+
schema: OpenAPISchema;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface OpenAPISchema {
|
|
77
|
+
type?: string;
|
|
78
|
+
items?: OpenAPISchema;
|
|
79
|
+
properties?: Record<string, OpenAPISchema>;
|
|
80
|
+
required?: string[];
|
|
81
|
+
$ref?: string;
|
|
82
|
+
nullable?: boolean;
|
|
83
|
+
anyOf?: OpenAPISchema[];
|
|
84
|
+
oneOf?: OpenAPISchema[];
|
|
85
|
+
allOf?: OpenAPISchema[];
|
|
86
|
+
[key: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function fetchSpec(source: string): Promise<OpenAPISpec> {
|
|
90
|
+
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const client = source.startsWith('https://') ? https : http;
|
|
93
|
+
client.get(source, (res) => {
|
|
94
|
+
let data = '';
|
|
95
|
+
res.on('data', (chunk) => data += chunk);
|
|
96
|
+
res.on('end', () => {
|
|
97
|
+
try {
|
|
98
|
+
resolve(JSON.parse(data) as OpenAPISpec);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
reject(new Error(`Failed to parse OpenAPI spec: ${(e as Error).message}`));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
res.on('error', reject);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return JSON.parse(fs.readFileSync(source, 'utf8')) as OpenAPISpec;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveType(schema?: OpenAPISchema): string {
|
|
112
|
+
if (!schema) return 'any';
|
|
113
|
+
|
|
114
|
+
if (schema.$ref) {
|
|
115
|
+
return schema.$ref.split('/').pop() || 'any';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (schema.type === 'array' && schema.items) {
|
|
119
|
+
return `Array<${resolveType(schema.items)}>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (schema.type === 'object') {
|
|
123
|
+
if (!schema.properties) return 'Record<string, any>';
|
|
124
|
+
|
|
125
|
+
const props = Object.entries(schema.properties).map(([key, prop]) => {
|
|
126
|
+
const isRequired = schema.required?.includes(key);
|
|
127
|
+
const optional = isRequired ? '' : '?';
|
|
128
|
+
return ` ${key}${optional}: ${resolveType(prop)};`;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return `{\n${props.join('\n')}\n}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (schema.anyOf || schema.oneOf) {
|
|
135
|
+
return (schema.anyOf || schema.oneOf || []).map(resolveType).join(' | ');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (schema.allOf) {
|
|
139
|
+
return schema.allOf.map(resolveType).join(' & ');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (schema.nullable) {
|
|
143
|
+
return `${resolveType({ ...schema, nullable: false })} | null`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const typeMap: Record<string, string> = {
|
|
147
|
+
string: 'string',
|
|
148
|
+
number: 'number',
|
|
149
|
+
integer: 'number',
|
|
150
|
+
boolean: 'boolean',
|
|
151
|
+
null: 'null',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return (schema.type && typeMap[schema.type]) || 'any';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function generateTypes(spec: OpenAPISpec): string {
|
|
158
|
+
const definitions: string[] = [];
|
|
159
|
+
|
|
160
|
+
if (spec.components?.schemas) {
|
|
161
|
+
for (const [name, schema] of Object.entries(spec.components.schemas)) {
|
|
162
|
+
definitions.push(`export type ${name} = ${resolveType(schema)};`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const [pathUrl, pathItem] of Object.entries(spec.paths)) {
|
|
167
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
168
|
+
if (!['get', 'post', 'put', 'patch', 'delete'].includes(method)) continue;
|
|
169
|
+
|
|
170
|
+
const op = operation as OpenAPIOperation;
|
|
171
|
+
const opId = op.operationId || `${method}${pathUrl.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
172
|
+
const response = op.responses?.['200'];
|
|
173
|
+
|
|
174
|
+
if (response?.content?.['application/json']?.schema) {
|
|
175
|
+
const responseType = resolveType(response.content['application/json'].schema);
|
|
176
|
+
definitions.push(`export type ${capitalize(opId)}Response = ${responseType};`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const params = op.parameters || [];
|
|
180
|
+
if (params.length > 0) {
|
|
181
|
+
const paramProps = params.map(param => {
|
|
182
|
+
const required = param.required ? '' : '?';
|
|
183
|
+
return ` ${param.name}${required}: ${resolveType(param.schema)};`;
|
|
184
|
+
});
|
|
185
|
+
definitions.push(`export type ${capitalize(opId)}Params = {\n${paramProps.join('\n')}\n};`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (op.requestBody?.content?.['application/json']?.schema) {
|
|
189
|
+
const bodyType = resolveType(op.requestBody.content['application/json'].schema);
|
|
190
|
+
definitions.push(`export type ${capitalize(opId)}Body = ${bodyType};`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return definitions.join('\n\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function generateHooks(spec: OpenAPISpec): string {
|
|
199
|
+
const hooks: string[] = [];
|
|
200
|
+
const baseUrl = spec.servers?.[0]?.url || '';
|
|
201
|
+
|
|
202
|
+
for (const [pathUrl, pathItem] of Object.entries(spec.paths)) {
|
|
203
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
204
|
+
if (!['get', 'post', 'put', 'patch', 'delete'].includes(method)) continue;
|
|
205
|
+
|
|
206
|
+
const op = operation as OpenAPIOperation;
|
|
207
|
+
const opId = op.operationId || `${method}${pathUrl.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
208
|
+
|
|
209
|
+
const hasResponse = !!op.responses?.['200']?.content?.['application/json']?.schema;
|
|
210
|
+
const responseType = hasResponse ? `${capitalize(opId)}Response` : 'any';
|
|
211
|
+
|
|
212
|
+
const params = op.parameters || [];
|
|
213
|
+
const hasParams = params.length > 0;
|
|
214
|
+
const paramsType = hasParams ? `${capitalize(opId)}Params` : 'void';
|
|
215
|
+
|
|
216
|
+
const hasBody = !!op.requestBody?.content?.['application/json']?.schema;
|
|
217
|
+
const bodyType = hasBody ? `${capitalize(opId)}Body` : 'void';
|
|
218
|
+
|
|
219
|
+
if (method === 'get') {
|
|
220
|
+
const queryParams = params.map(p => p.name);
|
|
221
|
+
const queryString = queryParams.length > 0
|
|
222
|
+
? `?${queryParams.map((p: string) => `\${params?.${p} !== undefined ? '${p}=' + params.${p} : ''}`).join('&')}`
|
|
223
|
+
: '';
|
|
224
|
+
|
|
225
|
+
hooks.push(`
|
|
226
|
+
export const use${capitalize(opId)} = (
|
|
227
|
+
params${hasParams ? '' : '?'}: ${paramsType},
|
|
228
|
+
options?: Omit<UseQueryOptions<${responseType}>, 'queryKey' | 'queryFn'>
|
|
229
|
+
) => {
|
|
230
|
+
return useQuery<${responseType}>({
|
|
231
|
+
queryKey: ['${opId}', params],
|
|
232
|
+
queryFn: async () => {
|
|
233
|
+
const res = await fetch(\`\${baseUrl}${pathUrl}${queryString}\`);
|
|
234
|
+
if (!res.ok) throw new Error('API Error');
|
|
235
|
+
return res.json();
|
|
236
|
+
},
|
|
237
|
+
...options,
|
|
238
|
+
});
|
|
239
|
+
};`);
|
|
240
|
+
} else {
|
|
241
|
+
let inputType = 'void';
|
|
242
|
+
let inputArg = '';
|
|
243
|
+
|
|
244
|
+
if (hasBody) {
|
|
245
|
+
inputType = bodyType;
|
|
246
|
+
inputArg = 'body';
|
|
247
|
+
} else if (hasParams) {
|
|
248
|
+
inputType = paramsType;
|
|
249
|
+
inputArg = 'params';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const hasInput = inputType !== 'void';
|
|
253
|
+
|
|
254
|
+
hooks.push(`
|
|
255
|
+
export const use${capitalize(opId)} = (
|
|
256
|
+
options?: UseMutationOptions<${responseType}, Error, ${inputType}>
|
|
257
|
+
) => {
|
|
258
|
+
return useMutation<${responseType}, Error, ${inputType}>({
|
|
259
|
+
mutationFn: async (${hasInput ? inputArg : ''}) => {
|
|
260
|
+
const res = await fetch(\`\${baseUrl}${pathUrl}\`, {
|
|
261
|
+
method: '${method.toUpperCase()}',
|
|
262
|
+
headers: { 'Content-Type': 'application/json' },
|
|
263
|
+
body: JSON.stringify(${hasInput ? inputArg : '{}'}),
|
|
264
|
+
});
|
|
265
|
+
if (!res.ok) throw new Error('API Error');
|
|
266
|
+
return res.json();
|
|
267
|
+
},
|
|
268
|
+
...options,
|
|
269
|
+
});
|
|
270
|
+
};`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return hooks.join('\n');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function capitalize(str: string): string {
|
|
279
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function parseArgs() {
|
|
283
|
+
const args = process.argv.slice(2);
|
|
284
|
+
const config = {
|
|
285
|
+
input: '',
|
|
286
|
+
output: '',
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
for (let i = 0; i < args.length; i++) {
|
|
290
|
+
const arg = args[i];
|
|
291
|
+
if (!arg) continue;
|
|
292
|
+
|
|
293
|
+
if (arg === '-i' || arg === '--input') {
|
|
294
|
+
const value = args[++i];
|
|
295
|
+
if (!value) {
|
|
296
|
+
console.error(`Error: ${arg} flag requires a value`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
config.input = value;
|
|
300
|
+
} else if (arg === '-o' || arg === '--output') {
|
|
301
|
+
const value = args[++i];
|
|
302
|
+
if (!value) {
|
|
303
|
+
console.error(`Error: ${arg} flag requires a value`);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
config.output = value;
|
|
307
|
+
} else if (!arg.startsWith('-')) {
|
|
308
|
+
if (!config.input) config.input = arg;
|
|
309
|
+
else if (!config.output) config.output = arg;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return config;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function showHelp() {
|
|
317
|
+
console.log(`
|
|
318
|
+
Elysia OpenAPI Code Generator
|
|
319
|
+
Generate React Query hooks and TypeScript types from OpenAPI specifications.
|
|
320
|
+
|
|
321
|
+
Usage:
|
|
322
|
+
elysia-codegen -i <source> -o <output>
|
|
323
|
+
elysia-codegen --input <source> --output <output>
|
|
324
|
+
elysia-codegen <source> <output>
|
|
325
|
+
|
|
326
|
+
Arguments:
|
|
327
|
+
-i, --input <source> OpenAPI spec source (URL or file path)
|
|
328
|
+
-o, --output <output> Output directory for generated files
|
|
329
|
+
|
|
330
|
+
Examples:
|
|
331
|
+
# Using flags
|
|
332
|
+
elysia-codegen -i https://api.example.com/openapi.json -o ./src/api
|
|
333
|
+
elysia-codegen --input ./openapi.json --output ./generated
|
|
334
|
+
|
|
335
|
+
# Using positional arguments
|
|
336
|
+
elysia-codegen https://api.example.com/openapi.json ./src/api
|
|
337
|
+
elysia-codegen ./openapi.json ./generated
|
|
338
|
+
`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function main() {
|
|
342
|
+
const args = process.argv.slice(2);
|
|
343
|
+
|
|
344
|
+
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
|
345
|
+
showHelp();
|
|
346
|
+
process.exit(0);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const { input, output } = await parseArgs();
|
|
350
|
+
|
|
351
|
+
if (!input || !output) {
|
|
352
|
+
console.error('Error: Both input source and output directory are required.\n');
|
|
353
|
+
showHelp();
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log('Fetching OpenAPI spec...');
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const spec = await fetchSpec(input);
|
|
361
|
+
const types = generateTypes(spec);
|
|
362
|
+
const hooks = generateHooks(spec);
|
|
363
|
+
const baseUrl = spec.servers?.[0]?.url || '';
|
|
364
|
+
|
|
365
|
+
const fileContent = `/* eslint-disable */
|
|
366
|
+
/**
|
|
367
|
+
* Auto-generated by Elysia OpenAPI Codegen
|
|
368
|
+
*/
|
|
369
|
+
|
|
370
|
+
import { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
|
371
|
+
|
|
372
|
+
const baseUrl = '${baseUrl}';
|
|
373
|
+
|
|
374
|
+
${types}
|
|
375
|
+
|
|
376
|
+
${hooks}
|
|
377
|
+
`;
|
|
378
|
+
|
|
379
|
+
if (!fs.existsSync(output)) {
|
|
380
|
+
fs.mkdirSync(output, { recursive: true });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const outputPath = path.join(output, 'generated.ts');
|
|
384
|
+
fs.writeFileSync(outputPath, fileContent);
|
|
385
|
+
|
|
386
|
+
console.log(`Successfully generated hooks at ${outputPath}`);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.error('Generation failed:', err instanceof Error ? err.message : String(err));
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "elysia-openapi-codegen",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate React Query hooks and fully typed TypeScript interfaces from Elysia OpenAPI specs.",
|
|
5
|
+
"module": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"elysia-codegen": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"author": "Khantamir mkhantamir77@gmail.com",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/mkhantamir/elysia-openapi-codegen.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/mkhantamir/elysia-openapi-codegen/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/mkhantamir/elysia-openapi-codegen#readme",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"elysia",
|
|
26
|
+
"openapi",
|
|
27
|
+
"codegen",
|
|
28
|
+
"typescript",
|
|
29
|
+
"react-query",
|
|
30
|
+
"tanstack-query"
|
|
31
|
+
],
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/bun": "latest"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"typescript": "^5"
|
|
37
|
+
}
|
|
38
|
+
}
|