@soinlabs/sybase
Version:
This library provides a Node.js bridge to connect to a Sybase database. It uses a Java bridge to facilitate the connection and query execution.
471 lines (415 loc) • 13.1 kB
JavaScript
const spawn = require("child_process").spawn;
const path = require("path");
const { chain } = require("stream-chain");
const { parser } = require("stream-json");
const { streamValues } = require("stream-json/streamers/StreamValues");
function Sybase({
host,
port,
database,
username,
password,
minConnections = 1,
maxConnections = 1,
connectionTimeout = 30000,
idleTimeout = 60000,
keepaliveTime = 0,
maxLifetime = 1800000,
transactionConnections = 5,
logTiming,
pathToJavaBridge,
encoding = "utf8",
logs = false,
}) {
this.connected = false;
this.host = host;
this.port = port;
this.database = database;
this.username = username;
this.password = password;
this.minConnections = minConnections;
this.maxConnections = maxConnections;
this.connectionTimeout = connectionTimeout;
this.idleTimeout = idleTimeout;
this.keepaliveTime = keepaliveTime;
this.maxLifetime = maxLifetime;
this.transactionConnections = transactionConnections;
this.transactionConnections
this.logTiming = logTiming === true;
this.encoding = encoding;
this.logs = logs;
this.pathToJavaBridge = pathToJavaBridge;
if (this.pathToJavaBridge === undefined) {
this.pathToJavaBridge = path.resolve(
__dirname,
"..",
"JavaSybaseLink",
"dist",
"JavaSybaseLink.jar"
);
}
this.queryCount = 0;
this.transactionCount = 0;
this.currentMessages = {}; // look up msgId to message sent and call back details.
this.jsonParser = chain([
parser({ jsonStreaming: true }),
streamValues(),
(data) => data.value,
]);
/**
* Handles the SQL response from the database.
*
* @param {Object} jsonMsg - The JSON message received from the database.
*
* @example
* this.onSQLResponse({
* msgId: 1,
* result: [...],
* javaStartTime: 1633027200000,
* javaEndTime: 1633027201000,
* error: undefined
* });
*/
const onSQLResponse = function (jsonMsg) {
let err = null;
const request = this.currentMessages[jsonMsg.msgId];
delete this.currentMessages[jsonMsg.msgId];
let result = jsonMsg.result;
if (result.length === 1) {
result = result[0]; // if there is only one just return the first RS not a set of RS's
}
const currentTime = new Date().getTime();
const sendTimeMS = currentTime - jsonMsg.javaEndTime;
const hrend = process.hrtime(request.hrstart);
const javaDuration = jsonMsg.javaEndTime - jsonMsg.javaStartTime;
if (jsonMsg.error !== undefined) {
err = new Error(jsonMsg.error);
}
if (this.logTiming) {
this.log(
"Execution time (hr): %ds %dms dbTime: %dms dbSendTime: %d sql=%s",
hrend[0],
hrend[1] / 1000000,
javaDuration,
sendTimeMS,
request.sql
);
}
// Check if request.callback is a function before invoking it
if (typeof request.callback === "function") {
request.callback(err, result);
}
}.bind(this);
/**
* Handles SQL errors from the database.
*
* @param {string|Object} data - The error data received from the database.
*
* @example
* this.onSQLError("Some SQL error message");
*/
const onSQLError = function (data) {
const error = new Error(data);
const callBackFuncitons = [];
for (const k in this.currentMessages) {
if (this.currentMessages.hasOwnProperty(k)) {
callBackFuncitons.push(this.currentMessages[k].callback);
}
}
this.currentMessages = [];
callBackFuncitons.forEach(function (cb) {
cb(error);
});
}.bind(this);
const connectCore = function (callback) {
this.javaDB = spawn("java", [
"-jar",
this.pathToJavaBridge,
this.host,
this.port,
this.database,
this.username,
this.password,
this.minConnections,
this.maxConnections,
this.connectionTimeout,
this.idleTimeout,
this.keepaliveTime,
this.maxLifetime,
this.transactionConnections
]);
const handleConnection = (resolve, reject) => {
let dataStr;
this.javaDB.stdout.once("data", (data) => {
dataStr = data.toString().trim();
if (dataStr !== "connected") {
const error = new Error(`Error connecting ${dataStr}`);
if (callback) callback(error, null);
else reject(error);
return;
}
this.javaDB.stderr.removeAllListeners("data");
this.connected = true;
this.javaDB.stdout
// .setEncoding(this.encoding)
.pipe(this.jsonParser)
.on("data", (jsonMsg) => {
onSQLResponse(jsonMsg);
});
this.javaDB.stderr.on("data", (err) => {
onSQLError(err);
});
if (callback) callback(null, dataStr);
else resolve(dataStr);
});
this.javaDB.stderr.once("data", (data) => {
this.javaDB.stdout.removeAllListeners("data");
this.javaDB.kill();
const error = new Error(data.toString());
if (callback) callback(error, null);
else reject(error);
});
};
if (callback) {
handleConnection(null, null);
} else {
return new Promise(handleConnection);
}
}.bind(this);
this.connect = function (callback) {
connectCore(callback);
};
this.connectAsync = function () {
return connectCore();
};
/**
* Prepares a SQL query to be executed asynchronously.
* @param {string} sql - The SQL query to execute.
* @param {number} transactionId - The transaction ID to use, defaults to -1 (no transaction).
* @param {boolean} finishTransaction - Whether to finish the transaction after the query is done. Defaults to false.
* @param {function} callback - The callback function to execute once the query is done.
* Returns a stringified message to be sent to the Java process.
*/
const prepareQuery = function (sql, transactionId, finishTransaction = false, callback) {
if (!this.isConnected()) {
if (callback) callback(new Error("Database isn't connected."));
return null;
}
const hrstart = process.hrtime();
this.queryCount++;
const msg = {
msgId: this.queryCount,
transId: transactionId,
finishTrans: finishTransaction,
sql: sql,
sentTime: new Date().getTime(),
callback: callback,
hrstart: hrstart,
};
const strMsg = JSON.stringify(msg).replace(/[\n]/g, "\\n");
this.log(
`prepareQuery: msgId: ${msg.msgId} currentMessages: ${Object.keys(this.currentMessages).length} this.queryCount: ${this.queryCount}`
);
this.currentMessages[msg.msgId] = msg;
return strMsg;
}.bind(this);
/**
* Executes a SQL query asynchronously and returns the result via a callback.
*
* @param {string} sql - The SQL query to execute.
* @param {function} callback - The callback function to execute once the query is done.
*
* @example
* const sybase = new Sybase(...);
* sybase.query('SELECT * FROM users', (err, result) => {
* if (err) {
* console.error(err);
* return;
* }
* console.log(result);
* });
*/
this.query = function (sql, callback) {
const strMsg = prepareQuery(sql, -1, false, callback);
if (strMsg === null) return;
this.javaDB.stdin.write(strMsg + "\n");
this.log(`SQL request written: ${strMsg}`);
};
/**
* Executes a SQL query synchronously and returns the result.
*
* @param {string} sql - The SQL query to execute.
* @param {number} transactionId - The transaction ID to use, defaults to -1 (no transaction).
* @param {boolean} finishTransaction - Whether to finish the transaction after the query is done. Defaults to false.
* @returns {Object} The result of the query.
*
* @example
* const sybase = new Sybase(...);
* try {
* const result = sybase.querySync('SELECT * FROM users');
* console.log(result);
* } catch (err) {
* console.error(err);
* }
*/
this.querySync = function (sql, transactionId = -1, finishTransaction = false) {
return new Promise((resolve, reject) => {
const strMsg = prepareQuery(sql, transactionId, finishTransaction, null);
if (strMsg === null) {
reject(new Error("Database isn't connected."));
return;
}
const onResponse = (jsonMsg) => {
if (jsonMsg.msgId === this.queryCount) {
this.jsonParser.removeListener("data", onResponse);
if (jsonMsg.error !== undefined) reject(new Error(jsonMsg.error));
else resolve(jsonMsg.result);
}
};
this.jsonParser.on("data", onResponse);
try {
this.javaDB.stdin.write(strMsg + "\n");
this.log(`SQL request written: ${strMsg}`);
} catch (err) {
reject(err); // Reject if writing to stdin fails
}
});
};
/**
* Executes a series of queries within a transaction.
*
* @async
* @param {Function} queriesFunction - A function that takes the Sybase connection as an argument and returns a Promise.
* @returns {Promise<any>} - Returns a Promise that resolves with the result of the queries or rejects with an error.
*
* @example
* async function main() {
* try {
* const result = await sybase.transaction(async (connection, transactionId) => {
* const user = await connection.querySync('SELECT * FROM users WHERE id = 1', transactionId);
* await connection.querySync(`UPDATE users SET name = 'John' WHERE id = 1`, transactionId);
* return user;
* });
* console.log('Transaction successful, result:', result);
* } catch (err) {
* console.error('Transaction failed:', err);
* }
* }
*
* main();
*/
this.transaction = async function (queriesFunction) {
let result;
let error;
const transactionId = this.transactionCount++;
try {
await this.querySync("BEGIN TRANSACTION", transactionId);
result = await queriesFunction(this);
await this.querySync("COMMIT TRANSACTION", transactionId, true);
} catch (err) {
error = err;
await this.querySync("ROLLBACK TRANSACTION", transactionId, true);
}
if (error) {
throw error;
}
return result;
};
/**
* Retrieves the version of the Sybase database.
*
* @returns {Promise<string>} A promise that resolves with the version of the Sybase database.
* @throws {Error} Throws an error if the process fails or if not connected.
*
* @example
* const sybase = new Sybase(...);
* try {
* const version = await sybase.getVersion();
* console.log(`Connected to Sybase version: ${version}`);
* } catch (error) {
* console.error(`Error retrieving Sybase version: ${error.message}`);
* }
*/
this.getVersion = async function () {
if (!this.connected) {
throw new Error("Not connected to the Sybase database.");
}
try {
const result = await this.querySync("SELECT @@version AS version");
if (result && result.length && result[0].length && result[0][0].version) {
return result[0][0].version;
}
throw new Error("Failed to retrieve Sybase version.");
} catch (error) {
throw new Error(
`Failed to retrieve Sybase version due to: ${error.message}`
);
}
};
/**
* Disconnects from the database and kills the Java process.
*
* @example
* const sybase = new Sybase(...);
* sybase.disconnect();
*/
this.disconnect = function () {
this.javaDB.kill();
this.connected = false;
};
/**
* Disconnects synchronously from the database and kills the Java process.
*
* @example
* const sybase = new Sybase(...);
* await sybase.disconnectSync();
*/
this.disconnectSync = function () {
return new Promise((resolve, reject) => {
if (!this.connected) {
resolve(); // Resolve immediately if not connected
return;
}
this.javaDB.on("exit", (code) => {
this.connected = false;
if (code !== null && code !== 0 && code !== 143) {
reject(new Error(`Java process exited with code ${code}`));
} else {
resolve();
}
});
this.javaDB.on("error", (err) => {
reject(err); // Reject on error
});
this.javaDB.kill(); // Attempt to kill the Java process
});
};
/**
* Checks if the database is connected.
*
* @returns {boolean} True if connected, false otherwise.
*
* @example
* const sybase = new Sybase(...);
* const isConnected = sybase.isConnected();
* console.log(`Is connected: ${isConnected}`);
*/
this.isConnected = function () {
return this.connected;
};
/**
* Logs a message to the console if logs is enabled.
*
* @param {string} msg - The message to log.
*
* @example
* const sybase = new Sybase(...);
* sybase.log('This is a log message.');
*/
this.log = function (msg) {
if (this.logs) {
console.log(msg);
}
};
}
module.exports = Sybase;