UNPKG

node-hdb-pool

Version:

SAP HANA connection pool

536 lines (465 loc) 17.8 kB
HDB connection pool ============= SAP HANA connection pool based on [node-hdb](https://github.com/SAP/node-hdb) driver and [node-pool](https://github.com/coopernurse/node-pool) generic pool. The HDB connection pool is able to connect with: * username + password * SAML bearer assertion * username + SAP HANA session cookie The connection pool can be used in **standalone mode**, or with a webserver in **HTTP mode**. ## Installation $ npm install node-hdb-pool ## Getting started This is a very simple example how to configure the pool: ```javascript var hdbPool = require('node-hdb-pool'); var hpool = hdbPool.createPool({ host: 'hostname', env: 'PROD', port: 30015, user: 'user', password: 'secret', maxPoolSize: 5 }); ``` And how to use it in **standalone mode**: ```javascript if (hpool) { var sql = '...'; // can be either DDL or DML var prepArgs = []; // prepared statement arguments hpool.exec(sql, prepArgs, function(err, result) { if (err) { // handle error return; } // use result } } ``` And how to use it in **HTTP mode**: ```javascript var express = require('express'), app = express(); app.get('/insertrow', function(req, res) { if (hpool) { var sql = 'INSERT INTO ... VALUES (?, ?, ?)'; // can be either DDL or DML var prepArgs = ['val1', 'val2', 'val3']; // prepared statement arguments hpool.exec(sql, prepArgs, req, res, function(err) { if (err) { console.log(err); res.send('ERROR'); } // if no error happened the result will be automatically sent back to the client. (in case of insert, the result contains the number of affected rows) }); } else { console.log('No pool is available!'); res.send('No pool is available!'); } }); app.listen(3000, function () { console.log('Example app listening on port 3000!'); }); ``` And how to use it in **HTTP mode** with **streaming**: ```javascript var express = require('express'), app = express(); app.get('/csvdata', function(req, res) { if (hpool) { var sql = '...'; var prepArgs = []; // prepared statement arguments hpool.stream(sql, prepArgs, req, [hdbPool.createCsvStringifier, res], function(err) { if (err) { console.log(err); res.send('ERROR'); } }); } else { console.log('No pool is available!'); res.send('No pool is available!'); } }); app.listen(3000, function () { console.log('Example app listening on port 3000!'); }); ``` And how to use it with **socket.io**: ```javascript var express = require('express'), http = require('http'), app = express(); var HttpServer = http.createServer(app).listen(3000), SocketIO = require('socket.io')(HttpServer); SocketIO.on('connection', function(sock) { var req = sock.request; // pass socket-events to the underlying request (hdb-pool registers events on the request like close, error) sock.on('disconnect', function() { req.emit('close'); }); sock.on('error', function(err) { req.emit('error', err); }); sock.on('insertrow', function(payload) { if (hpool) { var sql = 'INSERT INTO ... VALUES (?, ?, ?)'; // can be either DDL or DML var prepArgs = payload; // prepared statement arguments can be sent by the client // the 4th argument (response) needs to be null hpool.exec(sql, prepArgs, req, null, function(err) { if (err) { console.log(err); // emit message if the client needs to know about this error } // emit message if the client needs to know if the insert was successful }); } else { console.log('No pool is available!'); // emit message if the client needs to know about this error } }); }); ``` Config parameter details: * `host`: hostname. Required. * `port`: port. Required. * `env`: short symbolic name of the environment, e.g. SAND, PROD, etc. Optional, if not specified `hostname` will be used. * `user`: username for SAP HANA Database. Optional when `assertion` is used, otherwise required. * `password`: password for the SAP HANA user. Optional when `assertion` or `sessionCookie` is used, otherwise required. * `assertion`: a signed SAML bearer assertion or assertion factory function. [See more](#notes-about-saml-bearer-assertion-and-hana-session-cookie). * `sessionCookie`: an SAP HANA sessionCookie. [See more](#notes-about-saml-bearer-assertion-and-hana-session-cookie). * `maxPoolSize`: maximum number of resources to create at any given time. Optional, default to 1. * `minPoolSize`: minimum number of resources to keep in the pool at any given time. Optional, default to 0. * `idleTimeoutMillis`: max milliseconds a resource can stay idle in the pool before being removed. Optional, default to 30000. * `refreshIdle`: boolean that specifies whether idle resources at or below the min threshold should be destroyed/re-created. Optional, default to true. * `genericPoolLog`: if verbose log of [node-pool](https://github.com/coopernurse/node-pool) package should be displayed. Optional, default to false. * `resphPrefix`: Optional prefix concatenated to response header in case of HTTP mode. * `logger`: Optional. False by default. In order to enable logging, reference to a configured [tracer](https://github.com/baryon/tracer) logger should be set. * `defaultSchema`: Optional. Default schema. By default no schema is setup, so the default schema of the `user` will be used. Recommended logger setup: ``` var colors = require('colors'); var logger = require('tracer').colorConsole({ dateformat: 'HH:MM:ss', stackIndex: 1, // this is important to be setup in order to see a full log filters: { trace: colors.cyan, debug: colors.yellow, info: colors.green, warn: colors.yellow, error: [ colors.red ] } }); ``` ## Standalone mode ### Pool configuration connecting with username + password ```javascript var hdbPool = require('node-hdb-pool'); var hpool = hdbPool.createPool({ host: 'hostname', env: 'PROD', port: 30015, user: 'user', password: 'secret', maxPoolSize: 5, idleTimeoutMillis: 30000, logger: logger, genericPoolLog: true }); ``` ### Pool configuration connecting with SAML bearer assertion ```javascript var hdbPool = require('node-hdb-pool'), fs = require('fs'); // fake pid to pass to HANA in order to retain sessionCookie between restarts hdbPool.setPid(1111); var hpool = hdbPool.createPool({ host: 'hostname', env: 'PROD', port: 30015, assertion: fs.readFileSync('path/to/signed_saml_assertion.xml', 'utf-8'), maxPoolSize: 5, minPoolSize: 1, // keep at least one connection alive in order to keep HANA session cookie alive idleTimeoutMillis: 30000, refreshIdle: false, // keep alive at least _minPoolSize_ connections (if it would be true, connections are destroyed and re-created in every _idleTimeoutMillis_) logger: logger, genericPoolLog: true }); ``` ### Pool configuration connecting with SAP HANA session cookie ```javascript var hdbPool = require('node-hdb-pool'); // fake pid to pass to HANA in order to retain sessionCookie between restarts hdbPool.setPid(1111); var hpool = hdbPool.createPool({ host: 'hostname', env: 'PROD', port: 30015, user: 'user', sessionCookie: 'sessionCookie' maxPoolSize: 5, minPoolSize: 1, // keep at least one connection alive in order to keep HANA session cookie alive idleTimeoutMillis: 30000, refreshIdle: false, // keep alive at least _minPoolSize_ connections (if it would be true, connections are destroyed and re-created in every _idleTimeoutMillis_) logger: logger, genericPoolLog: true }); ``` ### Usage of the pool in **standalone mode** ```javascript if (hpool) { hpool.exec('SELECT system_id SID, host, version FROM M_DATABASE', [], function(err, rows) { if (err || rows.length !== 1) { console.error('Error while getting system information on ' + env); } else { var env = hpool.env; console.log('System information of ' + env + ':'); console.log(rows[0]); } }); } ``` ## HTTP mode ### Pool configuration connecting with username + password ```javascript var hdbPool = require('node-hdb-pool'); var hpool = hdbPool.createPool({ host: 'hostname', env: 'PROD', port: 30015, user: 'user', password: 'secret', maxPoolSize: 5, idleTimeoutMillis: 30000, logger: logger, resphPrefix: 'x-customprefix' }); ``` ### Pool configuration connecting with SAML bearer assertion ```javascript var hdbPool = require('node-hdb-pool'), fs = require('fs'); // fake pid to pass to HANA in order to retain sessionCookie between restarts hdbPool.setPid(1111); var hpool = hdbPool.createPool({ host: 'hostname', env: 'PROD', port: 30015, assertion: fs.readFileSync('path/to/signed_saml_assertion.xml', 'utf-8'), maxPoolSize: 5, minPoolSize: 1, // keep at least one connection alive in order to keep HANA sessionCookie alive idleTimeoutMillis: 30000, refreshIdle: false, // keep alive at least _minPoolSize_ connections (if it would be true, connections are destroyed and recreated in every _idleTimeoutMillis_) logger: logger, resphPrefix: 'x-customprefix' }); ``` ### Usage of the pool in **HTTP mode** Default response headers: * **query-time**: actual execution time in milliseconds (it does not include waiting time for free resource) * **waiting-time**: time in milliseconds spent on waiting for free resource ```javascript // setup a server var express = require('express'), app = express(); var sql = 'SELECT system_id SID, host, version FROM M_DATABASE'; var prepArgs = []; // prepared statement arguments app.get('/csvsysinfo', function(req, res) { if (hpool) { hpool.exec(sql, prepArgs, req, res, function(err) { if (err) { console.log(err); res.send('ERROR'); } }); } else { console.log('No pool is available!'); res.send('No pool is available!'); } }); app.listen(3000, function () { console.log('Example app listening on port 3000!'); }); // NOTE: in case of minPoolSize > 0, pool needs to be killed manually setTimeout(function() { if (hpool && hpool.pool) { hpool.pool.min = 0; hpool.pool.drain(function() { hpool.pool.destroyAllNow(); console.log('Pool destroyed on ' + hpool.env); hpool = null; // clean the reference }); } }, 1*60*60*1000); // kill the pool periodically in every hour ``` ### Usage of the pool in **HTTP mode** with **streaming** Available streaming outputs: * **CSV** (Comma Separated Values): hdbPool.createCsvStringifier * **SSV** (Semi-colon Separated Values): hdbPool.createSsvStringifier * **JSON**: hdbPool.createJSONStringifier Default response headers: * **query-time**: actual execution time in milliseconds (it does not include waiting time for free resource) * **waiting-time**: time in milliseconds spent on waiting for free resource ```javascript // setup a server var express = require('express'), app = express(); var sql = 'SELECT system_id SID, host, version FROM M_DATABASE'; var prepArgs = []; // prepared statement arguments app.get('/csvsysinfo', function(req, res) { if (hpool) { hpool.stream(sql, prepArgs, req, [hdbPool.createCsvStringifier, res], function(err) { if (err) { console.log(err); res.send('ERROR'); } }); } else { console.log('No pool is available!'); res.send('No pool is available!'); } }); app.listen(3000, function () { console.log('Example app listening on port 3000!'); }); // NOTE: in case of minPoolSize > 0, pool needs to be killed manually setTimeout(function() { if (hpool && hpool.pool) { hpool.pool.min = 0; hpool.pool.drain(function() { hpool.pool.destroyAllNow(); console.log('Pool destroyed on ' + hpool.env); hpool = null; // clean the reference }); } }, 1*60*60*1000); // kill the pool periodically in every hour ``` ### Configure the pool with SessionStore In case of **HTTP mode** if `minPoolSize > 0` and SAML bearer assertion is used for connecting to SAP HANA, the acquired HANA session cookie can be stored in the **sessionStore** by simply passing the `req` as second argument. ``` var hpool = hdbPool.createPool(poolOpts, req); ``` ## Notes about SAML bearer assertion and HANA session cookie * SAML bearer assertion has to be signed otherwise SAP HANA does not accept it * in SAP HANA, one needs to setup the public key-pair of the private key which was used to sign the assertion * an example SAML bearer assertion is ```xml <saml2:Assertion ID="_2068a633d1c4b262c8fdc768a1d53346" IssueInstant="2016-01-14T09:38:49.437Z" Version="2.0" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <saml2:Issuer></saml2:Issuer> <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:SignedInfo> <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/> <ds:Reference URI=""> <ds:Transforms> <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/> <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"> <ec:InclusiveNamespaces PrefixList="xsd" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/> </ds:Transform> </ds:Transforms> <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/> <ds:DigestValue /> </ds:Reference> </ds:SignedInfo> <ds:SignatureValue /> <ds:KeyInfo> <ds:X509Data /> </ds:KeyInfo> </ds:Signature> <saml2:Subject xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"> <saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" NameQualifier="http://idp.example.org/" SPNameQualifier="sp1">USERNAME</saml2:NameID> <saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> <saml2:SubjectConfirmationData Address="127.0.0.1" InResponseTo="_aeefedf622b60752a564" NotOnOrAfter="2016-01-14T09:43:49.487Z" Recipient="http://sp1.example.org/callback"/> </saml2:SubjectConfirmation> </saml2:Subject> <saml2:Conditions NotBefore="2010-01-01T00:00:00Z" NotOnOrAfter="2017-01-01T00:00:00Z" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"> </saml2:Conditions> <saml2:AuthnStatement AuthnInstant="2016-01-14T09:37:07.687Z" SessionIndex="_eab1b512b946bcfaeb0c9b5dd85bd275" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"> <saml2:SubjectLocality Address="example.org"/> <saml2:AuthnContext> <saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef> </saml2:AuthnContext> </saml2:AuthnStatement> </saml2:Assertion> ``` * once connection with a valid SAML bearer assertion is done, the user gets a HANA session cookie * HANA session cookies can be seen in _SESSION_COOKIES_ system view * HANA session cookies without alive connections are wiped out periodically * quick reconnect with a previously used cookie usually works * a HANA session cookie is valid only for a given user from a given host+PID (OS process ID) * if the OS process is restarted and it gets a new PID, a previously used session cookie cannot be used to connect to HANA * to avoid this behaviour, use the `hdbPool.setPid(XXXX);` to setup a static PID before calling `hdbPool.createPool(...)` ## Query metadata Metadata for a query can be retrieved by `hpool.meta()`. ```javascript if (hpool) { hpool.meta('SELECT system_id SID, host, version FROM M_DATABASE', [], function(err, meta) { if (err) { console.error('Error while getting metadata ' + env); } else { console.log(meta); } }); } ``` ## Running tests For the tests a database connection has to be established to an SAP HANA database. Therefore you need to copy the configuration `test/config.js.tpl` file into `test/config.js` and change it with your connection data. An example config file can be seen here: ```javascript var fs = required('fs'); var config = { host: 'example.host.com', port: 30015, user: 'username', password: 'secret', assertion: fs.readFileSync('path/to/signed_saml_assertion.xml', 'utf-8') }; exports.config = config; ``` To run the unit tests go to the `node-hdb-pool` folder and type: $ npm test ## Todo * Implement periodic reconnection-logic if database host becomes unavailable * Improve documentation * Improve test coverage ## FAQ ### Calling stored procedures and sending messages Example stored procedure to insert a user into a table: ```sql CREATE PROCEDURE INSERTUSER ( p_username varchar(255) ) SQL SECURITY DEFINER AS BEGIN SEQUENTIAL EXECUTION DECLARE STATUS_MESSAGE CONDITION FOR SQL_ERROR_CODE 10001; DECLARE EXIT HANDLER FOR STATUS_MESSAGE SELECT ::SQL_ERROR_CODE AS "errno", ::SQL_ERROR_MESSAGE AS "errmsg" FROM DUMMY; DECLARE USER_EXISTS INT = 0; SELECT COUNT(USER_NAME) INTO USER_EXISTS FROM USERS WHERE USER_NAME = :p_username; IF (:USER_EXISTS > 0) THEN SIGNAL STATUS_MESSAGE SET MESSAGE_TEXT = '{"status": "error", "code": 1, "message": "user already exist"}'; ELSE INSERT INTO USERS VALUES (:p_username); SIGNAL STATUS_MESSAGE SET MESSAGE_TEXT = \'{"status": "success", "code": 0, "message": "user created"}'; END IF; END; ``` Example code how to call the above procedure: ```javascript hpool.exec('CALL INSERTUSER(?)', ['newuser'], function(err, rows) { if (err || !rows || rows.status) { // handle error } else { if (rows.status === 'error') { // handle error from stored proc } else if (rows.status === 'success') { // handle success from stored proc } } }); ```