@veloxts/router 0.6.57 → 0.6.58

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,442 @@
1
+ /**
2
+ * OpenAPI Generator
3
+ *
4
+ * Generates OpenAPI 3.0.3 specifications from VeloxTS procedure collections.
5
+ *
6
+ * @module @veloxts/router/openapi/generator
7
+ */
8
+ import { generateRestRoutes } from '../rest/adapter.js';
9
+ import { buildParameters, convertToOpenAPIPath, joinPaths } from './path-extractor.js';
10
+ import { removeSchemaProperties, zodSchemaToJsonSchema } from './schema-converter.js';
11
+ import { extractUsedSecuritySchemes, filterUsedSecuritySchemes, guardsToSecurity, mergeSecuritySchemes, } from './security-mapper.js';
12
+ // ============================================================================
13
+ // Main Generator
14
+ // ============================================================================
15
+ /**
16
+ * Generates an OpenAPI 3.0.3 specification from procedure collections
17
+ *
18
+ * @param collections - Array of procedure collections to document
19
+ * @param options - Generator options
20
+ * @returns Complete OpenAPI specification
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * import { generateOpenApiSpec } from '@veloxts/router';
25
+ *
26
+ * const spec = generateOpenApiSpec([userProcedures, postProcedures], {
27
+ * info: {
28
+ * title: 'My API',
29
+ * version: '1.0.0',
30
+ * description: 'A VeloxTS-powered API',
31
+ * },
32
+ * prefix: '/api',
33
+ * servers: [{ url: 'http://localhost:3030' }],
34
+ * });
35
+ * ```
36
+ */
37
+ export function generateOpenApiSpec(collections, options) {
38
+ const prefix = options.prefix ?? '/api';
39
+ const paths = {};
40
+ const tags = [];
41
+ const allGuards = [];
42
+ // Process each collection
43
+ for (const collection of collections) {
44
+ const routes = generateRestRoutes(collection);
45
+ // Add tag for this namespace
46
+ tags.push({
47
+ name: collection.namespace,
48
+ description: options.tagDescriptions?.[collection.namespace],
49
+ });
50
+ // Process each route
51
+ for (const route of routes) {
52
+ // Build full path with prefix
53
+ const fullPath = joinPaths(prefix, route.path);
54
+ const openApiPath = convertToOpenAPIPath(fullPath);
55
+ // Initialize path item if not exists
56
+ if (!paths[openApiPath]) {
57
+ paths[openApiPath] = {};
58
+ }
59
+ // Collect guards for security scheme filtering
60
+ // Cast to unknown to avoid variance issues - we only use guard names
61
+ allGuards.push(...route.procedure.guards);
62
+ // Generate operation
63
+ const operation = generateOperation(route, collection.namespace, options);
64
+ // Add to path item
65
+ const method = route.method.toLowerCase();
66
+ paths[openApiPath][method] = operation;
67
+ }
68
+ }
69
+ // Build security schemes (only include those that are used)
70
+ const allSchemes = mergeSecuritySchemes(options.securitySchemes);
71
+ const usedSchemes = extractUsedSecuritySchemes(allGuards, {
72
+ customMappings: options.guardToSecurityMap,
73
+ });
74
+ const securitySchemes = filterUsedSecuritySchemes(allSchemes, usedSchemes);
75
+ // Build the spec
76
+ const spec = {
77
+ openapi: '3.0.3',
78
+ info: options.info,
79
+ paths,
80
+ tags,
81
+ };
82
+ // Add optional fields
83
+ if (options.servers?.length) {
84
+ spec.servers = options.servers;
85
+ }
86
+ if (Object.keys(securitySchemes).length > 0) {
87
+ spec.components = { securitySchemes };
88
+ }
89
+ if (options.defaultSecurity?.length) {
90
+ spec.security = options.defaultSecurity;
91
+ }
92
+ if (options.externalDocs) {
93
+ spec.externalDocs = options.externalDocs;
94
+ }
95
+ // Add extensions
96
+ if (options.extensions) {
97
+ Object.assign(spec, options.extensions);
98
+ }
99
+ return spec;
100
+ }
101
+ // ============================================================================
102
+ // Operation Generation
103
+ // ============================================================================
104
+ /**
105
+ * Generates an OpenAPI operation from a REST route
106
+ */
107
+ function generateOperation(route, namespace, options) {
108
+ const { procedure, procedureName, method, path } = route;
109
+ // Convert Zod schemas to JSON Schema
110
+ const inputSchema = procedure.inputSchema
111
+ ? zodSchemaToJsonSchema(procedure.inputSchema)
112
+ : undefined;
113
+ const outputSchema = procedure.outputSchema
114
+ ? zodSchemaToJsonSchema(procedure.outputSchema)
115
+ : undefined;
116
+ // Build parameters
117
+ const { pathParams, queryParams, pathParamNames } = buildParameters({
118
+ path,
119
+ method,
120
+ inputSchema,
121
+ });
122
+ // Combine all parameters
123
+ const parameters = [...pathParams, ...queryParams];
124
+ // Build request body for POST, PUT, PATCH
125
+ const requestBody = buildRequestBody(method, inputSchema, pathParamNames);
126
+ // Map guards to security requirements
127
+ // Cast to unknown to avoid variance issues - we only use guard names
128
+ const security = guardsToSecurity(procedure.guards, {
129
+ customMappings: options.guardToSecurityMap,
130
+ });
131
+ // Build responses
132
+ const responses = buildResponses(method, outputSchema, security.length > 0);
133
+ // Build operation
134
+ const operation = {
135
+ operationId: `${namespace}_${procedureName}`,
136
+ summary: inferSummary(procedureName),
137
+ tags: [namespace],
138
+ responses,
139
+ };
140
+ // Add optional fields
141
+ if (parameters.length > 0) {
142
+ operation.parameters = parameters;
143
+ }
144
+ if (requestBody) {
145
+ operation.requestBody = requestBody;
146
+ }
147
+ if (security.length > 0) {
148
+ operation.security = security;
149
+ }
150
+ return operation;
151
+ }
152
+ // ============================================================================
153
+ // Request Body Generation
154
+ // ============================================================================
155
+ /**
156
+ * Builds a request body definition for mutation methods
157
+ */
158
+ function buildRequestBody(method, inputSchema, pathParamNames) {
159
+ // Only POST, PUT, PATCH have request bodies
160
+ if (!['POST', 'PUT', 'PATCH'].includes(method)) {
161
+ return undefined;
162
+ }
163
+ if (!inputSchema) {
164
+ return undefined;
165
+ }
166
+ // Remove path parameters from body schema
167
+ const bodySchema = removeSchemaProperties(inputSchema, pathParamNames);
168
+ // If no properties left, no body needed
169
+ if (!bodySchema || !hasProperties(bodySchema)) {
170
+ return undefined;
171
+ }
172
+ return {
173
+ required: true,
174
+ content: {
175
+ 'application/json': {
176
+ schema: bodySchema,
177
+ },
178
+ },
179
+ };
180
+ }
181
+ /**
182
+ * Checks if a schema has any properties
183
+ */
184
+ function hasProperties(schema) {
185
+ if (schema.type !== 'object')
186
+ return true; // Non-object schemas are valid
187
+ if (!schema.properties)
188
+ return false;
189
+ return Object.keys(schema.properties).length > 0;
190
+ }
191
+ // ============================================================================
192
+ // Response Generation
193
+ // ============================================================================
194
+ /**
195
+ * Builds response definitions for an operation
196
+ */
197
+ function buildResponses(method, outputSchema, hasAuth) {
198
+ const responses = {};
199
+ // Determine success status code
200
+ const successCode = getSuccessStatusCode(method);
201
+ const successDescription = getSuccessDescription(method);
202
+ // Success response
203
+ if (successCode === '204') {
204
+ // No content
205
+ responses['204'] = { description: successDescription };
206
+ }
207
+ else {
208
+ responses[successCode] = {
209
+ description: successDescription,
210
+ ...(outputSchema && {
211
+ content: {
212
+ 'application/json': { schema: outputSchema },
213
+ },
214
+ }),
215
+ };
216
+ }
217
+ // Error responses
218
+ responses['400'] = {
219
+ description: 'Bad Request - Validation error',
220
+ content: {
221
+ 'application/json': {
222
+ schema: {
223
+ type: 'object',
224
+ properties: {
225
+ error: {
226
+ type: 'object',
227
+ properties: {
228
+ code: { type: 'string', example: 'VALIDATION_ERROR' },
229
+ message: { type: 'string' },
230
+ details: { type: 'object' },
231
+ },
232
+ required: ['code', 'message'],
233
+ },
234
+ },
235
+ required: ['error'],
236
+ },
237
+ },
238
+ },
239
+ };
240
+ // Auth-related responses
241
+ if (hasAuth) {
242
+ responses['401'] = {
243
+ description: 'Unauthorized - Authentication required',
244
+ content: {
245
+ 'application/json': {
246
+ schema: {
247
+ type: 'object',
248
+ properties: {
249
+ error: {
250
+ type: 'object',
251
+ properties: {
252
+ code: { type: 'string', example: 'UNAUTHORIZED' },
253
+ message: { type: 'string' },
254
+ },
255
+ required: ['code', 'message'],
256
+ },
257
+ },
258
+ required: ['error'],
259
+ },
260
+ },
261
+ },
262
+ };
263
+ responses['403'] = {
264
+ description: 'Forbidden - Insufficient permissions',
265
+ content: {
266
+ 'application/json': {
267
+ schema: {
268
+ type: 'object',
269
+ properties: {
270
+ error: {
271
+ type: 'object',
272
+ properties: {
273
+ code: { type: 'string', example: 'FORBIDDEN' },
274
+ message: { type: 'string' },
275
+ },
276
+ required: ['code', 'message'],
277
+ },
278
+ },
279
+ required: ['error'],
280
+ },
281
+ },
282
+ },
283
+ };
284
+ }
285
+ // Not found for single-resource operations
286
+ if (['GET', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
287
+ responses['404'] = {
288
+ description: 'Not Found - Resource does not exist',
289
+ content: {
290
+ 'application/json': {
291
+ schema: {
292
+ type: 'object',
293
+ properties: {
294
+ error: {
295
+ type: 'object',
296
+ properties: {
297
+ code: { type: 'string', example: 'NOT_FOUND' },
298
+ message: { type: 'string' },
299
+ },
300
+ required: ['code', 'message'],
301
+ },
302
+ },
303
+ required: ['error'],
304
+ },
305
+ },
306
+ },
307
+ };
308
+ }
309
+ // Internal server error
310
+ responses['500'] = {
311
+ description: 'Internal Server Error',
312
+ content: {
313
+ 'application/json': {
314
+ schema: {
315
+ type: 'object',
316
+ properties: {
317
+ error: {
318
+ type: 'object',
319
+ properties: {
320
+ code: { type: 'string', example: 'INTERNAL_ERROR' },
321
+ message: { type: 'string' },
322
+ },
323
+ required: ['code', 'message'],
324
+ },
325
+ },
326
+ required: ['error'],
327
+ },
328
+ },
329
+ },
330
+ };
331
+ return responses;
332
+ }
333
+ /**
334
+ * Gets the success status code for an HTTP method
335
+ */
336
+ function getSuccessStatusCode(method) {
337
+ switch (method) {
338
+ case 'POST':
339
+ return '201';
340
+ case 'DELETE':
341
+ return '204';
342
+ default:
343
+ return '200';
344
+ }
345
+ }
346
+ /**
347
+ * Gets the success description for an HTTP method
348
+ */
349
+ function getSuccessDescription(method) {
350
+ switch (method) {
351
+ case 'POST':
352
+ return 'Created';
353
+ case 'DELETE':
354
+ return 'No Content';
355
+ case 'PUT':
356
+ case 'PATCH':
357
+ return 'Updated';
358
+ default:
359
+ return 'Success';
360
+ }
361
+ }
362
+ // ============================================================================
363
+ // Summary Generation
364
+ // ============================================================================
365
+ /**
366
+ * Infers a human-readable summary from a procedure name
367
+ *
368
+ * Converts camelCase to Title Case with spaces.
369
+ *
370
+ * @example
371
+ * ```typescript
372
+ * inferSummary('getUser') // 'Get User'
373
+ * inferSummary('createPost') // 'Create Post'
374
+ * inferSummary('listUsers') // 'List Users'
375
+ * ```
376
+ */
377
+ function inferSummary(procedureName) {
378
+ // Insert space before capital letters
379
+ const spaced = procedureName.replace(/([A-Z])/g, ' $1').trim();
380
+ // Capitalize first letter
381
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
382
+ }
383
+ // ============================================================================
384
+ // Utility Functions
385
+ // ============================================================================
386
+ /**
387
+ * Gets route summary information for debugging/logging
388
+ *
389
+ * @param collections - Procedure collections
390
+ * @param prefix - API prefix
391
+ * @returns Array of route summaries
392
+ */
393
+ export function getOpenApiRouteSummary(collections, prefix = '/api') {
394
+ const routes = [];
395
+ for (const collection of collections) {
396
+ const collectionRoutes = generateRestRoutes(collection);
397
+ for (const route of collectionRoutes) {
398
+ const fullPath = joinPaths(prefix, route.path);
399
+ routes.push({
400
+ method: route.method,
401
+ path: convertToOpenAPIPath(fullPath),
402
+ operationId: `${collection.namespace}_${route.procedureName}`,
403
+ namespace: collection.namespace,
404
+ });
405
+ }
406
+ }
407
+ return routes;
408
+ }
409
+ /**
410
+ * Validates an OpenAPI spec for common issues
411
+ *
412
+ * @param spec - OpenAPI spec to validate
413
+ * @returns Array of validation warnings
414
+ */
415
+ export function validateOpenApiSpec(spec) {
416
+ const warnings = [];
417
+ // Check for empty paths
418
+ if (Object.keys(spec.paths).length === 0) {
419
+ warnings.push('OpenAPI spec has no paths defined');
420
+ }
421
+ // Check for missing info
422
+ if (!spec.info.title) {
423
+ warnings.push('OpenAPI spec is missing info.title');
424
+ }
425
+ if (!spec.info.version) {
426
+ warnings.push('OpenAPI spec is missing info.version');
427
+ }
428
+ // Check for duplicate operation IDs
429
+ const operationIds = new Set();
430
+ for (const pathItem of Object.values(spec.paths)) {
431
+ for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
432
+ const operation = pathItem[method];
433
+ if (operation?.operationId) {
434
+ if (operationIds.has(operation.operationId)) {
435
+ warnings.push(`Duplicate operationId: ${operation.operationId}`);
436
+ }
437
+ operationIds.add(operation.operationId);
438
+ }
439
+ }
440
+ }
441
+ return warnings;
442
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * OpenAPI Module
3
+ *
4
+ * Generates OpenAPI 3.0.3 specifications from VeloxTS procedure collections.
5
+ *
6
+ * @module @veloxts/router/openapi
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import {
11
+ * generateOpenApiSpec,
12
+ * swaggerUIPlugin,
13
+ * createSwaggerUI,
14
+ * } from '@veloxts/router';
15
+ *
16
+ * // Generate spec programmatically
17
+ * const spec = generateOpenApiSpec([userProcedures, postProcedures], {
18
+ * info: { title: 'My API', version: '1.0.0' },
19
+ * prefix: '/api',
20
+ * });
21
+ *
22
+ * // Or register Swagger UI plugin
23
+ * app.register(swaggerUIPlugin, {
24
+ * routePrefix: '/docs',
25
+ * collections: [userProcedures],
26
+ * openapi: {
27
+ * info: { title: 'My API', version: '1.0.0' },
28
+ * },
29
+ * });
30
+ * ```
31
+ */
32
+ export { generateOpenApiSpec, getOpenApiRouteSummary, validateOpenApiSpec, } from './generator.js';
33
+ export { createSwaggerUI, getOpenApiSpec, registerDocs, swaggerUIPlugin, } from './plugin.js';
34
+ export { createStringSchema, extractSchemaProperties, mergeSchemas, removeSchemaProperties, type SchemaConversionOptions, schemaHasProperties, zodSchemaToJsonSchema, } from './schema-converter.js';
35
+ export { type BuildParametersOptions, type BuildParametersResult, buildParameters, convertFromOpenAPIPath, convertToOpenAPIPath, extractPathParamNames, extractQueryParameters, extractResourceFromPath, hasPathParameters, joinPaths, normalizePath, parsePathParameters, type QueryParamExtractionOptions, } from './path-extractor.js';
36
+ export { createSecurityRequirement, DEFAULT_GUARD_MAPPINGS, DEFAULT_SECURITY_SCHEMES, extractGuardScopes, extractUsedSecuritySchemes, filterUsedSecuritySchemes, type GuardMappingOptions, guardsRequireAuth, guardsToSecurity, mapGuardToSecurity, mergeSecuritySchemes, } from './security-mapper.js';
37
+ export type { JSONSchema, OpenAPIComponents, OpenAPIContact, OpenAPIEncoding, OpenAPIExample, OpenAPIExternalDocs, OpenAPIGeneratorOptions, OpenAPIHeader, OpenAPIHttpMethod, OpenAPIInfo, OpenAPILicense, OpenAPILink, OpenAPIMediaType, OpenAPIOAuthFlow, OpenAPIOAuthFlows, OpenAPIOperation, OpenAPIParameter, OpenAPIPathItem, OpenAPIRequestBody, OpenAPIResponse, OpenAPISecurityRequirement, OpenAPISecurityScheme, OpenAPIServer, OpenAPISpec, OpenAPITag, ParameterIn, RouteInfo, SecuritySchemeType, SwaggerUIConfig, SwaggerUIPluginOptions, } from './types.js';
@@ -0,0 +1,51 @@
1
+ /**
2
+ * OpenAPI Module
3
+ *
4
+ * Generates OpenAPI 3.0.3 specifications from VeloxTS procedure collections.
5
+ *
6
+ * @module @veloxts/router/openapi
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import {
11
+ * generateOpenApiSpec,
12
+ * swaggerUIPlugin,
13
+ * createSwaggerUI,
14
+ * } from '@veloxts/router';
15
+ *
16
+ * // Generate spec programmatically
17
+ * const spec = generateOpenApiSpec([userProcedures, postProcedures], {
18
+ * info: { title: 'My API', version: '1.0.0' },
19
+ * prefix: '/api',
20
+ * });
21
+ *
22
+ * // Or register Swagger UI plugin
23
+ * app.register(swaggerUIPlugin, {
24
+ * routePrefix: '/docs',
25
+ * collections: [userProcedures],
26
+ * openapi: {
27
+ * info: { title: 'My API', version: '1.0.0' },
28
+ * },
29
+ * });
30
+ * ```
31
+ */
32
+ // ============================================================================
33
+ // Generator
34
+ // ============================================================================
35
+ export { generateOpenApiSpec, getOpenApiRouteSummary, validateOpenApiSpec, } from './generator.js';
36
+ // ============================================================================
37
+ // Plugin
38
+ // ============================================================================
39
+ export { createSwaggerUI, getOpenApiSpec, registerDocs, swaggerUIPlugin, } from './plugin.js';
40
+ // ============================================================================
41
+ // Schema Converter
42
+ // ============================================================================
43
+ export { createStringSchema, extractSchemaProperties, mergeSchemas, removeSchemaProperties, schemaHasProperties, zodSchemaToJsonSchema, } from './schema-converter.js';
44
+ // ============================================================================
45
+ // Path Extractor
46
+ // ============================================================================
47
+ export { buildParameters, convertFromOpenAPIPath, convertToOpenAPIPath, extractPathParamNames, extractQueryParameters, extractResourceFromPath, hasPathParameters, joinPaths, normalizePath, parsePathParameters, } from './path-extractor.js';
48
+ // ============================================================================
49
+ // Security Mapper
50
+ // ============================================================================
51
+ export { createSecurityRequirement, DEFAULT_GUARD_MAPPINGS, DEFAULT_SECURITY_SCHEMES, extractGuardScopes, extractUsedSecuritySchemes, filterUsedSecuritySchemes, guardsRequireAuth, guardsToSecurity, mapGuardToSecurity, mergeSecuritySchemes, } from './security-mapper.js';