UNPKG

@sap/xsodata

Version:

Expose data from a HANA database as OData V2 service with help of .xsodata files.

497 lines (416 loc) 15.3 kB
'use strict'; const async = require('async'); //contexts const NetworkContext = require('./utils/networkContext'); const RequestContext = require('./utils/requestContext'); //processing steps const db = require('./db/connect'); const uriParser = require('./http/uriParser'); const xsodataFileReader = require('./model/xsodataReader'); const metadataDbReader = require('./model/metadataReader'); const oDataUriParser = require('./uri/oDataUriParser'); const uriChecks = require('./uri/checks'); const applyUriChecks = require('./uri/applyChecks'); const oDataProcessor = require('./processor/processor'); const errorProcessor = require('./processor/errorProcessor'); const ConditionalHttpHandler = require('./http/conditionalHttpHandler'); const httpRequestValidator = require('./http/validator/httpRequestValidator'); const authorizationProcessor = require('./processor/authorizationProcessor'); const tableCleanup = require('./utils/tableCleanup'); const packageJson = require('../package.json'); //Tools const Logger = require('./utils/logger'); const configuration = require('./configuration'); const handlerConfiguration = require('./handlerConfiguration'); const JsonSerializer = require('./serializer/jsonSerializer').JsonSerializer; const simpleRequest = require('./http/simpleHttpRequest'); const simpleResponse = require('./http/simpleHttpResponse'); const utils = require('./utils/utils'); const debugView = require('./utils/debugView'); const Measurement = require('./utils/measurement'); //Errors const HttpErrorDebugInfo = require('./utils/errors/debugInfo'); const InternalError = require('./utils/errors/internalError'); global.execCounter = 0; global.dropCounter = 0; 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 { const 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) { let 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 ) { let networkContext; let context; let 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) { const 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 const 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) { const rTo = context.httpResponse; const 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 */