Comparing version 0.1.0 to 0.1.1
@@ -191,5 +191,5 @@ # Purpose | ||
**Responsibility:** Wrap a `ConnectionAdapter` and return it to the connection pool | ||
when the transaction is either committed or rolled back. Unhandled query errors will | ||
be emitted by the transaction object itself and cause an automatic rollback. | ||
**Responsibility:** Wrap a `ConnectionAdapter` and so that all queries take place | ||
within a single database transaction. Unhandled query errors will be emitted by | ||
the transaction object itself and cause an automatic rollback. | ||
@@ -202,3 +202,3 @@ ### Interface | ||
except queries are guaranteed to be performed within the transaction. If the | ||
transaction is committed or rolled back calls to `query` will fail. | ||
transaction has been committed or rolled back calls to `query` will fail. | ||
* `commit([callback])` - Issue a `COMMIT` statement to the database. If | ||
@@ -205,0 +205,0 @@ `callback` is given it will be called with any errors (or `null`) after the |
@@ -44,5 +44,17 @@ var EventProxy = require('../event-proxy') | ||
MySQL.prototype.query = helpers.queryMethod(function (stmt, params, callback, qa) { | ||
var query = this._connection.query(stmt, params) | ||
var queryArgs, queryAdapter | ||
try { | ||
queryArgs = helpers.fixPlaceholders(stmt, params, '?') | ||
} catch (err) { | ||
if (callback) return callback(err) | ||
else process.nextTick(function () { | ||
// next tick to ensure we have a QueryAdapter instance | ||
queryAdapter.emit('error', err) | ||
}) | ||
} | ||
var query = this._connection.query(queryArgs[0], queryArgs[1]) | ||
var res = [] | ||
return QueryAdapter.wrap(qa, query, { | ||
queryAdapter = QueryAdapter.wrap(qa, query, { | ||
error: function (err) { callback ? callback(err) : this.emit('error', err) }, | ||
@@ -58,2 +70,4 @@ result: function (row) { | ||
}) | ||
return queryAdapter | ||
}) | ||
@@ -60,0 +74,0 @@ |
@@ -52,4 +52,3 @@ var EventProxy = require('../event-proxy') | ||
SQLite3.prototype.query = helpers.queryMethod(function (stmt, params, callback, qa) { | ||
var args = [stmt, params] | ||
, res = [] | ||
var args, res = [] | ||
@@ -59,2 +58,11 @@ if (!qa) qa = new QueryAdapter | ||
try { | ||
args = helpers.fixPlaceholders(stmt, params, '?') | ||
} catch (err) { | ||
if (callback) callback(err) | ||
else process.nextTick(qa.emit.bind(qa, 'error', err)) | ||
return | ||
} | ||
function handleError (err) { | ||
@@ -65,5 +73,7 @@ if (callback) callback(err) | ||
var _errored = false; | ||
args.push( | ||
function handleRow (err, row) { | ||
if (err) return handleError(err) | ||
if (_errored) return | ||
if (err) return _errored = true | ||
if (qa._buffer) res.push(row) | ||
@@ -70,0 +80,0 @@ qa.emit('row', row) |
@@ -27,1 +27,37 @@ var QueryAdapter = require('./query-adapter') | ||
} | ||
var singleQuote = /'[^']/ | ||
exports.fixPlaceholders = function (stmt, params, placeholder) { | ||
if (!placeholder) placeholder = '?' | ||
var _params = [] | ||
var inQuotes = false | ||
stmt = stmt.split("'").map(function (chunk) { | ||
if (inQuotes) { | ||
if (chunk) inQuotes = !inQuotes // empty chunk means we had a double ' | ||
return chunk | ||
} else { | ||
inQuotes = !inQuotes | ||
return chunk.replace(/\$([\a\d]+)/g, function (m) { | ||
var name = m[1] | ||
if (Array.isArray(params)) name = parseInt(name) - 1 | ||
if (!params.hasOwnProperty(name)) { | ||
throw new Error("Parameter " + name + " not present in params") | ||
} | ||
_params.push(params[name]) | ||
return placeholder | ||
}) | ||
} | ||
}).join("'") | ||
return [stmt, _params] | ||
} | ||
if (require.main === module) { | ||
var fixPlaceholders = exports.fixPlaceholders | ||
console.log(fixPlaceholders('SELECT blah FROM nah WHERE a = $1', [10])) | ||
console.log(fixPlaceholders('INSERT INTO streaming_test (a) VALUES ($1)', [10])) | ||
console.log(fixPlaceholders('SELECT blah FROM nah WHERE a = $1', [10], '$')) | ||
console.log(fixPlaceholders("SELECT blah FROM nah WHERE a = '$1'", [10], '$')) | ||
console.log(fixPlaceholders("SELECT blah FROM nah WHERE a = 'like ''$money'''", [10], '$')) | ||
} |
@@ -14,3 +14,3 @@ module.exports = StateMachine | ||
removeMethods = (function () { | ||
var removeMethods = (function () { | ||
for (var methodName in methods) { | ||
@@ -24,3 +24,3 @@ if (currentState in methods[methodName]) delete this[methodName] | ||
if (to === currentState) return; | ||
if (to === currentState) return true; | ||
@@ -27,0 +27,0 @@ var extra = Array.prototype.slice.call(arguments, 1) |
@@ -40,15 +40,11 @@ var queryMethod = require("./helpers").queryMethod | ||
function _handleError (err, callback) { | ||
var self = this | ||
var propagate = callback || this.emit.bind(this, 'error') | ||
if (this.state() != 'rolled back') this.rollback(function (rollbackErr) { | ||
if (rollbackErr) { | ||
err = new Error('Failed to rollback transaction: ' + rollbackErr | ||
+ '\nError causing rollback: ' + err) | ||
+ '\nError causing rollback: ' + err) | ||
} | ||
if (callback) callback(err) | ||
else self.emit('error', err) | ||
propagate(err) | ||
}) | ||
else process.nextTick(function () { | ||
if (callback) callback(err) | ||
else self.emit('error', err) | ||
}) | ||
else process.nextTick(propagate) | ||
} | ||
@@ -55,0 +51,0 @@ |
{ | ||
"name": "any-db", | ||
"version": "0.1.0", | ||
"version": "0.1.1", | ||
"description": "Database-agnostic connection pooling, querying, and result sets", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -20,8 +20,8 @@ # any-db - a less-opinionated database abstraction layer. | ||
* Exposes a uniform transaction API. | ||
* Uses one style of parameter placeholders (Postgres-style $n or $named) with | ||
all drivers. | ||
Things it will do soon: | ||
* Optionally replace db-agnostic parameter placeholders with driver specific | ||
ones so you can use the exact same code against all drivers. | ||
* Have lot's and lot's of tests | ||
* Have more and more tests. | ||
* Provide a common result set API. | ||
@@ -39,3 +39,2 @@ | ||
for that. | ||
* Leave it's dishes in the sink and leave town for the weekend. | ||
@@ -66,6 +65,19 @@ ## Usage | ||
You can also create or get an existing connection pool with `anyDB.getPool`. It | ||
takes the following options: | ||
To use bound parameters simply pass an array as the second argument to query: | ||
var pool = anyDB.getPool('postgres://user:pass@localhost/dbname', { | ||
conn.query('SELECT * FROM users WHERE gh_username = $1', ['grncdr']) | ||
You can also use named parameters by passing an object instead: | ||
conn.query('SELECT * FROM users WHERE gh_username = $username', {username: 'grncdr'}) | ||
Any-db doesn't do any parameter escaping on it's own, so you can use any | ||
advanced parameter escaping features of the underlying driver exactly as though | ||
any-db wasn't there. | ||
### Connection pools | ||
You can create a connection pool with `anyDB.createPool`: | ||
var pool = anyDB.createPool('postgres://user:pass@localhost/dbname', { | ||
min: 5, // Minimum connections | ||
@@ -86,31 +98,42 @@ max: 10, // Maximum connections | ||
A connection pool has the following methods available: | ||
A connection pool has a `query` method that acts exactly like the one on | ||
connections, but the underlying connection is returned to the pool when the | ||
query completes. | ||
// Exactly like conn.query above, but the underlying connection will be | ||
// auto-released back into the pool when the query completes. | ||
pool.query(...) | ||
### Transactions | ||
Transactions can be started with `begin`, in this example we stream all users | ||
and then apply updates based on the results from an external service: | ||
Both connections and pools have a `begin` method that starts a new transaction | ||
and returns a `Transaction` object. Transaction objects behave much like | ||
connections, but instead of an `end` method, they have `commit` and `rollback` | ||
methods. Additionally, an unhandled error emitted by a transaction query will | ||
cause an automatic rollback of the transaction before being re-emitted by the | ||
transaction itself. | ||
Here's an example where we stream all of our user ids, check them against an | ||
external abuse-monitoring service, and flag or delete users as necessary, if | ||
for any reason we only get part way through, the entire transaction is rolled | ||
back and nobody is flagged or deleted: | ||
var tx = pool.begin() | ||
tx.on('error', function (err) { | ||
// Called for any query errors without an associated callback | ||
tx.rollback() | ||
finished(err) | ||
}) | ||
tx.on('error', finished) | ||
tx.query('SELECT id FROM users').on('row', function (user) { | ||
/* | ||
Why query with the pool and not the transaction? | ||
Because it allows the transaction queries to begin executing immediately, | ||
rather than queueing them all up behind the initial SELECT. | ||
*/ | ||
pool.query('SELECT id FROM users').on('row', function (user) { | ||
if (tx.state() == 'rolled back') return | ||
externalService.method(user.id, function (err, result) { | ||
abuseService.checkUser(user.id, function (err, result) { | ||
if (err) return tx.handleError(err) | ||
// Errors from these queries will propagate up to the transaction object | ||
if (result.flag) { | ||
tx.query('UPDATE users SET flag = 1 WHERE id = ?', [user.id]) | ||
} else if (result.deleteme) { | ||
tx.query('DELETE FROM users WHERE id = ?', [user.id]) | ||
tx.query('UPDATE users SET abuse_flag = 1 WHERE id = $1', [user.id]) | ||
} else if (result.destroy) { | ||
tx.query('DELETE FROM users WHERE id = $1', [user.id]) | ||
} | ||
}) | ||
}).on('error', function (err) { | ||
tx.handleError(err) | ||
}).on('end', function () { | ||
@@ -117,0 +140,0 @@ tx.commit(finished) |
@@ -34,5 +34,4 @@ var anyDB = require('../') | ||
var tx = conn.begin() | ||
tx.driver = connString.split(':').shift() | ||
t.on('end', tx.rollback.bind(tx, conn.end.bind(conn))) | ||
tx.on('error', function (err) { debugger; t.emit('error', err) }) | ||
tx.on('error', function (err) { t.emit('error', err) }) | ||
callback(tx, t) | ||
@@ -39,0 +38,0 @@ }) |
@@ -5,6 +5,4 @@ require('./helpers').allTransactions("Streaming results", function (tx, t) { | ||
var vals = [] | ||
var placeholder = tx.driver == 'postgres' ? '$1' : '?' | ||
var insert = 'INSERT INTO streaming_test (a) VALUES (' + placeholder + ')' | ||
for (var i = 0; i < 10; i++) { | ||
tx.query(insert, [i]) | ||
tx.query('INSERT INTO streaming_test (a) VALUES ($1)', [i]) | ||
vals.push(i) | ||
@@ -17,4 +15,7 @@ } | ||
t.deepEqual(vals, []) | ||
tx.query('DROP TABLE streaming_test', t.end.bind(t)) | ||
tx.query('DROP TABLE streaming_test', function (err) { | ||
if (err) t.emit('error', err) | ||
else t.end() | ||
}) | ||
}) | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
720
147
0
37433
22
1