UNPKG

@aarconada/urserver

Version:

Basic Server definitions to develope REST API with a node + express Server

386 lines (372 loc) 24.2 kB
/** * Created by ubuntu on 16/08/18. */ 'use strict'; const _ = require('lodash'); const utils = require('./utils'); const express = require('express'); const token = require('./token'); const session = require('./session'); const response = require('./response'); const server = require('./server')(); module.exports = class endpoint { static generateInstance(configuration) { return new endpoint(configuration); } constructor(endpointConfiguration) { this.configuration = endpointConfiguration || {}; this.configuration.name = endpointConfiguration.name || 'endpointName'; this.configuration.description = endpointConfiguration.description || 'endpointDescription'; this.configuration.route = endpointConfiguration.route || 'endpointRoute'; this.configuration.method = endpointConfiguration.method || utils.method.UNDEFINED; this.configuration.callback = endpointConfiguration.callback || null; this.configuration.htmlResponse = endpointConfiguration.htmlResponse || false; this.configuration.token = endpointConfiguration.token || {}; this.configuration.token.required = endpointConfiguration.token.required || false; this.configuration.token.ignoreExpiration = endpointConfiguration.token.ignoreExpiration || false; this.configuration.help = endpointConfiguration.help || {}; this.configuration.help.enabled = endpointConfiguration.help.enabled || true; this.configuration.help.route = endpointConfiguration.help.route || '/help'; this.configuration.session = endpointConfiguration.session || {}; this.configuration.session.required = endpointConfiguration.session.required || false; this.configuration.session.allowedRoles = endpointConfiguration.session.allowedRoles || {}; this.configuration.parameters = endpointConfiguration.parameters || []; this.configuration.responses = endpointConfiguration.responses || {}; this.configuration.success = endpointConfiguration.success || {}; this.configuration.transactional = endpointConfiguration.transactional || false; }; addQueryParameter(name, dataType, required, description) { this.configuration.parameters.push({ name : name, dataType : dataType, parameterType : utils.parameterType.QUERY, required : required, description : description }); }; addBodyParameter(name, dataType, required, description) { this.configuration.parameters.push({ name : name, dataType : dataType, parameterType : utils.parameterType.BODY, required : required, description : description }); }; addFileParameter(name, required, description) { this.configuration.parameters.push({ name : name, dataType : null, parameterType : utils.parameterType.FILE, required : required, description : description }); }; documentation() { var endpointTable = '<table style="font-family: Arial, Helvetica, sans-serif;border: 1px solid #AAAAAA; width: 100%; text-align: left; border-collapse: collapse;">'; endpointTable += '<tr>'; endpointTable += '<td colspan="5" style="font-size: large;font-weight: bold;background-color: #696F89; text-align:center;border: 1px solid #AAAAAA;padding: 10px;">' + this.configuration.name + '</td>'; endpointTable += '</tr>'; endpointTable += '<tr>'; endpointTable += '<td colspan="2" style="font-weight: bold;background-color: #DDDDDD; width: 25%;border: 1px solid #AAAAAA;padding: 10px;">Description</td>'; endpointTable += '<td colspan="3" style="width: auto;border: 1px solid #AAAAAA;padding: 10px;background: #FFFFFF;">' + this.configuration.description + '</td>'; endpointTable += '</tr>'; endpointTable += '<tr>'; endpointTable += '<td colspan="2" style="font-weight: bold;background-color: #DDDDDD; width: 25%;border: 1px solid #AAAAAA;padding: 10px;">Route name</td>'; endpointTable += '<td colspan="3" style="width: auto;border: 1px solid #AAAAAA;padding: 10px;background: #FFFFFF;">' + this.configuration.route + '</td>'; endpointTable += '</tr>'; endpointTable += '<tr>'; endpointTable += '<td colspan="2" style="font-weight: bold;background-color: #DDDDDD; width: 25%;border: 1px solid #AAAAAA;padding: 10px;">Request Method</td>'; endpointTable += '<td colspan="3" style="width: auto;border: 1px solid #AAAAAA;padding: 10px;background: #FFFFFF;">' + this.configuration.method.name + '</td>'; endpointTable += '</tr>'; endpointTable += '<tr>'; endpointTable += '<td colspan="2" style="font-weight: bold;background-color: #DDDDDD; width: 25%;border: 1px solid #AAAAAA;padding: 10px;">JSON Response Method</td>'; if(this.configuration.htmlResponse) endpointTable += '<td colspan="3" style="width: auto;border: 1px solid #AAAAAA;padding: 10px;background: #FFFFFF;">False</td>'; else endpointTable += '<td colspan="3" style="width: auto;border: 1px solid #AAAAAA;padding: 10px;background: #FFFFFF;">True</td>'; endpointTable += '</tr>'; if(this.configuration.token.required) { endpointTable += '<tr>'; endpointTable += '<td colspan="2" style="font-weight: bold;background-color: #DDDDDD; width: 25%;border: 1px solid #AAAAAA;padding: 10px;">Headers</td>'; endpointTable += '<td colspan="3" style="width: auto;border: 1px solid #AAAAAA;padding: 10px;background: #FFFFFF;">'; endpointTable += '<table style="font-family: Arial, Helvetica, sans-serif;border: 1px solid #AAAAAA; width: 100%; text-align: left; border-collapse: collapse;">'; endpointTable += '<tr>'; endpointTable += '<td style="font-weight: bold;width: 200px;border: 1px solid #AAAAAA;padding: 10px;background: #DDDDDD;">Name</td>'; endpointTable += '<td style="font-weight: bold;width: auto;border: 1px solid #AAAAAA;padding: 10px;background: #DDDDDD;">Expected value</td>'; endpointTable += '</tr>'; endpointTable += '<tr>'; endpointTable += '<td style="width: 200px;border: 1px solid #AAAAAA;padding: 10px;background: #FFFFFF;">' + server.configuration.token.header + '</td>'; endpointTable += '<td style="width: auto;border: 1px solid #AAAAAA;padding: 10px;background: #FFFFFF;">Bearer Valid_Bearer_Token</td>'; endpointTable += '</tr>'; endpointTable += '</table>'; endpointTable += '</td>'; endpointTable += '</tr>'; } if(Object.keys(this.configuration.session.allowedRoles).length > 0) { endpointTable += '<tr>'; endpointTable += '<td colspan="5" style="font-weight: bold;background-color: #EEEEEE; text-align:center;border: 1px solid #AAAAAA;padding: 10px;">ALLOWED ROLES</td>'; endpointTable += '</tr>'; for(var index = 0; index < this.configuration.session.allowedRoles.length; index ++) { const currentRoleId = this.configuration.session.allowedRoles[index]; const currentRole = server.roles.getRoleById(currentRoleId); const currentRoleName = currentRole.name; endpointTable += '<tr>'; endpointTable += '<td colspan="5" style="font-weight: bold;background-color: #A4A4A4; text-align:center;border: 1px solid #BBBBBB;padding: 10px;">'; endpointTable += '<table style="font-family: Arial, Helvetica, sans-serif;border: 1px solid #AAAAAA; width: 100%; text-align: left; border-collapse: collapse;">'; endpointTable += '<tr>'; endpointTable += '<td colspan="2" style="font-weight: bold;background-color: #BABABA; text-align:center;border: 1px solid #AAAAAA;padding: 10px;">' + currentRoleName + '</td>'; endpointTable += '</tr>'; endpointTable += '</table>'; endpointTable += '</td>'; endpointTable += '</tr>'; }; } if(this.configuration.parameters.length > 0) { endpointTable += '<tr>'; endpointTable += '<td colspan="5" style="font-weight: bold;background-color: #EEEEEE; text-align:center;border: 1px solid #AAAAAA;padding: 10px;">PARAMETERS</td>'; endpointTable += '</tr>'; endpointTable += '<tr>'; endpointTable += '<td style="font-weight: bold;text-align: center;background-color: #DDDDDD; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">Name</td>'; endpointTable += '<td style="font-weight: bold;text-align: center;background-color: #DDDDDD; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">DataType</td>'; endpointTable += '<td style="font-weight: bold;text-align: center;background-color: #DDDDDD; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">Required</td>'; endpointTable += '<td style="font-weight: bold;text-align: center;background-color: #DDDDDD; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">ParameterType</td>'; endpointTable += '<td style="font-weight: bold;text-align: center;background-color: #DDDDDD; width: 50%;border: 1px solid #AAAAAA;padding: 10px;">Description</td>'; endpointTable += '</tr>'; this.configuration.parameters.sort((a, b) => (a.name > b.name) ? 1 : 0).forEach(currentParameter => { endpointTable += '<tr>'; endpointTable += '<td style="text-align: center;background-color: #FFFFFF; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">' + currentParameter.name + '</td>'; if(!currentParameter.dataType) endpointTable += '<td style="text-align: center;background-color: #FFFFFF; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">N/A</td>'; else endpointTable += '<td style="text-align: center;background-color: #FFFFFF; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">' + currentParameter.dataType.name + '</td>'; endpointTable += '<td style="text-align: center;background-color: #FFFFFF; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">' + (currentParameter.required ? 'YES' : 'NO') + '</td>'; endpointTable += '<td style="text-align: center;background-color: #FFFFFF; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">' + currentParameter.parameterType.name + '</td>'; endpointTable += '<td style="text-align: center;background-color: #FFFFFF; width: 50%;border: 1px solid #AAAAAA;padding: 10px;">' + currentParameter.description + '</td>'; endpointTable += '</tr>'; }); } endpointTable += '<tr>'; endpointTable += '<td colspan="5" style="font-weight: bold;background-color: #EEEEEE; text-align:center;border: 1px solid #AAAAAA;padding: 10px;">SUCCESS RESPONSE</td>'; endpointTable += '</tr>'; endpointTable += '<tr>'; endpointTable += '<td colspan="5" style="text-align: left;background-color: #DDDDDD; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">' const completeSuccess = response.success; completeSuccess.data = this.configuration.success; endpointTable += utils.documentation(0, completeSuccess); endpointTable += '</td>'; endpointTable += '</tr>'; if(Object.keys(this.configuration.responses).length > 0) { endpointTable += '<tr>'; endpointTable += '<td colspan="5" style="font-weight: bold;background-color: #EEEEEE; text-align:center;border: 1px solid #AAAAAA;padding: 10px;">RESPONSES</td>'; endpointTable += '</tr>'; endpointTable += '<tr>'; endpointTable += '<td style="font-weight: bold;text-align: center;background-color: #DDDDDD; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">Code</td>'; endpointTable += '<td colspan="2" style="font-weight: bold;text-align: center;background-color: #DDDDDD; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">Name</td>'; endpointTable += '<td colspan="2" style="font-weight: bold;text-align: center;background-color: #DDDDDD; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">Message</td>'; endpointTable += '</tr>'; Object.keys(this.configuration.responses).sort((a, b) => {return (+this.configuration.responses[a].code) > (+this.configuration.responses[b].code) ? 1 : 0;}).forEach(currentResponse => { endpointTable += '<tr>'; endpointTable += '<td style="text-align: center;background-color: #FFFFFF; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">' + this.configuration.responses[currentResponse].code + '</td>'; endpointTable += '<td colspan="2" style="text-align: center;background-color: #FFFFFF; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">' + currentResponse + '</td>'; endpointTable += '<td colspan="2" style="text-align: center;background-color: #FFFFFF; width: 10%;border: 1px solid #AAAAAA;padding: 10px;">' + this.configuration.responses[currentResponse].message + '</td>'; endpointTable += '</tr>'; }); } endpointTable += '</table>'; return endpointTable; } router() { const router = express.Router(); const route = router.route(this.configuration.route); switch (this.configuration.method) { case utils.method.GET: route.get(this.handleEndpoint(this)); break; case utils.method.DELETE: route.delete(this.handleEndpoint(this)); break; case utils.method.OPTIONS: route.options(this.handleEndpoint(this)); break; case utils.method.POST: route.post(this.handleEndpoint(this)); break; case utils.method.PUT: route.put(this.handleEndpoint(this)); break; default: } if(this.configuration.help.enabled) { const helpRoute = router.route(this.configuration.route + this.configuration.help.route); switch (this.configuration.method) { case utils.method.GET: helpRoute.get(this.handleEndpointHelp(this)); break; case utils.method.DELETE: helpRoute.delete(this.handleEndpointHelp(this)); break; case utils.method.OPTIONS: helpRoute.options(this.handleEndpointHelp(this)); break; case utils.method.POST: helpRoute.post(this.handleEndpointHelp(this)); break; case utils.method.PUT: helpRoute.put(this.handleEndpointHelp(this)); break; default: } } return router; }; isValid() { return this.configuration.route !== null && this.configuration.method !== utils.method.UNDEFINED && this.configuration.callback !== null; }; checkEndpointParameters(req) { this.configuration.parameters.forEach(currentParameter => { var receivedValue; switch (currentParameter.parameterType) { case utils.parameterType.BODY: receivedValue = req.body[currentParameter.name]; break; case utils.parameterType.FILE: receivedValue = req.files ? req.files[currentParameter.name] : undefined; break; case utils.parameterType.QUERY: receivedValue = req.query[currentParameter.name]; break; case utils.parameterType.PARAM: receivedValue = req.params[currentParameter.name]; break; case utils.parameterType.HEADER: receivedValue = req.headers[currentParameter.name]; break; } if(!_.isUndefined(req.currentSession) && !_.isUndefined(req.currentSession.filters[currentParameter.name])) { receivedValue = req.currentSession.filters[currentParameter.name]; } if(currentParameter.required && (_.isUndefined(receivedValue) || _.isNull(receivedValue))) { server.debug('Parameter missing:', currentParameter); throw response.missing_parameter; } else { if (currentParameter.parameterType != utils.parameterType.FILE && !_.isUndefined(receivedValue) && !_.isNull(receivedValue) && ( (currentParameter.dataType === utils.dataType.INTEGER && parseInt(receivedValue) !== +receivedValue) || (currentParameter.dataType === utils.dataType.DATE && _.isNaN(Date.parse(receivedValue))) || (currentParameter.dataType === utils.dataType.STRING && !_.isString(receivedValue)) || (currentParameter.dataType === utils.dataType.FLOAT && parseFloat(receivedValue) !== +receivedValue) || (currentParameter.dataType === utils.dataType.BOOLEAN && !utils.isBoolean(receivedValue)) || (currentParameter.dataType === utils.dataType.JSON && !utils.isJSON(receivedValue)) )) throw response.invalid_parameter_data_type; } }); }; handleEndpoint(endpointInstance) { return function(req, res, next) { if(endpointInstance.configuration.token.required) { server.debug('Checking bearer token:'); if(endpointInstance.configuration.token.ignoreExpiration) { const bearerTokenCheckResult = token.bearerTokenCheckIgnoringExpiration(req); server.debug('Checking result (Ignoring expiration):', bearerTokenCheckResult); if (bearerTokenCheckResult != response.success) return utils.sendCustomErrorMessage(res, bearerTokenCheckResult); } else { const bearerTokenCheckResult = token.bearerTokenCheck(req); server.debug('Checking result:', bearerTokenCheckResult); if (bearerTokenCheckResult != response.success) return utils.sendCustomErrorMessage(res, bearerTokenCheckResult); } } if(endpointInstance.configuration.session.required) { server.debug('Checking session'); session.sessionCheck(req).then(sessionCheckResult => { server.debug('Checking result:', sessionCheckResult); if (sessionCheckResult != response.success) return utils.sendCustomErrorMessage(res, sessionCheckResult); server.debug('Checking user role'); server.debug('User role:', req.currentSession.role); const roles = endpointInstance.configuration.session.allowedRoles; server.debug('Allowed roles:', roles); if (roles.length > 0) { const existingRole = roles.filter(currentRole => currentRole === req.currentSession.role); server.debug('Allowed roles includes user role:', existingRole.length > 0); if (existingRole.length == 0) { server.debug('The user has no permissions to execute this endpoint'); return utils.sendCustomErrorMessage(res, response.unauthorized); } } server.debug('The user has permissions to execute this endpoint'); endpointInstance.executeCallback(req, res, next, endpointInstance.configuration.responses); }).catch(err => { return utils.sendCustomErrorMessage(res, response.unauthorized); }); } else { endpointInstance.executeCallback(req, res, next, endpointInstance.configuration.responses); } } }; handleEndpointHelp(endpointInstance) { return function(req, res, next) { server.debug('Generating documentation to endpoint ' + endpointInstance.configuration.name); utils.sendHtml(res, endpointInstance.documentation()); }; }; executeCallback(req, res, next, allowedResponses) { try { server.debug('Executing callback ' + this.configuration.name); this.checkEndpointParameters(req); if (this.configuration.transactional) { return server.sequelize.model .transaction(transaction => { return this.configuration.callback(req, res, next, allowedResponses, transaction); }) .then(result => { server.debug('Transactional callback ' + this.configuration.name + ' ended'); if(this.configuration.htmlResponse) return utils.sendHtml(res, result); else return utils.sendOk(res, result); }) .catch(err => { server.debug('Transactional callback error:', err); server.logger.log('Exceptions', 'Exception on transactional callback', this.configuration.name); server.logger.log('Exceptions', err); if(this.configuration.htmlResponse) return utils.sendHtml(res, err); else return utils.sendCustomErrorMessage(res, err); }) } else { const promise = new Promise((resolve, reject) => { resolve(this.configuration.callback(req, res, next, allowedResponses)); }); Promise.resolve(promise).then(result => { server.debug('Callback ' + this.configuration.name + ' ended'); if(this.configuration.htmlResponse) return utils.sendHtml(res, result); else return utils.sendOk(res, result); }).catch(err => { server.debug('ERROR ON CALLBACK', err); server.logger.log('Exceptions', 'Exception on callback', this.configuration.name); server.logger.log('Exceptions', err); if(this.configuration.htmlResponse) return utils.sendHtml(res, err); else return utils.sendCustomErrorMessage(res, err); }); } } catch (err) { server.debug('Callback ' + this.configuration.name + ' ended with error', err); server.logger.log('Exceptions', 'Exception on callback', this.configuration.name); server.logger.log('Exceptions', err); if(this.configuration.htmlResponse) return utils.sendHtml(res, err); else return utils.sendCustomErrorMessage(res, err); } }; };