ts-typed-api 0.1.15 → 0.1.17
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/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/openapi-self.d.ts +84 -0
- package/dist/openapi-self.js +441 -0
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.js +2 -2
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/openapi-self.ts +599 -0
- package/src/openapi.ts +3 -3
- package/tests/openapi-spec.test.ts +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { ApiClient, FetchHttpClientAdapter } from './client';
|
|
2
2
|
export { generateOpenApiSpec } from './openapi';
|
|
3
|
+
export { generateOpenApiSpec as generateOpenApiSpec2 } from './openapi-self';
|
|
3
4
|
export { CreateApiDefinition, CreateResponses, ApiDefinitionSchema } from './definition';
|
|
4
5
|
export { RegisterHandlers, EndpointMiddleware, UniversalEndpointMiddleware, SimpleMiddleware, EndpointInfo } from './object-handlers';
|
|
5
6
|
export { File as UploadedFile } from './router';
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ZodSchema = exports.RegisterHandlers = exports.CreateResponses = exports.CreateApiDefinition = exports.generateOpenApiSpec = exports.FetchHttpClientAdapter = exports.ApiClient = void 0;
|
|
3
|
+
exports.ZodSchema = exports.RegisterHandlers = exports.CreateResponses = exports.CreateApiDefinition = exports.generateOpenApiSpec2 = exports.generateOpenApiSpec = exports.FetchHttpClientAdapter = exports.ApiClient = void 0;
|
|
4
4
|
var client_1 = require("./client");
|
|
5
5
|
Object.defineProperty(exports, "ApiClient", { enumerable: true, get: function () { return client_1.ApiClient; } });
|
|
6
6
|
Object.defineProperty(exports, "FetchHttpClientAdapter", { enumerable: true, get: function () { return client_1.FetchHttpClientAdapter; } });
|
|
7
7
|
var openapi_1 = require("./openapi");
|
|
8
8
|
Object.defineProperty(exports, "generateOpenApiSpec", { enumerable: true, get: function () { return openapi_1.generateOpenApiSpec; } });
|
|
9
|
+
var openapi_self_1 = require("./openapi-self");
|
|
10
|
+
Object.defineProperty(exports, "generateOpenApiSpec2", { enumerable: true, get: function () { return openapi_self_1.generateOpenApiSpec; } });
|
|
9
11
|
var definition_1 = require("./definition");
|
|
10
12
|
Object.defineProperty(exports, "CreateApiDefinition", { enumerable: true, get: function () { return definition_1.CreateApiDefinition; } });
|
|
11
13
|
Object.defineProperty(exports, "CreateResponses", { enumerable: true, get: function () { return definition_1.CreateResponses; } });
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ApiDefinitionSchema } from './definition';
|
|
2
|
+
export interface OpenAPISpec {
|
|
3
|
+
openapi: string;
|
|
4
|
+
info: {
|
|
5
|
+
title: string;
|
|
6
|
+
version: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
};
|
|
9
|
+
servers?: Array<{
|
|
10
|
+
url: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}>;
|
|
13
|
+
paths: Record<string, PathItem>;
|
|
14
|
+
components?: {
|
|
15
|
+
schemas?: Record<string, SchemaObject>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface PathItem {
|
|
19
|
+
get?: Operation;
|
|
20
|
+
post?: Operation;
|
|
21
|
+
put?: Operation;
|
|
22
|
+
delete?: Operation;
|
|
23
|
+
patch?: Operation;
|
|
24
|
+
options?: Operation;
|
|
25
|
+
head?: Operation;
|
|
26
|
+
}
|
|
27
|
+
export interface Operation {
|
|
28
|
+
summary?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
parameters?: Parameter[];
|
|
31
|
+
requestBody?: RequestBody;
|
|
32
|
+
responses: Record<string, Response>;
|
|
33
|
+
tags?: string[];
|
|
34
|
+
}
|
|
35
|
+
export interface Parameter {
|
|
36
|
+
name: string;
|
|
37
|
+
in: 'query' | 'path' | 'header' | 'cookie';
|
|
38
|
+
required?: boolean;
|
|
39
|
+
description?: string;
|
|
40
|
+
schema: SchemaObject;
|
|
41
|
+
}
|
|
42
|
+
export interface RequestBody {
|
|
43
|
+
description?: string;
|
|
44
|
+
required?: boolean;
|
|
45
|
+
content: Record<string, MediaType>;
|
|
46
|
+
}
|
|
47
|
+
export interface Response {
|
|
48
|
+
description: string;
|
|
49
|
+
content?: Record<string, MediaType>;
|
|
50
|
+
}
|
|
51
|
+
export interface MediaType {
|
|
52
|
+
schema: SchemaObject;
|
|
53
|
+
}
|
|
54
|
+
export interface SchemaObject {
|
|
55
|
+
type?: string;
|
|
56
|
+
format?: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
enum?: any[];
|
|
59
|
+
items?: SchemaObject;
|
|
60
|
+
properties?: Record<string, SchemaObject>;
|
|
61
|
+
required?: string[];
|
|
62
|
+
additionalProperties?: boolean | SchemaObject;
|
|
63
|
+
nullable?: boolean;
|
|
64
|
+
oneOf?: SchemaObject[];
|
|
65
|
+
anyOf?: SchemaObject[];
|
|
66
|
+
allOf?: SchemaObject[];
|
|
67
|
+
$ref?: string;
|
|
68
|
+
minLength?: number;
|
|
69
|
+
maxLength?: number;
|
|
70
|
+
minimum?: number;
|
|
71
|
+
maximum?: number;
|
|
72
|
+
}
|
|
73
|
+
export interface OpenAPIOptions {
|
|
74
|
+
info?: {
|
|
75
|
+
title?: string;
|
|
76
|
+
version?: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
};
|
|
79
|
+
servers?: Array<{
|
|
80
|
+
url: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
}>;
|
|
83
|
+
}
|
|
84
|
+
export declare function generateOpenApiSpec(definitions: ApiDefinitionSchema | ApiDefinitionSchema[], options?: OpenAPIOptions): OpenAPISpec;
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateOpenApiSpec = generateOpenApiSpec;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
// Type-safe helper functions for accessing Zod internal properties
|
|
6
|
+
function getZodDef(schema) {
|
|
7
|
+
try {
|
|
8
|
+
return schema._def;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function getZodInnerType(schema) {
|
|
15
|
+
try {
|
|
16
|
+
const def = getZodDef(schema);
|
|
17
|
+
return def?.innerType;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function getZodTypeName(schema) {
|
|
24
|
+
try {
|
|
25
|
+
const def = getZodDef(schema);
|
|
26
|
+
return def?.typeName;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function getZodOptions(schema) {
|
|
33
|
+
try {
|
|
34
|
+
const def = getZodDef(schema);
|
|
35
|
+
return Array.isArray(def?.options) ? def.options : undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function getZodValues(schema) {
|
|
42
|
+
try {
|
|
43
|
+
const def = getZodDef(schema);
|
|
44
|
+
return Array.isArray(def?.values) ? def.values : undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function getZodValue(schema) {
|
|
51
|
+
try {
|
|
52
|
+
const def = getZodDef(schema);
|
|
53
|
+
return def?.value;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function getZodType(schema) {
|
|
60
|
+
try {
|
|
61
|
+
const def = getZodDef(schema);
|
|
62
|
+
return def?.type;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function getZodValueType(schema) {
|
|
69
|
+
try {
|
|
70
|
+
const def = getZodDef(schema);
|
|
71
|
+
return def?.valueType;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function getZodShape(schema) {
|
|
78
|
+
try {
|
|
79
|
+
const def = getZodDef(schema);
|
|
80
|
+
return def?.shape && typeof def.shape === 'object' ? def.shape : undefined;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Schema registry to avoid duplicate schema definitions
|
|
87
|
+
class SchemaRegistry {
|
|
88
|
+
constructor() {
|
|
89
|
+
this.schemas = new Map();
|
|
90
|
+
this.schemaCounter = 0;
|
|
91
|
+
}
|
|
92
|
+
register(zodSchema, name) {
|
|
93
|
+
const schemaObject = this.zodToOpenAPI(zodSchema);
|
|
94
|
+
const schemaKey = name || this.generateSchemaName(schemaObject);
|
|
95
|
+
if (!this.schemas.has(schemaKey)) {
|
|
96
|
+
this.schemas.set(schemaKey, schemaObject);
|
|
97
|
+
}
|
|
98
|
+
return schemaKey;
|
|
99
|
+
}
|
|
100
|
+
getSchemas() {
|
|
101
|
+
return Object.fromEntries(this.schemas);
|
|
102
|
+
}
|
|
103
|
+
generateSchemaName(schema) {
|
|
104
|
+
if (schema.type === 'object' && schema.properties) {
|
|
105
|
+
const keys = Object.keys(schema.properties).slice(0, 3).join('');
|
|
106
|
+
return `Schema${keys}${this.schemaCounter++}`;
|
|
107
|
+
}
|
|
108
|
+
return `Schema${this.schemaCounter++}`;
|
|
109
|
+
}
|
|
110
|
+
zodToOpenAPI(zodSchema, shouldRegister = false) {
|
|
111
|
+
// Simplified approach - use try/catch and fallbacks for type detection
|
|
112
|
+
try {
|
|
113
|
+
// Handle ZodOptional
|
|
114
|
+
if (zodSchema instanceof zod_1.ZodOptional) {
|
|
115
|
+
const innerType = getZodInnerType(zodSchema);
|
|
116
|
+
if (innerType) {
|
|
117
|
+
return this.zodToOpenAPI(innerType, shouldRegister);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Handle ZodNullable
|
|
121
|
+
if (zodSchema instanceof zod_1.ZodNullable) {
|
|
122
|
+
const innerType = getZodInnerType(zodSchema);
|
|
123
|
+
if (innerType) {
|
|
124
|
+
const innerSchema = this.zodToOpenAPI(innerType, shouldRegister);
|
|
125
|
+
return { ...innerSchema, nullable: true };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Handle ZodUnion
|
|
129
|
+
if (zodSchema instanceof zod_1.ZodUnion) {
|
|
130
|
+
const options = getZodOptions(zodSchema);
|
|
131
|
+
if (options) {
|
|
132
|
+
return { oneOf: options.map((option) => this.zodToOpenAPI(option, shouldRegister)) };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Handle basic types with simple instanceof checks and fallbacks
|
|
136
|
+
if (zodSchema instanceof zod_1.ZodString) {
|
|
137
|
+
return { type: 'string' };
|
|
138
|
+
}
|
|
139
|
+
if (zodSchema instanceof zod_1.ZodNumber) {
|
|
140
|
+
return { type: 'number' };
|
|
141
|
+
}
|
|
142
|
+
if (zodSchema instanceof zod_1.ZodBoolean) {
|
|
143
|
+
return { type: 'boolean' };
|
|
144
|
+
}
|
|
145
|
+
if (zodSchema instanceof zod_1.ZodEnum) {
|
|
146
|
+
const values = getZodValues(zodSchema);
|
|
147
|
+
return {
|
|
148
|
+
type: 'string',
|
|
149
|
+
enum: values || []
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (zodSchema instanceof zod_1.ZodLiteral) {
|
|
153
|
+
const value = getZodValue(zodSchema);
|
|
154
|
+
return {
|
|
155
|
+
type: typeof value,
|
|
156
|
+
enum: [value]
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (zodSchema instanceof zod_1.ZodArray) {
|
|
160
|
+
const itemType = getZodType(zodSchema);
|
|
161
|
+
return {
|
|
162
|
+
type: 'array',
|
|
163
|
+
items: itemType ? this.zodToOpenAPI(itemType, shouldRegister) : { type: 'string' }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (zodSchema instanceof zod_1.ZodObject) {
|
|
167
|
+
const shape = getZodShape(zodSchema);
|
|
168
|
+
if (shape) {
|
|
169
|
+
// For complex objects, register them as components if requested
|
|
170
|
+
if (shouldRegister && Object.keys(shape).length > 0) {
|
|
171
|
+
const schemaName = this.register(zodSchema);
|
|
172
|
+
return { $ref: `#/components/schemas/${schemaName}` };
|
|
173
|
+
}
|
|
174
|
+
const properties = {};
|
|
175
|
+
const required = [];
|
|
176
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
177
|
+
const zodValue = value;
|
|
178
|
+
properties[key] = this.zodToOpenAPI(zodValue, false); // Don't register nested objects
|
|
179
|
+
// Check if field is required (not optional)
|
|
180
|
+
if (!(zodValue instanceof zod_1.ZodOptional)) {
|
|
181
|
+
required.push(key);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const schema = {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties,
|
|
187
|
+
additionalProperties: false
|
|
188
|
+
};
|
|
189
|
+
if (required.length > 0) {
|
|
190
|
+
schema.required = required;
|
|
191
|
+
}
|
|
192
|
+
return schema;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (zodSchema instanceof zod_1.ZodRecord) {
|
|
196
|
+
const valueType = getZodValueType(zodSchema);
|
|
197
|
+
return {
|
|
198
|
+
type: 'object',
|
|
199
|
+
additionalProperties: valueType ? this.zodToOpenAPI(valueType, shouldRegister) : { type: 'string' }
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (zodSchema instanceof zod_1.ZodVoid) {
|
|
203
|
+
return { type: 'null' };
|
|
204
|
+
}
|
|
205
|
+
if (zodSchema instanceof zod_1.ZodAny || zodSchema instanceof zod_1.ZodUnknown) {
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
// Fallback based on constructor name
|
|
209
|
+
const constructorName = zodSchema.constructor.name;
|
|
210
|
+
switch (constructorName) {
|
|
211
|
+
case 'ZodString':
|
|
212
|
+
case 'String':
|
|
213
|
+
return { type: 'string' };
|
|
214
|
+
case 'ZodNumber':
|
|
215
|
+
case 'Number':
|
|
216
|
+
return { type: 'number' };
|
|
217
|
+
case 'ZodBoolean':
|
|
218
|
+
case 'Boolean':
|
|
219
|
+
return { type: 'boolean' };
|
|
220
|
+
case 'ZodArray':
|
|
221
|
+
case 'Array':
|
|
222
|
+
return { type: 'array', items: { type: 'string' } };
|
|
223
|
+
case 'ZodObject':
|
|
224
|
+
case 'Object':
|
|
225
|
+
return { type: 'object', additionalProperties: true };
|
|
226
|
+
default: {
|
|
227
|
+
// Final fallback - try to infer from the schema structure
|
|
228
|
+
const typeName = getZodTypeName(zodSchema);
|
|
229
|
+
if (typeName) {
|
|
230
|
+
switch (typeName) {
|
|
231
|
+
case 'ZodString':
|
|
232
|
+
return { type: 'string' };
|
|
233
|
+
case 'ZodNumber':
|
|
234
|
+
return { type: 'number' };
|
|
235
|
+
case 'ZodBoolean':
|
|
236
|
+
return { type: 'boolean' };
|
|
237
|
+
case 'ZodArray':
|
|
238
|
+
return { type: 'array', items: { type: 'string' } };
|
|
239
|
+
case 'ZodObject':
|
|
240
|
+
return { type: 'object', additionalProperties: true };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { type: 'string' }; // Ultimate fallback
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
// If anything fails, return a basic string type
|
|
249
|
+
return { type: 'string' };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function convertPathToOpenAPI(path) {
|
|
254
|
+
// Convert Express-style path parameters (:param) to OpenAPI style ({param})
|
|
255
|
+
return path.replace(/:([^/]+)/g, '{$1}');
|
|
256
|
+
}
|
|
257
|
+
function extractPathParameters(path) {
|
|
258
|
+
const matches = path.match(/:([^/]+)/g);
|
|
259
|
+
return matches ? matches.map(match => match.substring(1)) : [];
|
|
260
|
+
}
|
|
261
|
+
function createParameters(pathParams, paramsSchema, querySchema, registry) {
|
|
262
|
+
const parameters = [];
|
|
263
|
+
// Add path parameters
|
|
264
|
+
if (pathParams.length > 0 && paramsSchema instanceof zod_1.ZodObject && registry) {
|
|
265
|
+
try {
|
|
266
|
+
const shape = getZodShape(paramsSchema);
|
|
267
|
+
if (shape) {
|
|
268
|
+
for (const paramName of pathParams) {
|
|
269
|
+
const paramSchema = shape[paramName];
|
|
270
|
+
if (paramSchema) {
|
|
271
|
+
parameters.push({
|
|
272
|
+
name: paramName,
|
|
273
|
+
in: 'path',
|
|
274
|
+
required: true,
|
|
275
|
+
schema: registry.zodToOpenAPI(paramSchema)
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
// If shape parsing fails, add basic string parameters
|
|
283
|
+
for (const paramName of pathParams) {
|
|
284
|
+
parameters.push({
|
|
285
|
+
name: paramName,
|
|
286
|
+
in: 'path',
|
|
287
|
+
required: true,
|
|
288
|
+
schema: { type: 'string' }
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Add query parameters
|
|
294
|
+
if (querySchema instanceof zod_1.ZodObject && registry) {
|
|
295
|
+
try {
|
|
296
|
+
const shape = getZodShape(querySchema);
|
|
297
|
+
if (shape) {
|
|
298
|
+
for (const [queryName, queryZodSchema] of Object.entries(shape)) {
|
|
299
|
+
const zodValue = queryZodSchema;
|
|
300
|
+
parameters.push({
|
|
301
|
+
name: queryName,
|
|
302
|
+
in: 'query',
|
|
303
|
+
required: !(zodValue instanceof zod_1.ZodOptional),
|
|
304
|
+
schema: registry.zodToOpenAPI(zodValue)
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
// If shape parsing fails, skip query parameters
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return parameters;
|
|
314
|
+
}
|
|
315
|
+
function createRequestBody(bodySchema, registry) {
|
|
316
|
+
if (!bodySchema || !registry) {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
required: true,
|
|
321
|
+
content: {
|
|
322
|
+
'application/json': {
|
|
323
|
+
schema: registry.zodToOpenAPI(bodySchema, true) // Register complex schemas
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function createResponses(responses, registry) {
|
|
329
|
+
const openApiResponses = {};
|
|
330
|
+
for (const [statusCode, responseSchema] of Object.entries(responses)) {
|
|
331
|
+
const status = statusCode.toString();
|
|
332
|
+
// Handle void responses (like 204 No Content)
|
|
333
|
+
if (responseSchema instanceof zod_1.ZodVoid) {
|
|
334
|
+
openApiResponses[status] = {
|
|
335
|
+
description: getResponseDescription(parseInt(status))
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
openApiResponses[status] = {
|
|
340
|
+
description: getResponseDescription(parseInt(status)),
|
|
341
|
+
content: {
|
|
342
|
+
'application/json': {
|
|
343
|
+
schema: registry.zodToOpenAPI(responseSchema, true) // Register complex schemas
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return openApiResponses;
|
|
350
|
+
}
|
|
351
|
+
function getResponseDescription(statusCode) {
|
|
352
|
+
const descriptions = {
|
|
353
|
+
200: 'OK',
|
|
354
|
+
201: 'Created',
|
|
355
|
+
202: 'Accepted',
|
|
356
|
+
204: 'No Content',
|
|
357
|
+
400: 'Bad Request',
|
|
358
|
+
401: 'Unauthorized',
|
|
359
|
+
403: 'Forbidden',
|
|
360
|
+
404: 'Not Found',
|
|
361
|
+
409: 'Conflict',
|
|
362
|
+
422: 'Unprocessable Entity',
|
|
363
|
+
500: 'Internal Server Error'
|
|
364
|
+
};
|
|
365
|
+
return descriptions[statusCode] || `HTTP ${statusCode}`;
|
|
366
|
+
}
|
|
367
|
+
function processRoute(route, fullPath, registry, domain) {
|
|
368
|
+
const pathParams = extractPathParameters(route.path);
|
|
369
|
+
const parameters = createParameters(pathParams, route.params, route.query, registry);
|
|
370
|
+
const requestBody = createRequestBody(route.body, registry);
|
|
371
|
+
const responses = createResponses(route.responses, registry);
|
|
372
|
+
const operation = {
|
|
373
|
+
summary: `${route.method} ${fullPath}`,
|
|
374
|
+
description: `${route.method} operation for ${fullPath}`,
|
|
375
|
+
responses,
|
|
376
|
+
tags: [domain]
|
|
377
|
+
};
|
|
378
|
+
if (parameters.length > 0) {
|
|
379
|
+
operation.parameters = parameters;
|
|
380
|
+
}
|
|
381
|
+
if (requestBody) {
|
|
382
|
+
operation.requestBody = requestBody;
|
|
383
|
+
}
|
|
384
|
+
return operation;
|
|
385
|
+
}
|
|
386
|
+
function processApiDefinition(definition, registry) {
|
|
387
|
+
const paths = {};
|
|
388
|
+
for (const [domain, routes] of Object.entries(definition.endpoints)) {
|
|
389
|
+
for (const route of Object.values(routes)) {
|
|
390
|
+
const fullPath = (definition.prefix || '') + route.path;
|
|
391
|
+
const openApiPath = convertPathToOpenAPI(fullPath);
|
|
392
|
+
if (!paths[openApiPath]) {
|
|
393
|
+
paths[openApiPath] = {};
|
|
394
|
+
}
|
|
395
|
+
const operation = processRoute(route, fullPath, registry, domain);
|
|
396
|
+
const method = route.method.toLowerCase();
|
|
397
|
+
paths[openApiPath][method] = operation;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return paths;
|
|
401
|
+
}
|
|
402
|
+
function generateOpenApiSpec(definitions, options = {}) {
|
|
403
|
+
const registry = new SchemaRegistry();
|
|
404
|
+
const definitionsArray = Array.isArray(definitions) ? definitions : [definitions];
|
|
405
|
+
const allPaths = {};
|
|
406
|
+
// Process each definition
|
|
407
|
+
for (const definition of definitionsArray) {
|
|
408
|
+
const paths = processApiDefinition(definition, registry);
|
|
409
|
+
// Merge paths, handling potential conflicts
|
|
410
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
411
|
+
if (allPaths[path]) {
|
|
412
|
+
// Merge operations for the same path
|
|
413
|
+
allPaths[path] = { ...allPaths[path], ...pathItem };
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
allPaths[path] = pathItem;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const spec = {
|
|
421
|
+
openapi: '3.0.0',
|
|
422
|
+
info: {
|
|
423
|
+
title: options.info?.title || 'API Documentation',
|
|
424
|
+
version: options.info?.version || '1.0.0',
|
|
425
|
+
description: options.info?.description || 'Generated API documentation'
|
|
426
|
+
},
|
|
427
|
+
paths: allPaths
|
|
428
|
+
};
|
|
429
|
+
// Add servers if provided
|
|
430
|
+
if (options.servers && options.servers.length > 0) {
|
|
431
|
+
spec.servers = options.servers;
|
|
432
|
+
}
|
|
433
|
+
// Add components with schemas if any were registered
|
|
434
|
+
const schemas = registry.getSchemas();
|
|
435
|
+
if (Object.keys(schemas).length > 0) {
|
|
436
|
+
spec.components = {
|
|
437
|
+
schemas
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return spec;
|
|
441
|
+
}
|
package/dist/openapi.d.ts
CHANGED
package/dist/openapi.js
CHANGED
|
@@ -109,9 +109,9 @@ function generateOpenApiSpec(definitions, options = {}) {
|
|
|
109
109
|
});
|
|
110
110
|
});
|
|
111
111
|
// Generate the OpenAPI document
|
|
112
|
-
const generator = new zod_to_openapi_1.
|
|
112
|
+
const generator = new zod_to_openapi_1.OpenApiGeneratorV31(registry.definitions);
|
|
113
113
|
const openApiDocument = generator.generateDocument({
|
|
114
|
-
openapi: '3.
|
|
114
|
+
openapi: '3.1.0',
|
|
115
115
|
info: {
|
|
116
116
|
title: options.info?.title ?? 'My API',
|
|
117
117
|
version: options.info?.version ?? '1.0.0',
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { ApiClient, FetchHttpClientAdapter } from './client';
|
|
2
2
|
export { generateOpenApiSpec } from './openapi'
|
|
3
|
+
export { generateOpenApiSpec as generateOpenApiSpec2 } from './openapi-self'
|
|
3
4
|
export { CreateApiDefinition, CreateResponses, ApiDefinitionSchema } from './definition';
|
|
4
5
|
export { RegisterHandlers, EndpointMiddleware, UniversalEndpointMiddleware, SimpleMiddleware, EndpointInfo } from './object-handlers';
|
|
5
6
|
export { File as UploadedFile } from './router';
|
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import { ZodTypeAny, ZodObject, ZodArray, ZodString, ZodNumber, ZodBoolean, ZodEnum, ZodOptional, ZodNullable, ZodUnion, ZodRecord, ZodLiteral, ZodVoid, ZodAny, ZodUnknown } from 'zod';
|
|
2
|
+
import { ApiDefinitionSchema, RouteSchema } from './definition';
|
|
3
|
+
|
|
4
|
+
// OpenAPI 3.0 specification types
|
|
5
|
+
export interface OpenAPISpec {
|
|
6
|
+
openapi: string;
|
|
7
|
+
info: {
|
|
8
|
+
title: string;
|
|
9
|
+
version: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
};
|
|
12
|
+
servers?: Array<{
|
|
13
|
+
url: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
}>;
|
|
16
|
+
paths: Record<string, PathItem>;
|
|
17
|
+
components?: {
|
|
18
|
+
schemas?: Record<string, SchemaObject>;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PathItem {
|
|
23
|
+
get?: Operation;
|
|
24
|
+
post?: Operation;
|
|
25
|
+
put?: Operation;
|
|
26
|
+
delete?: Operation;
|
|
27
|
+
patch?: Operation;
|
|
28
|
+
options?: Operation;
|
|
29
|
+
head?: Operation;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Operation {
|
|
33
|
+
summary?: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
parameters?: Parameter[];
|
|
36
|
+
requestBody?: RequestBody;
|
|
37
|
+
responses: Record<string, Response>;
|
|
38
|
+
tags?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Parameter {
|
|
42
|
+
name: string;
|
|
43
|
+
in: 'query' | 'path' | 'header' | 'cookie';
|
|
44
|
+
required?: boolean;
|
|
45
|
+
description?: string;
|
|
46
|
+
schema: SchemaObject;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RequestBody {
|
|
50
|
+
description?: string;
|
|
51
|
+
required?: boolean;
|
|
52
|
+
content: Record<string, MediaType>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface Response {
|
|
56
|
+
description: string;
|
|
57
|
+
content?: Record<string, MediaType>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface MediaType {
|
|
61
|
+
schema: SchemaObject;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SchemaObject {
|
|
65
|
+
type?: string;
|
|
66
|
+
format?: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
enum?: any[];
|
|
69
|
+
items?: SchemaObject;
|
|
70
|
+
properties?: Record<string, SchemaObject>;
|
|
71
|
+
required?: string[];
|
|
72
|
+
additionalProperties?: boolean | SchemaObject;
|
|
73
|
+
nullable?: boolean;
|
|
74
|
+
oneOf?: SchemaObject[];
|
|
75
|
+
anyOf?: SchemaObject[];
|
|
76
|
+
allOf?: SchemaObject[];
|
|
77
|
+
$ref?: string;
|
|
78
|
+
minLength?: number;
|
|
79
|
+
maxLength?: number;
|
|
80
|
+
minimum?: number;
|
|
81
|
+
maximum?: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface OpenAPIOptions {
|
|
85
|
+
info?: {
|
|
86
|
+
title?: string;
|
|
87
|
+
version?: string;
|
|
88
|
+
description?: string;
|
|
89
|
+
};
|
|
90
|
+
servers?: Array<{
|
|
91
|
+
url: string;
|
|
92
|
+
description?: string;
|
|
93
|
+
}>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Type-safe helper functions for accessing Zod internal properties
|
|
97
|
+
function getZodDef(schema: ZodTypeAny): any {
|
|
98
|
+
try {
|
|
99
|
+
return (schema as any)._def;
|
|
100
|
+
} catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getZodInnerType(schema: ZodTypeAny): ZodTypeAny | undefined {
|
|
106
|
+
try {
|
|
107
|
+
const def = getZodDef(schema);
|
|
108
|
+
return def?.innerType;
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getZodTypeName(schema: ZodTypeAny): string | undefined {
|
|
115
|
+
try {
|
|
116
|
+
const def = getZodDef(schema);
|
|
117
|
+
return def?.typeName;
|
|
118
|
+
} catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getZodOptions(schema: ZodTypeAny): any[] | undefined {
|
|
124
|
+
try {
|
|
125
|
+
const def = getZodDef(schema);
|
|
126
|
+
return Array.isArray(def?.options) ? def.options : undefined;
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getZodValues(schema: ZodTypeAny): any[] | undefined {
|
|
133
|
+
try {
|
|
134
|
+
const def = getZodDef(schema);
|
|
135
|
+
return Array.isArray(def?.values) ? def.values : undefined;
|
|
136
|
+
} catch {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getZodValue(schema: ZodTypeAny): any {
|
|
142
|
+
try {
|
|
143
|
+
const def = getZodDef(schema);
|
|
144
|
+
return def?.value;
|
|
145
|
+
} catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getZodType(schema: ZodTypeAny): ZodTypeAny | undefined {
|
|
151
|
+
try {
|
|
152
|
+
const def = getZodDef(schema);
|
|
153
|
+
return def?.type;
|
|
154
|
+
} catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getZodValueType(schema: ZodTypeAny): ZodTypeAny | undefined {
|
|
160
|
+
try {
|
|
161
|
+
const def = getZodDef(schema);
|
|
162
|
+
return def?.valueType;
|
|
163
|
+
} catch {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getZodShape(schema: ZodTypeAny): Record<string, ZodTypeAny> | undefined {
|
|
169
|
+
try {
|
|
170
|
+
const def = getZodDef(schema);
|
|
171
|
+
return def?.shape && typeof def.shape === 'object' ? def.shape : undefined;
|
|
172
|
+
} catch {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Schema registry to avoid duplicate schema definitions
|
|
178
|
+
class SchemaRegistry {
|
|
179
|
+
private schemas: Map<string, SchemaObject> = new Map();
|
|
180
|
+
private schemaCounter = 0;
|
|
181
|
+
|
|
182
|
+
register(zodSchema: ZodTypeAny, name?: string): string {
|
|
183
|
+
const schemaObject = this.zodToOpenAPI(zodSchema);
|
|
184
|
+
const schemaKey = name || this.generateSchemaName(schemaObject);
|
|
185
|
+
|
|
186
|
+
if (!this.schemas.has(schemaKey)) {
|
|
187
|
+
this.schemas.set(schemaKey, schemaObject);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return schemaKey;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
getSchemas(): Record<string, SchemaObject> {
|
|
194
|
+
return Object.fromEntries(this.schemas);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private generateSchemaName(schema: SchemaObject): string {
|
|
198
|
+
if (schema.type === 'object' && schema.properties) {
|
|
199
|
+
const keys = Object.keys(schema.properties).slice(0, 3).join('');
|
|
200
|
+
return `Schema${keys}${this.schemaCounter++}`;
|
|
201
|
+
}
|
|
202
|
+
return `Schema${this.schemaCounter++}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
zodToOpenAPI(zodSchema: ZodTypeAny, shouldRegister: boolean = false): SchemaObject {
|
|
206
|
+
// Simplified approach - use try/catch and fallbacks for type detection
|
|
207
|
+
try {
|
|
208
|
+
// Handle ZodOptional
|
|
209
|
+
if (zodSchema instanceof ZodOptional) {
|
|
210
|
+
const innerType = getZodInnerType(zodSchema);
|
|
211
|
+
if (innerType) {
|
|
212
|
+
return this.zodToOpenAPI(innerType, shouldRegister);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Handle ZodNullable
|
|
217
|
+
if (zodSchema instanceof ZodNullable) {
|
|
218
|
+
const innerType = getZodInnerType(zodSchema);
|
|
219
|
+
if (innerType) {
|
|
220
|
+
const innerSchema = this.zodToOpenAPI(innerType, shouldRegister);
|
|
221
|
+
return { ...innerSchema, nullable: true };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Handle ZodUnion
|
|
226
|
+
if (zodSchema instanceof ZodUnion) {
|
|
227
|
+
const options = getZodOptions(zodSchema);
|
|
228
|
+
if (options) {
|
|
229
|
+
return { oneOf: options.map((option: any) => this.zodToOpenAPI(option, shouldRegister)) };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Handle basic types with simple instanceof checks and fallbacks
|
|
234
|
+
if (zodSchema instanceof ZodString) {
|
|
235
|
+
return { type: 'string' };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (zodSchema instanceof ZodNumber) {
|
|
239
|
+
return { type: 'number' };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (zodSchema instanceof ZodBoolean) {
|
|
243
|
+
return { type: 'boolean' };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (zodSchema instanceof ZodEnum) {
|
|
247
|
+
const values = getZodValues(zodSchema);
|
|
248
|
+
return {
|
|
249
|
+
type: 'string',
|
|
250
|
+
enum: values || []
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (zodSchema instanceof ZodLiteral) {
|
|
255
|
+
const value = getZodValue(zodSchema);
|
|
256
|
+
return {
|
|
257
|
+
type: typeof value as any,
|
|
258
|
+
enum: [value]
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (zodSchema instanceof ZodArray) {
|
|
263
|
+
const itemType = getZodType(zodSchema);
|
|
264
|
+
return {
|
|
265
|
+
type: 'array',
|
|
266
|
+
items: itemType ? this.zodToOpenAPI(itemType, shouldRegister) : { type: 'string' }
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (zodSchema instanceof ZodObject) {
|
|
271
|
+
const shape = getZodShape(zodSchema);
|
|
272
|
+
if (shape) {
|
|
273
|
+
// For complex objects, register them as components if requested
|
|
274
|
+
if (shouldRegister && Object.keys(shape).length > 0) {
|
|
275
|
+
const schemaName = this.register(zodSchema);
|
|
276
|
+
return { $ref: `#/components/schemas/${schemaName}` };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const properties: Record<string, SchemaObject> = {};
|
|
280
|
+
const required: string[] = [];
|
|
281
|
+
|
|
282
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
283
|
+
const zodValue = value as ZodTypeAny;
|
|
284
|
+
properties[key] = this.zodToOpenAPI(zodValue, false); // Don't register nested objects
|
|
285
|
+
|
|
286
|
+
// Check if field is required (not optional)
|
|
287
|
+
if (!(zodValue instanceof ZodOptional)) {
|
|
288
|
+
required.push(key);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const schema: SchemaObject = {
|
|
293
|
+
type: 'object',
|
|
294
|
+
properties,
|
|
295
|
+
additionalProperties: false
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
if (required.length > 0) {
|
|
299
|
+
schema.required = required;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return schema;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (zodSchema instanceof ZodRecord) {
|
|
307
|
+
const valueType = getZodValueType(zodSchema);
|
|
308
|
+
return {
|
|
309
|
+
type: 'object',
|
|
310
|
+
additionalProperties: valueType ? this.zodToOpenAPI(valueType, shouldRegister) : { type: 'string' }
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (zodSchema instanceof ZodVoid) {
|
|
315
|
+
return { type: 'null' };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (zodSchema instanceof ZodAny || zodSchema instanceof ZodUnknown) {
|
|
319
|
+
return {};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Fallback based on constructor name
|
|
323
|
+
const constructorName = zodSchema.constructor.name;
|
|
324
|
+
switch (constructorName) {
|
|
325
|
+
case 'ZodString':
|
|
326
|
+
case 'String':
|
|
327
|
+
return { type: 'string' };
|
|
328
|
+
case 'ZodNumber':
|
|
329
|
+
case 'Number':
|
|
330
|
+
return { type: 'number' };
|
|
331
|
+
case 'ZodBoolean':
|
|
332
|
+
case 'Boolean':
|
|
333
|
+
return { type: 'boolean' };
|
|
334
|
+
case 'ZodArray':
|
|
335
|
+
case 'Array':
|
|
336
|
+
return { type: 'array', items: { type: 'string' } };
|
|
337
|
+
case 'ZodObject':
|
|
338
|
+
case 'Object':
|
|
339
|
+
return { type: 'object', additionalProperties: true };
|
|
340
|
+
default: {
|
|
341
|
+
// Final fallback - try to infer from the schema structure
|
|
342
|
+
const typeName = getZodTypeName(zodSchema);
|
|
343
|
+
if (typeName) {
|
|
344
|
+
switch (typeName) {
|
|
345
|
+
case 'ZodString':
|
|
346
|
+
return { type: 'string' };
|
|
347
|
+
case 'ZodNumber':
|
|
348
|
+
return { type: 'number' };
|
|
349
|
+
case 'ZodBoolean':
|
|
350
|
+
return { type: 'boolean' };
|
|
351
|
+
case 'ZodArray':
|
|
352
|
+
return { type: 'array', items: { type: 'string' } };
|
|
353
|
+
case 'ZodObject':
|
|
354
|
+
return { type: 'object', additionalProperties: true };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return { type: 'string' }; // Ultimate fallback
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} catch (error) {
|
|
361
|
+
// If anything fails, return a basic string type
|
|
362
|
+
return { type: 'string' };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function convertPathToOpenAPI(path: string): string {
|
|
368
|
+
// Convert Express-style path parameters (:param) to OpenAPI style ({param})
|
|
369
|
+
return path.replace(/:([^/]+)/g, '{$1}');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function extractPathParameters(path: string): string[] {
|
|
373
|
+
const matches = path.match(/:([^/]+)/g);
|
|
374
|
+
return matches ? matches.map(match => match.substring(1)) : [];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function createParameters(
|
|
378
|
+
pathParams: string[],
|
|
379
|
+
paramsSchema?: ZodTypeAny,
|
|
380
|
+
querySchema?: ZodTypeAny,
|
|
381
|
+
registry?: SchemaRegistry
|
|
382
|
+
): Parameter[] {
|
|
383
|
+
const parameters: Parameter[] = [];
|
|
384
|
+
|
|
385
|
+
// Add path parameters
|
|
386
|
+
if (pathParams.length > 0 && paramsSchema instanceof ZodObject && registry) {
|
|
387
|
+
try {
|
|
388
|
+
const shape = getZodShape(paramsSchema);
|
|
389
|
+
if (shape) {
|
|
390
|
+
for (const paramName of pathParams) {
|
|
391
|
+
const paramSchema = shape[paramName];
|
|
392
|
+
if (paramSchema) {
|
|
393
|
+
parameters.push({
|
|
394
|
+
name: paramName,
|
|
395
|
+
in: 'path',
|
|
396
|
+
required: true,
|
|
397
|
+
schema: registry.zodToOpenAPI(paramSchema)
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch (error) {
|
|
403
|
+
// If shape parsing fails, add basic string parameters
|
|
404
|
+
for (const paramName of pathParams) {
|
|
405
|
+
parameters.push({
|
|
406
|
+
name: paramName,
|
|
407
|
+
in: 'path',
|
|
408
|
+
required: true,
|
|
409
|
+
schema: { type: 'string' }
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Add query parameters
|
|
416
|
+
if (querySchema instanceof ZodObject && registry) {
|
|
417
|
+
try {
|
|
418
|
+
const shape = getZodShape(querySchema);
|
|
419
|
+
if (shape) {
|
|
420
|
+
for (const [queryName, queryZodSchema] of Object.entries(shape)) {
|
|
421
|
+
const zodValue = queryZodSchema as ZodTypeAny;
|
|
422
|
+
parameters.push({
|
|
423
|
+
name: queryName,
|
|
424
|
+
in: 'query',
|
|
425
|
+
required: !(zodValue instanceof ZodOptional),
|
|
426
|
+
schema: registry.zodToOpenAPI(zodValue)
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} catch (error) {
|
|
431
|
+
// If shape parsing fails, skip query parameters
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return parameters;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function createRequestBody(bodySchema?: ZodTypeAny, registry?: SchemaRegistry): RequestBody | undefined {
|
|
439
|
+
if (!bodySchema || !registry) {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
required: true,
|
|
445
|
+
content: {
|
|
446
|
+
'application/json': {
|
|
447
|
+
schema: registry.zodToOpenAPI(bodySchema, true) // Register complex schemas
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function createResponses(responses: Record<number, ZodTypeAny>, registry: SchemaRegistry): Record<string, Response> {
|
|
454
|
+
const openApiResponses: Record<string, Response> = {};
|
|
455
|
+
|
|
456
|
+
for (const [statusCode, responseSchema] of Object.entries(responses)) {
|
|
457
|
+
const status = statusCode.toString();
|
|
458
|
+
|
|
459
|
+
// Handle void responses (like 204 No Content)
|
|
460
|
+
if (responseSchema instanceof ZodVoid) {
|
|
461
|
+
openApiResponses[status] = {
|
|
462
|
+
description: getResponseDescription(parseInt(status))
|
|
463
|
+
};
|
|
464
|
+
} else {
|
|
465
|
+
openApiResponses[status] = {
|
|
466
|
+
description: getResponseDescription(parseInt(status)),
|
|
467
|
+
content: {
|
|
468
|
+
'application/json': {
|
|
469
|
+
schema: registry.zodToOpenAPI(responseSchema, true) // Register complex schemas
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return openApiResponses;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function getResponseDescription(statusCode: number): string {
|
|
480
|
+
const descriptions: Record<number, string> = {
|
|
481
|
+
200: 'OK',
|
|
482
|
+
201: 'Created',
|
|
483
|
+
202: 'Accepted',
|
|
484
|
+
204: 'No Content',
|
|
485
|
+
400: 'Bad Request',
|
|
486
|
+
401: 'Unauthorized',
|
|
487
|
+
403: 'Forbidden',
|
|
488
|
+
404: 'Not Found',
|
|
489
|
+
409: 'Conflict',
|
|
490
|
+
422: 'Unprocessable Entity',
|
|
491
|
+
500: 'Internal Server Error'
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
return descriptions[statusCode] || `HTTP ${statusCode}`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function processRoute(
|
|
498
|
+
route: RouteSchema,
|
|
499
|
+
fullPath: string,
|
|
500
|
+
registry: SchemaRegistry,
|
|
501
|
+
domain: string
|
|
502
|
+
): Operation {
|
|
503
|
+
const pathParams = extractPathParameters(route.path);
|
|
504
|
+
const parameters = createParameters(pathParams, route.params, route.query, registry);
|
|
505
|
+
const requestBody = createRequestBody(route.body, registry);
|
|
506
|
+
const responses = createResponses(route.responses, registry);
|
|
507
|
+
|
|
508
|
+
const operation: Operation = {
|
|
509
|
+
summary: `${route.method} ${fullPath}`,
|
|
510
|
+
description: `${route.method} operation for ${fullPath}`,
|
|
511
|
+
responses,
|
|
512
|
+
tags: [domain]
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
if (parameters.length > 0) {
|
|
516
|
+
operation.parameters = parameters;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (requestBody) {
|
|
520
|
+
operation.requestBody = requestBody;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return operation;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function processApiDefinition(
|
|
527
|
+
definition: ApiDefinitionSchema,
|
|
528
|
+
registry: SchemaRegistry
|
|
529
|
+
): Record<string, PathItem> {
|
|
530
|
+
const paths: Record<string, PathItem> = {};
|
|
531
|
+
|
|
532
|
+
for (const [domain, routes] of Object.entries(definition.endpoints)) {
|
|
533
|
+
for (const route of Object.values(routes)) {
|
|
534
|
+
const fullPath = (definition.prefix || '') + route.path;
|
|
535
|
+
const openApiPath = convertPathToOpenAPI(fullPath);
|
|
536
|
+
|
|
537
|
+
if (!paths[openApiPath]) {
|
|
538
|
+
paths[openApiPath] = {};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const operation = processRoute(route, fullPath, registry, domain);
|
|
542
|
+
const method = route.method.toLowerCase() as keyof PathItem;
|
|
543
|
+
|
|
544
|
+
(paths[openApiPath] as any)[method] = operation;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return paths;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function generateOpenApiSpec(
|
|
552
|
+
definitions: ApiDefinitionSchema | ApiDefinitionSchema[],
|
|
553
|
+
options: OpenAPIOptions = {}
|
|
554
|
+
): OpenAPISpec {
|
|
555
|
+
const registry = new SchemaRegistry();
|
|
556
|
+
const definitionsArray = Array.isArray(definitions) ? definitions : [definitions];
|
|
557
|
+
|
|
558
|
+
const allPaths: Record<string, PathItem> = {};
|
|
559
|
+
|
|
560
|
+
// Process each definition
|
|
561
|
+
for (const definition of definitionsArray) {
|
|
562
|
+
const paths = processApiDefinition(definition, registry);
|
|
563
|
+
|
|
564
|
+
// Merge paths, handling potential conflicts
|
|
565
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
566
|
+
if (allPaths[path]) {
|
|
567
|
+
// Merge operations for the same path
|
|
568
|
+
allPaths[path] = { ...allPaths[path], ...pathItem };
|
|
569
|
+
} else {
|
|
570
|
+
allPaths[path] = pathItem;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const spec: OpenAPISpec = {
|
|
576
|
+
openapi: '3.0.0',
|
|
577
|
+
info: {
|
|
578
|
+
title: options.info?.title || 'API Documentation',
|
|
579
|
+
version: options.info?.version || '1.0.0',
|
|
580
|
+
description: options.info?.description || 'Generated API documentation'
|
|
581
|
+
},
|
|
582
|
+
paths: allPaths
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
// Add servers if provided
|
|
586
|
+
if (options.servers && options.servers.length > 0) {
|
|
587
|
+
spec.servers = options.servers;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Add components with schemas if any were registered
|
|
591
|
+
const schemas = registry.getSchemas();
|
|
592
|
+
if (Object.keys(schemas).length > 0) {
|
|
593
|
+
spec.components = {
|
|
594
|
+
schemas
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return spec;
|
|
599
|
+
}
|
package/src/openapi.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ApiDefinitionSchema, RouteSchema } from './definition';
|
|
2
|
-
import { OpenAPIRegistry,
|
|
2
|
+
import { OpenAPIRegistry, OpenApiGeneratorV31, extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
|
|
3
3
|
import { z, ZodTypeAny } from 'zod';
|
|
4
4
|
|
|
5
5
|
// Extend Zod with OpenAPI capabilities
|
|
@@ -129,9 +129,9 @@ export function generateOpenApiSpec(
|
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
// Generate the OpenAPI document
|
|
132
|
-
const generator = new
|
|
132
|
+
const generator = new OpenApiGeneratorV31(registry.definitions);
|
|
133
133
|
const openApiDocument = generator.generateDocument({
|
|
134
|
-
openapi: '3.
|
|
134
|
+
openapi: '3.1.0',
|
|
135
135
|
info: {
|
|
136
136
|
title: options.info?.title ?? 'My API',
|
|
137
137
|
version: options.info?.version ?? '1.0.0',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from '@jest/globals';
|
|
2
|
-
import { generateOpenApiSpec } from '../src/openapi';
|
|
2
|
+
import { generateOpenApiSpec } from '../src/openapi-self';
|
|
3
3
|
import { PublicApiDefinition } from '../examples/simple/definitions';
|
|
4
4
|
import { PrivateApiDefinition } from '../examples/simple/definitions';
|
|
5
5
|
import { z } from 'zod';
|