/** * 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 any>( target: TClass, context: ClassDecoratorContext ): TClass { addClassInterceptor(target, options); return target; } // Method decorator function methodDecorator( target: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext 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( guardOrGuards: TGuardFunction | TGuardFunction[], options?: IGuardOptions ) { const guards = Array.isArray(guardOrGuards) ? guardOrGuards : [guardOrGuards]; const interceptor: TRequestInterceptor = 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 } as IInterceptOptions); } /** * @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( transformOrTransforms: TResponseInterceptor | TResponseInterceptor[] ) { const transforms = Array.isArray(transformOrTransforms) ? transformOrTransforms : [transformOrTransforms]; return createInterceptDecorator({ response: transforms } as IInterceptOptions); } /** * @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( options: IInterceptOptions ) { 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(); 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(data: T): { success: true; data: T } { return { success: true, data }; } /** * Add timestamp to response */ export function addTimestamp(data: T): T & { timestamp: number } { return { ...data, timestamp: Date.now() }; }