initial
This commit is contained in:
198
ts/decorators/decorators.registry.ts
Normal file
198
ts/decorators/decorators.registry.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user