UNPKG

@sap/xsodata

Version:

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

397 lines (355 loc) 12.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 = /^(\d)+\..*$/; 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(); }); };