@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
JavaScript
;
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
*/