@aarconada/urserver
Version:
Basic Server definitions to develope REST API with a node + express Server
386 lines (372 loc) • 24.2 kB
JavaScript
/**
* 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);
}
};
};