@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.
@@ -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, '&amp;')
88
+ .replace(/</g, '&lt;')
89
+ .replace(/>/g, '&gt;')
90
+ .replace(/"/g, '&quot;')
91
+ .replace(/'/g, '&#039;');
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
+ }