elysia-openapi-codegen 0.1.8 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +10 -3
  2. package/dist/index.js +36 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,6 +11,12 @@ Generate fully-typed React Query hooks and TypeScript interfaces from OpenAPI sp
11
11
  - **React Query Integration**: Generates `useQuery` and `useMutation` hooks ready to use
12
12
  - **Flexible Arguments**: Supports both flag-based and positional arguments
13
13
 
14
+ ### Using Bunx (Recommended)
15
+
16
+ ```bash
17
+ bunx elysia-openapi-codegen -i https://api.example.com/openapi.json -o ./src/api
18
+ ```
19
+
14
20
  ## Installation
15
21
 
16
22
  ### Using Bun (Recommended)
@@ -50,6 +56,7 @@ elysia-codegen -i <source> -o <output>
50
56
  ```
51
57
 
52
58
  **Arguments:**
59
+
53
60
  - `-i, --input <source>` - OpenAPI spec source (URL or file path)
54
61
  - `-o, --output <output>` - Output directory for generated files
55
62
  - `-h, --help` - Show help message
@@ -98,13 +105,13 @@ The generator creates a single `generated.ts` file containing all types and hook
98
105
  All request/response types are automatically generated:
99
106
 
100
107
  ```typescript
101
- import type { User, CreateUserBody, GetUsersResponse } from './generated';
108
+ import type { User, CreateUserBody, GetUsersResponse } from "./generated";
102
109
 
103
110
  // Use types in your components
104
111
  const user: User = {
105
112
  id: 1,
106
- name: 'John Doe',
107
- email: 'john@example.com'
113
+ name: "John Doe",
114
+ email: "john@example.com",
108
115
  };
109
116
  ```
110
117
 
package/dist/index.js CHANGED
@@ -43,7 +43,8 @@ ${props.join(`
43
43
  }`;
44
44
  }
45
45
  if (schema.anyOf || schema.oneOf) {
46
- return (schema.anyOf || schema.oneOf || []).map(resolveType).join(" | ");
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(" & ");
@@ -77,6 +78,8 @@ function generateGroups(spec) {
77
78
  }
78
79
  }
79
80
  for (const [pathUrl, pathItem] of Object.entries(spec.paths)) {
81
+ if (pathUrl.includes("*"))
82
+ continue;
80
83
  const g = getGroup(getPathGroup(pathUrl));
81
84
  for (const [method, operation] of Object.entries(pathItem)) {
82
85
  if (!["get", "post", "put", "patch", "delete"].includes(method))
@@ -100,6 +103,8 @@ ${paramProps.join(`
100
103
  }
101
104
  const isMultipart = !op.requestBody?.content?.["application/json"] && !!op.requestBody?.content?.["multipart/form-data"];
102
105
  const bodySchema = op.requestBody?.content?.["application/json"]?.schema ?? op.requestBody?.content?.["multipart/form-data"]?.schema;
106
+ const hasBinaryField = (s) => !!s && (s.format === "binary" || Object.values(s.properties ?? {}).some((p) => p.format === "binary"));
107
+ const useFormData = isMultipart || hasBinaryField(bodySchema);
103
108
  if (bodySchema) {
104
109
  g.types.push(`export type ${capitalize(opId)}Body = ${resolveType(bodySchema)};`);
105
110
  }
@@ -116,6 +121,7 @@ ${paramProps.join(`
116
121
  for (const p of pathParamsList) {
117
122
  urlPath = urlPath.replace(`{${p.name}}`, `\${params.${p.name}}`);
118
123
  }
124
+ urlPath = urlPath.replace(/\/+$/, "");
119
125
  const qsLines = (indent) => queryParamsList.map((p) => `${indent}if (params?.${p.name} !== undefined) _qs.set('${p.name}', String(params.${p.name}));`).join(`
120
126
  `);
121
127
  if (!hasResponse) {
@@ -133,8 +139,12 @@ ${body}
133
139
  ${qsLines(" ")}
134
140
  const _qstr = _qs.toString();
135
141
  const res = await fetch(\`\${baseUrl}${urlPath}\${_qstr ? '?' + _qstr : ''}\`);` : ` const res = await fetch(\`\${baseUrl}${urlPath}\`);`;
136
- const enabledGuard = pathParamsList.length > 0 ? `
137
- enabled: ${pathParamsList.map((p) => `params.${p.name} != null`).join(" && ")},` : "";
142
+ const guardParams = [
143
+ ...pathParamsList,
144
+ ...queryParamsList.filter((p) => p.required)
145
+ ];
146
+ const enabledGuard = guardParams.length > 0 ? `
147
+ enabled: ${guardParams.map((p) => `params.${p.name} != null`).join(" && ")},` : "";
138
148
  g.hooks.push(`
139
149
  export const use${capitalize(opId)} = (
140
150
  params${hasParams ? "" : "?"}: ${paramsType},
@@ -152,27 +162,41 @@ ${fetchStatement}
152
162
  };`);
153
163
  } else {
154
164
  const pathParamsList = params.filter((p) => p.in === "path");
165
+ const queryParamsList = params.filter((p) => p.in === "query");
155
166
  const hasPathParams = pathParamsList.length > 0;
167
+ const hasQueryParams = queryParamsList.length > 0;
168
+ const hasAnyParams = hasPathParams || hasQueryParams;
156
169
  let urlPath = pathUrl;
157
170
  for (const p of pathParamsList) {
158
171
  urlPath = urlPath.replace(`{${p.name}}`, `\${${p.name}}`);
159
172
  }
173
+ urlPath = urlPath.replace(/\/+$/, "");
174
+ const mutQsCode = hasQueryParams ? `
175
+ const _qs = new URLSearchParams();
176
+ ${queryParamsList.map((p) => ` if (${p.name} !== undefined) _qs.set('${p.name}', String(${p.name}));`).join(`
177
+ `)}
178
+ const _qstr = _qs.toString();` : "";
179
+ const fetchUrlExpr = hasQueryParams ? `\`\${baseUrl}${urlPath}\${_qstr ? '?' + _qstr : ''}\`` : `\`\${baseUrl}${urlPath}\``;
180
+ const allParamNames = [
181
+ ...pathParamsList.map((p) => p.name),
182
+ ...queryParamsList.map((p) => p.name)
183
+ ];
160
184
  let inputType;
161
185
  let mutationArg;
162
- if (hasBody && hasPathParams) {
186
+ if (hasBody && hasAnyParams) {
163
187
  inputType = `${capitalize(opId)}Params & ${capitalize(opId)}Body`;
164
- mutationArg = `{ ${pathParamsList.map((p) => p.name).join(", ")}, ...body }`;
188
+ mutationArg = `{ ${allParamNames.join(", ")}, ...body }`;
165
189
  } else if (hasBody) {
166
190
  inputType = bodyType;
167
191
  mutationArg = "body";
168
- } else if (hasPathParams) {
192
+ } else if (hasAnyParams) {
169
193
  inputType = paramsType;
170
- mutationArg = `{ ${pathParamsList.map((p) => p.name).join(", ")} }`;
194
+ mutationArg = `{ ${allParamNames.join(", ")} }`;
171
195
  } else {
172
196
  inputType = "void";
173
197
  mutationArg = "";
174
198
  }
175
- const fetchBodyLines = hasBody ? isMultipart ? `
199
+ const fetchBodyLines = hasBody ? useFormData ? `
176
200
  body: Object.entries(body as Record<string, unknown>).reduce((f, [k, v]) => { f.append(k, v as any); return f; }, new FormData()),` : `
177
201
  headers: { 'Content-Type': 'application/json' },
178
202
  body: JSON.stringify(body),` : "";
@@ -181,8 +205,8 @@ export const use${capitalize(opId)} = (
181
205
  options?: UseMutationOptions<${responseType}, Error, ${inputType}>
182
206
  ) => {
183
207
  return useMutation<${responseType}, Error, ${inputType}>({
184
- mutationFn: async (${mutationArg}) => {
185
- const res = await fetch(\`\${baseUrl}${urlPath}\`, {
208
+ mutationFn: async (${mutationArg}) => {${mutQsCode}
209
+ const res = await fetch(${fetchUrlExpr}, {
186
210
  method: '${method.toUpperCase()}',${fetchBodyLines}
187
211
  });
188
212
  if (!res.ok) throw new Error(\`\${res.status}: \${await res.text()}\`);
@@ -268,7 +292,8 @@ async function main() {
268
292
  fs.mkdirSync(output, { recursive: true });
269
293
  }
270
294
  fs.writeFileSync(path.join(output, "base.ts"), `/* eslint-disable */
271
- export const baseUrl = '${baseUrl}';
295
+ /** Override this before making any API calls if your base URL changes at runtime */
296
+ export let baseUrl = '${baseUrl}';
272
297
  `);
273
298
  const groupNames = [];
274
299
  for (const [groupName, { types, hooks }] of Object.entries(groups)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elysia-openapi-codegen",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "author": "Khantamir mkhantamir77@gmail.com",
5
5
  "repository": {
6
6
  "type": "git",