199 lines
5.5 KiB
TypeScript
199 lines
5.5 KiB
TypeScript
|
|
/**
|
||
|
|
* 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;
|
||
|
|
}
|
||
|
|
}
|