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 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
@@ -9,4 +9,4 @@ export declare function generateOpenApiSpec(definitions: ApiDefinitionSchema | A
9
9
  url: string;
10
10
  description?: string;
11
11
  }[];
12
- }): import("openapi3-ts/oas30").OpenAPIObject;
12
+ }): import("openapi3-ts/oas31").OpenAPIObject;
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.OpenApiGeneratorV3(registry.definitions);
112
+ const generator = new zod_to_openapi_1.OpenApiGeneratorV31(registry.definitions);
113
113
  const openApiDocument = generator.generateDocument({
114
- openapi: '3.0.0',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-typed-api",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "A lightweight, type-safe RPC library for TypeScript with Zod validation",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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, OpenApiGeneratorV3, extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
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 OpenApiGeneratorV3(registry.definitions);
132
+ const generator = new OpenApiGeneratorV31(registry.definitions);
133
133
  const openApiDocument = generator.generateDocument({
134
- openapi: '3.0.0',
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';