@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
345 lines (291 loc) • 11.2 kB
JavaScript
;
/**
* Add <db> attribute to <context>
* Uses <context>.<handlerConfiguration.dbConfiguration>
* <db> = {
* client : hdb-client
* }
*/
const async = require('async');
const utils = require('./../utils/utils');
const Measurement = require('./../utils/measurement');
// load the db module lazy
let hdb_module = null;
let hana_client_module = null;
const InternalError = require('./../utils/errors/internalError');
const ApplicationError = require('./../utils/errors/applicationError');
const dbVersionChecks = require('./dbVersionChecks');
const tableCleanup = require('./../utils/tableCleanup');
const SqlError = require('../utils/errors/sqlError');
/**
* Executes an sql command on the current db connection
*
* @param sql
* @param context
* @param asyncDone
*/
function execSQL(sql, context, asyncDone) {
context.logger.silly('connect - db', 'exec sql ' + sql);
context.db.client.exec(sql, function (err) {
if (err) {
return asyncDone(new SqlError(context, err), context);
}
return asyncDone(null, context);
});
}
function loadDbVersion(context, asyncDone) {
if (!context.gModel) {
return asyncDone(null, context);
}
const version = context.gModel.getDbVersion();
if (version !== undefined || !context.gModel) {
return asyncDone(null, context);
}
return context.db.client.exec('select version from "SYS"."M_DATABASE"', function (err, rows) {
if (err) {
return asyncDone(new SqlError(context, err), context);
}
if (rows && rows[0] && rows[0].VERSION) {
const version = rows[0].VERSION;
context.logger.info('xsodata', 'db version: ' + version);
context.gModel.setDbVersion(version);
} else {
context.gModel.setDbVersion(null); // don't try reload
}
return asyncDone(null, context);
});
}
function setHanaCloudContext(context, asyncDone) {
return context.db.client.exec('select version from "SYS"."M_DATABASE"', function (err, rows) {
if (err) {
return asyncDone(new SqlError(context, err), context);
}
if (rows && rows[0] && rows[0].VERSION) {
const version = rows[0].VERSION;
context.logger.info('xsodata', 'db version: ' + version);
context.db.isHanaCloudDb = isHanaCloudDb(context, rows[0].VERSION);
context.logger.info('xsodata', `isHanaCloud: ${context.db.isHanaCloudDb} (DB version based)`);
} else {
// 06/2022:
// Currently all SQL from Hana Cloud work also on Hana Service, i.e. take that version as default;
// could be changed in the future, i.e. using the context setting of 'context.db.isHanaCloudDb'
context.db.isHanaCloudDb = true;
context.logger.info('xsodata', `isHanaCloud: ${context.db.isHanaCloudDb} (default)`);
}
return asyncDone(null, context);
});
}
function isHanaCloudDb(context, version) {
const regex = /^([0-9])+\..*$/;
let result = version.match(regex);
if (result && result[1]) {
return (result[1] >= 4);
} else {
context.logger.error(`DB Version parsing failed: ${version}`);
return true; // default: Hana Cloud
}
}
function getDbClientType(dbClient) {
if (dbClient._settings) {
return 'hdb';
}
return 'hana-client';
}
/**
* Sets the schema and isolation level on the current db connection
*
* @param context
* @param asyncDone
*/
function prepareConnection(context, asyncDone) {
context.logger.debug('connect - db', 'prepare connection via ' + (getDbClientType(context.db.client) === 'hdb' ? 'hdb' : 'hana-client'));
let executeList = [
utils.injectContext(context)
];
// Set the default schema
if (context.defaultSchema) {
executeList.push(utils.tryAndMeasure(execSQL, 'SET SCHEMA ' + context.defaultSchema, 'execSQL (default schema)'));
}
// Set the isolation level
executeList.push(utils.tryAndMeasure(execSQL, 'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ', 'execSQL (isolation level)'));
executeList.push(loadDbVersion);
// Set Hana Cloud indicator in context
executeList.push(setHanaCloudContext);
async.waterfall(
executeList,
function (err) {
context.db.client.setAutoCommit(false);
asyncDone(err, context);
}
);
}
/**
* Calls the callback function used by the application to open the cb connection only on demand
*
* @param context
* @param asyncDone
* @private
*/
exports._openConnection = function (context, asyncDone) {
context.logger.debug('connect - db', '_openConnection');
context.db.opener(context.handlerConfiguration.dbConfiguration, function (err, newClient) {
if (err) {
return asyncDone(new ApplicationError('Internal error occurred', err), context);
}
context.db.client = newClient;
return asyncDone(null, context);
});
};
/**
* Connects to a database, there are 3 way to define which database
* 1. By providing a hdbClient via requestOptions.dbClient
* 2. By providing a callback function to open the connection via requestOptions.dbOpenCB (and optionally requestOptions.dbCloseCB)
* 3. By providing the dbConfiguration which is passed to the hdb module
*
* @param context
* @param asyncDone
* @returns {*}
* @private
*/
function _connectInternal(context, asyncDone) {
let client;
let child;
context.logger.debug('connect - db', '_connectInternal');
context.db.isExternalHandledConnection = false;
if (context.db) {
if (context.db.client) {
//use existing db client
context.logger.debug('connect - db', 'use existing db client');
context.db.isExternalHandledConnection = true; // indicator for temp table truncation/drop
return prepareConnection(context, asyncDone);
} else if (context.db.opener) {
context.logger.debug('connect - db', 'use opener');
context.db.isExternalHandledConnection = true; // indicator for temp table truncation/drop
return exports._openConnection(context, (err) => {
if (err) {
return asyncDone(err, context);
}
return prepareConnection(context, asyncDone);
});
}
}
try {
let db_module = null;
if (context.handlerConfiguration.dbClient === 'hdb') {
if (!hdb_module) {
hdb_module = require('hdb');
}
db_module = hdb_module;
context.logger.silly('connect - db', 'use hdb.createClient');
} else {
if (!hana_client_module) {
hana_client_module = require('@sap/hana-client');
}
db_module = hana_client_module;
context.logger.silly('connect - db', 'use hana-client');
}
client = Measurement.measureSync(db_module.createClient, context.handlerConfiguration.dbConfiguration, 'hdb.createClient');
} catch (exception) {
context.logger.error('connect - db', 'createClient failed: ' + JSON.stringify(exception));
return asyncDone(exception, context);
}
if (getDbClientType(client) === 'hdb') {
client.on('error', function (err) {
context.logger.error('connect - db', 'client error event: ' + JSON.stringify(err));
return asyncDone(new InternalError(err, context), context);
});
}
if (Measurement.isActive()) {
/* perform an explicit measurement:
* extract the current parent from the global stack
* assign a new child: implicitly starts a counter, is used later to stop the counter
*/
let parent = Measurement.getRunningMeasurement();
child = parent.newChild('client.connect');
child.counterStart();
}
client.connect(function (err) {
context.logger.info('connect - db', 'client.connect done');
if (Measurement.isActive()) {
child.counterStop();
}
if (err) {
context.logger.error('connect - db', 'connect failed: ' + JSON.stringify(err));
client.end();
return asyncDone(new InternalError(err, context), context);
}
context.db.client = client;
context.db.openedConnection = true;
context.logger.info('connect - db', 'connection opened');
return prepareConnection(context, asyncDone);
});
}
/**
* Open and configures the db connection
*
* @param context
* @param asyncDone
* @returns {*}
*/
exports.connect = function (context, asyncDone) {
context.db = context.db || {};
if (context.db.client && (context.db.connectionInitialized === true)) {
context.logger.info('connect - db', 'connect already done');
return asyncDone(null, context);
}
context.logger.info('connect - db', 'connect');
return _connectInternal(context, function (err) {
if (err) {
context.logger.debug('connect - db', 'connect error ' + err.toString ? err.toString() : JSON.stringify(err));
return asyncDone(err, context);
}
context.logger.debug('connect - db', 'connection is usable');
context.db.connectionInitialized = true;
context.logger.debug('connect - context setting - Is Hana Cloud used?', `${context.db.isHanaCloudDb}`);
return asyncDone(null, context);
});
};
/**
* Ends the db connection
*
* @param {Object} context
* @param {Function} cb Callback
*/
exports.disconnect = function (context, cb) {
context.logger.info('connect - db', 'disconnect');
context.db = context.db || {};
if (context.db.openedConnection) {
context.logger.info('connect - db', 'disconnect done');
if (context.db.client) {
context.db.client.end((err) => {
if (err) {
context.logger.error('connect - db', `disconnect failed (callback): ${err}`);
}
context.logger.info('connect - db', 'disconnect done (callback)');
});
}
context.db.openedConnection = false;
return cb();
} else if (context.db.opener && context.db.closer) {
context.logger.info('connect - db', 'calling db closer');
return context.db.closer(context.db.client, () => {
context.logger.info('connect - db', 'db closer called');
return cb();
});
}
return cb();
};
exports.dbRollback = (context, dbClient, cb) => {
context.logger.info('connect - db', 'dbRollback');
return dbClient.rollback(function (errDB) {
if (errDB) {
//Error in rollback ends whole request
return cb(errDB);
}
if (dbVersionChecks.shouldNotCleanTmpTables(context)) {
context.logger.info('connect - db', 'no tmp table delete due to rollback');
tableCleanup.emptyLists(context);
}
return cb();
});
};