UNPKG

sap-hdb-promisfied

Version:

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

321 lines (292 loc) 11.3 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 import debugModule from 'debug' // @ts-ignore export const debug = new debugModule('hdb-promisified') import * as dotenv from 'dotenv' import * as xsenv from '@sap/xsenv' // @ts-ignore import * as hdb from 'hdb' import * as path from 'path' import { promisify } from 'util' /** * @module sap-hdb-promisfied - promises version of hdb */ const 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" export default class dbClass { /** * 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) { return new Promise((resolve, reject) => { dotenv.config({ quiet: true }) 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`) } } 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 dbClass.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 createConnection(options) { return new Promise((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((/** @type {any} */ err) => { if (err) { reject(err) return } dbClass.setSchema(options, client).then(() => resolve(client)).catch(reject) }) }) } /** * 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 */ static 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 */ static async fetchSPMetadata(db, procInfo) { 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 {any} client - HANA DB Client instance of type hdb */ constructor(client) { this.client = client // @ts-ignore this.client.promisePrepare = 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) { if (!statement.promiseExecBatch) statement.promiseExecBatch = 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) { if (!statement.promiseExec) statement.promiseExec = 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 dbClass.schemaCalc({ schema: '**CURRENT_SCHEMA**' }, this) } let procedureMetaData = await dbClass.fetchSPMetadata(this, { schema: schema, name: procedure }) let callString = '' procedureMetaData.forEach(() => { if (callString === '') { callString += `?` } else { 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 */ async execSQL(sql) { const statement = await this.preparePromisified(sql) return this.statementExecPromisified(statement, []) } /** * 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) } } }) }) } }