142 lines
3.6 KiB
TypeScript
142 lines
3.6 KiB
TypeScript
/**
|
|
* Metadata storage for decorators using Symbol.metadata (TC39 Stage 3)
|
|
* Falls back to WeakMap for environments without Symbol.metadata
|
|
*/
|
|
|
|
import type { IControllerMetadata, IRouteMetadata } from './decorators.types.js';
|
|
import type { IInterceptOptions } from '../core/smartserve.interfaces.js';
|
|
|
|
// Symbol for storing metadata when Symbol.metadata is not available
|
|
const CONTROLLER_METADATA = Symbol('smartserve:controller');
|
|
|
|
/**
|
|
* Get or create controller metadata for a class
|
|
* Uses symbol property on the class itself for metadata storage
|
|
*/
|
|
export function getControllerMetadata(target: any): IControllerMetadata {
|
|
// Store metadata on the class itself using symbol
|
|
if (!target[CONTROLLER_METADATA]) {
|
|
target[CONTROLLER_METADATA] = createEmptyMetadata();
|
|
}
|
|
return target[CONTROLLER_METADATA];
|
|
}
|
|
|
|
/**
|
|
* Get controller metadata from prototype (for instance lookup)
|
|
*/
|
|
export function getMetadataFromInstance(instance: any): IControllerMetadata | undefined {
|
|
const constructor = instance.constructor;
|
|
return getControllerMetadata(constructor);
|
|
}
|
|
|
|
/**
|
|
* Set base path for a controller
|
|
*/
|
|
export function setBasePath(target: any, path: string): void {
|
|
const metadata = getControllerMetadata(target);
|
|
metadata.basePath = normalizePath(path);
|
|
metadata.target = target;
|
|
}
|
|
|
|
/**
|
|
* Add a route to a controller
|
|
*/
|
|
export function addRoute(
|
|
target: any,
|
|
methodName: string | symbol,
|
|
route: Omit<IRouteMetadata, 'methodName' | 'interceptors'>
|
|
): void {
|
|
const metadata = getControllerMetadata(target.constructor);
|
|
|
|
// Get existing route or create new one
|
|
let existingRoute = metadata.routes.get(methodName);
|
|
if (!existingRoute) {
|
|
existingRoute = {
|
|
...route,
|
|
methodName,
|
|
interceptors: [],
|
|
};
|
|
metadata.routes.set(methodName, existingRoute);
|
|
} else {
|
|
// Update existing route
|
|
existingRoute.method = route.method;
|
|
existingRoute.path = route.path;
|
|
existingRoute.options = { ...existingRoute.options, ...route.options };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add class-level interceptor
|
|
*/
|
|
export function addClassInterceptor(target: any, interceptor: IInterceptOptions): void {
|
|
const metadata = getControllerMetadata(target);
|
|
metadata.classInterceptors.push(interceptor);
|
|
}
|
|
|
|
/**
|
|
* Add method-level interceptor
|
|
*/
|
|
export function addMethodInterceptor(
|
|
target: any,
|
|
methodName: string | symbol,
|
|
interceptor: IInterceptOptions
|
|
): void {
|
|
const metadata = getControllerMetadata(target.constructor);
|
|
|
|
let route = metadata.routes.get(methodName);
|
|
if (!route) {
|
|
// Create placeholder route (will be completed by @Get/@Post/etc.)
|
|
route = {
|
|
method: 'GET',
|
|
path: '',
|
|
methodName,
|
|
interceptors: [],
|
|
options: {},
|
|
};
|
|
metadata.routes.set(methodName, route);
|
|
}
|
|
|
|
route.interceptors.push(interceptor);
|
|
}
|
|
|
|
/**
|
|
* Create empty metadata object
|
|
*/
|
|
function createEmptyMetadata(): IControllerMetadata {
|
|
return {
|
|
basePath: '',
|
|
classInterceptors: [],
|
|
routes: new Map(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Normalize path to ensure consistent format
|
|
*/
|
|
export function normalizePath(path: string): string {
|
|
if (!path) return '';
|
|
|
|
// Ensure leading slash
|
|
let normalized = path.startsWith('/') ? path : `/${path}`;
|
|
|
|
// Remove trailing slash (unless it's just '/')
|
|
if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
normalized = normalized.slice(0, -1);
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
/**
|
|
* Combine base path and route path
|
|
*/
|
|
export function combinePaths(basePath: string, routePath: string): string {
|
|
const base = normalizePath(basePath);
|
|
const route = normalizePath(routePath);
|
|
|
|
if (!base) return route || '/';
|
|
if (!route) return base;
|
|
|
|
return `${base}${route}`;
|
|
}
|