@veloxts/router 0.6.57 → 0.6.59
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/CHANGELOG.md +18 -0
- package/GUIDE.md +237 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +11 -0
- package/dist/openapi/generator.d.ts +52 -0
- package/dist/openapi/generator.js +442 -0
- package/dist/openapi/index.d.ts +37 -0
- package/dist/openapi/index.js +51 -0
- package/dist/openapi/path-extractor.d.ts +212 -0
- package/dist/openapi/path-extractor.js +256 -0
- package/dist/openapi/plugin.d.ts +101 -0
- package/dist/openapi/plugin.js +228 -0
- package/dist/openapi/schema-converter.d.ts +119 -0
- package/dist/openapi/schema-converter.js +246 -0
- package/dist/openapi/security-mapper.d.ts +131 -0
- package/dist/openapi/security-mapper.js +226 -0
- package/dist/openapi/types.d.ts +437 -0
- package/dist/openapi/types.js +8 -0
- package/package.json +8 -3
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swagger UI Fastify Plugin
|
|
3
|
+
*
|
|
4
|
+
* Serves Swagger UI documentation for VeloxTS APIs.
|
|
5
|
+
*
|
|
6
|
+
* @module @veloxts/router/openapi/plugin
|
|
7
|
+
*/
|
|
8
|
+
import { generateOpenApiSpec } from './generator.js';
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Swagger UI HTML Generation
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Default Swagger UI configuration
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_UI_CONFIG = {
|
|
16
|
+
deepLinking: true,
|
|
17
|
+
displayOperationId: false,
|
|
18
|
+
defaultModelsExpandDepth: 1,
|
|
19
|
+
defaultModelExpandDepth: 1,
|
|
20
|
+
docExpansion: 'list',
|
|
21
|
+
filter: false,
|
|
22
|
+
showExtensions: false,
|
|
23
|
+
tryItOutEnabled: true,
|
|
24
|
+
persistAuthorization: false,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Swagger UI CDN URLs
|
|
28
|
+
*/
|
|
29
|
+
const SWAGGER_UI_CDN = {
|
|
30
|
+
css: 'https://unpkg.com/swagger-ui-dist@5/swagger-ui.css',
|
|
31
|
+
bundle: 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js',
|
|
32
|
+
standalonePreset: 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js',
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Generates the Swagger UI HTML page
|
|
36
|
+
*/
|
|
37
|
+
function generateSwaggerUIHtml(specUrl, config, title, favicon) {
|
|
38
|
+
const configJson = JSON.stringify({
|
|
39
|
+
url: specUrl,
|
|
40
|
+
dom_id: '#swagger-ui',
|
|
41
|
+
deepLinking: config.deepLinking,
|
|
42
|
+
displayOperationId: config.displayOperationId,
|
|
43
|
+
defaultModelsExpandDepth: config.defaultModelsExpandDepth,
|
|
44
|
+
defaultModelExpandDepth: config.defaultModelExpandDepth,
|
|
45
|
+
docExpansion: config.docExpansion,
|
|
46
|
+
filter: config.filter,
|
|
47
|
+
showExtensions: config.showExtensions,
|
|
48
|
+
tryItOutEnabled: config.tryItOutEnabled,
|
|
49
|
+
persistAuthorization: config.persistAuthorization,
|
|
50
|
+
presets: ['SwaggerUIBundle.presets.apis', 'SwaggerUIStandalonePreset'],
|
|
51
|
+
plugins: ['SwaggerUIBundle.plugins.DownloadUrl'],
|
|
52
|
+
layout: 'StandaloneLayout',
|
|
53
|
+
});
|
|
54
|
+
const faviconTag = favicon ? `<link rel="icon" type="image/x-icon" href="${favicon}">` : '';
|
|
55
|
+
return `<!DOCTYPE html>
|
|
56
|
+
<html lang="en">
|
|
57
|
+
<head>
|
|
58
|
+
<meta charset="UTF-8">
|
|
59
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
60
|
+
<title>${escapeHtml(title)}</title>
|
|
61
|
+
${faviconTag}
|
|
62
|
+
<link rel="stylesheet" href="${SWAGGER_UI_CDN.css}">
|
|
63
|
+
<style>
|
|
64
|
+
html { box-sizing: border-box; overflow-y: scroll; }
|
|
65
|
+
*, *:before, *:after { box-sizing: inherit; }
|
|
66
|
+
body { margin: 0; background: #fafafa; }
|
|
67
|
+
.swagger-ui .topbar { display: none; }
|
|
68
|
+
</style>
|
|
69
|
+
</head>
|
|
70
|
+
<body>
|
|
71
|
+
<div id="swagger-ui"></div>
|
|
72
|
+
<script src="${SWAGGER_UI_CDN.bundle}"></script>
|
|
73
|
+
<script src="${SWAGGER_UI_CDN.standalonePreset}"></script>
|
|
74
|
+
<script>
|
|
75
|
+
window.onload = function() {
|
|
76
|
+
window.ui = SwaggerUIBundle(${configJson.replace(/"(SwaggerUIBundle\.presets\.apis|SwaggerUIStandalonePreset|SwaggerUIBundle\.plugins\.DownloadUrl)"/g, '$1')});
|
|
77
|
+
};
|
|
78
|
+
</script>
|
|
79
|
+
</body>
|
|
80
|
+
</html>`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Escapes HTML special characters
|
|
84
|
+
*/
|
|
85
|
+
function escapeHtml(text) {
|
|
86
|
+
return text
|
|
87
|
+
.replace(/&/g, '&')
|
|
88
|
+
.replace(/</g, '<')
|
|
89
|
+
.replace(/>/g, '>')
|
|
90
|
+
.replace(/"/g, '"')
|
|
91
|
+
.replace(/'/g, ''');
|
|
92
|
+
}
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Fastify Plugin
|
|
95
|
+
// ============================================================================
|
|
96
|
+
/**
|
|
97
|
+
* Swagger UI Fastify plugin
|
|
98
|
+
*
|
|
99
|
+
* Registers routes for serving Swagger UI and the OpenAPI specification.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* import { swaggerUIPlugin } from '@veloxts/router';
|
|
104
|
+
*
|
|
105
|
+
* app.register(swaggerUIPlugin, {
|
|
106
|
+
* routePrefix: '/docs',
|
|
107
|
+
* collections: [userProcedures, postProcedures],
|
|
108
|
+
* openapi: {
|
|
109
|
+
* info: {
|
|
110
|
+
* title: 'My API',
|
|
111
|
+
* version: '1.0.0',
|
|
112
|
+
* description: 'A VeloxTS-powered API',
|
|
113
|
+
* },
|
|
114
|
+
* servers: [{ url: 'http://localhost:3030' }],
|
|
115
|
+
* },
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export const swaggerUIPlugin = async (fastify, options) => {
|
|
120
|
+
const { routePrefix = '/docs', specRoute = `${routePrefix}/openapi.json`, uiConfig = {}, openapi, collections, title = 'API Documentation', favicon, } = options;
|
|
121
|
+
// Merge UI config with defaults
|
|
122
|
+
const mergedConfig = {
|
|
123
|
+
...DEFAULT_UI_CONFIG,
|
|
124
|
+
...uiConfig,
|
|
125
|
+
};
|
|
126
|
+
// Generate the OpenAPI specification
|
|
127
|
+
let spec;
|
|
128
|
+
try {
|
|
129
|
+
spec = generateOpenApiSpec(collections, openapi);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
fastify.log.error(error, '[VeloxTS] Failed to generate OpenAPI specification');
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
// Register OpenAPI JSON route
|
|
136
|
+
fastify.get(specRoute, async (_request, reply) => {
|
|
137
|
+
return reply.header('Content-Type', 'application/json').send(spec);
|
|
138
|
+
});
|
|
139
|
+
// Register Swagger UI HTML route
|
|
140
|
+
const htmlContent = generateSwaggerUIHtml(specRoute, mergedConfig, title, favicon);
|
|
141
|
+
fastify.get(routePrefix, async (_request, reply) => {
|
|
142
|
+
return reply.header('Content-Type', 'text/html; charset=utf-8').send(htmlContent);
|
|
143
|
+
});
|
|
144
|
+
// Also serve at /docs/ (with trailing slash)
|
|
145
|
+
if (!routePrefix.endsWith('/')) {
|
|
146
|
+
fastify.get(`${routePrefix}/`, async (_request, reply) => {
|
|
147
|
+
return reply.header('Content-Type', 'text/html; charset=utf-8').send(htmlContent);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
fastify.log.info(`[VeloxTS] Swagger UI available at ${routePrefix}, spec at ${specRoute}`);
|
|
151
|
+
};
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Utility Functions
|
|
154
|
+
// ============================================================================
|
|
155
|
+
/**
|
|
156
|
+
* Creates a Swagger UI plugin with pre-configured options
|
|
157
|
+
*
|
|
158
|
+
* @param options - Plugin options
|
|
159
|
+
* @returns Configured plugin
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* import { createSwaggerUI } from '@veloxts/router';
|
|
164
|
+
*
|
|
165
|
+
* const docs = createSwaggerUI({
|
|
166
|
+
* collections: [userProcedures],
|
|
167
|
+
* openapi: {
|
|
168
|
+
* info: { title: 'My API', version: '1.0.0' },
|
|
169
|
+
* },
|
|
170
|
+
* });
|
|
171
|
+
*
|
|
172
|
+
* app.register(docs);
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export function createSwaggerUI(options) {
|
|
176
|
+
return async (fastify) => {
|
|
177
|
+
await swaggerUIPlugin(fastify, options);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Registers multiple procedure collections with Swagger UI
|
|
182
|
+
*
|
|
183
|
+
* Convenience function that sets up both REST routes and documentation.
|
|
184
|
+
*
|
|
185
|
+
* @param fastify - Fastify instance
|
|
186
|
+
* @param options - Documentation options
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* import { registerDocs } from '@veloxts/router';
|
|
191
|
+
*
|
|
192
|
+
* await registerDocs(app, {
|
|
193
|
+
* collections: [userProcedures, postProcedures],
|
|
194
|
+
* openapi: {
|
|
195
|
+
* info: { title: 'My API', version: '1.0.0' },
|
|
196
|
+
* },
|
|
197
|
+
* });
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
export async function registerDocs(fastify, options) {
|
|
201
|
+
await fastify.register(swaggerUIPlugin, options);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Gets the generated OpenAPI specification without registering routes
|
|
205
|
+
*
|
|
206
|
+
* Useful for testing or exporting the spec programmatically.
|
|
207
|
+
*
|
|
208
|
+
* @param options - Plugin options
|
|
209
|
+
* @returns Generated OpenAPI specification
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* import { getOpenApiSpec } from '@veloxts/router';
|
|
214
|
+
* import fs from 'fs';
|
|
215
|
+
*
|
|
216
|
+
* const spec = getOpenApiSpec({
|
|
217
|
+
* collections: [userProcedures],
|
|
218
|
+
* openapi: {
|
|
219
|
+
* info: { title: 'My API', version: '1.0.0' },
|
|
220
|
+
* },
|
|
221
|
+
* });
|
|
222
|
+
*
|
|
223
|
+
* fs.writeFileSync('openapi.json', JSON.stringify(spec, null, 2));
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export function getOpenApiSpec(options) {
|
|
227
|
+
return generateOpenApiSpec(options.collections, options.openapi);
|
|
228
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Converter
|
|
3
|
+
*
|
|
4
|
+
* Converts Zod schemas to JSON Schema format for OpenAPI specifications.
|
|
5
|
+
*
|
|
6
|
+
* @module @veloxts/router/openapi/schema-converter
|
|
7
|
+
*/
|
|
8
|
+
import type { ZodType } from 'zod';
|
|
9
|
+
import type { JSONSchema } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Options for Zod to JSON Schema conversion
|
|
12
|
+
*/
|
|
13
|
+
export interface SchemaConversionOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Schema name for $ref generation
|
|
16
|
+
*/
|
|
17
|
+
name?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Target specification format
|
|
20
|
+
* @default 'openApi3'
|
|
21
|
+
*/
|
|
22
|
+
target?: 'jsonSchema7' | 'jsonSchema2019-09' | 'openApi3';
|
|
23
|
+
/**
|
|
24
|
+
* How to handle $ref references
|
|
25
|
+
* - 'none': Inline all definitions (default)
|
|
26
|
+
* - 'root': Use $ref at root level
|
|
27
|
+
* - 'seen': Use $ref for seen schemas
|
|
28
|
+
* @default 'none'
|
|
29
|
+
*/
|
|
30
|
+
refStrategy?: 'none' | 'root' | 'seen';
|
|
31
|
+
/**
|
|
32
|
+
* Base path for $ref URIs
|
|
33
|
+
* @default '#/components/schemas'
|
|
34
|
+
*/
|
|
35
|
+
basePath?: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Remove default values from schema
|
|
38
|
+
* @default false
|
|
39
|
+
*/
|
|
40
|
+
removeDefaults?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Converts a Zod schema to JSON Schema format for OpenAPI
|
|
44
|
+
*
|
|
45
|
+
* @param schema - Zod schema to convert
|
|
46
|
+
* @param options - Conversion options
|
|
47
|
+
* @returns JSON Schema representation
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const UserSchema = z.object({
|
|
52
|
+
* id: z.string().uuid(),
|
|
53
|
+
* email: z.string().email(),
|
|
54
|
+
* name: z.string().min(1).max(100),
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* const jsonSchema = zodSchemaToJsonSchema(UserSchema);
|
|
58
|
+
* // {
|
|
59
|
+
* // type: 'object',
|
|
60
|
+
* // properties: {
|
|
61
|
+
* // id: { type: 'string', format: 'uuid' },
|
|
62
|
+
* // email: { type: 'string', format: 'email' },
|
|
63
|
+
* // name: { type: 'string', minLength: 1, maxLength: 100 },
|
|
64
|
+
* // },
|
|
65
|
+
* // required: ['id', 'email', 'name'],
|
|
66
|
+
* // }
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function zodSchemaToJsonSchema(schema: ZodType | undefined, options?: SchemaConversionOptions): JSONSchema | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Removes specified properties from a JSON Schema
|
|
72
|
+
*
|
|
73
|
+
* Useful for removing path parameters from request body schemas
|
|
74
|
+
*
|
|
75
|
+
* @param schema - Original JSON Schema
|
|
76
|
+
* @param propertyNames - Properties to remove
|
|
77
|
+
* @returns New schema without specified properties
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const schema = {
|
|
82
|
+
* type: 'object',
|
|
83
|
+
* properties: { id: { type: 'string' }, name: { type: 'string' } },
|
|
84
|
+
* required: ['id', 'name'],
|
|
85
|
+
* };
|
|
86
|
+
*
|
|
87
|
+
* const bodySchema = removeSchemaProperties(schema, ['id']);
|
|
88
|
+
* // { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare function removeSchemaProperties(schema: JSONSchema | undefined, propertyNames: string[]): JSONSchema | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Extracts specific properties from a JSON Schema
|
|
94
|
+
*
|
|
95
|
+
* @param schema - Original JSON Schema
|
|
96
|
+
* @param propertyNames - Properties to extract
|
|
97
|
+
* @returns New schema with only specified properties
|
|
98
|
+
*/
|
|
99
|
+
export declare function extractSchemaProperties(schema: JSONSchema | undefined, propertyNames: string[]): JSONSchema | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Merges multiple JSON Schemas into one
|
|
102
|
+
*
|
|
103
|
+
* Uses allOf composition for complex cases
|
|
104
|
+
*
|
|
105
|
+
* @param schemas - Schemas to merge
|
|
106
|
+
* @returns Merged schema
|
|
107
|
+
*/
|
|
108
|
+
export declare function mergeSchemas(...schemas: (JSONSchema | undefined)[]): JSONSchema | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Creates a simple string schema for path parameters
|
|
111
|
+
*
|
|
112
|
+
* @param format - Optional format (e.g., 'uuid', 'date')
|
|
113
|
+
* @returns JSON Schema for string parameter
|
|
114
|
+
*/
|
|
115
|
+
export declare function createStringSchema(format?: string): JSONSchema;
|
|
116
|
+
/**
|
|
117
|
+
* Checks if a schema has any properties
|
|
118
|
+
*/
|
|
119
|
+
export declare function schemaHasProperties(schema: JSONSchema | undefined): boolean;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Converter
|
|
3
|
+
*
|
|
4
|
+
* Converts Zod schemas to JSON Schema format for OpenAPI specifications.
|
|
5
|
+
*
|
|
6
|
+
* @module @veloxts/router/openapi/schema-converter
|
|
7
|
+
*/
|
|
8
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
9
|
+
/**
|
|
10
|
+
* Converts a Zod schema to JSON Schema format for OpenAPI
|
|
11
|
+
*
|
|
12
|
+
* @param schema - Zod schema to convert
|
|
13
|
+
* @param options - Conversion options
|
|
14
|
+
* @returns JSON Schema representation
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const UserSchema = z.object({
|
|
19
|
+
* id: z.string().uuid(),
|
|
20
|
+
* email: z.string().email(),
|
|
21
|
+
* name: z.string().min(1).max(100),
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* const jsonSchema = zodSchemaToJsonSchema(UserSchema);
|
|
25
|
+
* // {
|
|
26
|
+
* // type: 'object',
|
|
27
|
+
* // properties: {
|
|
28
|
+
* // id: { type: 'string', format: 'uuid' },
|
|
29
|
+
* // email: { type: 'string', format: 'email' },
|
|
30
|
+
* // name: { type: 'string', minLength: 1, maxLength: 100 },
|
|
31
|
+
* // },
|
|
32
|
+
* // required: ['id', 'email', 'name'],
|
|
33
|
+
* // }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function zodSchemaToJsonSchema(schema, options = {}) {
|
|
37
|
+
if (!schema) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const { name, target = 'openApi3', refStrategy = 'none', basePath = ['components', 'schemas'], removeDefaults = false, } = options;
|
|
41
|
+
try {
|
|
42
|
+
// Cast needed because zod-to-json-schema types don't include all options we use
|
|
43
|
+
const result = zodToJsonSchema(schema, {
|
|
44
|
+
name,
|
|
45
|
+
target,
|
|
46
|
+
$refStrategy: refStrategy,
|
|
47
|
+
basePath,
|
|
48
|
+
// OpenAPI 3.0 doesn't support $schema
|
|
49
|
+
removeAdditionalStrategy: 'passthrough',
|
|
50
|
+
});
|
|
51
|
+
// Clean up the schema for OpenAPI compatibility
|
|
52
|
+
const cleaned = cleanJsonSchema(result, { removeDefaults });
|
|
53
|
+
return cleaned;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// Log error but don't fail - return a generic schema
|
|
57
|
+
console.warn('[VeloxTS] Failed to convert Zod schema to JSON Schema:', error);
|
|
58
|
+
return { type: 'object' };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Cleans up JSON Schema for OpenAPI compatibility
|
|
63
|
+
*
|
|
64
|
+
* Removes properties that aren't valid in OpenAPI 3.0
|
|
65
|
+
*/
|
|
66
|
+
function cleanJsonSchema(schema, options = {}) {
|
|
67
|
+
const cleaned = { ...schema };
|
|
68
|
+
// Remove $schema as OpenAPI doesn't use it
|
|
69
|
+
delete cleaned.$schema;
|
|
70
|
+
// Remove definitions if using inline mode
|
|
71
|
+
delete cleaned.definitions;
|
|
72
|
+
// Optionally remove defaults
|
|
73
|
+
if (options.removeDefaults) {
|
|
74
|
+
delete cleaned.default;
|
|
75
|
+
}
|
|
76
|
+
// Recursively clean nested schemas
|
|
77
|
+
if (cleaned.properties) {
|
|
78
|
+
const cleanedProps = {};
|
|
79
|
+
for (const [key, value] of Object.entries(cleaned.properties)) {
|
|
80
|
+
cleanedProps[key] = cleanJsonSchema(value, options);
|
|
81
|
+
}
|
|
82
|
+
cleaned.properties = cleanedProps;
|
|
83
|
+
}
|
|
84
|
+
if (cleaned.items) {
|
|
85
|
+
if (Array.isArray(cleaned.items)) {
|
|
86
|
+
cleaned.items = cleaned.items.map((item) => cleanJsonSchema(item, options));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
cleaned.items = cleanJsonSchema(cleaned.items, options);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (cleaned.additionalProperties && typeof cleaned.additionalProperties === 'object') {
|
|
93
|
+
cleaned.additionalProperties = cleanJsonSchema(cleaned.additionalProperties, options);
|
|
94
|
+
}
|
|
95
|
+
// Clean composition keywords
|
|
96
|
+
for (const keyword of ['allOf', 'anyOf', 'oneOf']) {
|
|
97
|
+
if (cleaned[keyword]) {
|
|
98
|
+
cleaned[keyword] = cleaned[keyword].map((s) => cleanJsonSchema(s, options));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (cleaned.not) {
|
|
102
|
+
cleaned.not = cleanJsonSchema(cleaned.not, options);
|
|
103
|
+
}
|
|
104
|
+
return cleaned;
|
|
105
|
+
}
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Schema Manipulation
|
|
108
|
+
// ============================================================================
|
|
109
|
+
/**
|
|
110
|
+
* Removes specified properties from a JSON Schema
|
|
111
|
+
*
|
|
112
|
+
* Useful for removing path parameters from request body schemas
|
|
113
|
+
*
|
|
114
|
+
* @param schema - Original JSON Schema
|
|
115
|
+
* @param propertyNames - Properties to remove
|
|
116
|
+
* @returns New schema without specified properties
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* const schema = {
|
|
121
|
+
* type: 'object',
|
|
122
|
+
* properties: { id: { type: 'string' }, name: { type: 'string' } },
|
|
123
|
+
* required: ['id', 'name'],
|
|
124
|
+
* };
|
|
125
|
+
*
|
|
126
|
+
* const bodySchema = removeSchemaProperties(schema, ['id']);
|
|
127
|
+
* // { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export function removeSchemaProperties(schema, propertyNames) {
|
|
131
|
+
if (!schema || schema.type !== 'object' || !schema.properties) {
|
|
132
|
+
return schema;
|
|
133
|
+
}
|
|
134
|
+
const properties = { ...schema.properties };
|
|
135
|
+
const required = [...(schema.required ?? [])];
|
|
136
|
+
for (const name of propertyNames) {
|
|
137
|
+
delete properties[name];
|
|
138
|
+
const idx = required.indexOf(name);
|
|
139
|
+
if (idx !== -1) {
|
|
140
|
+
required.splice(idx, 1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// If no properties left, return undefined
|
|
144
|
+
if (Object.keys(properties).length === 0) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
...schema,
|
|
149
|
+
properties,
|
|
150
|
+
required: required.length > 0 ? required : undefined,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Extracts specific properties from a JSON Schema
|
|
155
|
+
*
|
|
156
|
+
* @param schema - Original JSON Schema
|
|
157
|
+
* @param propertyNames - Properties to extract
|
|
158
|
+
* @returns New schema with only specified properties
|
|
159
|
+
*/
|
|
160
|
+
export function extractSchemaProperties(schema, propertyNames) {
|
|
161
|
+
if (!schema || schema.type !== 'object' || !schema.properties) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const sourceProps = schema.properties;
|
|
165
|
+
const sourceRequired = schema.required ?? [];
|
|
166
|
+
const properties = {};
|
|
167
|
+
const required = [];
|
|
168
|
+
for (const name of propertyNames) {
|
|
169
|
+
if (sourceProps[name]) {
|
|
170
|
+
properties[name] = sourceProps[name];
|
|
171
|
+
if (sourceRequired.includes(name)) {
|
|
172
|
+
required.push(name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (Object.keys(properties).length === 0) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties,
|
|
182
|
+
required: required.length > 0 ? required : undefined,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Merges multiple JSON Schemas into one
|
|
187
|
+
*
|
|
188
|
+
* Uses allOf composition for complex cases
|
|
189
|
+
*
|
|
190
|
+
* @param schemas - Schemas to merge
|
|
191
|
+
* @returns Merged schema
|
|
192
|
+
*/
|
|
193
|
+
export function mergeSchemas(...schemas) {
|
|
194
|
+
const validSchemas = schemas.filter((s) => s !== undefined);
|
|
195
|
+
if (validSchemas.length === 0) {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
if (validSchemas.length === 1) {
|
|
199
|
+
return validSchemas[0];
|
|
200
|
+
}
|
|
201
|
+
// If all are objects, merge properties directly
|
|
202
|
+
if (validSchemas.every((s) => s.type === 'object')) {
|
|
203
|
+
const mergedProperties = {};
|
|
204
|
+
const mergedRequired = [];
|
|
205
|
+
for (const schema of validSchemas) {
|
|
206
|
+
if (schema.properties) {
|
|
207
|
+
Object.assign(mergedProperties, schema.properties);
|
|
208
|
+
}
|
|
209
|
+
if (schema.required) {
|
|
210
|
+
mergedRequired.push(...schema.required);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: mergedProperties,
|
|
216
|
+
required: [...new Set(mergedRequired)],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// Use allOf for complex merges
|
|
220
|
+
return { allOf: validSchemas };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Creates a simple string schema for path parameters
|
|
224
|
+
*
|
|
225
|
+
* @param format - Optional format (e.g., 'uuid', 'date')
|
|
226
|
+
* @returns JSON Schema for string parameter
|
|
227
|
+
*/
|
|
228
|
+
export function createStringSchema(format) {
|
|
229
|
+
const schema = { type: 'string' };
|
|
230
|
+
if (format) {
|
|
231
|
+
schema.format = format;
|
|
232
|
+
}
|
|
233
|
+
return schema;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Checks if a schema has any properties
|
|
237
|
+
*/
|
|
238
|
+
export function schemaHasProperties(schema) {
|
|
239
|
+
if (!schema)
|
|
240
|
+
return false;
|
|
241
|
+
if (schema.type !== 'object')
|
|
242
|
+
return false;
|
|
243
|
+
if (!schema.properties)
|
|
244
|
+
return false;
|
|
245
|
+
return Object.keys(schema.properties).length > 0;
|
|
246
|
+
}
|