/** * Router * * @class * @constructor * @public */ class Router { constructor() { /** * Router Array * * @protected * @type {Route[]} */ this.routes = [] /** * Global Handlers */ this.globalHandlers = [] /** * Debug Mode * * @protected * @type {boolean} */ this.debugMode = false /** * CORS Config * * @protected * @type {RouterCorsConfig} */ this.corsConfig = {} } /** * Route Object * * @typedef Route * @property {string} method HTTP request method * @property {string} url URL String * @property {RouterHandler[]} handlers Array of handler functions */ /** * Router Context * * @typedef RouterContext * @property {Object} env Environment * @property {RouterRequest} req Request Object * @property {RouterResponse} res Response Object * @property {RouterNext} next Next Handler */ /** * Request Object * * @typedef RouterRequest * @property {string} method HTTP request method * @property {Object} params Object containing all parameters defined in the url string * @property {Object} query Object containing all query parameters * @property {Headers} headers Request headers object * @property {Object | string} body Only available if method is `POST`, `PUT`, `PATCH` or `DELETE`. Contains either the received body string or a parsed object if valid JSON was sent. * @property {Object} cf object containing custom Cloudflare properties. (https://developers.cloudflare.com/workers/examples/accessing-the-cloudflare-object) */ /** * Response Object * * @typedef RouterResponse * @property {Headers} headers Response headers object * @property {number} status Return status code (default: `204`) * @property {Object | string} body Either an `object` (will be converted to JSON) or a string * @property {Response} raw A response object that is to be returned, this will void all other res properties and return this as is. */ /** * Next Function * * @callback RouterNext * @returns {Promise} */ /** * Handler Function * * @callback RouterHandler * @param {RouterContext} ctx */ /** * Register global handler * * @param {RouterHandler} handler */ use(handlers) { this.globalHandlers.push(handlers) return this } /** * Register CONNECT route * * @param {string} url * @param {...RouterHandler} handlers * @returns {Router} */ connect(url, ...handlers) { return this.register('CONNECT', url, handlers) } /** * Register DELETE route * * @param {string} url * @param {...RouterHandler} handlers * @returns {Router} */ delete(url, ...handlers) { return this.register('DELETE', url, handlers) } /** * Register GET route * * @param {string} url * @param {...RouterHandler} handlers * @returns {Router} */ get(url, ...handlers) { return this.register('GET', url, handlers) } /** * Register HEAD route * * @param {string} url * @param {...RouterHandler} handlers * @returns {Router} */ head(url, ...handlers) { return this.register('HEAD', url, handlers) } /** * Register OPTIONS route * * @param {string} url * @param {...RouterHandler} handlers * @returns {Router} */ options(url, ...handlers) { return this.register('OPTIONS', url, handlers) } /** * Register PATCH route * * @param {string} url * @param {...RouterHandler} handlers * @returns {Router} */ patch(url, ...handlers) { return this.register('PATCH', url, handlers) } /** * Register POST route * * @param {string} url * @param {...RouterHandler} handlers * @returns {Router} */ post(url, ...handlers) { return this.register('POST', url, handlers) } /** * Register PUT route * * @param {string} url * @param {...RouterHandler} handlers * @returns {Router} */ put(url, ...handlers) { return this.register('PUT', url, handlers) } /** * Register TRACE route * * @param {string} url * @param {...RouterHandler} handlers * @returns {Router} */ trace(url, ...handlers) { return this.register('TRACE', url, handlers) } /** * Register route, ignoring method * * @param {string} url * @param {...RouterHandler} handlers * @returns {Router} */ any(url, ...handlers) { return this.register('*', url, handlers) } /** * Register route, ignoring method * * @deprecated since version 1.0.2, use .any(url, ...handlers) instead. * @param {string} url * @param {...RouterHandler} handlers * @returns {Router} */ all(url, ...handlers) { console.warn('WARNING: This function is deprecated and will be removed in a future release, please use .any(url, ...handlers) instead.') return this.any(url, handlers) } /** * Debug Mode * * @param {boolean} [state=true] Whether to turn on or off debug mode (default: true) */ debug(state = true) { this.debugMode = state } /** * CORS Config * * @typedef RouterCorsConfig * @property {string} allowOrigin Access-Control-Allow-Origin (default: `*`) * @property {string} allowMethods Access-Control-Allow-Methods (default: `*`) * @property {string} allowHeaders Access-Control-Allow-Headers (default: `*`) * @property {number} maxAge Access-Control-Max-Age (default: `86400`) * @property {number} optionsSuccessStatus Return status code for OPTIONS request (default: `204`) */ /** * Enable CORS support * * @param {RouterCorsConfig} config * @returns {Router} */ cors(config) { config = config || {} this.corsConfig = { allowOrigin: config.allowOrigin || '*', allowMethods: config.allowMethods || '*', allowHeaders: config.allowHeaders || '*, Authorization', maxAge: config.maxAge || 86400, optionsSuccessStatus: config.optionsSuccessStatus || 204 } return this } /** * Register route * * @private * @param {string} method HTTP request method * @param {string} url URL String * @param {RouterHandler[]} handlers Arrar of handler functions * @returns {Router} */ register(method, url, handlers) { this.routes.push({ method, url, handlers }) return this } /** * Get Route by request * * @private * @param {Request} request * @returns {Route | undefined} */ getRoute(request) { const url = new URL(request.url) const pathArr = url.pathname.split('/').filter(i => i) return this.routes.find(r => { const routeArr = r.url.split('/').filter(i => i) if (![request.method, '*'].includes(r.method) || routeArr.length !== pathArr.length) return false const params = {} for (let i = 0; i < routeArr.length; i++) { if (routeArr[i] !== pathArr[i] && routeArr[i][0] !== ':') return false if (routeArr[i][0] === ':') params[routeArr[i].substring(1)] = pathArr[i] } request.params = params const query = {} for (const [k, v] of url.searchParams.entries()) { query[k] = v } request.query = query return true }) || this.routes.find(r => r.url === '*' && [request.method, '*'].includes(r.method)) } /** * Handle requests * * @param {any} env * @param {Request} request * @param {any=} extend * @returns {Response} */ async handle(env, request, extend = {}) { try { if (request instanceof Event) { request = request.request console.warn("Warning: Using `event` on `router.handle()` is deprecated and might go away in future versions, please use `event.request` instead.") } const req = { ...extend, method: request.method, headers: request.headers, url: request.url, params: [], query: {}, body: '' } if (req.method === 'OPTIONS' && Object.keys(this.corsConfig).length) { return new Response(null, { headers: { 'Access-Control-Allow-Origin': this.corsConfig.allowOrigin, 'Access-Control-Allow-Methods': this.corsConfig.allowMethods, 'Access-Control-Allow-Headers': this.corsConfig.allowHeaders, 'Access-Control-Max-Age': this.corsConfig.maxAge }, status: this.corsConfig.optionsSuccessStatus }) } if (['POST', 'PUT', 'PATCH'].includes(req.method)) { if (req.headers.has('Content-Type') && req.headers.get('Content-Type').includes('json')) { try { req.body = await request.json() } catch { req.body = {} } } else { try { req.body = await request.text() } catch { req.body = '' } } } const route = this.getRoute(req) if (!route) { return new Response(this.debugMode ? 'Route not found!' : null, { status: 404 }) } const res = { headers: new Headers() } if (Object.keys(this.corsConfig).length) { res.headers.set('Access-Control-Allow-Origin', this.corsConfig.allowOrigin) res.headers.set('Access-Control-Allow-Methods', this.corsConfig.allowMethods) res.headers.set('Access-Control-Allow-Headers', this.corsConfig.allowHeaders) res.headers.set('Access-Control-Max-Age', this.corsConfig.maxAge) } const handlers = [...this.globalHandlers, ...route.handlers] let prevIndex = -1 const runner = async index => { if (index === prevIndex) throw new Error('next() called multiple times') prevIndex = index if (typeof handlers[index] === 'function') await handlers[index]({ env, req, res, next: async () => await runner(index + 1) }) } await runner(0) if (typeof res.body === 'object') { if (!res.headers.has('Content-Type')) res.headers.set('Content-Type', 'application/json') res.body = JSON.stringify(res.body) } if (res.raw) { return res.raw } const resOpts = { status: res.status || (res.body ? 200 : 204), headers: res.headers } if (res.webSocket) { resOpts.webSocket = res.webSocket } return new Response([101, 204, 205, 304].includes(resOpts.status) ? null : res.body, resOpts) } catch(err) { console.error(err) return new Response(this.debugMode ? err.stack : '', { status: 500 }) } } } module.exports = Router