feat(openapi): Add OpenAPI module: decorators, spec generator, runtime validation and Swagger UI

This commit is contained in:
2025-12-08 17:43:51 +00:00
parent 15848b9c9c
commit cc3e335112
19 changed files with 2405 additions and 19 deletions

View File

@@ -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)
*/