UNPKG

@spfn/core

Version:

SPFN Framework Core - File-based routing, transactions, repository pattern

558 lines (548 loc) 14.9 kB
import { NextRequest, NextResponse } from 'next/server'; /** * SPFN Next.js Proxy Interceptor Types */ /** * Request Interceptor Context * * Available before calling SPFN API */ interface RequestInterceptorContext { /** * Request path (e.g., '/_auth/login') */ path: string; /** * HTTP method (e.g., 'POST') */ method: string; /** * Request headers (mutable) */ headers: Record<string, string>; /** * Request body (mutable) */ body?: any; /** * Query parameters from original request */ query: Record<string, string | string[]>; /** * Cookies from Next.js request */ cookies: Map<string, string>; /** * Original Next.js request */ request: NextRequest; /** * Metadata for sharing data between interceptors */ metadata: Record<string, any>; } /** * Response Interceptor Context * * Available after SPFN API responds */ interface ResponseInterceptorContext { /** * Request path */ path: string; /** * HTTP method */ method: string; /** * Original request data (immutable) */ request: { headers: Record<string, string>; body?: any; }; /** * Response data (mutable) */ response: { status: number; statusText: string; headers: Headers; body: any; }; /** * Cookies to set in response * * @example * ```typescript * ctx.setCookies.push({ * name: 'session', * value: 'xxx', * options: { httpOnly: true, maxAge: 3600 } * }); * ``` */ setCookies: Array<{ name: string; value: string; options?: { httpOnly?: boolean; secure?: boolean; sameSite?: 'strict' | 'lax' | 'none'; maxAge?: number; path?: string; domain?: string; }; }>; /** * Metadata shared from request interceptors */ metadata: Record<string, any>; } /** * Request Interceptor Function * * @param context - Request context (mutable) * @param next - Call to continue to next interceptor * * @example * ```typescript * const interceptor: RequestInterceptor = async (ctx, next) => { * // Modify headers * ctx.headers['Authorization'] = 'Bearer token'; * * // Store data for response interceptor * ctx.metadata.userId = '123'; * * // Continue to next interceptor * await next(); * }; * ``` */ type RequestInterceptor = (context: RequestInterceptorContext, next: () => Promise<void>) => Promise<void>; /** * Response Interceptor Function * * @param context - Response context (mutable) * @param next - Call to continue to next interceptor * * @example * ```typescript * const interceptor: ResponseInterceptor = async (ctx, next) => { * // Modify response body * ctx.response.body = { ...ctx.response.body, extra: 'data' }; * * // Set cookie * ctx.setCookies.push({ * name: 'session', * value: 'xxx', * options: { httpOnly: true } * }); * * // Continue to next interceptor * await next(); * }; * ``` */ type ResponseInterceptor = (context: ResponseInterceptorContext, next: () => Promise<void>) => Promise<void>; /** * Interceptor Rule * * Defines when and how to intercept requests/responses */ interface InterceptorRule { /** * Path pattern to match * * - String with wildcards: '/_auth/*', '/users/:id' * - RegExp: /^\/_auth\/.+$/ * - '*' matches all paths * * @example * ```typescript * pathPattern: '/_auth/*' // matches /_auth/login, /_auth/register * pathPattern: '/users/:id' // matches /users/123, /users/456 * pathPattern: /^\/_auth\/.+$/ // regex match * pathPattern: '*' // matches all paths * ``` */ pathPattern: string | RegExp; /** * HTTP method(s) to match (optional) * * - Single method: 'POST' * - Multiple methods: ['POST', 'PUT'] * - Omit to match all methods * * @default undefined (matches all methods) */ method?: string | string[]; /** * Request interceptor * * Called before SPFN API request */ request?: RequestInterceptor; /** * Response interceptor * * Called after SPFN API response */ response?: ResponseInterceptor; } /** * Proxy Configuration */ interface ProxyConfig { /** * SPFN API base URL * * @default process.env.SERVER_API_URL || process.env.SPFN_API_URL || 'http://localhost:8790' */ apiUrl?: string; /** * Additional custom interceptors * * These are executed after auto-discovered interceptors * * Executed in order: first registered -> last registered */ interceptors?: InterceptorRule[]; /** * Enable automatic interceptor discovery from registry * * When enabled, all interceptors registered via registerInterceptors() * are automatically applied to the proxy. * * @default true */ autoDiscoverInterceptors?: boolean; /** * Disable interceptors from specific packages * * Use this to exclude auto-discovered interceptors from certain packages * when you want to provide custom implementations. * * @example ['auth', 'storage'] */ disableAutoInterceptors?: string[]; /** * Enable debug logging * * @default false */ debug?: boolean; } /** * SPFN Next.js API Route Proxy with Interceptor Pattern * * Automatically proxies requests to SPFN API server with: * - Cookie forwarding * - Request/Response interceptors * - Flexible header manipulation * * Usage: * ```typescript * // Basic usage (no interceptors) * // app/api/actions/[...path]/route.ts * export { GET, POST, PUT, DELETE, PATCH } from '@spfn/core/nextjs'; * * // With interceptors * import { createProxy } from '@spfn/core/nextjs'; * * export const { GET, POST } = createProxy({ * interceptors: [ * { * pathPattern: '/_auth/*', * request: async (ctx, next) => { * ctx.headers['Authorization'] = 'Bearer token'; * await next(); * } * } * ] * }); * ``` */ /** * Create proxy with custom configuration and interceptors * * @param config - Proxy configuration with interceptors * @returns HTTP method handlers for Next.js API routes * * @example * ```typescript * // app/api/actions/[...path]/route.ts * import { createProxy } from '@spfn/core/nextjs'; * * export const { GET, POST, PUT, DELETE, PATCH } = createProxy({ * apiUrl: 'http://localhost:8790', * debug: true, * interceptors: [ * { * pathPattern: '/_auth/*', * method: 'POST', * request: async (ctx, next) => { * const session = await getSession(); * if (session) { * ctx.headers['Authorization'] = `Bearer ${session.token}`; * } * await next(); * }, * response: async (ctx, next) => { * if (ctx.response.status === 200) { * ctx.setCookies.push({ * name: 'session', * value: ctx.response.body.token, * options: { httpOnly: true, maxAge: 3600 } * }); * } * await next(); * } * } * ] * }); * ``` */ declare function createProxy(config?: ProxyConfig): { GET: (request: NextRequest, context: { params: Promise<{ path: string[]; }> | { path: string[]; }; }) => Promise<NextResponse<unknown>>; POST: (request: NextRequest, context: { params: Promise<{ path: string[]; }> | { path: string[]; }; }) => Promise<NextResponse<unknown>>; PUT: (request: NextRequest, context: { params: Promise<{ path: string[]; }> | { path: string[]; }; }) => Promise<NextResponse<unknown>>; PATCH: (request: NextRequest, context: { params: Promise<{ path: string[]; }> | { path: string[]; }; }) => Promise<NextResponse<unknown>>; DELETE: (request: NextRequest, context: { params: Promise<{ path: string[]; }> | { path: string[]; }; }) => Promise<NextResponse<unknown>>; }; declare const GET: (request: NextRequest, context: { params: Promise<{ path: string[]; }> | { path: string[]; }; }) => Promise<NextResponse<unknown>>; declare const POST: (request: NextRequest, context: { params: Promise<{ path: string[]; }> | { path: string[]; }; }) => Promise<NextResponse<unknown>>; declare const PUT: (request: NextRequest, context: { params: Promise<{ path: string[]; }> | { path: string[]; }; }) => Promise<NextResponse<unknown>>; declare const PATCH: (request: NextRequest, context: { params: Promise<{ path: string[]; }> | { path: string[]; }; }) => Promise<NextResponse<unknown>>; declare const DELETE: (request: NextRequest, context: { params: Promise<{ path: string[]; }> | { path: string[]; }; }) => Promise<NextResponse<unknown>>; /** * Global Interceptor Registry * * Allows packages to automatically register their interceptors * for Next.js proxy without manual configuration. */ /** * Global interceptor registry * * Packages register their interceptors on import, * and proxy automatically discovers and applies them. */ declare class InterceptorRegistry { private interceptors; /** * Register interceptors for a package * * @param packageName - Unique package identifier (e.g., 'auth', 'storage') * @param interceptors - Array of interceptor rules * * @example * ```typescript * registerInterceptors('auth', [ * { * pathPattern: '/_auth/*', * request: async (ctx, next) => { ... } * } * ]); * ``` */ register(packageName: string, interceptors: InterceptorRule[]): void; /** * Get all registered interceptors * * @param exclude - Package names to exclude * @returns Flat array of all interceptor rules */ getAll(exclude?: string[]): InterceptorRule[]; /** * Get interceptors for specific package * * @param packageName - Package identifier * @returns Interceptor rules or undefined */ get(packageName: string): InterceptorRule[] | undefined; /** * Get list of registered package names */ getPackageNames(): string[]; /** * Check if package has registered interceptors */ has(packageName: string): boolean; /** * Unregister interceptors for a package * * @param packageName - Package identifier */ unregister(packageName: string): void; /** * Clear all registered interceptors * * Useful for testing */ clear(): void; /** * Get total count of registered interceptors */ count(): number; } /** * Global singleton registry instance */ declare const interceptorRegistry: InterceptorRegistry; /** * Register interceptors for a package * * This should be called during package initialization (on import). * The interceptors will be automatically applied by the Next.js proxy. * * @param packageName - Unique package identifier (e.g., 'auth', 'storage') * @param interceptors - Array of interceptor rules * * @example * ```typescript * // packages/auth/src/adapters/nextjs/interceptors/index.ts * import { registerInterceptors } from '@spfn/core/client/nextjs'; * * const authInterceptors = [ * { * pathPattern: '/_auth/*', * request: async (ctx, next) => { * // Add JWT token * ctx.headers['Authorization'] = 'Bearer token'; * await next(); * } * } * ]; * * // Auto-register on import * registerInterceptors('auth', authInterceptors); * ``` */ declare function registerInterceptors(packageName: string, interceptors: InterceptorRule[]): void; /** * SPFN Next.js Proxy Interceptor Execution Engine */ /** * Check if path matches pattern * * Supports: * - Wildcards: '/_auth/*' matches '/_auth/login' * - Path params: '/users/:id' matches '/users/123' * - RegExp: /^\/_auth\/.+$/ matches '/_auth/login' * - Exact match: '/_auth/login' matches '/_auth/login' * - All: '*' matches any path * * @param path - Request path to test * @param pattern - Pattern to match against * @returns True if path matches pattern */ declare function matchPath(path: string, pattern: string | RegExp): boolean; /** * Check if method matches pattern * * @param method - Request method (e.g., 'POST') * @param pattern - Method pattern (e.g., 'POST' or ['POST', 'PUT']) * @returns True if method matches pattern */ declare function matchMethod(method: string, pattern?: string | string[]): boolean; /** * Filter interceptors that match the request * * @param rules - All interceptor rules * @param path - Request path * @param method - Request method * @returns Matched interceptors */ declare function filterMatchingInterceptors(rules: InterceptorRule[], path: string, method: string): InterceptorRule[]; /** * Execute request interceptors in chain * * Interceptors are executed in order: * 1. First registered interceptor * 2. Second registered interceptor * 3. ... and so on * * Each interceptor must call next() to continue the chain. * If next() is not called, the chain stops and remaining interceptors are skipped. * * @param context - Request interceptor context * @param interceptors - Interceptors to execute */ declare function executeRequestInterceptors(context: RequestInterceptorContext, interceptors: RequestInterceptor[]): Promise<void>; /** * Execute response interceptors in chain * * Interceptors are executed in order: * 1. First registered interceptor * 2. Second registered interceptor * 3. ... and so on * * Each interceptor must call next() to continue the chain. * If next() is not called, the chain stops and remaining interceptors are skipped. * * @param context - Response interceptor context * @param interceptors - Interceptors to execute */ declare function executeResponseInterceptors(context: ResponseInterceptorContext, interceptors: ResponseInterceptor[]): Promise<void>; export { DELETE, GET, type InterceptorRule, PATCH, POST, PUT, type ProxyConfig, type RequestInterceptor, type RequestInterceptorContext, type ResponseInterceptor, type ResponseInterceptorContext, createProxy, executeRequestInterceptors, executeResponseInterceptors, filterMatchingInterceptors, interceptorRegistry, matchMethod, matchPath, registerInterceptors };