UNPKG

@sap/hdbext

Version:

Hana-client extension library and utility functions for using SAP HANA in node.js

212 lines (176 loc) 6.3 kB
'use strict'; var async = require('async'); var VError = require('verror'); var format = require('util').format; var safeSql = require('../safe-sql'); var debug = require('debug')('hdbext:sp'); var wrapParams = require('./wrap-params'); var isInput = require('./utils').isInput; var isInputTable = require('./utils').isInputTable; var hasDefaultValue = require('./utils').hasDefaultValue; var TempTable = require('./TempTable'); var dbStream = require('@sap/hana-client/extension/Stream'); module.exports = StoredProcedure; function StoredProcedure(client, schema, name, metadata) { this._client = client; this._schema = schema; this._name = name; this._metadata = metadata; this._hasInputTables = metadata.some(isInputTable); this._hasParamWithDefaultValue = metadata.some(hasDefaultValue); } StoredProcedure.prototype.exec = function () { var args = Array.prototype.slice.call(arguments); var callback = args.pop(); var params = wrapParams(this._metadata, args); if (!this._hasInputTables) { this._execNoInputTables(params, callback); } else { this._execWithInputTables(params, callback); } }; StoredProcedure.prototype._execNoInputTables = function (params, callback) { var paramPlaceholders = []; this._metadata.forEach(function (paramMeta) { if (isInput(paramMeta) && hasDefaultValue(paramMeta) && !params.has(paramMeta.PARAMETER_NAME)) { return; } paramPlaceholders.push(paramPlaceHolder(paramMeta)); }); var sqlCallStatement = this._buildCallStatement(paramPlaceholders.join(', ')); debug(sqlCallStatement); dbStream.createProcStatement(this._client, sqlCallStatement, function (err, statement) { if (err) { return callback(err); } execute(statement, params.getAll(), function () { callback.apply(null, arguments); }); }); }; StoredProcedure.prototype._execWithInputTables = function (params, callback) { if (params.empty()) { return callback(new Error('Stored procedure ' + this._name + ' expects input parameters')); } try { var setup = setupProcedureCall(this, params); var tempTables = setup.tempTables; var sqlCallStatement = this._buildCallStatement(setup.paramPlaceholders.join(', ')); debug(sqlCallStatement); } catch (err) { return callback(err); } async.waterfall([ createTempTables.bind(null, tempTables), dbStream.createProcStatement.bind(null, this._client, sqlCallStatement) ], function (err, statement) { if (err) { dropTempTablesInBackground(tempTables); return callback(err); } execute(statement, setup.input, function () { dropTempTablesInBackground(tempTables); callback.apply(null, arguments); }); }); }; StoredProcedure.prototype._buildCallStatement = function (paramPlaceholders) { return format('CALL %s.%s(%s)', safeSql.identifier(this._schema), safeSql.identifier(this._name), paramPlaceholders); }; function setupProcedureCall(sp, params) { var paramPlaceholders = []; var tempTables = []; var input = []; sp._metadata.forEach(function (paramMeta) { if (!isInput(paramMeta)) { paramPlaceholders.push(paramPlaceHolder(paramMeta)); return; } if (hasDefaultValue(paramMeta) && !params.has(paramMeta.PARAMETER_NAME)) { return; } var paramName = paramMeta.PARAMETER_NAME; var paramValue = params.get(paramName); if (!isInputTable(paramMeta)) { paramPlaceholders.push(paramPlaceHolder(paramMeta)); input.push(paramValue); return; } if (paramValue && typeof paramValue === 'object' && Object.prototype.hasOwnProperty.call(paramValue, 'table')) { paramPlaceholders.push(paramPlaceHolder(paramMeta, processTableDescriptor(paramValue))); return; } if (!Array.isArray(paramValue)) { throw new Error('Table parameter ' + paramName + ' is expected to be an array of objects or an object with "table" (mandatory) and "schema" (optional) properties'); } var tempTable = new TempTable(sp, paramMeta, paramValue); tempTables.push(tempTable); paramPlaceholders.push(paramPlaceHolder(paramMeta, safeSql.identifier(tempTable.getName()))); }); return { paramPlaceholders: paramPlaceholders, tempTables: tempTables, input: input }; } function createTempTables(tempTables, cb) { // async.each -> async.eachLimit // The same as each but runs a maximum of limit async operations at a time. async.eachLimit(tempTables, 1, function (tempTable, eachCb) { tempTable.create(function (err) { if (err) { // error objects returned from @sap/hana-client are not // instances of Error, so we need to wrap them for VError var e = new Error(err.message); if ('code' in err) { e.code = err.code; } return eachCb(new VError(e, 'Could not create temporary table: %s', tempTable.getName())); } eachCb(); }); }, cb); } function dropTempTablesInBackground(tempTables) { tempTables.forEach(function (tempTable) { tempTable.dropInBackground(); }); } function paramPlaceHolder(paramMeta, value) { return paramMeta.PARAMETER_NAME + ' => ' + (value || '?'); } function processTableDescriptor(descriptor) { var table = safeSql.identifier(descriptor.table); var schema = descriptor.schema; if (!schema) { return table; } return safeSql.identifier(schema) + '.' + table; } function execute(statement, params, cb) { statement.execute(params, function () { var procResults = Array.prototype.slice.call(arguments); statement.drop(function (err) { if (err) { debug('Could not drop proc statement:', err); } var results = normalizeResults(statement, procResults); cb.apply(null, results); }); }); } function normalizeResults(statement, results) { var err = results[0]; if (err) { return [err]; } var indexOfFirstTable = 2; var columnsInfos = statement.getColumnInfo(); if (columnsInfos.length === 0) { return Array.prototype.slice.call(results, 0, indexOfFirstTable); } columnsInfos.forEach(function (columnInfo, columnInfoIndex) { Object.defineProperty(results[indexOfFirstTable + columnInfoIndex], 'columnInfo', { value: columnInfo }); }); return results; }