import { Server as HTTPServer } from 'http' import express from 'express' import { z } from 'zod' import { CacheConfig } from '../Cache/types.js' import ContainerServer from './lib/ContainerServer.js' import Server from './lib/Server.js' import { HttpMethod } from '../API/Request.js' import { ResponseErrorType } from '../API/Response.js' import Transaction, { TransactionConfig, TransactionExecution, StringMap, } from '../BaseEvent/Transaction.js' import Utils from '../Util/Utils.js' /** * Configuration options for rate limiting on a specific route. * @property {number} [windowMs] - Time window in milliseconds for rate limiting (default: 60000 - 1 minute) * @property {number} [limit] - Maximum number of requests allowed per window (default: 60) * @property {string} [message] - Custom error message for rate limit exceeded * @property {'ip' | 'userId' | ((req: express.Request) => string)} [keyGenerator] - Strategy for generating rate limit keys */ export interface RateLimitConfig { windowMs?: number limit?: number message?: string keyGenerator?: 'ip' | 'userId' | ((req: express.Request) => string) skip?: (req: express.Request) => boolean } /** * Global rate limiting configuration for the router. * @property {boolean} [enabled] - Whether rate limiting is enabled (default: true if config provided) * @property {number} [windowMs] - Time window in milliseconds (default: 60000 - 1 minute) * @property {number} [limit] - Maximum requests per window per key (default: 60) * @property {(req: express.Request) => string} [keyGenerator] - Function to generate rate limit key (default: IP-based) * @property {(req: express.Request, res: express.Response) => void} [handler] - Custom handler for rate limit exceeded * @property {(req: express.Request) => boolean} [skip] - Function to skip rate limiting for certain requests * @property {'memory' | 'redis'} [store] - Storage backend for rate limit data * @property {object} [redis] - Redis rate limit options when using Redis store */ export interface GlobalRateLimitConfig { enabled?: boolean windowMs?: number limit?: number keyGenerator?: (req: express.Request) => string handler?: (req: express.Request, res: express.Response) => void skip?: (req: express.Request) => boolean store?: 'memory' | 'redis' redis?: { prefix?: string } } /** * Represents a route in an API. * @template InputType - The type of the input data for the route. * @template OutputType - The type of the output data for the route. * @property {string} path - The path of the route. * @property {string} method - The HTTP method of the route. * @property {TransactionExecution, OutputType | ResponseErrorType>} handler - The handler function for the route. * @property {?z.ZodObject | z.ZodUnion | z.ZodIntersection | z.ZodEffects}[inputSchema] - The input schema for validating the input data. ZodEffects is supported so schemas using .refine/.superRefine/.transform can be passed directly. * @property {?z.ZodObject | z.ZodUnion | z.ZodIntersection | z.ZodEffects}[pathSchema] - The path schema for validating the path data. * @property {?z.ZodObject | z.ZodUnion | z.ZodIntersection | z.ZodEffects}[querySchema] - The query schema for validating the query data. */ export interface Route< InputType = never, OutputType = never, PathParamsType = StringMap, QueryParamsType = StringMap, > { /** * Represents a file path as a string. * @param {string} path - The file path. * @returns None */ path: string | string[] /** * Represents the method used in an API request. * @type {string} */ method: HttpMethod /* If you are here to know why implementing this method does not auto infer the param type, check long discussion on TS - https://github.com/Microsoft/TypeScript/issues/1373 - https://github.com/microsoft/TypeScript/issues/23911#issuecomment-1351020050 (proposed solution) - https://github.com/microsoft/TypeScript/issues/10570 */ /** * Represents a handler for executing a transaction with the given input type and output type. * @param {Transaction} transaction - The transaction to execute. * @param {OutputType | ResponseErrorType} - The output type or response error type of the transaction. */ handler: TransactionExecution< Transaction, OutputType | ResponseErrorType > /** * An optional input schema for validating the structure of the input data. * ZodEffects is supported so schemas wrapped with .refine/.superRefine/.transform can be used directly. * * @type {?z.ZodObject | z.ZodUnion | z.ZodIntersection | z.ZodEffects} */ inputSchema?: z.ZodObject | z.ZodUnion | z.ZodIntersection | z.ZodEffects /** * An optional input schema for validating the structure of the path params. * * @type {?z.ZodObject | z.ZodUnion | z.ZodIntersection | z.ZodEffects} */ pathSchema?: z.ZodObject | z.ZodUnion | z.ZodIntersection | z.ZodEffects /** * An optional input schema for validating the structure of the query params. * * @type {?z.ZodObject | z.ZodUnion | z.ZodIntersection | z.ZodEffects} */ querySchema?: z.ZodObject | z.ZodUnion | z.ZodIntersection | z.ZodEffects /** * An optional openApi object with extra metadata for docs generation. */ openApi?: { // Descriptive summary: string description: string tags?: string[] // Response outputSchema?: z.ZodObject | z.ZodUnion | z.ZodIntersection | z.ZodType successCode?: number /* defaults to 200 */ // Sec security?: { [key: string]: string[] | never[] }[] } /** * Optional rate limiting configuration for this specific route. * Set to `false` to disable global rate limiting for this route. * @type {RateLimitConfig | false} */ rateLimit?: RateLimitConfig | false } export type AnyRoute = Route /** * Represents the configuration options for a router. * @typedef {TransactionConfig & { * routes: Route[] * port?: number * timeout?: number * cors?: { * origin?: string | string[] * headers?: string[] * allowCredentials?: boolean * } * healthCheckRoute?: string * }} RouterConfig * @property {Route[]} routes - The routes to be configured in the router. * @property {number} [port] - The port number to listen on. If not specified, a default port will be used. * @property {number} [timeout] - The timeout duration for requests in milliseconds. If not specified, a default timeout will be */ export type RouterConfig = TransactionConfig & { /** * An array of route objects representing the available routes in the application. * @type {Route[]} */ routes: AnyRoute[] /** * The port number for the server to listen on. * @type {number | undefined} */ port?: number /** * Optional timeout value in milliseconds. * @type {number | undefined} */ timeout?: number /** * Configuration options for Cross-Origin Resource Sharing (CORS). * @property {string | string[]} [origin] - The allowed origin(s) for CORS requests. * @property {string[]} [headers] - The allowed headers for CORS requests. * @property {boolean} [allowCredentials] - Whether to allow credentials (cookies, HTTP authentication, and client-side SSL certificates) to be sent in CORS requests. */ cors?: { origin?: string | string[] headers?: string[] allowCredentials?: boolean } /** * Shared cache configuration used by WAPI-managed Redis integrations. * Required when rateLimit.store is set to 'redis'. * @type {CacheConfig<'redis'> | undefined} */ cache?: CacheConfig<'redis'> /** * The route for the health check endpoint. * @type {string | undefined} */ healthCheckRoute?: string /** * Paths that should not be logged (e.g., health checks, metrics). * These requests will still be processed but won't create verbose logs. * @type {string[] | undefined} */ noLogPaths?: string[] /** * Global rate limiting configuration for all routes. * Individual routes can override this with their own rateLimit config. * @type {GlobalRateLimitConfig | undefined} */ rateLimit?: GlobalRateLimitConfig containerSetupHook?: (server: HTTPServer, app: express.Express) => Promise } /** * Represents a router that handles routing logic for a web application. */ export default class Router { /** * The configuration object for the router. */ private readonly config: RouterConfig /** * The private readonly server instance. */ private readonly server: Server /** * Constructs a new instance of the Router class. * @param {RouterConfig} config - The configuration object for the router. * @returns None */ constructor(config: RouterConfig) { this.config = config this.server = this.isContainer() ? new ContainerServer(config) : new Server(config) } /** * Retrieves the export function from the server. * @returns {CallableFunction} The export function from the server. */ public getExport(): CallableFunction { return this.server.getExport() } /** * Checks if the current element is a container. * @returns {boolean} - True if the element is a container, false otherwise. */ private isContainer(): boolean { return Utils.isHybridlessContainer() } }