UNPKG

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