feat(openapi): Add OpenAPI module: decorators, spec generator, runtime validation and Swagger UI
This commit is contained in:
@@ -135,7 +135,7 @@ export function combinePaths(basePath: string, routePath: string): string {
|
||||
const route = normalizePath(routePath);
|
||||
|
||||
if (!base) return route || '/';
|
||||
if (!route) return base;
|
||||
if (!route || route === '/') return base || '/';
|
||||
|
||||
return `${base}${route}`;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
* Controller registry - stores all registered controllers
|
||||
*/
|
||||
|
||||
import type { IControllerMetadata, IRegisteredController, ICompiledRoute } from './decorators.types.js';
|
||||
import type { IControllerMetadata, IRegisteredController, ICompiledRoute, IOpenApiRouteMeta } from './decorators.types.js';
|
||||
import type { IRequestContext, IInterceptOptions, THttpMethod } from '../core/smartserve.interfaces.js';
|
||||
import { getControllerMetadata, combinePaths } from './decorators.metadata.js';
|
||||
import { createValidationInterceptor } from '../openapi/openapi.validator.js';
|
||||
|
||||
/**
|
||||
* Global registry of all controllers
|
||||
@@ -92,7 +93,7 @@ export class ControllerRegistry {
|
||||
/**
|
||||
* Compile all routes for fast matching
|
||||
*/
|
||||
static compileRoutes(): ICompiledRoute[] {
|
||||
static compileRoutes(enableValidation = true): ICompiledRoute[] {
|
||||
if (this.routesCompiled) {
|
||||
return this.compiledRoutes;
|
||||
}
|
||||
@@ -105,10 +106,19 @@ export class ControllerRegistry {
|
||||
const { regex, paramNames } = this.pathToRegex(fullPath);
|
||||
|
||||
// Combine class and method interceptors
|
||||
const interceptors: IInterceptOptions[] = [
|
||||
...metadata.classInterceptors,
|
||||
...route.interceptors,
|
||||
];
|
||||
const interceptors: IInterceptOptions[] = [];
|
||||
|
||||
// Add OpenAPI validation interceptor first (before other interceptors)
|
||||
// This ensures validation happens before any other processing
|
||||
if (enableValidation && route.openapi && this.hasValidationMetadata(route.openapi)) {
|
||||
interceptors.push({
|
||||
request: createValidationInterceptor(route.openapi),
|
||||
});
|
||||
}
|
||||
|
||||
// Then add class-level and method-level interceptors
|
||||
interceptors.push(...metadata.classInterceptors);
|
||||
interceptors.push(...route.interceptors);
|
||||
|
||||
// Create bound handler
|
||||
const handler = async (ctx: IRequestContext): Promise<any> => {
|
||||
@@ -127,6 +137,7 @@ export class ControllerRegistry {
|
||||
handler,
|
||||
interceptors,
|
||||
compression: route.compression,
|
||||
openapi: route.openapi,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -214,6 +225,18 @@ export class ControllerRegistry {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OpenAPI metadata contains validation-relevant information
|
||||
*/
|
||||
private static hasValidationMetadata(openapi: IOpenApiRouteMeta): boolean {
|
||||
return !!(
|
||||
openapi.requestBody ||
|
||||
(openapi.params && openapi.params.size > 0) ||
|
||||
(openapi.query && openapi.query.size > 0) ||
|
||||
(openapi.headers && openapi.headers.size > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered controllers (useful for testing)
|
||||
*/
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface IControllerMetadata {
|
||||
routes: Map<string | symbol, IRouteMetadata>;
|
||||
/** Controller class reference */
|
||||
target?: new (...args: any[]) => any;
|
||||
/** OpenAPI metadata for controller */
|
||||
openapi?: IOpenApiControllerMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,6 +60,8 @@ export interface IRouteMetadata {
|
||||
handler?: Function;
|
||||
/** Route-specific compression settings */
|
||||
compression?: IRouteCompressionOptions;
|
||||
/** OpenAPI metadata for this route */
|
||||
openapi?: IOpenApiRouteMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,4 +92,138 @@ export interface ICompiledRoute {
|
||||
interceptors: IInterceptOptions[];
|
||||
/** Route-specific compression settings */
|
||||
compression?: IRouteCompressionOptions;
|
||||
/** OpenAPI metadata for this route */
|
||||
openapi?: IOpenApiRouteMeta;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// OpenAPI / JSON Schema Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* JSON Schema type for OpenAPI schemas
|
||||
* Supports JSON Schema draft 2020-12 (used by OpenAPI 3.1)
|
||||
*/
|
||||
export type TJsonSchema = {
|
||||
type?: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null' | Array<'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null'>;
|
||||
format?: string;
|
||||
properties?: Record<string, TJsonSchema>;
|
||||
items?: TJsonSchema;
|
||||
required?: string[];
|
||||
enum?: unknown[];
|
||||
const?: unknown;
|
||||
default?: unknown;
|
||||
description?: string;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
pattern?: string;
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
uniqueItems?: boolean;
|
||||
additionalProperties?: boolean | TJsonSchema;
|
||||
allOf?: TJsonSchema[];
|
||||
anyOf?: TJsonSchema[];
|
||||
oneOf?: TJsonSchema[];
|
||||
not?: TJsonSchema;
|
||||
$ref?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* OpenAPI operation metadata for documentation
|
||||
*/
|
||||
export interface IOpenApiOperationMeta {
|
||||
/** Short summary of what the operation does */
|
||||
summary?: string;
|
||||
/** Longer description with markdown support */
|
||||
description?: string;
|
||||
/** Unique operation identifier (auto-generated if omitted) */
|
||||
operationId?: string;
|
||||
/** Mark operation as deprecated */
|
||||
deprecated?: boolean;
|
||||
/** External documentation */
|
||||
externalDocs?: {
|
||||
url: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAPI parameter metadata (path, query, header, cookie)
|
||||
*/
|
||||
export interface IOpenApiParamMeta {
|
||||
/** Human-readable description */
|
||||
description?: string;
|
||||
/** Whether parameter is required (path params always required) */
|
||||
required?: boolean;
|
||||
/** JSON Schema for the parameter */
|
||||
schema?: TJsonSchema;
|
||||
/** Example value */
|
||||
example?: unknown;
|
||||
/** Multiple examples */
|
||||
examples?: Record<string, { value: unknown; summary?: string; description?: string }>;
|
||||
/** Allow empty value */
|
||||
allowEmptyValue?: boolean;
|
||||
/** Deprecation flag */
|
||||
deprecated?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAPI request body metadata
|
||||
*/
|
||||
export interface IOpenApiRequestBodyMeta {
|
||||
/** Human-readable description */
|
||||
description?: string;
|
||||
/** Whether body is required (default: true) */
|
||||
required?: boolean;
|
||||
/** JSON Schema for the body (assumes application/json) */
|
||||
schema: TJsonSchema;
|
||||
/** Example value */
|
||||
example?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAPI response body metadata
|
||||
*/
|
||||
export interface IOpenApiResponseBodyMeta {
|
||||
/** Human-readable description */
|
||||
description: string;
|
||||
/** JSON Schema for the response (assumes application/json) */
|
||||
schema?: TJsonSchema;
|
||||
/** Example value */
|
||||
example?: unknown;
|
||||
/** Response headers */
|
||||
headers?: Record<string, { description?: string; schema?: TJsonSchema }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined OpenAPI metadata for a route method
|
||||
*/
|
||||
export interface IOpenApiRouteMeta {
|
||||
/** Operation details (summary, description, etc.) */
|
||||
operation?: IOpenApiOperationMeta;
|
||||
/** Path parameters by name */
|
||||
params?: Map<string, IOpenApiParamMeta>;
|
||||
/** Query parameters by name */
|
||||
query?: Map<string, IOpenApiParamMeta>;
|
||||
/** Header parameters by name */
|
||||
headers?: Map<string, IOpenApiParamMeta>;
|
||||
/** Request body schema */
|
||||
requestBody?: IOpenApiRequestBodyMeta;
|
||||
/** Response bodies by status code */
|
||||
responses?: Map<number, IOpenApiResponseBodyMeta>;
|
||||
/** Security requirements */
|
||||
security?: Array<Record<string, string[]>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAPI metadata for a controller class
|
||||
*/
|
||||
export interface IOpenApiControllerMeta {
|
||||
/** Tags for grouping routes in documentation */
|
||||
tags?: string[];
|
||||
/** Security requirements for all routes in controller */
|
||||
security?: Array<Record<string, string[]>>;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,14 @@ export type {
|
||||
IRouteCompressionOptions,
|
||||
IRegisteredController,
|
||||
ICompiledRoute,
|
||||
// OpenAPI types
|
||||
TJsonSchema,
|
||||
IOpenApiOperationMeta,
|
||||
IOpenApiParamMeta,
|
||||
IOpenApiRequestBodyMeta,
|
||||
IOpenApiResponseBodyMeta,
|
||||
IOpenApiRouteMeta,
|
||||
IOpenApiControllerMeta,
|
||||
} from './decorators.types.js';
|
||||
|
||||
// Route decorator
|
||||
@@ -49,3 +57,15 @@ export {
|
||||
normalizePath,
|
||||
combinePaths,
|
||||
} from './decorators.metadata.js';
|
||||
|
||||
// OpenAPI decorators
|
||||
export {
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
ApiHeader,
|
||||
ApiRequestBody,
|
||||
ApiResponseBody,
|
||||
ApiSecurity,
|
||||
ApiTag,
|
||||
} from '../openapi/openapi.decorators.js';
|
||||
|
||||
Reference in New Issue
Block a user