Files
smartserve/ts/decorators/decorators.registry.ts

199 lines
5.5 KiB
TypeScript
Raw Normal View History

2025-11-29 15:24:00 +00:00
/**
* Controller registry - stores all registered controllers
*/
import type { IControllerMetadata, IRegisteredController, ICompiledRoute } from './decorators.types.js';
import type { IRequestContext, IInterceptOptions, THttpMethod } from '../core/smartserve.interfaces.js';
import { getControllerMetadata, combinePaths } from './decorators.metadata.js';
/**
* Global registry of all controllers
*/
export class ControllerRegistry {
private static controllers: Map<Function, IControllerMetadata> = new Map();
private static instances: Map<Function, any> = new Map();
private static compiledRoutes: ICompiledRoute[] = [];
private static routesCompiled = false;
/**
* Register a controller class
*/
static registerClass(target: Function): void {
const metadata = getControllerMetadata(target);
metadata.target = target as new (...args: any[]) => any;
this.controllers.set(target, metadata);
this.routesCompiled = false;
}
/**
* Register a controller instance
*/
static registerInstance(instance: any): void {
const constructor = instance.constructor;
const metadata = getControllerMetadata(constructor);
// Store instance
this.instances.set(constructor, instance);
// Register class if not already registered
if (!this.controllers.has(constructor)) {
this.registerClass(constructor);
}
this.routesCompiled = false;
}
/**
* Get all registered controllers
*/
static getControllers(): IRegisteredController[] {
const result: IRegisteredController[] = [];
for (const [constructor, metadata] of this.controllers) {
// Get or create instance
let instance = this.instances.get(constructor);
if (!instance && metadata.target) {
instance = new metadata.target();
this.instances.set(constructor, instance);
}
if (instance) {
result.push({ instance, metadata });
}
}
return result;
}
/**
* Compile all routes for fast matching
*/
static compileRoutes(): ICompiledRoute[] {
if (this.routesCompiled) {
return this.compiledRoutes;
}
this.compiledRoutes = [];
for (const { instance, metadata } of this.getControllers()) {
for (const [methodName, route] of metadata.routes) {
const fullPath = combinePaths(metadata.basePath, route.path);
const { regex, paramNames } = this.pathToRegex(fullPath);
// Combine class and method interceptors
const interceptors: IInterceptOptions[] = [
...metadata.classInterceptors,
...route.interceptors,
];
// Create bound handler
const handler = async (ctx: IRequestContext): Promise<any> => {
const method = instance[methodName];
if (typeof method !== 'function') {
throw new Error(`Method ${String(methodName)} not found on controller`);
}
return method.call(instance, ctx);
};
this.compiledRoutes.push({
pattern: fullPath,
regex,
paramNames,
method: route.method,
handler,
interceptors,
});
}
}
// Sort routes by specificity (more specific paths first)
this.compiledRoutes.sort((a, b) => {
// Routes without wildcards come first
const aHasWildcard = a.pattern.includes('*');
const bHasWildcard = b.pattern.includes('*');
if (aHasWildcard !== bHasWildcard) return aHasWildcard ? 1 : -1;
// Routes with more segments come first
const aSegments = a.pattern.split('/').length;
const bSegments = b.pattern.split('/').length;
if (aSegments !== bSegments) return bSegments - aSegments;
// Routes without params come first
const aParams = a.paramNames.length;
const bParams = b.paramNames.length;
return aParams - bParams;
});
this.routesCompiled = true;
return this.compiledRoutes;
}
/**
* Match a request to a compiled route
*/
static matchRoute(path: string, method: THttpMethod): {
route: ICompiledRoute;
params: Record<string, string>;
} | null {
const routes = this.compileRoutes();
for (const route of routes) {
// Check method match
if (route.method !== 'ALL' && route.method !== method) {
continue;
}
// Check path match
const match = route.regex.exec(path);
if (match) {
// Extract params
const params: Record<string, string> = {};
route.paramNames.forEach((name, index) => {
params[name] = match[index + 1];
});
return { route, params };
}
}
return null;
}
/**
* Convert path pattern to regex
* Supports :param and * wildcard
*/
private static pathToRegex(path: string): { regex: RegExp; paramNames: string[] } {
const paramNames: string[] = [];
let regexStr = path
// Escape special regex chars (except : and *)
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
// Convert :param to capture group
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
paramNames.push(name);
return '([^/]+)';
})
// Convert * to wildcard
.replace(/\*/g, '(.*)');
// Anchor the regex
regexStr = `^${regexStr}$`;
return {
regex: new RegExp(regexStr),
paramNames,
};
}
/**
* Clear all registered controllers (useful for testing)
*/
static clear(): void {
this.controllers.clear();
this.instances.clear();
this.compiledRoutes = [];
this.routesCompiled = false;
}
}