sql-client
Version:
A dirt-simple SQL client abstraction (currently) supporting PostgreSQL, MySQL and SQLite.
629 lines (580 loc) • 23.4 kB
JavaScript
// Generated by CoffeeScript 2.6.0
(function() {
var SQLClient, SQLClientPool, Transaction,
splice = [].splice,
indexOf = [].indexOf;
SQLClient = require('./sql-client').SQLClient;
SQLClientPool = (function() {
class SQLClientPool {
create_transaction() {
return new Transaction(this);
}
create(callback) {
var client;
client = new SQLClient(...this.sql_options, this.factory);
return client.connect((err) => {
return callback(err, client);
});
}
activate(client, callback) {
return callback(null, client);
}
validate(client, callback) {
var ref;
if ((client != null) && ((client.pooled_at == null) || (((ref = this.pool_options) != null ? ref.max_age : void 0) == null) || ((Date.now() - client.pooled_at) < this.pool_options.max_age))) {
return callback(null, true, client);
} else {
return callback(null, false, client);
}
}
passivate(client, callback) {
return callback(null, client);
}
destroy(client, callback) {
if (client != null) {
return client.disconnect(callback);
} else {
return callback();
}
}
constructor(...sql_options) {
var factory, ref;
ref = sql_options, [...sql_options] = ref, [factory] = splice.call(sql_options, -1);
this.create_transaction = this.create_transaction.bind(this);
this.create = this.create.bind(this);
this.activate = this.activate.bind(this);
this.validate = this.validate.bind(this);
this.passivate = this.passivate.bind(this);
this.destroy = this.destroy.bind(this);
this.open = this.open.bind(this);
this.close = this.close.bind(this);
// borrow, execute, return, callback
this.execute = this.execute.bind(this);
this.borrow = this.borrow.bind(this);
this.return = this.return.bind(this);
// CONFIGUATION OPTIONS:
// - `min_idle` - minimum number of idle connections in an "empty" pool
// - `max_idle` - maximum number of idle connections in a "full" pool
// - `max_active` - maximum number of connections active at one time
// - `when_exhausted` - what to do when max_active is reached (`grow`,`block`,`fail`),
// - `max_wait` - when `when_exhausted` is `block` max time (in millis) to wait before failure, use < 0 for no maximum
// - `wait_interval` - when `when_exhausted` is `BLOCK`, amount of time (in millis) to wait before rechecking if connections are available
// - `max_retries` - number of times to attempt to create another new connection when a newly created connection is invalid; when `null` no retry attempts will be made; when < 0 an infinite number of retries will be attempted
// - `retry_interval` - when `max_retries` is > 0, amount of time (in millis) to wait before retrying
// - `max_age` - when a positive integer, connections that have been idle for `max_age` milliseconds will be considered invalid and eligable for eviction
// - `eviction_run_interval` - when a positive integer, the number of milliseconds between eviction runs; during an eviction run idle connections will be tested for validity and if invalid, evicted from the pool
// - `eviction_run_length` - when a positive integer, the maxiumum number of connections to examine per eviction run (when not set, all idle connections will be evamined during each eviction run)
// - `unref_eviction_runner` - unless `false`, `unref` (https://nodejs.org/api/timers.html#timers_unref) will be called on the eviction run interval timer
this._config = this._config.bind(this);
this._reconfig = this._reconfig.bind(this);
this._evict = this._evict.bind(this);
this._eviction_run = this._eviction_run.bind(this);
this._prepopulate = this._prepopulate.bind(this);
this._borrow_n = this._borrow_n.bind(this);
this._return_n = this._return_n.bind(this);
this._activate_and_validate_or_destroy = this._activate_and_validate_or_destroy.bind(this);
this.sql_options = sql_options;
this.factory = factory;
this.open(function() {
return void 0;
});
}
open(opts, callback) {
if ((callback == null) && typeof opts === 'function') {
callback = opts;
opts = null;
}
if (typeof callback !== 'function') {
throw new Error(this.MESSAGES.INVALID_ARGUMENT);
} else {
return this._config(opts, (err) => {
this.pool_is_open = true;
return typeof callback === "function" ? callback(err) : void 0;
});
}
}
close(callback) {
if ((callback != null) && typeof callback !== 'function') {
throw new Error(this.MESSAGES.INVALID_ARGUMENT);
} else {
this.pool_is_open = false;
if (this.eviction_runner != null) {
this.eviction_runner.ref();
clearTimeout(this.eviction_runner);
this.eviction_runner = null;
}
if (this.pool.length > 0) {
return this.destroy(this.pool.shift(), () => {
return this.close(callback);
});
} else {
if (this.active > 0) {
return typeof callback === "function" ? callback(new Error(this.MESSAGES.CLOSED_WITH_ACTIVE)) : void 0;
} else {
return typeof callback === "function" ? callback() : void 0;
}
}
}
}
execute(sql, bindvars, callback) {
if (typeof bindvars === 'function' && (callback == null)) {
callback = bindvars;
bindvars = [];
}
return this.borrow((err, client) => {
if (err != null) {
return callback(err);
} else if (client == null) {
return callback(new Error("non-null client expected"));
} else {
return client.execute(sql, bindvars, (...response) => {
return this.return(client, () => {
return callback(...response);
});
});
}
});
}
borrow(callback, blocked_since = null, retry_count = 0) {
var client;
if (typeof callback !== 'function') {
throw new Error(this.MESSAGES.INVALID_ARGUMENT);
} else {
if (!this.pool_is_open) {
return callback(new Error(this.MESSAGES.POOL_NOT_OPEN));
} else {
if (this.active >= this.pool_options.max_active && this.pool_options.when_exhausted === 'fail') {
return callback(new Error(this.MESSAGES.EXHAUSTED));
} else if (this.active >= this.pool_options.max_active && this.pool_options.when_exhausted === 'block') {
if ((blocked_since != null) && (Date.now() - blocked_since) >= this.pool_options.max_wait) {
return callback(new Error(this.MESSAGES.MAX_WAIT));
} else {
if (blocked_since == null) {
blocked_since = Date.now();
}
return setTimeout((() => {
return this.borrow(callback, blocked_since, retry_count);
}), this.pool_options.wait_interval);
}
} else if (this.pool.length > 0) {
client = this.pool.shift();
return this._activate_and_validate_or_destroy(client, (err, valid, client) => {
if (err != null) {
return callback(err);
} else if (!valid) {
return this.borrow(callback);
} else {
client.pooled_at = null;
client.borrowed_at = Date.now();
this.active++;
return callback(null, client);
}
});
} else {
return this.create((err, client) => {
if (err != null) {
return callback(err);
} else {
return this._activate_and_validate_or_destroy(client, (err, valid, client) => {
var ref;
if (err != null) {
return callback(err);
} else if (!valid) {
if ((this.pool_options.max_retries != null) && this.pool_options.max_retries > retry_count) {
return setTimeout((() => {
return this.borrow(callback, blocked_since, retry_count + 1);
}), (ref = this.pool_options.retry_interval) != null ? ref : 0);
} else {
return callback(new Error(this.MESSAGES.INVALID));
}
} else {
client.pooled_at = null;
client.borrowed_at = Date.now();
this.active++;
return callback(null, client);
}
});
}
});
}
}
}
}
return(client, callback) {
if (((client == null) && (callback == null)) || ((callback != null) && typeof callback !== 'function')) {
throw new Error(this.MESSAGES.INVALID_ARGUMENT);
} else if (client == null) {
return callback(new Error(this.MESSAGES.NULL_RETURNED));
} else if (this.active <= 0) {
return callback(new Error(this.MESSAGES.TOO_MANY_RETURNED));
} else {
this.returned++;
this.active--;
if (client != null) {
return this.passivate(client, (err, client) => {
if (err) {
return callback(err);
} else {
client.pooled_at = Date.now();
client.borrowed_at = null;
if (this.pool.length >= this.pool_options.max_idle) {
return this.destroy(client, callback);
} else {
this.pool.push(client);
return typeof callback === "function" ? callback() : void 0;
}
}
});
}
}
}
_config(opts, callback) {
var i, keys, len, new_opts, prop, ref, ref1;
if (opts == null) {
opts = {};
}
new_opts = this._clone(this.pool_options);
keys = Object.keys(opts);
ref = ['min_idle', 'max_idle', 'max_active', 'when_exhausted', 'max_wait', 'wait_interval', 'max_age', 'eviction_run_interval', 'eviction_run_length', 'unref_eviction_runner', 'max_retries', 'retry_interval'];
for (i = 0, len = ref.length; i < len; i++) {
prop = ref[i];
if (indexOf.call(keys, prop) >= 0) {
new_opts[prop] = opts[prop];
}
}
if ((new_opts.max_retries != null) && (typeof new_opts.max_retries !== 'number' || new_opts.max_retries <= 0)) {
new_opts.max_retries = null;
}
if (new_opts.max_retries != null) {
if ((new_opts.retry_interval != null) && (typeof new_opts.retry_interval !== 'number' || new_opts.retry_interval <= 0)) {
new_opts.retry_interval = 0;
} else if (new_opts.retry_interval == null) {
new_opts.retry_interval = this.DEFAULT_RETRY_INTERVAL;
}
}
if (typeof new_opts.max_idle === 'number' && new_opts.max_idle < 0) {
new_opts.max_idle = Number.MAX_VALUE;
} else if (typeof new_opts.max_idle !== 'number') {
new_opts.max_idle = 0;
}
if (typeof new_opts.min_idle !== 'number' || new_opts.min_idle < 0) {
new_opts.min_idle = 0;
}
if (new_opts.min_idle > new_opts.max_idle) {
new_opts.min_idle = new_opts.max_idle;
}
if (typeof new_opts.max_active !== 'number' || new_opts.max_active < 0) {
new_opts.max_active = Number.MAX_VALUE;
}
if (typeof new_opts.max_wait !== 'number' || new_opts.max_wait < 0) {
new_opts.max_wait = Number.MAX_VALUE;
}
if (typeof new_opts.wait_interval !== 'number' || new_opts.wait_interval < 0) {
new_opts.wait_interval = this.DEFAULT_WAIT_INTERVAL;
}
if ((ref1 = new_opts.when_exhausted) !== 'grow' && ref1 !== 'block' && ref1 !== 'fail') {
new_opts.when_exhausted = 'grow';
}
if ((new_opts.max_age == null) || new_opts.max_age < 0) {
new_opts.max_age = Number.MAX_VALUE;
}
this.pool_options = new_opts;
return this._reconfig(callback);
}
_reconfig(callback) {
if (this.eviction_runner != null) {
this.eviction_runner.ref();
clearTimeout(this.eviction_runner);
this.eviction_runner = null;
}
return this._evict((err) => {
if (err != null) {
return typeof callback === "function" ? callback(err) : void 0;
} else {
return this._prepopulate((...x) => {
var ref, ref1;
if ((((ref = this.pool_options) != null ? ref.eviction_run_interval : void 0) != null) && this.pool_options.eviction_run_interval > 0) {
this.eviction_runner = setInterval(this._eviction_run, this.pool_options.eviction_run_interval);
if (((ref1 = this.pool_options) != null ? ref1.unref_eviction_runner : void 0) !== false) {
this.eviction_runner.unref();
}
}
return callback(...x);
});
}
});
}
_evict(callback) {
return this._eviction_run(0, callback);
}
_eviction_run(num_to_check, callback) {
var client, new_pool, num_checked;
if (typeof num_to_check === 'function' && (callback == null)) {
callback = num_to_check;
num_to_check = null;
}
new_pool = [];
num_checked = 0;
while (this.pool.length > 0) {
client = this.pool.shift();
if ((num_to_check == null) || num_to_check <= 0 || num_checked < num_to_check) {
num_checked += 1;
if (new_pool.length < this.pool_options.max_idle && this._is_valid(client)) {
new_pool.push(client);
} else {
client.disconnect();
}
} else {
new_pool.push(client);
}
}
this.pool = new_pool;
return typeof callback === "function" ? callback() : void 0;
}
_prepopulate(callback) {
var n;
n = this.pool_options.min_idle - this.pool.length;
if (n > 0) {
return this._borrow_n(n, [], (err, borrowed) => {
if (err != null) {
return callback(err);
} else {
return this._return_n(borrowed, callback);
}
});
} else {
return callback();
}
}
_borrow_n(n, borrowed, callback) {
if (typeof n !== 'number' || !Array.isArray(borrowed)) {
return callback(new Error(this.MESSAGES.INTERNAL_ERROR));
} else {
if (n > borrowed.length) {
return this.borrow((err, client) => {
if (client != null) {
borrowed.push(client);
}
if (err != null) {
return this._return_n(borrowed, () => {
return callback(err);
});
} else {
return this._borrow_n(n, borrowed, callback);
}
});
} else {
return callback(null, borrowed);
}
}
}
_return_n(borrowed, callback) {
var client;
if (!Array.isArray(borrowed)) {
return callback(new Error(this.MESSAGES.INTERNAL_ERROR));
} else if (borrowed.length > 0) {
client = borrowed.shift();
return this.return(client, () => {
return this._return_n(borrowed, callback);
});
} else {
return callback(null);
}
}
_activate_and_validate_or_destroy(client, callback) {
return this.activate(client, (err, client) => {
if (err != null) {
if (client != null) {
return this.destroy(client, () => {
return callback(err, false, null);
});
} else {
return callback(err, false, null);
}
} else {
return this.validate(client, (err, valid, client) => {
if (err != null) {
if (client != null) {
return this.destroy(client, () => {
return callback(err, false, null);
});
} else {
return callback(err, false, null);
}
} else if (!valid) {
return this.destroy(client, () => {
return callback(null, false, null);
});
} else {
return callback(null, true, client);
}
});
}
});
}
_clone(map) {
var cloned, n, v;
if (map == null) {
return null;
} else {
cloned = {};
for (n in map) {
v = map[n];
cloned[n] = v;
}
return cloned;
}
}
};
SQLClientPool.prototype.MESSAGES = {
POOL_NOT_OPEN: "The pool is not open; please call 'open' before invoking this method.",
TOO_MANY_RETURNED: "More clients have been returned to the pool than were active. A client may have been returned twice.",
EXHAUSTED: "The maxiumum number of clients are already active; cannot obtain a new client.",
MAX_WAIT: "The maxiumum number of clients are already active and the maximum wait time has been exceeded; cannot obtain a new client.",
INVALID: "Unable to create a valid client.",
INTERNAL_ERROR: "Internal error.",
INVALID_ARGUMENT: "Invalid argument.",
NULL_RETURNED: "A null object was returned.",
CLOSED_WITH_ACTIVE: "The pool was closed, but some clients remain active (were never returned)."
};
SQLClientPool.prototype.DEFAULT_WAIT_INTERVAL = 50;
SQLClientPool.prototype.DEFAULT_RETRY_INTERVAL = 50;
SQLClientPool.prototype.pool = [];
SQLClientPool.prototype.pool_options = {};
SQLClientPool.prototype.pool_is_open = false;
SQLClientPool.prototype.borrowed = 0;
SQLClientPool.prototype.returned = 0;
SQLClientPool.prototype.active = 0;
SQLClientPool.prototype.eviction_runner = null;
return SQLClientPool;
}).call(this);
Transaction = class Transaction {
constructor(pool_or_client) {
this.begin = this.begin.bind(this);
this._end = this._end.bind(this);
this.rollback = this.rollback.bind(this);
this.commit = this.commit.bind(this);
this.rollback_and_end = this.rollback_and_end.bind(this);
this.commit_and_end = this.commit_and_end.bind(this);
this._borrow = this._borrow.bind(this);
this.execute = this.execute.bind(this);
if ((pool_or_client != null) && pool_or_client instanceof SQLClientPool) {
this._pool = pool_or_client;
} else if ((pool_or_client != null) && pool_or_client instanceof SQLClient) {
this._client = pool_or_client;
} else {
throw new Error(`Expected SQLClientPool or SQLClient instance, found ${pool_or_client}`);
}
}
begin(callback) {
return this.execute("BEGIN", [], (...r) => {
this._began = true;
return callback(...r);
});
}
_end(command, options, callback) {
var maybe_return;
if (typeof options === "function" && (callback == null)) {
[callback, options] = [options, callback];
}
maybe_return = (cb) => {
var ref;
this._ended = true;
if ((this._pool != null) && (this._client != null)) {
return this._pool.return(this._client, () => {
this._pool = null;
this._client = null;
return typeof cb === "function" ? cb() : void 0;
});
} else if ((this._client != null) && ((ref = this._client) != null ? ref._auto_disconnect_on_transaction_end : void 0)) {
this._client.disconnect(options, cb);
return this._client = null;
} else {
return typeof cb === "function" ? cb() : void 0;
}
};
if (this._began) {
return this.execute(command, [], (err) => {
return maybe_return(() => {
return callback(err);
});
});
} else {
return maybe_return(() => {
return callback();
});
}
}
rollback(options, callback) {
if (typeof options === "function" && (callback == null)) {
[callback, options] = [options, callback];
}
if (this._ended) {
return callback(new Error("This transaction has already been closed."));
} else {
return this._end("ROLLBACK", options, callback);
}
}
commit(options, callback) {
if (typeof options === "function" && (callback == null)) {
[callback, options] = [options, callback];
}
if (this._ended) {
return callback(new Error("This transaction has already been closed."));
} else {
return this._end("COMMIT", options, callback);
}
}
rollback_and_end(callback) {
return this.rollback({
end_pg_pool: true
}, callback);
}
commit_and_end(callback) {
return this.commit({
end_pg_pool: true
}, callback);
}
_borrow(callback) {
if (this._client != null) {
if (this._client.connected_at == null) {
return this._client.connect((err) => {
this._client._auto_disconnect_on_transaction_end = true;
return callback(err, this._client);
});
} else {
return callback(null, this._client);
}
} else if (this._pool == null) {
return callback(new Error("No SQLClient or SQLClientPool from which to obtain a connection. This transaction may have already been closed."));
} else {
return this._pool.borrow((err, client) => {
if (err != null) {
return callback(err);
} else {
this._client = client;
return callback(null, client);
}
});
}
}
execute(sql, bindvars, callback) {
if (typeof bindvars === 'function' && (callback == null)) {
[callback, bindvars] = [bindvars, callback];
}
if (this._ended) {
return callback(new Error("This transaction has already been closed."));
} else {
return this._borrow((err, client) => {
if (err != null) {
return callback(err);
} else {
return client.execute(sql, bindvars, callback);
}
});
}
}
};
exports.SQLClientPool = SQLClientPool;
exports.Transaction = Transaction;
}).call(this);