@sap/hdbext
Version:
Hana-client extension library and utility functions for using SAP HANA in node.js
212 lines (176 loc) • 6.3 kB
JavaScript
;
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;
}