UNPKG

restifizer

Version:

Restful database-agnostic service

323 lines (280 loc) 7.91 kB
'use strict'; const path = require('path'); const _ = require('lodash'); const HTTP_STATUSES = require('http-statuses'); const RestifizerScope = require('./scope'); const DataService = require('./data-service'); const utils = require('./utils'); const { requireOptions } = utils; const ERROR_TYPE = 'restifizer'; class Controller extends DataService { constructor(options) { requireOptions(options, ['path', 'transports']); const { path, transports, ...restOptions } = options; super(restOptions); this.path = typeof path === 'string' ? [path] : path; this.transports = transports; this.actions = this.actions || []; // init this.defaultOptions = _.merge( { enabled: true, method: 'get', priority: 1, }, this.actions.default || {}, ); this.actions.select = _.merge( {}, this.actions.default, { method: ['get', 'head'], handler: 'select', name: 'select', path: '', }, this.actions.select, ); this.actions.count = _.merge( {}, this.actions.select, { method: ['get', 'head'], handler: 'count', name: 'count', path: 'count', priority: -100, }, this.actions.count, ); this.actions.selectOne = _.merge( {}, this.actions.select, { method: ['get', 'head'], handler: 'selectOne', name: 'selectOne', path: `:${this.idField}`, }, this.actions.selectOne, ); this.actions.insert = _.merge( {}, this.actions.default, { method: 'post', handler: 'insert', name: 'insert', path: '', }, this.actions.insert, ); this.actions.update = _.merge( {}, this.actions.insert, { method: 'patch', handler: 'update', name: 'update', path: `:${this.idField}`, }, this.actions.update, ); this.actions.replace = _.merge( {}, this.actions.update, { method: 'put', handler: 'replace', name: 'replace', path: `:${this.idField}`, }, this.actions.replace, ); this.actions.delete = _.merge( {}, this.actions.update, { method: 'delete', handler: 'delete', name: 'delete', path: `:${this.idField}`, }, this.actions.delete, ); delete this.actions.default; this.actions = _.mapValues( this.actions, (action, actionKey) => this.normalizeAction(action, actionKey), ); if (options.plugins) { options.plugins.forEach((pluginData) => { pluginData.plugin(this, pluginData.options); }); } } createScope(controller, transport) { const scope = new RestifizerScope(this, this.contextFactory); scope.owner = controller; scope.model = {}; scope.transport = transport; scope.transportData = {}; return scope; } bind() { if (typeof this.path === 'string') { this.path = [this.path]; } _.sortBy(this.actions, 'priority').forEach((action) => { try { if (action.enabled) { if (typeof action.method === 'string') { action.method = [action.method]; } const restifizerRouteHandler = async (scope) => { try { const data = await action.handler(scope); action.setResData(data, scope); } catch (err) { action.setResError(err, scope); } action.sendResult(scope); }; action.method.forEach((method) => { action.transports.forEach((transport) => ( transport.addRoute(this, method, this.path, action, restifizerRouteHandler) )); }); } } catch (err) { const log = this.log || console; log.error(`Cannot set route for action: ${action.name} and path ${this.path}/${action.path}`); log.error('Error', err); throw err; } }); } unbind() { if (typeof this.path === 'string') { this.path = [this.path]; } _.sortBy(this.actions, 'priority').forEach((action) => { try { if (action.enabled) { if (typeof action.method === 'string') { action.method = [action.method]; } action.method.forEach((method) => { action.transports.forEach( (transport) => transport.removeRoute(method, this.path, action), ); }); } } catch (err) { const log = this.log || console; log.error( `Cannot unset route for action: ${action.name} and path ${this.path}/${action.path}`); log.error('Error', err); throw err; } }); } setResData(data, scope, statusCode) { if (!statusCode) { if (typeof data !== 'undefined') { statusCode = (scope.newContent ? HTTP_STATUSES.CREATED.code : HTTP_STATUSES.OK.code); } else { statusCode = HTTP_STATUSES.NO_CONTENT.code; } } scope.transport.setResData(data, scope, statusCode); } setResError( err, scope, log = this.log || console, controllerParseError = this.parseError, dsParseError = this.dataSource.parseError, ) { const { type = ERROR_TYPE, httpStatus = false, error, message = httpStatus.message, details, } = err; if (!err) { err = HTTP_STATUSES.INTERNAL_SERVER_ERROR.createError(); } else if (!(err instanceof Error)) { err = new Error(err.message, err.details); } const result = { type, status: httpStatus.code, error, message, details, }; if (!httpStatus) { const parseResult = ((controllerParseError && controllerParseError(err)) || (dsParseError && dsParseError(err))); if (parseResult) { Object.assign(result, parseResult); if (result.status.code) { result.status = result.status.code; } } else { Object.assign(result, { status: HTTP_STATUSES.INTERNAL_SERVER_ERROR.code }); } } scope.transport.setResData(result, scope, result.status); log.error('Error(%d): %s: %s', result.status, result.message, result.details); // extract stack data const data = {}; try { const stacklist = err.stack.split('\n').slice(3); // Stack trace format : // http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi const s = stacklist[0]; const sp = /at\s+(.*)\s+\((.*):(\d*):(\d*)\)/gi .exec(s) || /at\s+()(.*):(\d*):(\d*)/gi.exec(s); if (sp && sp.length === 5) { [, data.method, data.path, data.line, data.pos] = sp; data.file = path.basename(data.path); data.stack = stacklist.join('\n'); } else { data.raw = err.stack; } } catch (err) { log.error('Error in error handler!'); data.raw = err.stack; } log.error(data); } sendResult(scope) { scope.transport.sendResult(scope.restfulResult, scope); } normalizeAction(action, actionKey) { if (typeof (action) !== 'object') { // interpret it as bool action = { enabled: !!action, }; } _.defaults(action, this.defaultOptions); if (action.path === undefined) { action.path = actionKey; } action.name = actionKey; action.priority = action.priority || 1; Object.setPrototypeOf(action, this); if (!_.isFunction(action.handler)) { action.handler = action[action.handler || actionKey]; } if (!_.isFunction(action.handler)) { throw new Error(`Wrong handler for ${actionKey}`); } return action; } } Controller.ACTIONS = RestifizerScope.ACTIONS; module.exports = Controller;