UNPKG

@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
'use strict'; 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 */