@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
379 lines (288 loc) • 13.6 kB
JavaScript
;
var async = require('async');
//contexts
var NetworkContext = require('./utils/networkContext');
var RequestContext = require('./utils/requestContext');
//processing steps
var db = require('./db/connect');
var uriParser = require('./http/uriParser');
var xsodataFileReader = require('./model/xsodataReader');
var metadataDbReader = require('./model/metadataReader');
var oDataUriParser = require('./uri/oDataUriParser');
var uriChecks = require('./uri/checks');
var applyUriChecks = require('./uri/applyChecks');
var oDataProcessor = require('./processor/processor');
var errorProcessor = require('./processor/errorProcessor');
var ConditionalHttpHandler = require('./http/conditionalHttpHandler');
var httpRequestValidator = require('./http/validator/httpRequestValidator');
var authorizationProcessor = require('./processor/authorizationProcessor');
var tableCleanup = require('./utils/tableCleanup');
var packageJson = require('../package.json');
//Tools
var Logger = require('./utils/logger');
var configuration = require('./configuration');
var handlerConfiguration = require('./handlerConfiguration');
var JsonSerializer = require('./serializer/jsonSerializer').JsonSerializer;
var simpleRequest = require('./http/simpleHttpRequest');
var simpleResponse = require('./http/simpleHttpResponse');
var utils = require('./utils/utils');
var debugView = require('./utils/debugView');
var Measurement = require('./utils/measurement');
//Errors
var HttpErrorDebugInfo = require('./utils/errors/debugInfo');
var InternalError = require('./utils/errors/internalError');
// segmentation fault check
// var SegfaultHandler = require('segfault-handler');
// SegfaultHandler.registerHandler('crash.log');
// SegfaultHandler.causeSegfault();
global.execCounter = 0;
global.dropCounter = 0;
// process.on('exit', () => {
// console.log(`global.execCounter: ${global.execCounter}`);
// console.log(`global.dropCounter: ${global.dropCounter}`);
// });
exports.testExits = {
afterConnectDb: 1,
afterPrepareUri: 2,
afterReadService: 3,
afterLoadMetadata: 4,
afterParseODataUri: 5,
beforeProcess: 6,
afterProcess: 7,
beforeSendHandler: 8,
scopeCheckFailed: 9, // don't change this
authorizationError: 10 // don't change this
};
exports.ODataHandler = function (handlerOptions) {
this.logger = new Logger(handlerOptions.logger);
this.handlerConfiguration = new handlerConfiguration.HandlerConfiguration(handlerOptions);
this.handlerConfiguration.setLogger(this.logger);
this.modelData = {};
this._registeredCallBacks = {};
this._metadataReader = metadataDbReader;
};
exports.ODataHandler.prototype.register = function (step, callBack) {
this._registeredCallBacks[step] = callBack;
};
function checkRegisteredSteps(step, err, context, asyncDone) {
try {
var done = function (err1) {
if (err1) {
context.logger.debug('xsodata', 'Developer exit "' + step + '" returned with error:');
context.logger.debug('xsodata', err1.message);
return asyncDone(err1, context);
}
context.logger.debug('xsodata', 'Developer exit "' + step + '" returned');
return asyncDone(err, context);
};
let rCB = context.registeredCallBacks;
if (!rCB) {
if (context.batchContext && context.batchContext.parentContext) {
rCB = context.batchContext.parentContext.registeredCallBacks;
}
}
if (!rCB) {
// rCB MUST be there
return asyncDone(new Error('Internal Error'), context);
}
if (rCB[step]) {
context.logger.debug('xsodata', 'Developer exit "' + step + '" called');
return context.registeredCallBacks[step](err, context, done);
} else {
return asyncDone(err, context);
}
} catch (ex) {
return asyncDone(ex, context);
}
}
/**
* Create an Array containing all functions used to process the OData request. The array is use to feed the async.waterfall method.
* @returns { Array }
*/
exports.ODataHandler.prototype.createRequestChain = function (context) {
var ret;
if (Measurement.isActive()) {
ret = [
utils.injectContext(context),
utils.tryAndMeasure(uriParser.prepareUri, 'prepareUri'),
utils.try(checkRegisteredSteps, exports.testExits.afterPrepareUri, null),
utils.tryAndMeasure(httpRequestValidator.validate, 'validateHttpRequest'),
//utils.tryAndMeasure(db.connect, 'dbConnect'),
utils.try(checkRegisteredSteps, exports.testExits.afterConnectDb, null),
utils.try(debugView.checkParameter),
utils.tryAndMeasure(xsodataFileReader.loadXsodataConfiguration, 'loadXsodataConfiguration'),
utils.try(checkRegisteredSteps, exports.testExits.afterReadService, null),
utils.tryAndMeasure(this._metadataReader.loadModelMetadata, 'loadModelMetadata'),
utils.try(checkRegisteredSteps, exports.testExits.afterLoadMetadata, null),
utils.tryAndMeasure(oDataUriParser.parseODataUri, 'parseODataUri'),
utils.tryAndMeasure(authorizationProcessor.processAuthorization, 'processAuthorization'),
Measurement.measureAsync(applyUriChecks.bind(null, uriChecks), 'applyUriChecks'),
utils.try(checkRegisteredSteps, exports.testExits.afterParseODataUri, null),
utils.try(checkRegisteredSteps, exports.testExits.beforeProcess, null),
utils.tryAndMeasure(ConditionalHttpHandler.processConditionalRequest, 'processConditionalRequest'),
utils.tryAndMeasure(oDataProcessor.processRequest, 'processRequest'),
utils.try(checkRegisteredSteps, exports.testExits.afterProcess, null)
];
} else {
ret = [
utils.injectContext(context),
utils.try(uriParser.prepareUri),
utils.try(checkRegisteredSteps, exports.testExits.afterPrepareUri, null),
utils.try(httpRequestValidator.validate),
//utils.try(db.connect),
utils.try(checkRegisteredSteps, exports.testExits.afterConnectDb, null),
utils.try(debugView.checkParameter),
utils.try(xsodataFileReader.loadXsodataConfiguration),
utils.try(checkRegisteredSteps, exports.testExits.afterReadService, null),
utils.try(this._metadataReader.loadModelMetadata),
utils.try(checkRegisteredSteps, exports.testExits.afterLoadMetadata, null),
utils.try(oDataUriParser.parseODataUri),
utils.try(authorizationProcessor.processAuthorization),
applyUriChecks.bind(null, uriChecks),
utils.try(checkRegisteredSteps, exports.testExits.afterParseODataUri, null),
utils.try(checkRegisteredSteps, exports.testExits.beforeProcess, null),
utils.try(ConditionalHttpHandler.processConditionalRequest),
utils.try(oDataProcessor.processRequest),
utils.try(checkRegisteredSteps, exports.testExits.afterProcess, null)
];
}
return ret;
};
/**
* Processes a OData request
* @param request
* @param response
* @param { module:configuration.RequestOptions } requestOptions @see module:configuration.RequestOptions
* @param applicationDone Callback call after request is processed. Signature ( err : Object, context : Object )
*/
exports.ODataHandler.prototype.processRequest = function(request, response, requestOptions, applicationDone) {
var networkContext;
var context;
var baseMeasurement;
try {
networkContext = new NetworkContext(this.handlerConfiguration, requestOptions);
context = new RequestContext(networkContext, requestOptions || {});
context.execCounter = 0;
context.registeredCallBacks = this._registeredCallBacks;
context.callRegisteredStep = checkRegisteredSteps;
// Create the truncate/drop containers for temp tables.
// We collect all create temp table statements here and check,
// in case of an error, if all tables have been truncated/deleted
// This check will be done in saveExit(...) method.
context.networkContext.cleanSessionTruncateContainer = [];
context.networkContext.cleanSessionDropContainer = [];
context.startTimeRequest = context.logger.getStartTimeRequest();
context.logger.info('xsodata', 'process method: ' + request.method);
context.logger.info('xsodata', 'process url: ' + request.url);
context.logger.info('xsodata', 'version: ' + packageJson.version);
context.logger.info('xsodata', 'uriPrefix: ' + context.uriPrefix);
context.logger.info('xsodata', 'node version: ' + process.version);
context.logger.info('xsodata', 'network request ID: ' + context.uniqueNetworkRequestID);
context.logger.info('xsodata', 'request ID: ' + context.uniqueRequestID);
context.url = request.url;
context.request = simpleRequest.createRequest(request, context);
context.response = simpleResponse.createResponse();
context.httpResponse = response;
context.modelData = this.modelData; //inject model into context
context.userName = '';
if (request.user && request.user.name) {
if (request.user.name.familyName) {
context.userName += request.user.name.familyName;
context.userName += (request.user.name.givenName ? ', ' : '');
}
if (request.user.name.givenName) {
context.userName += request.user.name.givenName;
}
}
//Start measurement if sap-ds-debug is inside URL (rough check)
if (request.url.search('sap-ds-debug') > 0) {
Measurement.setActive(true);
context.measurements = [];
baseMeasurement = new Measurement('ODataHandler.processRequest', true);
context.measurements.push(baseMeasurement);
}
context.isAuthorized = true;
async.waterfall(
this.createRequestChain(context),
function (err) {
if (Measurement.isActive()) {
baseMeasurement.counterStop();
Measurement.setActive(false);
}
tableCleanup.assertCleanTempTables(context, function (error, context) {
return this.saveExit(error || err, context, applicationDone);
}.bind(this));
}.bind(this)
);
} catch (exception) {
var err = new InternalError('InternalError', context, exception);
tableCleanup.assertCleanTempTables(context, function (error, context) {
return this.saveExit(error || err, context, applicationDone);
}.bind(this));
}
};
exports.ODataHandler.prototype.saveExit = function (err, context, applicationDone) {
let outErr = err;
context.logger.debug('xsodata', 'saveExit');
try {
if (err) {
if (err instanceof HttpErrorDebugInfo) {
//render debug info to response
var serializer = new JsonSerializer(context, 65536, 200);
serializer.write(err.message);
serializer.flush();
} else {
//normal error processing write error to response
errorProcessor.process(context, err);
}
}
if (context.mode !== configuration.modes.development) {
outErr = null; // error is send to client already
}
//cleanup
return db.disconnect(context, () => {
return this.finish(outErr, context, applicationDone);
});
} catch (ex) {
//don't kill client
context.logger.debug('xsodata', 'ERROR ODATA: ' + err);
context.logger.debug('xsodata', 'ERROR in Error Handling : ' + ex);
if (!applicationDone) {
context.logger.error('xsodata', 'ERROR ODATA: ' + err);
context.logger.error('xsodata', 'ERROR in Error Handling : ' + ex);
context.logger.error('Not handled by application');
}
return this.finish(outErr, context, applicationDone);
}
};
exports.ODataHandler.prototype.finish = function (err, context, applicationDone) {
context.logger.silly('xsodata', 'finish');
return checkRegisteredSteps(exports.testExits.beforeSendHandler, err, context, function (err, context) {
var rTo = context.httpResponse;
var rFrom = context.response;
if (context.debugView && context.isAuthorized === true) {
debugView.writeDebugInfo(context, rFrom, rTo);
} else {
rTo.writeHead(rFrom.statusCode || 500, rFrom.headers || {});
rTo.write(rFrom.data);
}
rTo.end();
if (applicationDone) {
const ret = applicationDone(err, context);
context.logger.logRequestTime('end', context.startTimeRequest, context.url);
return ret;
} else {
context.logger.logRequestTime('end with error', context.startTimeRequest, context.url);
if (err) {
throw err;
}
}
});
};
/**
* The next callback to be called when processing is finished
*
* @callback Next
* @param {Error|null} error An error if any occured before. Cann be null
* @param {Object} context The odata context
*/