sql-client
Version:
A dirt-simple SQL client abstraction (currently) supporting PostgreSQL, MySQL and SQLite.
409 lines (372 loc) • 14.9 kB
text/coffeescript
SQLClient = require( './sql-client' ).SQLClient
class SQLClientPool
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)."
}
DEFAULT_WAIT_INTERVAL: 50
DEFAULT_RETRY_INTERVAL: 50
create_transaction:()=>
new Transaction(this)
create:(callback)=>
client = new SQLClient( ..., )
client.connect (err)=>
callback(err,client)
activate:(client,callback)=>
callback(null,client)
validate:(client,callback)=>
if client? and ( (not client.pooled_at?) or (not ?.max_age?) or ((Date.now()-client.pooled_at) < .max_age) )
callback(null,true,client)
else
callback(null,false,client)
passivate:(client,callback)=>
callback(null,client)
destroy:(client,callback)=>
if client?
client.disconnect(callback)
else
callback()
pool:[]
pool_options:{}
pool_is_open:false
borrowed:0
returned:0
active:0
eviction_runner:null
constructor:( ..., )->
()->undefined
open:(opts,callback)=>
if not callback? and typeof opts is 'function'
callback = opts
opts = null
if typeof callback isnt 'function'
throw new Error( .INVALID_ARGUMENT)
else
opts, (err)=>
= true
callback?(err)
close:(callback)=>
if callback? and typeof callback isnt 'function'
throw new Error( .INVALID_ARGUMENT)
else
= false
if ?
.ref()
clearTimeout
= null
if .length > 0
.shift(),()=>
else
if > 0
callback?(new Error( .CLOSED_WITH_ACTIVE))
else
callback?()
# borrow, execute, return, callback
execute:(sql,bindvars,callback)=>
if typeof bindvars is 'function' and not callback?
callback = bindvars
bindvars = []
(err,client)=>
if err?
callback(err)
else unless client?
callback(new Error("non-null client expected"))
else
client.execute sql, bindvars, (response...)=>
client, ()=>
callback(response...)
borrow:(callback,blocked_since=null,retry_count=0)=>
if typeof callback isnt 'function'
throw new Error( .INVALID_ARGUMENT)
else
if not
callback new Error( .POOL_NOT_OPEN)
else
if >= .max_active and .when_exhausted is 'fail'
callback new Error( .EXHAUSTED)
else if >= .max_active and .when_exhausted is 'block'
if blocked_since? and (Date.now() - blocked_since) >= .max_wait
callback new Error( .MAX_WAIT)
else
blocked_since ?= Date.now()
setTimeout (()=> ), .wait_interval
else if .length > 0
client = .shift()
client, (err,valid,client)=>
if err?
callback(err)
else if not valid
else
client.pooled_at = null
client.borrowed_at = Date.now()
++
callback(null,client)
else
(err,client)=>
if err?
callback(err)
else
client, (err,valid,client)=>
if err?
callback(err)
else if not valid
if .max_retries? and .max_retries > retry_count
setTimeout (()=> ), ( .retry_interval ? 0)
else
callback new Error( .INVALID)
else
client.pooled_at = null
client.borrowed_at = Date.now()
++
callback(null,client)
return:(client,callback)=>
if (not client? and not callback?) or (callback? and typeof callback isnt 'function')
throw new Error( .INVALID_ARGUMENT)
else if not client?
callback(new Error( .NULL_RETURNED))
else if <= 0
callback(new Error( .TOO_MANY_RETURNED))
else
++
--
if client?
client, (err,client)=>
if err
callback(err)
else
client.pooled_at = Date.now()
client.borrowed_at = null
if .length >= .max_idle
client, callback
else
.push client
callback?()
# 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
_config:(opts,callback)=>
opts ?= {}
new_opts =
keys = Object.keys(opts)
for prop in [ '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']
if prop in keys
new_opts[prop] = opts[prop]
if new_opts.max_retries? and (typeof new_opts.max_retries isnt 'number' or new_opts.max_retries <= 0)
new_opts.max_retries = null
if new_opts.max_retries?
if new_opts.retry_interval? and (typeof new_opts.retry_interval isnt 'number' or new_opts.retry_interval <= 0)
new_opts.retry_interval = 0
else unless new_opts.retry_interval?
new_opts.retry_interval =
if typeof new_opts.max_idle is 'number' and new_opts.max_idle < 0
new_opts.max_idle = Number.MAX_VALUE
else if typeof new_opts.max_idle isnt 'number'
new_opts.max_idle = 0
new_opts.min_idle = 0 if typeof new_opts.min_idle isnt 'number' or new_opts.min_idle < 0
new_opts.min_idle = new_opts.max_idle if new_opts.min_idle > new_opts.max_idle
new_opts.max_active = Number.MAX_VALUE if typeof new_opts.max_active isnt 'number' or new_opts.max_active < 0
new_opts.max_wait = Number.MAX_VALUE if typeof new_opts.max_wait isnt 'number' or new_opts.max_wait < 0
new_opts.wait_interval = if typeof new_opts.wait_interval isnt 'number' or new_opts.wait_interval < 0
new_opts.when_exhausted = 'grow' unless new_opts.when_exhausted in [ 'grow','block','fail' ]
new_opts.max_age = Number.MAX_VALUE if not new_opts.max_age? or new_opts.max_age < 0
= new_opts
_reconfig:(callback)=>
if ?
.ref()
clearTimeout
= null
(err)=>
if err?
callback?(err)
else
(x...)=>
if ?.eviction_run_interval? and .eviction_run_interval > 0
= setInterval( , .eviction_run_interval)
unless ?.unref_eviction_runner is false
.unref()
callback(x...)
_evict:(callback)=>
_eviction_run:(num_to_check,callback)=>
if typeof num_to_check is 'function' and not callback?
callback = num_to_check
num_to_check = null
new_pool = []
num_checked = 0
while .length > 0
client = .shift()
if (not num_to_check?) or num_to_check <= 0 or num_checked < num_to_check
num_checked += 1
if new_pool.length < .max_idle and
new_pool.push client
else
client.disconnect()
else
new_pool.push client
= new_pool
callback?()
_prepopulate:(callback)=>
n = .min_idle - .length
if n > 0
n, [], (err,borrowed)=>
if err?
callback(err)
else
borrowed, callback
else
callback()
_borrow_n:(n,borrowed,callback)=>
if typeof n isnt 'number' or not Array.isArray(borrowed)
callback(new Error( .INTERNAL_ERROR))
else
if n > borrowed.length
(err,client)=>
if client?
borrowed.push client
if err?
borrowed, ()=>
callback(err)
else
else
callback(null,borrowed)
_return_n:(borrowed,callback)=>
if not Array.isArray(borrowed)
callback(new Error( .INTERNAL_ERROR))
else if borrowed.length > 0
client = borrowed.shift()
client,()=>
else
callback(null)
_activate_and_validate_or_destroy:(client,callback)=>
client, (err,client)=>
if err?
if client?
client, ()=>
callback(err,false,null)
else
callback(err,false,null)
else
client, (err,valid,client)=>
if err?
if client?
client, ()=>
callback(err,false,null)
else
callback(err,false,null)
else if not valid
client,()=>
callback(null,false,null)
else
callback(null,true,client)
_clone:(map)->
unless map?
return null
else
cloned = {}
for n,v of map
cloned[n] = v
return cloned
class Transaction
constructor:(pool_or_client)->
if pool_or_client? and pool_or_client instanceof SQLClientPool
= pool_or_client
else if pool_or_client? and pool_or_client instanceof SQLClient
= pool_or_client
else
throw new Error("Expected SQLClientPool or SQLClient instance, found #{pool_or_client}")
begin:(callback)=>
"BEGIN", [], (r...)=>
= true
callback(r...)
_end:(command,options,callback)=>
if typeof options is "function" and not callback?
[callback, options] = [options, callback]
maybe_return = (cb)=>
= true
if ? and ?
.return , ()=>
= null
= null
cb?()
else if ? and ?._auto_disconnect_on_transaction_end
.disconnect(options,cb)
= null
else
cb?()
if
command, [], (err)=>
maybe_return ()=>
callback(err)
else
maybe_return ()=>
callback()
rollback:(options, callback)=>
if typeof options is "function" and not callback?
[callback, options] = [options, callback]
if
callback(new Error("This transaction has already been closed."))
else
"ROLLBACK", options, callback
commit:(options, callback)=>
if typeof options is "function" and not callback?
[callback, options] = [options, callback]
if
callback(new Error("This transaction has already been closed."))
else
"COMMIT", options, callback
rollback_and_end:(callback)=>
return
commit_and_end:(callback)=>
return
_borrow:(callback)=>
if ?
unless .connected_at?
.connect (err)=>
._auto_disconnect_on_transaction_end = true
callback(err, )
else
callback(null, )
else unless ?
callback(new Error("No SQLClient or SQLClientPool from which to obtain a connection. This transaction may have already been closed."))
else
.borrow (err,client)=>
if err?
callback(err)
else
= client
callback(null,client)
execute:(sql,bindvars,callback)=>
if typeof bindvars is 'function' and (not callback?)
[callback,bindvars] = [bindvars,callback]
if
callback(new Error("This transaction has already been closed."))
else
(err,client)=>
if err?
callback(err)
else
client.execute sql, bindvars, callback
exports.SQLClientPool = SQLClientPool
exports.Transaction = Transaction