initial
This commit is contained in:
215
ts/decorators/decorators.interceptors.ts
Normal file
215
ts/decorators/decorators.interceptors.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Interceptor decorators (@Guard, @Transform, @Intercept)
|
||||
*
|
||||
* All three decorators have unified semantics:
|
||||
* - @Guard is sugar for @Intercept({ request: guardFn })
|
||||
* - @Transform is sugar for @Intercept({ response: transformFn })
|
||||
* - @Intercept provides full control over both request and response
|
||||
*/
|
||||
|
||||
import type {
|
||||
IRequestContext,
|
||||
IInterceptOptions,
|
||||
IGuardOptions,
|
||||
TGuardFunction,
|
||||
TRequestInterceptor,
|
||||
TResponseInterceptor,
|
||||
} from '../core/smartserve.interfaces.js';
|
||||
import { addClassInterceptor, addMethodInterceptor } from './decorators.metadata.js';
|
||||
|
||||
/**
|
||||
* Create a decorator that can be applied to both classes and methods
|
||||
*/
|
||||
function createInterceptDecorator(options: IInterceptOptions) {
|
||||
// Class decorator
|
||||
function classDecorator<TClass extends new (...args: any[]) => any>(
|
||||
target: TClass,
|
||||
context: ClassDecoratorContext<TClass>
|
||||
): TClass {
|
||||
addClassInterceptor(target, options);
|
||||
return target;
|
||||
}
|
||||
|
||||
// Method decorator
|
||||
function methodDecorator<This, Args extends any[], Return>(
|
||||
target: (this: This, ...args: Args) => Return,
|
||||
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
|
||||
) {
|
||||
context.addInitializer(function (this: This) {
|
||||
addMethodInterceptor(this, context.name, options);
|
||||
});
|
||||
return target;
|
||||
}
|
||||
|
||||
// Return overloaded function that works for both
|
||||
return function (
|
||||
target: any,
|
||||
context: ClassDecoratorContext | ClassMethodDecoratorContext
|
||||
) {
|
||||
if (context.kind === 'class') {
|
||||
return classDecorator(target, context as ClassDecoratorContext);
|
||||
} else if (context.kind === 'method') {
|
||||
return methodDecorator(target, context as ClassMethodDecoratorContext);
|
||||
}
|
||||
throw new Error('Interceptor decorators can only be applied to classes or methods');
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @Guard decorator - validates requests before handler execution
|
||||
*
|
||||
* Guards return boolean: true to allow, false to reject with 403 Forbidden
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Single guard
|
||||
* @Guard(isAuthenticated)
|
||||
*
|
||||
* // Multiple guards (all must pass)
|
||||
* @Guard([isAuthenticated, hasRole('admin')])
|
||||
*
|
||||
* // With custom rejection response
|
||||
* @Guard(isAuthenticated, {
|
||||
* onReject: () => new Response('Unauthorized', { status: 401 })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function Guard<TBody = unknown>(
|
||||
guardOrGuards: TGuardFunction<TBody> | TGuardFunction<TBody>[],
|
||||
options?: IGuardOptions
|
||||
) {
|
||||
const guards = Array.isArray(guardOrGuards) ? guardOrGuards : [guardOrGuards];
|
||||
|
||||
const interceptor: TRequestInterceptor<TBody> = async (ctx) => {
|
||||
for (const guard of guards) {
|
||||
const allowed = await guard(ctx);
|
||||
if (!allowed) {
|
||||
if (options?.onReject) {
|
||||
return options.onReject(ctx);
|
||||
}
|
||||
return new Response(JSON.stringify({ error: 'Forbidden' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
}
|
||||
// Return undefined to continue with original context
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return createInterceptDecorator({ request: interceptor });
|
||||
}
|
||||
|
||||
/**
|
||||
* @Transform decorator - modifies response after handler execution
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Single transform
|
||||
* @Transform(data => ({ success: true, data }))
|
||||
*
|
||||
* // Multiple transforms (applied in order)
|
||||
* @Transform([addTimestamp, wrapResponse])
|
||||
* ```
|
||||
*/
|
||||
export function Transform<TRes = unknown>(
|
||||
transformOrTransforms: TResponseInterceptor<TRes> | TResponseInterceptor<TRes>[]
|
||||
) {
|
||||
const transforms = Array.isArray(transformOrTransforms)
|
||||
? transformOrTransforms
|
||||
: [transformOrTransforms];
|
||||
|
||||
return createInterceptDecorator({ response: transforms });
|
||||
}
|
||||
|
||||
/**
|
||||
* @Intercept decorator - full control over request and response interception
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Intercept({
|
||||
* request: async (ctx) => {
|
||||
* ctx.state.startTime = Date.now();
|
||||
* return ctx;
|
||||
* },
|
||||
* response: (res, ctx) => ({
|
||||
* ...res,
|
||||
* duration: Date.now() - ctx.state.startTime
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function Intercept<TBody = unknown, TRes = unknown>(
|
||||
options: IInterceptOptions<TBody, TRes>
|
||||
) {
|
||||
return createInterceptDecorator(options as IInterceptOptions);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Common Guard Utilities
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Create a guard that checks for a specific header
|
||||
*/
|
||||
export function hasHeader(headerName: string, expectedValue?: string): TGuardFunction {
|
||||
return (ctx) => {
|
||||
const value = ctx.headers.get(headerName);
|
||||
if (!value) return false;
|
||||
if (expectedValue !== undefined) return value === expectedValue;
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a guard that checks for Bearer token
|
||||
*/
|
||||
export function hasBearerToken(): TGuardFunction {
|
||||
return (ctx) => {
|
||||
const auth = ctx.headers.get('Authorization');
|
||||
return auth?.startsWith('Bearer ') ?? false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a rate limiting guard
|
||||
*/
|
||||
export function rateLimit(
|
||||
maxRequests: number,
|
||||
windowMs: number
|
||||
): TGuardFunction {
|
||||
const requests = new Map<string, number[]>();
|
||||
|
||||
return (ctx) => {
|
||||
const ip = ctx.headers.get('x-forwarded-for')?.split(',')[0] ?? 'unknown';
|
||||
const now = Date.now();
|
||||
const windowStart = now - windowMs;
|
||||
|
||||
const timestamps = requests.get(ip)?.filter((t) => t > windowStart) ?? [];
|
||||
if (timestamps.length >= maxRequests) {
|
||||
return false;
|
||||
}
|
||||
|
||||
timestamps.push(now);
|
||||
requests.set(ip, timestamps);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Common Transform Utilities
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Wrap response in a success envelope
|
||||
*/
|
||||
export function wrapSuccess<T>(data: T): { success: true; data: T } {
|
||||
return { success: true, data };
|
||||
}
|
||||
|
||||
/**
|
||||
* Add timestamp to response
|
||||
*/
|
||||
export function addTimestamp<T extends object>(data: T): T & { timestamp: number } {
|
||||
return { ...data, timestamp: Date.now() };
|
||||
}
|
||||
141
ts/decorators/decorators.metadata.ts
Normal file
141
ts/decorators/decorators.metadata.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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}`;
|
||||
}
|
||||
117
ts/decorators/decorators.methods.ts
Normal file
117
ts/decorators/decorators.methods.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* HTTP method decorators (@Get, @Post, @Put, @Delete, @Patch, @All)
|
||||
*/
|
||||
|
||||
import type { THttpMethod, IMethodOptions } from '../core/smartserve.interfaces.js';
|
||||
import { addRoute, normalizePath } from './decorators.metadata.js';
|
||||
|
||||
/**
|
||||
* Factory for creating HTTP method decorators
|
||||
*/
|
||||
function createMethodDecorator(httpMethod: THttpMethod) {
|
||||
return function (pathOrOptions?: string | IMethodOptions) {
|
||||
return function <This, Args extends any[], Return>(
|
||||
target: (this: This, ...args: Args) => Return,
|
||||
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
|
||||
) {
|
||||
if (context.kind !== 'method') {
|
||||
throw new Error(`@${httpMethod} can only decorate methods`);
|
||||
}
|
||||
|
||||
const options: IMethodOptions = typeof pathOrOptions === 'string'
|
||||
? { path: pathOrOptions }
|
||||
: pathOrOptions ?? {};
|
||||
|
||||
// Use addInitializer to ensure we have access to the class prototype
|
||||
context.addInitializer(function (this: This) {
|
||||
addRoute(this, context.name, {
|
||||
method: httpMethod,
|
||||
path: normalizePath(options.path ?? ''),
|
||||
options,
|
||||
handler: target as unknown as Function,
|
||||
});
|
||||
});
|
||||
|
||||
return target;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @Get decorator - handles GET requests
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Get('/users')
|
||||
* listUsers(ctx: IRequestContext) { ... }
|
||||
*
|
||||
* @Get('/:id')
|
||||
* getUser(ctx: IRequestContext) { ... }
|
||||
* ```
|
||||
*/
|
||||
export const Get = createMethodDecorator('GET');
|
||||
|
||||
/**
|
||||
* @Post decorator - handles POST requests
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Post('/users')
|
||||
* createUser(ctx: IRequestContext<ICreateUserBody>) { ... }
|
||||
* ```
|
||||
*/
|
||||
export const Post = createMethodDecorator('POST');
|
||||
|
||||
/**
|
||||
* @Put decorator - handles PUT requests
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Put('/users/:id')
|
||||
* updateUser(ctx: IRequestContext<IUpdateUserBody>) { ... }
|
||||
* ```
|
||||
*/
|
||||
export const Put = createMethodDecorator('PUT');
|
||||
|
||||
/**
|
||||
* @Delete decorator - handles DELETE requests
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Delete('/users/:id')
|
||||
* deleteUser(ctx: IRequestContext) { ... }
|
||||
* ```
|
||||
*/
|
||||
export const Delete = createMethodDecorator('DELETE');
|
||||
|
||||
/**
|
||||
* @Patch decorator - handles PATCH requests
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Patch('/users/:id')
|
||||
* patchUser(ctx: IRequestContext<IPartialUser>) { ... }
|
||||
* ```
|
||||
*/
|
||||
export const Patch = createMethodDecorator('PATCH');
|
||||
|
||||
/**
|
||||
* @Head decorator - handles HEAD requests
|
||||
*/
|
||||
export const Head = createMethodDecorator('HEAD');
|
||||
|
||||
/**
|
||||
* @Options decorator - handles OPTIONS requests
|
||||
*/
|
||||
export const Options = createMethodDecorator('OPTIONS');
|
||||
|
||||
/**
|
||||
* @All decorator - handles all HTTP methods
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @All('/proxy/*')
|
||||
* proxyRequest(ctx: IRequestContext) { ... }
|
||||
* ```
|
||||
*/
|
||||
export const All = createMethodDecorator('ALL');
|
||||
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;
|
||||
}
|
||||
}
|
||||
45
ts/decorators/decorators.route.ts
Normal file
45
ts/decorators/decorators.route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @Route class decorator
|
||||
* Marks a class as a controller with a base path
|
||||
*/
|
||||
|
||||
import type { IRouteOptions } from '../core/smartserve.interfaces.js';
|
||||
import { setBasePath, normalizePath } from './decorators.metadata.js';
|
||||
import { ControllerRegistry } from './decorators.registry.js';
|
||||
|
||||
/**
|
||||
* @Route decorator - marks a class as a route controller
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Route('/api/users')
|
||||
* class UserController {
|
||||
* @Get('/:id')
|
||||
* getUser(ctx: IRequestContext) { ... }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function Route(pathOrOptions?: string | IRouteOptions) {
|
||||
return function <TClass extends new (...args: any[]) => any>(
|
||||
target: TClass,
|
||||
context: ClassDecoratorContext<TClass>
|
||||
): TClass {
|
||||
if (context.kind !== 'class') {
|
||||
throw new Error('@Route can only decorate classes');
|
||||
}
|
||||
|
||||
const path = typeof pathOrOptions === 'string'
|
||||
? pathOrOptions
|
||||
: pathOrOptions?.path ?? '';
|
||||
|
||||
// Store base path in metadata
|
||||
setBasePath(target, path);
|
||||
|
||||
// Register controller after class initialization
|
||||
context.addInitializer(function (this: TClass) {
|
||||
ControllerRegistry.registerClass(target);
|
||||
});
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
76
ts/decorators/decorators.types.ts
Normal file
76
ts/decorators/decorators.types.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Decorator type definitions for @push.rocks/smartserve
|
||||
* Uses TC39 Stage 3 decorators (TypeScript 5.2+)
|
||||
*/
|
||||
|
||||
import type {
|
||||
THttpMethod,
|
||||
IRequestContext,
|
||||
IInterceptOptions,
|
||||
IMethodOptions,
|
||||
IRouteOptions,
|
||||
} from '../core/smartserve.interfaces.js';
|
||||
|
||||
// =============================================================================
|
||||
// Metadata Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Metadata stored on controller classes
|
||||
*/
|
||||
export interface IControllerMetadata {
|
||||
/** Base path for all routes in this controller */
|
||||
basePath: string;
|
||||
/** Class-level interceptors (apply to all methods) */
|
||||
classInterceptors: IInterceptOptions[];
|
||||
/** Route definitions by method name */
|
||||
routes: Map<string | symbol, IRouteMetadata>;
|
||||
/** Controller class reference */
|
||||
target?: new (...args: any[]) => any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for individual route methods
|
||||
*/
|
||||
export interface IRouteMetadata {
|
||||
/** HTTP method */
|
||||
method: THttpMethod;
|
||||
/** Path segment (combined with class basePath) */
|
||||
path: string;
|
||||
/** Method-level interceptors */
|
||||
interceptors: IInterceptOptions[];
|
||||
/** Response options */
|
||||
options: IMethodOptions;
|
||||
/** Method name */
|
||||
methodName: string | symbol;
|
||||
/** Handler function reference */
|
||||
handler?: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered controller with metadata
|
||||
*/
|
||||
export interface IRegisteredController {
|
||||
/** Controller instance */
|
||||
instance: any;
|
||||
/** Controller metadata */
|
||||
metadata: IControllerMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiled route for fast matching
|
||||
*/
|
||||
export interface ICompiledRoute {
|
||||
/** Full path pattern */
|
||||
pattern: string;
|
||||
/** Regex for matching */
|
||||
regex: RegExp;
|
||||
/** Parameter names */
|
||||
paramNames: string[];
|
||||
/** HTTP method */
|
||||
method: THttpMethod;
|
||||
/** Handler function */
|
||||
handler: (ctx: IRequestContext) => Promise<any>;
|
||||
/** Combined interceptors (class + method) */
|
||||
interceptors: IInterceptOptions[];
|
||||
}
|
||||
47
ts/decorators/index.ts
Normal file
47
ts/decorators/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// Type exports
|
||||
export type {
|
||||
IControllerMetadata,
|
||||
IRouteMetadata,
|
||||
IRegisteredController,
|
||||
ICompiledRoute,
|
||||
} from './decorators.types.js';
|
||||
|
||||
// Route decorator
|
||||
export { Route } from './decorators.route.js';
|
||||
|
||||
// HTTP method decorators
|
||||
export {
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Patch,
|
||||
Head,
|
||||
Options,
|
||||
All,
|
||||
} from './decorators.methods.js';
|
||||
|
||||
// Interceptor decorators
|
||||
export {
|
||||
Guard,
|
||||
Transform,
|
||||
Intercept,
|
||||
// Utility guards
|
||||
hasHeader,
|
||||
hasBearerToken,
|
||||
rateLimit,
|
||||
// Utility transforms
|
||||
wrapSuccess,
|
||||
addTimestamp,
|
||||
} from './decorators.interceptors.js';
|
||||
|
||||
// Registry
|
||||
export { ControllerRegistry } from './decorators.registry.js';
|
||||
|
||||
// Metadata utilities
|
||||
export {
|
||||
getControllerMetadata,
|
||||
getMetadataFromInstance,
|
||||
normalizePath,
|
||||
combinePaths,
|
||||
} from './decorators.metadata.js';
|
||||
Reference in New Issue
Block a user