UNPKG

@trifrost/core

Version:

Blazingly fast, runtime-agnostic server framework for modern edge and node environments

301 lines (300 loc) 11.1 kB
import { isIntGt } from '@valkyriestudios/utils/number'; import { isNeObject } from '@valkyriestudios/utils/object'; import { limitMiddleware } from '../modules/RateLimit/_RateLimit'; import { HttpMethods, Sym_TriFrostName } from '../types/constants'; import { Route } from './Route'; import { RouteTree } from './Tree'; import { isValidBodyParser, isValidGrouper, isValidHandler, isValidLimit, isValidMiddleware, normalizeMiddleware } from './util'; import { Lazy } from '../utils/Lazy'; const RGX_SLASH = /\/{2,}/g; class Router { /* Base path for this router */ #path; /* Configured Rate limit instance from the app */ #rateLimit = null; /* Configured Body Parser options */ #bodyParser = null; /* Timeout effective for this router and subrouters/routes */ #timeout = null; /* Middleware */ #middleware; /* Tree passed by parent */ tree; constructor(options) { /* Check path */ if (typeof options?.path !== 'string') throw new Error('TriFrostRouter@ctor: Path is invalid'); /* Check timeout */ if (options.timeout !== null && !isIntGt(options.timeout, 0)) throw new Error('TriFrostRouter@ctor: Timeout is invalid'); /* Check rate limit instance */ if (options.rateLimit !== null && !(options.rateLimit instanceof Lazy)) throw new Error('TriFrostRouter@ctor: RateLimit is invalid'); /* Check rate limit instance */ if (!isValidBodyParser(options.bodyParser)) throw new Error('TriFrostRouter@ctor: BodyParser is invalid'); /* Check tree */ if (!(options.tree instanceof RouteTree)) throw new Error('TriFrostRouter@ctor: Tree is invalid'); /* Check middleware */ if (!Array.isArray(options.middleware)) throw new Error('TriFrostRouter@ctor: Middleware is invalid'); /* Configure path */ this.#path = options.path; /* Configure timeout */ this.#timeout = options.timeout; /* Configure RateLimit instance */ this.#rateLimit = options.rateLimit || null; /* Configure body parser */ this.#bodyParser = options.bodyParser; /* Configure tree */ this.tree = options.tree; /* Configure Middleware */ this.#middleware = [...options.middleware]; } /** * MARK: Getters */ /** * Get the base path of this router */ get path() { return this.#path; } /** * Returns the configured timeout */ get timeout() { return this.#timeout; } /** * MARK: Methods */ /** * Add a router or middleware to the router */ use(val) { if (!isValidMiddleware(val)) throw new Error('TriFrostRouter@use: Handler is expected'); const fn = val; /* Get name */ let name = Reflect.get(fn, Sym_TriFrostName) ?? fn.name; name = typeof name === 'string' && name.length ? name : 'anonymous'; /* Add symbols for introspection/use further down the line */ Reflect.set(fn, Sym_TriFrostName, name); this.#middleware.push(fn); return this; } /** * Attach a rate limit to the middleware chain for this router */ limit(limit) { if (!this.#rateLimit) throw new Error('TriFrostRouter@limit: RateLimit is not configured on App'); if (!isValidLimit(limit)) throw new Error('TriFrostRouter@limit: Invalid limit'); this.use(limitMiddleware(this.#rateLimit, limit)); return this; } /** * Configure body parser options for this router */ bodyParser(options) { if (!isValidBodyParser(options)) throw new Error('TriFrostRouter@bodyParser: Invalid bodyparser'); this.#bodyParser = options; return this; } /** * Add a subrouter with dynamic path handling. */ group(path, handler) { if (typeof path !== 'string') throw new Error('TriFrostRouter@group: Invalid path'); if (!isValidGrouper(handler)) throw new Error('TriFrostRouter@group: Invalid handler'); /* Create config */ const { fn, timeout = undefined } = (typeof handler === 'function' ? { fn: handler } : handler); /* Run router */ fn(new Router({ path: this.#path + path, tree: this.tree, rateLimit: this.#rateLimit, timeout: timeout !== undefined ? timeout : this.#timeout, middleware: this.#middleware, bodyParser: this.#bodyParser, })); return this; } /** * Add a subroute with a builder approach */ route(path, handler) { if (typeof handler !== 'function') throw new Error('TriFrostRouter@route: No handler provided for "' + path + '"'); /* Instantiate route builder */ const route = new Route({ rateLimit: this.#rateLimit, bodyParser: this.#bodyParser, }); /* Run route through handler */ handler(route); /* Loop through resulting stack and register */ for (let i = 0; i < route.stack.length; i++) { const el = route.stack[i]; this.#register(path, [...el.middleware, el.handler], el.methods, el.bodyParser); } return this; } /** * Configure a catch-all not found handler for subroutes of this router * * @param {Handler} handler - Handler to run */ onNotFound(handler) { if (typeof handler !== 'function') throw new Error('TriFrostRoute@onNotFound: Invalid handler provided for router on "' + this.#path + '"'); this.tree.addNotFound({ path: this.#path + '/*', fn: handler, middleware: normalizeMiddleware(this.#middleware), kind: 'notfound', timeout: this.#timeout, bodyParser: this.#bodyParser, name: 'notfound', description: '404 Not Found Handler', meta: null, }); return this; } /** * Configure a catch-all error handler for subroutes of this router * * @param {Handler} handler - Handler to run */ onError(handler) { if (typeof handler !== 'function') throw new Error('TriFrostRoute@onError: Invalid handler provided for router on "' + this.#path + '"'); this.tree.addError({ path: this.#path + '/*', fn: handler, middleware: normalizeMiddleware(this.#middleware), kind: 'error', timeout: this.#timeout, bodyParser: this.#bodyParser, name: 'error', description: 'Error Handler', meta: null, }); return this; } /** * Configure a HTTP Get route */ get(path, handler) { if (typeof path !== 'string') throw new Error('TriFrostRouter@get: Invalid path'); if (!isValidHandler(handler)) throw new Error('TriFrostRouter@get: Invalid handler'); return this.#register(path, [handler], [HttpMethods.GET, HttpMethods.HEAD], this.#bodyParser); } /** * Configure a HTTP Post route */ post(path, handler) { if (typeof path !== 'string') throw new Error('TriFrostRouter@post: Invalid path'); if (!isValidHandler(handler)) throw new Error('TriFrostRouter@post: Invalid handler'); return this.#register(path, [handler], [HttpMethods.POST], this.#bodyParser); } /** * Configure a HTTP Put route */ put(path, handler) { if (typeof path !== 'string') throw new Error('TriFrostRouter@put: Invalid path'); if (!isValidHandler(handler)) throw new Error('TriFrostRouter@put: Invalid handler'); return this.#register(path, [handler], [HttpMethods.PUT], this.#bodyParser); } /** * Configure a HTTP Patch route */ patch(path, handler) { if (typeof path !== 'string') throw new Error('TriFrostRouter@patch: Invalid path'); if (!isValidHandler(handler)) throw new Error('TriFrostRouter@patch: Invalid handler'); return this.#register(path, [handler], [HttpMethods.PATCH], this.#bodyParser); } /** * Configure a HTTP Delete route */ del(path, handler) { if (typeof path !== 'string') throw new Error('TriFrostRouter@del: Invalid path'); if (!isValidHandler(handler)) throw new Error('TriFrostRouter@del: Invalid handler'); return this.#register(path, [handler], [HttpMethods.DELETE], this.#bodyParser); } /** * Configure a health route */ health(path, handler) { return this.get(path, { kind: 'health', name: 'healthcheck', description: 'Healthcheck Route', fn: handler, }); } /** * MARK: Private Fn */ /** * Internal register route method */ #register(path, handlers, methods, bodyParser) { const fn = handlers[handlers.length - 1]; const config = (Object.prototype.toString.call(fn) === '[object Object]' ? fn : { fn }); /* Path */ const n_path = (this.#path + path.trim()).replace(RGX_SLASH, '/'); /* Kind */ const n_kind = typeof config.kind === 'string' && config.kind.length ? config.kind : 'std'; /* Name */ const n_name = typeof config.name === 'string' && config.name.length ? config.name : Reflect.get(config.fn, Sym_TriFrostName) || null; /* Description */ const n_desc = typeof config.description === 'string' && config.description.length ? config.description : null; /* Timeout */ const n_timeout = 'timeout' in config ? config.timeout : this.#timeout; /* Body Parser */ const n_bodyparser = 'bodyParser' in config && isValidBodyParser(config.bodyParser) ? config.bodyParser : bodyParser; /* Normalized middleware */ const n_middleware = [ /* Inherit router mware */ ...normalizeMiddleware(this.#middleware), /* Route-specific mware */ ...normalizeMiddleware(handlers.slice(0, -1)), /* Potential config mware */ ...normalizeMiddleware((config.middleware || [])), ]; for (let i = 0; i < methods.length; i++) { const method = methods[i]; this.tree.add({ name: n_name ? (method === 'HEAD' ? 'HEAD_' : '') + n_name : method + '_' + n_path, description: n_desc, meta: isNeObject(config.meta) ? config.meta : null, method, kind: n_kind, path: n_path, fn: config.fn, middleware: n_middleware, timeout: n_timeout, bodyParser: n_bodyparser, }); } return this; } } export { Router, Router as default };