UNPKG

sap-hdb-promisfied

Version:

Promise wrapper for hdb without any dependency to @sap/hana-client

338 lines (312 loc) 12.2 kB
/*eslint no-console: 0, no-unused-vars: 0, no-shadow: 0, new-cap: 0, dot-notation:0, no-use-before-define:0 */ /*eslint-env node, es6 */ // @ts-check "use strict" // @ts-ignore const debug = require('debug')('hdb-promisified') const dotenv = require('dotenv') const xsenv = require("@sap/xsenv") // @ts-ignore const hdb = require("hdb") const path = require("path") /** * @module sap-hdb-promisfied - promises version of hdb */ module.exports = class { /** * Create Database Connection From Environment * @param {string} [envFile] - Override with a specific Environment File * @returns {Promise<any>} - HANA Client instance of hdb */ static createConnectionFromEnv(envFile) { const setSchema = this.setSchema return new Promise((resolve, reject) => { dotenv.config() xsenv.loadEnv(envFile) /** @type any */ let options = '' try { if (!process.env.TARGET_CONTAINER) { options = xsenv.getServices({ hana: { tag: 'hana' } }) } else { options = xsenv.getServices({ hana: { name: process.env.TARGET_CONTAINER } }) } } catch (error) { try { options = xsenv.getServices({ hana: { tag: 'hana', plan: "hdi-shared" } }) } catch (error) { console.error(error) throw new Error(`Missing or badly formatted ${envFile}. No HANA configuration can be read or processed`) } } //console.log(options) if(options && options.hana && options.hana.encrypt){ options.hana.useTLS = true } debug(`Connection Options`, options) let client = hdb.createClient(options.hana) client.on('error', (/** @type {any} */ err) => { console.log(`In Client On Error`) reject(err) }) debug(`Client Ready State`, client.readyState) client.connect(async (/** @type {any} */ err) => { if (err) { reject(err) } await setSchema(options, client) resolve(client) }) }) } /** * Create Database Connection with specific connection options in format expected by hdb * @param {any} options - Input options or parameters * @returns {Promise<any>} - HANA Client instance of hdb */ static async createConnection(options) { const setSchema = this.setSchema return new Promise(async function (resolve, reject) { if(options && options.hana && options.hana.encrypt){ options.hana.useTLS = true } debug(`Connection Options`, options) let client = hdb.createClient(options.hana) client.on('error', (/** @type {any} */ err) => { reject(err) }) client.connect(async (/** @type {any} */ err) => { if (err) { reject(err) } await setSchema(options, client) resolve(client) }) }) } /** * Set default schema based upon connection parameters * @param {any} options - Input options or parameters * @param {any} client - HANA Client instance of hdb */ static async setSchema(options, client){ if(options.hana){ if(options.hana.schema){ client.exec(`SET SCHEMA ${options.hana.schema}`, function () { return }) } } } /** * Determine default env file name and location * @param {any} options - Input options or parameters * @returns string - default env file name and path */ static resolveEnv(options) { let file = 'default-env.json' if (options && options.hasOwnProperty('admin') && options.admin) { file = 'default-env-admin.json' } let envFile = path.resolve(process.cwd(), file) debug(`Environment File ${envFile}`) return envFile } /** * Calculation the current schema name * @param {any} options - Input options or parameters * @param {any} db - HANA Client instance of hdb * @returns {Promise<string>} - Schema */ async schemaCalc(options, db) { let schema = '' if (options.schema === '**CURRENT_SCHEMA**') { let schemaResults = await db.execSQL(`SELECT CURRENT_SCHEMA FROM DUMMY`) schema = schemaResults[0].CURRENT_SCHEMA } else if (options.schema === '*') { schema = "%" } else { schema = options.schema } debug(`Schema ${schema}`) return schema } /** * Load Metadata of a Stored Procedure * @param {any} db - HANA Client instance of hdb * @param {any} procInfo - Details of Schema/Stored Procedure to Lookup * @returns {Promise<any>} - Result Set */ async fetchSPMetadata(db, procInfo) { var sqlProcedureMetadata = "SELECT \ PARAMS.PARAMETER_NAME, \ PARAMS.DATA_TYPE_NAME, \ PARAMS.PARAMETER_TYPE, \ PARAMS.HAS_DEFAULT_VALUE, \ PARAMS.IS_INPLACE_TYPE, \ PARAMS.TABLE_TYPE_SCHEMA, \ PARAMS.TABLE_TYPE_NAME, \ \ CASE WHEN SYNONYMS.OBJECT_NAME IS NULL THEN 'FALSE' ELSE 'TRUE' END AS IS_TABLE_TYPE_SYNONYM, \ IFNULL(SYNONYMS.OBJECT_SCHEMA, '') AS OBJECT_SCHEMA, \ IFNULL(SYNONYMS.OBJECT_NAME, '') AS OBJECT_NAME \ \ FROM SYS.PROCEDURE_PARAMETERS AS PARAMS \ LEFT JOIN SYS.SYNONYMS AS SYNONYMS \ ON SYNONYMS.SCHEMA_NAME = PARAMS.TABLE_TYPE_SCHEMA AND SYNONYMS.SYNONYM_NAME = PARAMS.TABLE_TYPE_NAME \ WHERE PARAMS.SCHEMA_NAME = ? AND PARAMS.PROCEDURE_NAME = ? \ ORDER BY PARAMS.POSITION" return await db.statementExecPromisified(await db.preparePromisified(sqlProcedureMetadata), [procInfo.schema, procInfo.name]) } /** * Calculation Object name from wildcards * @param {string} name - DB object name * @returns {string} - final object name */ static objectName(name) { if (typeof name === "undefined" || name === null || name === '*') { name = "%" } else { name += "%" } return name } /** * @constructor * @param {object} client - HANA DB Client instance of type hdb */ constructor(client) { this.client = client // @ts-ignore this.util = require("util") this.client.promisePrepare = this.util.promisify(this.client.prepare) } /** * Destroy Client */ destroyClient() { // @ts-ignore if (!this.client.hadError && this.client.readyState !== 'closed') { // @ts-ignore this.client.end() } } /** * Destroy Client */ validateClient() { // @ts-ignore return (!this.client.hadError && this.client.readyState === 'connected') } /** * Prepare database statement * @param {string} query - database query * @returns {any} - prepared statement object */ preparePromisified(query) { debug(`Query:`, query) // @ts-ignore return this.client.promisePrepare(query) } /** * Execute DB Statement in Batch * @param {any} statement - prepared statement object * @param {any} parameters - query parameters * @returns {Promise<any>} - resultset */ statementExecBatchPromisified(statement, parameters) { statement.promiseExecBatch = this.util.promisify(statement.execBatch) return statement.promiseExecBatch(parameters) } /** * Execute DB Statement * @param {any} statement - prepared statement object * @param {any} parameters - query parameters * @returns {Promise<any>} - resultset */ statementExecPromisified(statement, parameters) { statement.promiseExec = this.util.promisify(statement.exec) return statement.promiseExec(parameters) } /** * Load stored procedure and return proxy function * @param {string} schema - Schema name can be null * @param {string} procedure - DB procedure name * @returns {Promise<function>} - proxy function */ async loadProcedurePromisified(schema, procedure) { if(!schema){ schema = await this.schemaCalc({schema: '**CURRENT_SCHEMA**'}, this) } let procedureMetaData = await this.fetchSPMetadata(this, {schema: schema, name: procedure}) let callString = '' procedureMetaData.forEach(() => { if(callString === ''){ callString += `?` }else { callString += `,?` } }) if(callString === ''){ callString += `?` } //console.log(callString) return this.preparePromisified(`CALL ${schema}."${procedure}"(${callString})`) } /** * Execute single SQL Statement and directly return result set * @param {string} sql - SQL Statement * @returns {Promise<any>} - result set object */ execSQL(sql) { return new Promise((resolve, reject) => { this.preparePromisified(sql) .then((/** @type {any} */ statement) => { this.statementExecPromisified(statement, []) .then(results => { resolve(results) }) .catch(err => { reject(err) }); }) .catch((/** @type {any} */ err) => { reject(err) }) }) } /** * Call Database Procedure * @param {any} storedProc - stored procedure proxy function * @param {any} inputParams - input parameters for the stored procedure * @returns */ callProcedurePromisified(storedProc, inputParams) { return new Promise((resolve, reject) => { // @ts-ignore storedProc.exec(inputParams, (/** @type {any} */ error, /** @type {any} */ outputScalar, /** @type {string | any[]} */ ...results) => { // storedProc(inputParams, (error, outputScalar, ...results) => { if (error) { reject(error) } else { if (results.length < 2) { resolve({ outputScalar: outputScalar, results: results[0] }) } else { let output = {}; output.outputScalar = outputScalar; for (let i = 0; i < results.length; i++) { // @ts-ignore output[`results${i}`] = results[i] } resolve(output) } } }) }) } }