react-query-lightbase-codegen 3.1.6 → 3.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generator/clientGenerator.js +36 -7
- package/dist/generator/schemaGenerator.js +61 -76
- package/dist/utils.js +51 -34
- package/package.json +1 -1
- package/src/generator/clientGenerator.ts +38 -7
- package/src/generator/schemaGenerator.ts +64 -78
- package/src/utils.ts +55 -37
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateApiClient = generateApiClient;
|
|
4
4
|
const utils_1 = require("../utils");
|
|
5
|
+
/**
|
|
6
|
+
* Walks a request body schema (including allOf/oneOf/anyOf composition) and returns
|
|
7
|
+
* the union of property names contributed by all branches. Used to pick body fields
|
|
8
|
+
* out of the merged Params object so path/query/header values aren't sent as the body.
|
|
9
|
+
*/
|
|
10
|
+
function collectBodyPropertyNames(schema, spec, visitedRefs = new Set()) {
|
|
11
|
+
if (!schema)
|
|
12
|
+
return [];
|
|
13
|
+
if ("$ref" in schema) {
|
|
14
|
+
if (visitedRefs.has(schema.$ref))
|
|
15
|
+
return [];
|
|
16
|
+
const next = new Set(visitedRefs).add(schema.$ref);
|
|
17
|
+
return collectBodyPropertyNames((0, utils_1.resolveSchema)(schema, spec), spec, next);
|
|
18
|
+
}
|
|
19
|
+
const names = new Set();
|
|
20
|
+
if (schema.properties)
|
|
21
|
+
Object.keys(schema.properties).forEach((k) => names.add(k));
|
|
22
|
+
for (const key of ["allOf", "oneOf", "anyOf"]) {
|
|
23
|
+
const branches = schema[key];
|
|
24
|
+
if (!branches)
|
|
25
|
+
continue;
|
|
26
|
+
for (const branch of branches) {
|
|
27
|
+
collectBodyPropertyNames(branch, spec, visitedRefs).forEach((n) => names.add(n));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return Array.from(names);
|
|
31
|
+
}
|
|
5
32
|
function generateAxiosMethod(operation, spec) {
|
|
6
33
|
const { method, path, operationId, summary, description, deprecated, parameters, requestBody, responses } = operation;
|
|
7
34
|
// Generate JSDoc
|
|
@@ -59,9 +86,13 @@ function generateAxiosMethod(operation, spec) {
|
|
|
59
86
|
: undefined;
|
|
60
87
|
const requestBodyContent = requestBody && "content" in requestBody ? (0, utils_1.getContentSchema)(requestBody.content) : undefined;
|
|
61
88
|
const requestBodySchema = requestBodyContent ? (0, utils_1.resolveSchema)(requestBodyContent, spec) : undefined;
|
|
89
|
+
// Resolve property names contributed by the body schema, walking allOf/oneOf/anyOf so
|
|
90
|
+
// composition-based bodies are still treated as object bodies (not "primitive").
|
|
91
|
+
const bodyPropertyNames = requestBodySchema ? collectBodyPropertyNames(requestBodySchema, spec) : [];
|
|
92
|
+
const hasObjectBody = bodyPropertyNames.length > 0;
|
|
62
93
|
// Check if request body is a primitive type (string, number, boolean)
|
|
63
94
|
const isPrimitiveRequestBody = requestBodySchema &&
|
|
64
|
-
!
|
|
95
|
+
!hasObjectBody &&
|
|
65
96
|
!requestBodySchema.type?.includes("object") &&
|
|
66
97
|
!requestBodySchema.type?.includes("array");
|
|
67
98
|
// Add request body type if it exists
|
|
@@ -98,11 +129,9 @@ function generateAxiosMethod(operation, spec) {
|
|
|
98
129
|
${queryParams.map((p) => `["${p.name}"]: data["${p.name}"]`).join(",\n ")}
|
|
99
130
|
};`
|
|
100
131
|
: "",
|
|
101
|
-
|
|
132
|
+
hasObjectBody && !formDataSchema?.properties
|
|
102
133
|
? `const bodyData = {
|
|
103
|
-
${
|
|
104
|
-
.map(([key]) => `["${key}"]: data["${key}"]`)
|
|
105
|
-
.join(",\n ")}
|
|
134
|
+
${bodyPropertyNames.map((key) => `["${key}"]: data["${key}"]`).join(",\n ")}
|
|
106
135
|
};`
|
|
107
136
|
: "",
|
|
108
137
|
formDataSchema?.properties
|
|
@@ -129,8 +158,8 @@ function generateAxiosMethod(operation, spec) {
|
|
|
129
158
|
// Note: Don't set Content-Type for FormData - Axios will set it automatically with the correct boundary
|
|
130
159
|
requestBody
|
|
131
160
|
? responseType === "void"
|
|
132
|
-
? `await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties ||
|
|
133
|
-
: `const res = await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties ||
|
|
161
|
+
? `await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || hasObjectBody ? "bodyData" : "data"}, axiosConfig);`
|
|
162
|
+
: `const res = await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || hasObjectBody ? "bodyData" : "data"}, axiosConfig);`
|
|
134
163
|
: responseType === "void"
|
|
135
164
|
? `await apiClient.${method}<${responseType}>(url, axiosConfig);`
|
|
136
165
|
: `const res = await apiClient.${method}<${responseType}>(url, axiosConfig);`,
|
|
@@ -18,10 +18,10 @@ function formatParamProperty(param, forceRequired = false) {
|
|
|
18
18
|
function generateTypeDefinition(name, schema) {
|
|
19
19
|
const description = !("$ref" in schema) && schema.description ? `/**\n * ${schema.description}\n */\n` : "";
|
|
20
20
|
const typeValue = (0, utils_1.getTypeFromSchema)(schema);
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
return
|
|
21
|
+
// Only emit `interface` when the body is a plain object literal. Composition
|
|
22
|
+
// (allOf/oneOf/anyOf) produces references or intersections that must use a type alias.
|
|
23
|
+
const canBeInterface = typeValue?.trimStart().startsWith("{");
|
|
24
|
+
return canBeInterface
|
|
25
25
|
? `${description}export interface ${(0, utils_1.sanitizeTypeName)(name)} ${typeValue}\n\n`
|
|
26
26
|
: `${description}export type ${(0, utils_1.sanitizeTypeName)(name)} = ${typeValue}\n\n`;
|
|
27
27
|
}
|
|
@@ -39,82 +39,67 @@ function generateTypeDefinitions(spec) {
|
|
|
39
39
|
output += generateTypeDefinition(name, schema);
|
|
40
40
|
generatedTypes.add(name);
|
|
41
41
|
}
|
|
42
|
-
// Generate request/response types
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
for (const [code, response] of Object.entries(responses)) {
|
|
66
|
-
const responseObj = response;
|
|
67
|
-
const responseSchema = (0, utils_1.getContentSchema)(responseObj.content);
|
|
68
|
-
if (responseSchema) {
|
|
69
|
-
const typeName = `${operationId}Response${code}`;
|
|
70
|
-
output += generateTypeDefinition(typeName, responseSchema);
|
|
71
|
-
// Track non-2xx responses for error union type
|
|
72
|
-
if (!code.startsWith("2")) {
|
|
73
|
-
errorTypes.push(typeName);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
// Generate error union type if there are error responses
|
|
79
|
-
if (errorTypes.length > 0) {
|
|
80
|
-
output += `export type ${(0, utils_1.pascalCase)(operationId)}Error = ${errorTypes.join(" | ")};\n\n`;
|
|
81
|
-
}
|
|
82
|
-
// Build data type parts
|
|
83
|
-
const dataProps = [];
|
|
84
|
-
const urlParams = (parameters?.filter((p) => "in" in p && p.in === "path") ||
|
|
85
|
-
[]);
|
|
86
|
-
const queryParams = (parameters?.filter((p) => "in" in p && p.in === "query") ||
|
|
87
|
-
[]);
|
|
88
|
-
const headerParams = (parameters?.filter((p) => "in" in p && p.in === "header") ||
|
|
89
|
-
[]);
|
|
90
|
-
const cookieParams = (parameters?.filter((p) => "in" in p && p.in === "cookie") ||
|
|
91
|
-
[]);
|
|
92
|
-
// Add path, query, header, and cookie parameters
|
|
93
|
-
urlParams.forEach((p) => dataProps.push(formatParamProperty(p, true))); // Path params always required
|
|
94
|
-
queryParams.forEach((p) => dataProps.push(formatParamProperty(p)));
|
|
95
|
-
headerParams.forEach((p) => dataProps.push(formatParamProperty(p)));
|
|
96
|
-
cookieParams.forEach((p) => dataProps.push(formatParamProperty(p)));
|
|
97
|
-
// Add request body type if it exists
|
|
98
|
-
const hasData = (parameters && parameters.length > 0) || requestBody;
|
|
99
|
-
let dataType = "undefined";
|
|
100
|
-
const namedType = (0, utils_1.pascalCase)(operationId);
|
|
101
|
-
if (hasData) {
|
|
102
|
-
if (requestBody && dataProps.length > 0) {
|
|
103
|
-
dataType = `${namedType}Request & { ${dataProps.join("; ")} }`;
|
|
104
|
-
}
|
|
105
|
-
else if (requestBody) {
|
|
106
|
-
dataType = `${namedType}Request`;
|
|
107
|
-
}
|
|
108
|
-
else if (dataProps.length > 0) {
|
|
109
|
-
dataType = `{ ${dataProps.join("; ")} }`;
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
dataType = "Record<string, never>";
|
|
42
|
+
// Generate request/response types. Use the same operation-collection helper as the
|
|
43
|
+
// client generator so pathItem-level parameters and $ref parameters stay in sync.
|
|
44
|
+
for (const { operationId, parameters, requestBody, responses } of (0, utils_1.collectOperations)(spec)) {
|
|
45
|
+
// Generate request body type
|
|
46
|
+
if (requestBody) {
|
|
47
|
+
const requestSchema = (0, utils_1.getContentSchema)(requestBody.content);
|
|
48
|
+
if (requestSchema) {
|
|
49
|
+
const typeName = `${operationId}Request`;
|
|
50
|
+
output += generateTypeDefinition(typeName, requestSchema);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Generate response types
|
|
54
|
+
const errorTypes = [];
|
|
55
|
+
if (responses) {
|
|
56
|
+
for (const [code, response] of Object.entries(responses)) {
|
|
57
|
+
const responseObj = response;
|
|
58
|
+
const responseSchema = (0, utils_1.getContentSchema)(responseObj.content);
|
|
59
|
+
if (responseSchema) {
|
|
60
|
+
const typeName = `${operationId}Response${code}`;
|
|
61
|
+
output += generateTypeDefinition(typeName, responseSchema);
|
|
62
|
+
// Track non-2xx responses for error union type
|
|
63
|
+
if (!code.startsWith("2")) {
|
|
64
|
+
errorTypes.push(typeName);
|
|
113
65
|
}
|
|
114
|
-
output += `\n\nexport type ${(0, utils_1.pascalCase)(operationId)}Params = ${dataType};\n\n`;
|
|
115
66
|
}
|
|
116
67
|
}
|
|
117
68
|
}
|
|
69
|
+
// Generate error union type if there are error responses
|
|
70
|
+
if (errorTypes.length > 0) {
|
|
71
|
+
output += `export type ${(0, utils_1.pascalCase)(operationId)}Error = ${errorTypes.join(" | ")};\n\n`;
|
|
72
|
+
}
|
|
73
|
+
// Build data type parts
|
|
74
|
+
const dataProps = [];
|
|
75
|
+
const urlParams = parameters.filter((p) => p.in === "path");
|
|
76
|
+
const queryParams = parameters.filter((p) => p.in === "query");
|
|
77
|
+
const headerParams = parameters.filter((p) => p.in === "header");
|
|
78
|
+
const cookieParams = parameters.filter((p) => p.in === "cookie");
|
|
79
|
+
// Add path, query, header, and cookie parameters
|
|
80
|
+
urlParams.forEach((p) => dataProps.push(formatParamProperty(p, true))); // Path params always required
|
|
81
|
+
queryParams.forEach((p) => dataProps.push(formatParamProperty(p)));
|
|
82
|
+
headerParams.forEach((p) => dataProps.push(formatParamProperty(p)));
|
|
83
|
+
cookieParams.forEach((p) => dataProps.push(formatParamProperty(p)));
|
|
84
|
+
// Add request body type if it exists
|
|
85
|
+
const hasData = parameters.length > 0 || requestBody;
|
|
86
|
+
let dataType = "undefined";
|
|
87
|
+
const namedType = (0, utils_1.pascalCase)(operationId);
|
|
88
|
+
if (hasData) {
|
|
89
|
+
if (requestBody && dataProps.length > 0) {
|
|
90
|
+
dataType = `${namedType}Request & { ${dataProps.join("; ")} }`;
|
|
91
|
+
}
|
|
92
|
+
else if (requestBody) {
|
|
93
|
+
dataType = `${namedType}Request`;
|
|
94
|
+
}
|
|
95
|
+
else if (dataProps.length > 0) {
|
|
96
|
+
dataType = `{ ${dataProps.join("; ")} }`;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
dataType = "Record<string, never>";
|
|
100
|
+
}
|
|
101
|
+
output += `\n\nexport type ${(0, utils_1.pascalCase)(operationId)}Params = ${dataType};\n\n`;
|
|
102
|
+
}
|
|
118
103
|
}
|
|
119
104
|
return output;
|
|
120
105
|
}
|
package/dist/utils.js
CHANGED
|
@@ -145,6 +145,36 @@ function specTitle(spec) {
|
|
|
145
145
|
}
|
|
146
146
|
return camelCase(title.toLowerCase().replace(/\s+/g, "-"));
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Builds the object-literal fragment ({...} or Record<...>) from a schema's own
|
|
150
|
+
* `properties` or `additionalProperties`, ignoring composition keywords. Returns
|
|
151
|
+
* undefined when the schema contributes no own object shape.
|
|
152
|
+
*/
|
|
153
|
+
function getOwnObjectFragment(schema) {
|
|
154
|
+
if (schema.properties && Object.keys(schema.properties).length > 0) {
|
|
155
|
+
const properties = Object.entries(schema.properties)
|
|
156
|
+
.map(([key, prop]) => {
|
|
157
|
+
const isRequired = schema.required?.includes(key);
|
|
158
|
+
const propertyType = getTypeFromSchema(prop);
|
|
159
|
+
const safeName = sanitizePropertyName(key);
|
|
160
|
+
const isDeprecated = "deprecated" in prop && prop.deprecated;
|
|
161
|
+
const hasDescription = "description" in prop && prop.description;
|
|
162
|
+
const desc = hasDescription || isDeprecated
|
|
163
|
+
? `/**${hasDescription ? `\n * ${prop.description}` : ""}${isDeprecated ? "\n * @deprecated" : ""}\n */\n`
|
|
164
|
+
: "";
|
|
165
|
+
return `${desc}${safeName}${isRequired ? "" : "?"}: ${propertyType};`;
|
|
166
|
+
})
|
|
167
|
+
.join("\n");
|
|
168
|
+
return `{${properties}\n}`;
|
|
169
|
+
}
|
|
170
|
+
if (schema.additionalProperties) {
|
|
171
|
+
const valueType = typeof schema.additionalProperties === "boolean"
|
|
172
|
+
? "unknown"
|
|
173
|
+
: getTypeFromSchema(schema.additionalProperties);
|
|
174
|
+
return `Record<string, ${valueType}>`;
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
148
178
|
/**
|
|
149
179
|
* Converts an OpenAPI schema object into a TypeScript type string.
|
|
150
180
|
*
|
|
@@ -174,29 +204,36 @@ function getTypeFromSchema(schema) {
|
|
|
174
204
|
}
|
|
175
205
|
// Add "| null" for nullable types
|
|
176
206
|
const nullable = "nullable" in schema && schema.nullable ? " | null" : "";
|
|
207
|
+
// Sibling properties / additionalProperties may appear next to allOf/oneOf/anyOf.
|
|
208
|
+
// Per the OpenAPI spec they constrain the parent and must be intersected with the composition.
|
|
209
|
+
const ownObjectFragment = getOwnObjectFragment(schema);
|
|
177
210
|
// Handle allOf (intersection types)
|
|
178
211
|
if ("allOf" in schema && schema.allOf) {
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
212
|
+
const parts = schema.allOf.map((s) => getTypeFromSchema(s)).filter(Boolean);
|
|
213
|
+
if (ownObjectFragment)
|
|
214
|
+
parts.push(ownObjectFragment);
|
|
215
|
+
if (parts.length === 0)
|
|
181
216
|
return "unknown";
|
|
182
|
-
const result =
|
|
217
|
+
const result = parts.length === 1 ? parts[0] : `(${parts.join(" & ")})`;
|
|
183
218
|
return `${result}${nullable}`;
|
|
184
219
|
}
|
|
185
220
|
// Handle oneOf (union types - exactly one)
|
|
186
221
|
if ("oneOf" in schema && schema.oneOf) {
|
|
187
222
|
const types = schema.oneOf.map((s) => getTypeFromSchema(s)).filter(Boolean);
|
|
188
|
-
if (types.length === 0)
|
|
223
|
+
if (types.length === 0 && !ownObjectFragment)
|
|
189
224
|
return "unknown";
|
|
190
|
-
const
|
|
191
|
-
|
|
225
|
+
const union = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
|
|
226
|
+
const composed = ownObjectFragment ? `(${union} & ${ownObjectFragment})` : union;
|
|
227
|
+
return `${composed}${nullable}`;
|
|
192
228
|
}
|
|
193
229
|
// Handle anyOf (union types - one or more)
|
|
194
230
|
if ("anyOf" in schema && schema.anyOf) {
|
|
195
231
|
const types = schema.anyOf.map((s) => getTypeFromSchema(s)).filter(Boolean);
|
|
196
|
-
if (types.length === 0)
|
|
232
|
+
if (types.length === 0 && !ownObjectFragment)
|
|
197
233
|
return "unknown";
|
|
198
|
-
const
|
|
199
|
-
|
|
234
|
+
const union = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
|
|
235
|
+
const composed = ownObjectFragment ? `(${union} & ${ownObjectFragment})` : union;
|
|
236
|
+
return `${composed}${nullable}`;
|
|
200
237
|
}
|
|
201
238
|
// Handle enums as union types
|
|
202
239
|
if ("enum" in schema && schema.enum) {
|
|
@@ -242,33 +279,13 @@ function getTypeFromSchema(schema) {
|
|
|
242
279
|
const itemType = getTypeFromSchema(schema.items);
|
|
243
280
|
return `Array<${itemType}>${nullable}`;
|
|
244
281
|
}
|
|
245
|
-
case "object":
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
.map(([key, prop]) => {
|
|
250
|
-
const isRequired = schema.required?.includes(key);
|
|
251
|
-
const propertyType = getTypeFromSchema(prop);
|
|
252
|
-
const safeName = sanitizePropertyName(key);
|
|
253
|
-
const isDeprecated = "deprecated" in prop && prop.deprecated;
|
|
254
|
-
const hasDescription = "description" in prop && prop.description;
|
|
255
|
-
const desc = hasDescription || isDeprecated
|
|
256
|
-
? `/**${hasDescription ? `\n * ${prop.description}` : ""}${isDeprecated ? "\n * @deprecated" : ""}\n */\n`
|
|
257
|
-
: "";
|
|
258
|
-
return `${desc}${safeName}${isRequired ? "" : "?"}: ${propertyType};`;
|
|
259
|
-
})
|
|
260
|
-
.join("\n");
|
|
261
|
-
return `{${properties}\n}${nullable}`;
|
|
262
|
-
}
|
|
263
|
-
// Handle objects with additionalProperties
|
|
264
|
-
if (schema.additionalProperties) {
|
|
265
|
-
const valueType = typeof schema.additionalProperties === "boolean"
|
|
266
|
-
? "unknown"
|
|
267
|
-
: getTypeFromSchema(schema.additionalProperties);
|
|
268
|
-
return `Record<string, ${valueType}>${nullable}`;
|
|
269
|
-
}
|
|
282
|
+
case "object": {
|
|
283
|
+
const fragment = getOwnObjectFragment(schema);
|
|
284
|
+
if (fragment)
|
|
285
|
+
return `${fragment}${nullable}`;
|
|
270
286
|
// Default object type when no properties specified
|
|
271
287
|
return `Record<string, unknown>${nullable}`;
|
|
288
|
+
}
|
|
272
289
|
default:
|
|
273
290
|
return `unknown${nullable}`;
|
|
274
291
|
}
|
package/package.json
CHANGED
|
@@ -9,6 +9,34 @@ import {
|
|
|
9
9
|
specTitle,
|
|
10
10
|
} from "../utils";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Walks a request body schema (including allOf/oneOf/anyOf composition) and returns
|
|
14
|
+
* the union of property names contributed by all branches. Used to pick body fields
|
|
15
|
+
* out of the merged Params object so path/query/header values aren't sent as the body.
|
|
16
|
+
*/
|
|
17
|
+
function collectBodyPropertyNames(
|
|
18
|
+
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
|
|
19
|
+
spec: OpenAPIV3.Document,
|
|
20
|
+
visitedRefs: Set<string> = new Set()
|
|
21
|
+
): string[] {
|
|
22
|
+
if (!schema) return [];
|
|
23
|
+
if ("$ref" in schema) {
|
|
24
|
+
if (visitedRefs.has(schema.$ref)) return [];
|
|
25
|
+
const next = new Set(visitedRefs).add(schema.$ref);
|
|
26
|
+
return collectBodyPropertyNames(resolveSchema(schema, spec), spec, next);
|
|
27
|
+
}
|
|
28
|
+
const names = new Set<string>();
|
|
29
|
+
if (schema.properties) Object.keys(schema.properties).forEach((k) => names.add(k));
|
|
30
|
+
for (const key of ["allOf", "oneOf", "anyOf"] as const) {
|
|
31
|
+
const branches = schema[key];
|
|
32
|
+
if (!branches) continue;
|
|
33
|
+
for (const branch of branches) {
|
|
34
|
+
collectBodyPropertyNames(branch, spec, visitedRefs).forEach((n) => names.add(n));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return Array.from(names);
|
|
38
|
+
}
|
|
39
|
+
|
|
12
40
|
function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document): string {
|
|
13
41
|
const { method, path, operationId, summary, description, deprecated, parameters, requestBody, responses } =
|
|
14
42
|
operation;
|
|
@@ -76,10 +104,15 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
|
|
|
76
104
|
requestBody && "content" in requestBody ? getContentSchema(requestBody.content) : undefined;
|
|
77
105
|
const requestBodySchema = requestBodyContent ? resolveSchema(requestBodyContent, spec) : undefined;
|
|
78
106
|
|
|
107
|
+
// Resolve property names contributed by the body schema, walking allOf/oneOf/anyOf so
|
|
108
|
+
// composition-based bodies are still treated as object bodies (not "primitive").
|
|
109
|
+
const bodyPropertyNames = requestBodySchema ? collectBodyPropertyNames(requestBodySchema, spec) : [];
|
|
110
|
+
const hasObjectBody = bodyPropertyNames.length > 0;
|
|
111
|
+
|
|
79
112
|
// Check if request body is a primitive type (string, number, boolean)
|
|
80
113
|
const isPrimitiveRequestBody =
|
|
81
114
|
requestBodySchema &&
|
|
82
|
-
!
|
|
115
|
+
!hasObjectBody &&
|
|
83
116
|
!requestBodySchema.type?.includes("object") &&
|
|
84
117
|
!requestBodySchema.type?.includes("array");
|
|
85
118
|
|
|
@@ -123,11 +156,9 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
|
|
|
123
156
|
};`
|
|
124
157
|
: "",
|
|
125
158
|
|
|
126
|
-
|
|
159
|
+
hasObjectBody && !formDataSchema?.properties
|
|
127
160
|
? `const bodyData = {
|
|
128
|
-
${
|
|
129
|
-
.map(([key]) => `["${key}"]: data["${key}"]`)
|
|
130
|
-
.join(",\n ")}
|
|
161
|
+
${bodyPropertyNames.map((key) => `["${key}"]: data["${key}"]`).join(",\n ")}
|
|
131
162
|
};`
|
|
132
163
|
: "",
|
|
133
164
|
|
|
@@ -155,8 +186,8 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
|
|
|
155
186
|
// Note: Don't set Content-Type for FormData - Axios will set it automatically with the correct boundary
|
|
156
187
|
requestBody
|
|
157
188
|
? responseType === "void"
|
|
158
|
-
? `await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties ||
|
|
159
|
-
: `const res = await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties ||
|
|
189
|
+
? `await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || hasObjectBody ? "bodyData" : "data"}, axiosConfig);`
|
|
190
|
+
: `const res = await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || hasObjectBody ? "bodyData" : "data"}, axiosConfig);`
|
|
160
191
|
: responseType === "void"
|
|
161
192
|
? `await apiClient.${method}<${responseType}>(url, axiosConfig);`
|
|
162
193
|
: `const res = await apiClient.${method}<${responseType}>(url, axiosConfig);`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from "openapi-types";
|
|
2
2
|
import {
|
|
3
|
+
collectOperations,
|
|
3
4
|
getContentSchema,
|
|
4
5
|
getTypeFromSchema,
|
|
5
6
|
pascalCase,
|
|
@@ -31,11 +32,11 @@ function generateTypeDefinition(
|
|
|
31
32
|
const description = !("$ref" in schema) && schema.description ? `/**\n * ${schema.description}\n */\n` : "";
|
|
32
33
|
const typeValue = getTypeFromSchema(schema);
|
|
33
34
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
const
|
|
35
|
+
// Only emit `interface` when the body is a plain object literal. Composition
|
|
36
|
+
// (allOf/oneOf/anyOf) produces references or intersections that must use a type alias.
|
|
37
|
+
const canBeInterface = typeValue?.trimStart().startsWith("{");
|
|
37
38
|
|
|
38
|
-
return
|
|
39
|
+
return canBeInterface
|
|
39
40
|
? `${description}export interface ${sanitizeTypeName(name)} ${typeValue}\n\n`
|
|
40
41
|
: `${description}export type ${sanitizeTypeName(name)} = ${typeValue}\n\n`;
|
|
41
42
|
}
|
|
@@ -56,86 +57,71 @@ export function generateTypeDefinitions(spec: OpenAPIV3.Document): string {
|
|
|
56
57
|
generatedTypes.add(name);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
// Generate request/response types
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Generate request body type
|
|
71
|
-
if (requestBody) {
|
|
72
|
-
const content = (requestBody as OpenAPIV3.RequestBodyObject).content;
|
|
73
|
-
const requestSchema = getContentSchema(content);
|
|
74
|
-
if (requestSchema) {
|
|
75
|
-
const typeName = `${operationId}Request`;
|
|
76
|
-
output += generateTypeDefinition(typeName, requestSchema as OpenAPIV3.SchemaObject);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
60
|
+
// Generate request/response types. Use the same operation-collection helper as the
|
|
61
|
+
// client generator so pathItem-level parameters and $ref parameters stay in sync.
|
|
62
|
+
for (const { operationId, parameters, requestBody, responses } of collectOperations(spec)) {
|
|
63
|
+
// Generate request body type
|
|
64
|
+
if (requestBody) {
|
|
65
|
+
const requestSchema = getContentSchema(requestBody.content);
|
|
66
|
+
if (requestSchema) {
|
|
67
|
+
const typeName = `${operationId}Request`;
|
|
68
|
+
output += generateTypeDefinition(typeName, requestSchema as OpenAPIV3.SchemaObject);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
79
71
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
}
|
|
72
|
+
// Generate response types
|
|
73
|
+
const errorTypes: string[] = [];
|
|
74
|
+
if (responses) {
|
|
75
|
+
for (const [code, response] of Object.entries(responses)) {
|
|
76
|
+
const responseObj = response as OpenAPIV3.ResponseObject;
|
|
77
|
+
const responseSchema = getContentSchema(responseObj.content);
|
|
78
|
+
if (responseSchema) {
|
|
79
|
+
const typeName = `${operationId}Response${code}`;
|
|
80
|
+
output += generateTypeDefinition(typeName, responseSchema as OpenAPIV3.SchemaObject);
|
|
81
|
+
|
|
82
|
+
// Track non-2xx responses for error union type
|
|
83
|
+
if (!code.startsWith("2")) {
|
|
84
|
+
errorTypes.push(typeName);
|
|
95
85
|
}
|
|
96
86
|
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
97
89
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
// Generate error union type if there are error responses
|
|
91
|
+
if (errorTypes.length > 0) {
|
|
92
|
+
output += `export type ${pascalCase(operationId)}Error = ${errorTypes.join(" | ")};\n\n`;
|
|
93
|
+
}
|
|
102
94
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} else if (dataProps.length > 0) {
|
|
132
|
-
dataType = `{ ${dataProps.join("; ")} }`;
|
|
133
|
-
} else {
|
|
134
|
-
dataType = "Record<string, never>";
|
|
135
|
-
}
|
|
136
|
-
output += `\n\nexport type ${pascalCase(operationId)}Params = ${dataType};\n\n`;
|
|
137
|
-
}
|
|
95
|
+
// Build data type parts
|
|
96
|
+
const dataProps: string[] = [];
|
|
97
|
+
|
|
98
|
+
const urlParams = parameters.filter((p) => p.in === "path");
|
|
99
|
+
const queryParams = parameters.filter((p) => p.in === "query");
|
|
100
|
+
const headerParams = parameters.filter((p) => p.in === "header");
|
|
101
|
+
const cookieParams = parameters.filter((p) => p.in === "cookie");
|
|
102
|
+
|
|
103
|
+
// Add path, query, header, and cookie parameters
|
|
104
|
+
urlParams.forEach((p) => dataProps.push(formatParamProperty(p, true))); // Path params always required
|
|
105
|
+
queryParams.forEach((p) => dataProps.push(formatParamProperty(p)));
|
|
106
|
+
headerParams.forEach((p) => dataProps.push(formatParamProperty(p)));
|
|
107
|
+
cookieParams.forEach((p) => dataProps.push(formatParamProperty(p)));
|
|
108
|
+
|
|
109
|
+
// Add request body type if it exists
|
|
110
|
+
const hasData = parameters.length > 0 || requestBody;
|
|
111
|
+
|
|
112
|
+
let dataType = "undefined";
|
|
113
|
+
const namedType = pascalCase(operationId);
|
|
114
|
+
if (hasData) {
|
|
115
|
+
if (requestBody && dataProps.length > 0) {
|
|
116
|
+
dataType = `${namedType}Request & { ${dataProps.join("; ")} }`;
|
|
117
|
+
} else if (requestBody) {
|
|
118
|
+
dataType = `${namedType}Request`;
|
|
119
|
+
} else if (dataProps.length > 0) {
|
|
120
|
+
dataType = `{ ${dataProps.join("; ")} }`;
|
|
121
|
+
} else {
|
|
122
|
+
dataType = "Record<string, never>";
|
|
138
123
|
}
|
|
124
|
+
output += `\n\nexport type ${pascalCase(operationId)}Params = ${dataType};\n\n`;
|
|
139
125
|
}
|
|
140
126
|
}
|
|
141
127
|
|
package/src/utils.ts
CHANGED
|
@@ -170,6 +170,41 @@ export function specTitle(spec: OpenAPIV3.Document): string {
|
|
|
170
170
|
return camelCase(title.toLowerCase().replace(/\s+/g, "-"));
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Builds the object-literal fragment ({...} or Record<...>) from a schema's own
|
|
175
|
+
* `properties` or `additionalProperties`, ignoring composition keywords. Returns
|
|
176
|
+
* undefined when the schema contributes no own object shape.
|
|
177
|
+
*/
|
|
178
|
+
function getOwnObjectFragment(schema: OpenAPIV3.SchemaObject): string | undefined {
|
|
179
|
+
if (schema.properties && Object.keys(schema.properties).length > 0) {
|
|
180
|
+
const properties = Object.entries(schema.properties)
|
|
181
|
+
.map(([key, prop]) => {
|
|
182
|
+
const isRequired = schema.required?.includes(key);
|
|
183
|
+
const propertyType = getTypeFromSchema(prop);
|
|
184
|
+
const safeName = sanitizePropertyName(key);
|
|
185
|
+
const isDeprecated = "deprecated" in prop && prop.deprecated;
|
|
186
|
+
const hasDescription = "description" in prop && prop.description;
|
|
187
|
+
const desc =
|
|
188
|
+
hasDescription || isDeprecated
|
|
189
|
+
? `/**${hasDescription ? `\n * ${prop.description}` : ""}${isDeprecated ? "\n * @deprecated" : ""}\n */\n`
|
|
190
|
+
: "";
|
|
191
|
+
return `${desc}${safeName}${isRequired ? "" : "?"}: ${propertyType};`;
|
|
192
|
+
})
|
|
193
|
+
.join("\n");
|
|
194
|
+
return `{${properties}\n}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (schema.additionalProperties) {
|
|
198
|
+
const valueType =
|
|
199
|
+
typeof schema.additionalProperties === "boolean"
|
|
200
|
+
? "unknown"
|
|
201
|
+
: getTypeFromSchema(schema.additionalProperties);
|
|
202
|
+
return `Record<string, ${valueType}>`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
173
208
|
/**
|
|
174
209
|
* Converts an OpenAPI schema object into a TypeScript type string.
|
|
175
210
|
*
|
|
@@ -202,28 +237,35 @@ export function getTypeFromSchema(
|
|
|
202
237
|
// Add "| null" for nullable types
|
|
203
238
|
const nullable = "nullable" in schema && schema.nullable ? " | null" : "";
|
|
204
239
|
|
|
240
|
+
// Sibling properties / additionalProperties may appear next to allOf/oneOf/anyOf.
|
|
241
|
+
// Per the OpenAPI spec they constrain the parent and must be intersected with the composition.
|
|
242
|
+
const ownObjectFragment = getOwnObjectFragment(schema as OpenAPIV3.SchemaObject);
|
|
243
|
+
|
|
205
244
|
// Handle allOf (intersection types)
|
|
206
245
|
if ("allOf" in schema && schema.allOf) {
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
|
|
246
|
+
const parts = schema.allOf.map((s) => getTypeFromSchema(s)).filter(Boolean) as string[];
|
|
247
|
+
if (ownObjectFragment) parts.push(ownObjectFragment);
|
|
248
|
+
if (parts.length === 0) return "unknown";
|
|
249
|
+
const result = parts.length === 1 ? parts[0] : `(${parts.join(" & ")})`;
|
|
210
250
|
return `${result}${nullable}`;
|
|
211
251
|
}
|
|
212
252
|
|
|
213
253
|
// Handle oneOf (union types - exactly one)
|
|
214
254
|
if ("oneOf" in schema && schema.oneOf) {
|
|
215
255
|
const types = schema.oneOf.map((s) => getTypeFromSchema(s)).filter(Boolean) as string[];
|
|
216
|
-
if (types.length === 0) return "unknown";
|
|
217
|
-
const
|
|
218
|
-
|
|
256
|
+
if (types.length === 0 && !ownObjectFragment) return "unknown";
|
|
257
|
+
const union = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
|
|
258
|
+
const composed = ownObjectFragment ? `(${union} & ${ownObjectFragment})` : union;
|
|
259
|
+
return `${composed}${nullable}`;
|
|
219
260
|
}
|
|
220
261
|
|
|
221
262
|
// Handle anyOf (union types - one or more)
|
|
222
263
|
if ("anyOf" in schema && schema.anyOf) {
|
|
223
264
|
const types = schema.anyOf.map((s) => getTypeFromSchema(s)).filter(Boolean) as string[];
|
|
224
|
-
if (types.length === 0) return "unknown";
|
|
225
|
-
const
|
|
226
|
-
|
|
265
|
+
if (types.length === 0 && !ownObjectFragment) return "unknown";
|
|
266
|
+
const union = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
|
|
267
|
+
const composed = ownObjectFragment ? `(${union} & ${ownObjectFragment})` : union;
|
|
268
|
+
return `${composed}${nullable}`;
|
|
227
269
|
}
|
|
228
270
|
|
|
229
271
|
// Handle enums as union types
|
|
@@ -274,37 +316,13 @@ export function getTypeFromSchema(
|
|
|
274
316
|
return `Array<${itemType}>${nullable}`;
|
|
275
317
|
}
|
|
276
318
|
|
|
277
|
-
case "object":
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
const properties = Object.entries(schema.properties)
|
|
281
|
-
.map(([key, prop]) => {
|
|
282
|
-
const isRequired = schema.required?.includes(key);
|
|
283
|
-
const propertyType = getTypeFromSchema(prop);
|
|
284
|
-
const safeName = sanitizePropertyName(key);
|
|
285
|
-
const isDeprecated = "deprecated" in prop && prop.deprecated;
|
|
286
|
-
const hasDescription = "description" in prop && prop.description;
|
|
287
|
-
const desc =
|
|
288
|
-
hasDescription || isDeprecated
|
|
289
|
-
? `/**${hasDescription ? `\n * ${prop.description}` : ""}${isDeprecated ? "\n * @deprecated" : ""}\n */\n`
|
|
290
|
-
: "";
|
|
291
|
-
return `${desc}${safeName}${isRequired ? "" : "?"}: ${propertyType};`;
|
|
292
|
-
})
|
|
293
|
-
.join("\n");
|
|
294
|
-
return `{${properties}\n}${nullable}`;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Handle objects with additionalProperties
|
|
298
|
-
if (schema.additionalProperties) {
|
|
299
|
-
const valueType =
|
|
300
|
-
typeof schema.additionalProperties === "boolean"
|
|
301
|
-
? "unknown"
|
|
302
|
-
: getTypeFromSchema(schema.additionalProperties);
|
|
303
|
-
return `Record<string, ${valueType}>${nullable}`;
|
|
304
|
-
}
|
|
319
|
+
case "object": {
|
|
320
|
+
const fragment = getOwnObjectFragment(schema);
|
|
321
|
+
if (fragment) return `${fragment}${nullable}`;
|
|
305
322
|
|
|
306
323
|
// Default object type when no properties specified
|
|
307
324
|
return `Record<string, unknown>${nullable}`;
|
|
325
|
+
}
|
|
308
326
|
|
|
309
327
|
default:
|
|
310
328
|
return `unknown${nullable}`;
|