UNPKG

nodecaf

Version:

Nodecaf is a light framework for developing RESTful Apps in a quick and convenient manner.

189 lines (142 loc) 5.6 kB
const METHODS = [ 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS' ]; const cookieSignature = require('cookie-signature'); const { handleError } = require('./error'); const { getDecoratedRes } = require('./response'); const { getDecoratedBody } = require('./body'); const { cors } = require('./cors'); function pathToRegexp(path){ const r = { params: [] }; let regexp = ''; path.split('/').forEach(seg => { if(!seg) return; if(seg[0] == ':'){ r.params.push(seg.substr(1)); regexp += '\\/([\\%\\w\\d\\-\\._~]+)'; return; } regexp += '\\/' + seg; }); r.regexp = new RegExp('^' + regexp + '$'); return r; } function matchRoute(method, path, params){ // this => API const route = method + ' ' + path; if(route in this.static) return this.static[route]; if(this.dynamic[method]) for(const r of this.dynamic[method]){ const match = r.regexp.exec(path); if(match){ r.params.forEach( (p, i) => params[p] = decodeURIComponent(match[i + 1])); return r.handler; } } return this.fallbackRoute ?? false; } function parseSignedCookies(cconf, input){ if(!cconf?.secret) return; for(const key in input.cookies){ const val = cookieSignature.unsign(input.cookies[key], cconf?.secret); if(val){ input.signedCookies[key] = val; delete input.cookies[key]; } } } module.exports = class API { constructor(context, spec){ this.routes = {}; this.static = {}; this.dynamic = {}; this.context = context; this.fallbackRoute = null; // Generate HTTP verb shortcut route methods const proxy = METHODS.reduce( (o, m) => ({ ...o, [m.toLowerCase()]: this.addEndpoint.bind(this, m.toLowerCase()) }), {}); // Needed because it's not possible to call a function called 'delete' proxy.del = this.addEndpoint.bind(this, 'delete'); proxy.all = handler => this.setFallbackRoute(handler); if(spec) this.context.log.warn('`api` is deprecated. This option will be dropped on `v0.14.0`. Use `routes` instead.'); spec?.call(context, proxy); } setFallbackRoute(handler){ if(this.fallbackRoute) throw new Error('Route for \'ALL\' is already defined'); if(typeof handler != 'function') throw new TypeError(`'ALL' handler must be a function. Found '${typeof handler}'`); this.fallbackRoute = handler.bind(this.context); } addEndpoint(method, path, handler){ const m = method.toUpperCase(); const route = m + ' ' + path; const dup = route in this.routes; if(dup) throw new Error('Route for \'' + route + '\' is already defined'); if(typeof handler != 'function') throw new TypeError(`'${route}' handler must be a function. Found '${typeof handler}'`); const nmHandler = handler.bind(this.context); this.routes[route] = true; if(path.indexOf('/:') < 0 && path.indexOf('*') < 0) return this.static[route] = nmHandler; this.dynamic[m] = this.dynamic[m] || []; const { regexp, params } = pathToRegexp(path); this.dynamic[m].push({ regexp, handler: nmHandler, params }); } async trigger(method, path, input){ method = method.toUpperCase(); const params = {}; const app = this.context; const signedCookies = {}; const signedCookiesProxy = new Proxy(signedCookies, { get(target, p){ app.log.warn('`signedCookies` is deprecated. This option will be dropped on `v0.14.0`. Signed cookies must be handled manually instead.'); return target[p]; } }); input = { conf: app.conf, cookies: {}, headers: {}, query: {}, ...input, params, log: app.log, signedCookies: signedCookiesProxy, method, path }; input.ip = input.headers.forwarded ?? input.headers['x-forwarded-for'] ?? input.body?.socket?.remoteAddress ?? '::1'; input.call = (fn, ...args) => fn.call(app, { ...app.global, ...input }, ...args); input.keep = (k, v) => input[k] = v; const reqInfo = { method, path, host: input.headers?.host, agent: input.headers?.['user-agent'], type: 'request', msg: 'Received ' + method + ' request to ' + path }; app.log.debug(reqInfo); const res = input.res = getDecoratedRes(input, reqInfo); cors(app.conf.cors, method, input.headers, res); if(res.finished) return Promise.resolve(res); const handler = matchRoute.call(this, method, path, params); try{ res.notFound(!handler); input.body = getDecoratedBody(input, app); if(app._autoParseBody && !input.websocket) input.body = await input.body.parse(); parseSignedCookies(app.conf.cookie, input); await handler({ ...app.global, ...input }); } catch(err){ handleError(err, input); } return res.ended; } }