Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ node_modules
coverage
.idea
.DS_Store
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
1 change: 0 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

64 changes: 32 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@ $ npm test

#### query()
Make a SQL query in which you expect zero or more results. Returns a promise which
either resolves to an array containing found records (as objects) or rejects if no records found.
either resolves to an array containing found records (as objects) or rejects if no records found.

#### querySafe()
Same as query but resolves an empty array if no records found.

##### Suggested model usage:
##### Suggested model usage:
```js

'use strict';

const mysql = require('mysql'),
config = require('config'),
dbPool = mysql.createPool(config.mysql);

const DB = require('alien-node-mysql-utils')(dbPool),
validateAccountData = require('../some-validator');

Expand All @@ -56,7 +56,7 @@ const getAccountsByStatus = status => {
module.exports = getAccountsByStatus;
```

##### Suggested controller usage
##### Suggested controller usage

*(using DB.query)*

Expand All @@ -70,7 +70,7 @@ getAccountsByStatus('active').then(accounts => {
.catch(err => {
// handle "No records found" or other errors here
});

```

*(using DB.querySafe)*
Expand All @@ -85,15 +85,15 @@ getAccountsByStatus('active').then(maybeAccounts => {
.catch(err => {
// handle errors here
});

```

#### lookup()
Make a SQL query in which you expect zero or one result. Returns a promise which
either resolves to an object matching the row schema or rejects if no records found.
either resolves to an object matching the row schema or rejects if no records found.

#### lookupSafe()
Same as lookup, but resolves `undefined` if no records are found.
Same as lookup, but resolves `undefined` if no records are found.

```js

Expand All @@ -102,7 +102,7 @@ Same as lookup, but resolves `undefined` if no records are found.
const mysql = require('mysql'),
config = require('config'),
dbPool = mysql.createPool(config.mysql);

const DB = require('alien-node-mysql-utils')(dbPool),
validateAccountData = require('../some-validator');

Expand Down Expand Up @@ -141,7 +141,7 @@ getAccountById(1234).then(account => {
.catch(err => {
// handle "No records found" or other errors here
});

```

*(using DB.lookupSafe)*
Expand All @@ -157,19 +157,19 @@ getAccountById(1234).then(maybeAccount => {
.catch(err => {
// handle errors here
});

```

## Transactions
This library supports some simple transaction abstractions to play nicely with your promise chains.
This library supports some simple transaction abstractions to play nicely with your promise chains.

The three methods you need to care about are :
The three methods you need to care about are :
- DB.beginTransaction()
- DB.addQueryToTransaction()
- DB.commit()
These methods have a unique signature compared to the other methods for querying. Let's break them down:

These methods have a unique signature compared to the other methods for querying. Let's break them down:

**DB.beginTransaction()** : `() -> Promise(connection)`

This method will use the curried `dbPool` object provided during require...
Expand All @@ -179,23 +179,23 @@ const DB = require('alien-node-mysql-utils')(dbPool);
```

... and call the native `getConnection()` on it, then resolve the connection on its promise.
This connection needs to be provided to the subsequent methods so the transaction knows how to commit and rollback.

This connection needs to be provided to the subsequent methods so the transaction knows how to commit and rollback.

**DB.addQueryToTransaction()** : `connection -> query -> Promise({ data, connection })`

This method accepts the connection object which you should have gotten from `DB.beginTransaction()`, along with the typical query which you give to
any other query method in this library. It behaves like `DB.querySafe()` in that it lets you
This method accepts the connection object which you should have gotten from `DB.beginTransaction()`, along with the typical query which you give to
any other query method in this library. It behaves like `DB.querySafe()` in that it lets you
deal with all the data scrubbing and null-checks (resolves zero-or-more result sets and all `SELECT` statements
return an array).
return an array).

Please notice that this method returns the connection along with the data, so in the spirit of
keeping the unary promise chain data flow in mind, the promise will resolve a single object,
Please notice that this method returns the connection along with the data, so in the spirit of
keeping the unary promise chain data flow in mind, the promise will resolve a single object,
where the data lives in a `data` property, and the connection on a `connection` property.

**DB.commit()** : `connection`

This method accepts the connection object which you should have gotten from `DB.beginTransaction()`. It simply
This method accepts the connection object which you should have gotten from `DB.beginTransaction()`. It simply
resolves `true` if there are no errors, otherwise it rejects the promise with whatever error may happen to ruin your day.

##### Suggested wrapper-model usage for transactions
Expand All @@ -206,22 +206,22 @@ const DB = require('alien-node-mysql-utils')(dbPool);
const getUserBalance = id => connection => {
const query = 'SELECT balance FROM users WHERE id = ?',
queryStatement = [query, [id]];

return DB.addQueryToTransaction(connection, queryStatement);
};

const updateUserBalance = (id, amount) => connection => {
const query = 'UPDATE users SET balance = balance + ? WHERE id = ?',
queryStatement = [query, [amount, id]];

return DB.addQueryToTransaction(connection, queryStatement);
};

const ensurePositiveTransfer = amount => connection => {
if (amount > 0) {
return connection;
} else {
throw {
throw {
error : new Error('What are you doing?'),
connection
};
Expand All @@ -231,11 +231,11 @@ const ensurePositiveTransfer = amount => connection => {
const ensureEnoughMoney = amount => transaction => {
const data = transaction.data || [{ balance : 0 }],
balance = data[0].balance || 0;

if (amount <= balance) {
return transaction;
} else {
throw {
throw {
error : new Error('Broke ass' ),
connection : transaction.connection
};
Expand All @@ -262,7 +262,7 @@ DB.beginTransaction()
exception.connection.rollback();
logger.error(exception.error);
});

```
## TODO
## TODO
- Make the transform to/from column methods unbiased with decorator injection
13 changes: 5 additions & 8 deletions lib/methods/_connectionHandle.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,30 @@
const _queryCallback = require('./_queryCallback'),
constants = require('../constants');

const _connectionHandle = (deferred, queryStatement, transaction, singleReturnItem, allowEmptyResponse) => (err, connection) => {
const _connectionHandle = ({ resolve, reject }, queryStatement, transaction, singleReturnItem, allowEmptyResponse, attempt = 1) => (err, connection) => {

const preparedStatement = queryStatement[0],
valueSwapIns = queryStatement[1];

if (!connection) {
deferred.reject(constants.errors.NO_DB_CONNECTION);
return deferred.promise;
reject(constants.errors.NO_DB_CONNECTION);
return;
}

if (err) {

if (transaction) {
connection.rollback();
}

connection.release();
deferred.reject(err);
reject(err);
}

connection.query(
preparedStatement,
valueSwapIns,
_queryCallback(deferred, connection, transaction, singleReturnItem, allowEmptyResponse)
_queryCallback({ resolve, reject }, queryStatement, connection, transaction, singleReturnItem, allowEmptyResponse, attempt, _connectionHandle)
);

return deferred.promise;
};

module.exports = _connectionHandle;
20 changes: 20 additions & 0 deletions lib/methods/_maybeRetryQuery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict';

const R = require('ramda');

const _maybeRollbackAndRelease = require('./_maybeRollbackAndRelease'),
constants = require('../constants');

const RETRY_IN_MILLISECONDS = 40,
MAX_RETRIES = 5;

const _maybeRetryQuery = ({ resolve, reject }, queryStatement, connection, transaction, singleReturnItem, allowEmptyResponse, attempt, _connectionHandle, err) => {
if (attempt >= MAX_RETRIES){
_maybeRollbackAndRelease(connection, transaction);
reject(constants.errors.UNKNOWN(R.prop('message', err)));
} else {
setTimeout(() => _connectionHandle({ resolve, reject }, queryStatement, connection, transaction, singleReturnItem, allowEmptyResponse, attempt+1)(null, connection), RETRY_IN_MILLISECONDS);
}
};

module.exports = _maybeRetryQuery;
10 changes: 10 additions & 0 deletions lib/methods/_maybeRollbackAndRelease.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

const _maybeRollbackAndRelease = (connection, transaction) => {
if (transaction) {
connection.rollback();
}
connection.release();
};

module.exports = _maybeRollbackAndRelease;
38 changes: 13 additions & 25 deletions lib/methods/_queryCallback.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,49 @@ const R = require('ramda');

const transformQueryResponse = require('./transformQueryResponse'),
ensureNotSingleItemArray = require('./ensureNotSingleItemArray'),
_maybeRetryQuery = require('./_maybeRetryQuery'),
_maybeRollbackAndRelease = require('./_maybeRollbackAndRelease'),
constants = require('../constants');

const isNilOrEmpty = R.anyPass([R.isNil, R.isEmpty]);

const maybeRollbackAndRelease = (connection, transaction) => {
if (transaction) {
connection.rollback();
}
connection.release();
};

const maybeEnsureSingleItemArray = singleReturnItem => data => singleReturnItem ? ensureNotSingleItemArray(data) : data;

const _queryCallback = (deferred, connection, transaction, singleReturnItem, allowEmptyResponse) => (err, data) => {
const _queryCallback = ({ resolve, reject }, queryStatement, connection, transaction, singleReturnItem, allowEmptyResponse, attempt, _connectionHandle) => (err, data) => {

if (err) {
switch (R.prop('code', err)) {
case 'ER_DUP_ENTRY' :
maybeRollbackAndRelease(connection, transaction);
deferred.reject(constants.errors.DUPLICATE(R.prop('message', err)));
_maybeRollbackAndRelease(connection, transaction);
reject(constants.errors.DUPLICATE(R.prop('message', err)));
break;
case 'ER_LOCK_DEADLOCK' :
_maybeRetryQuery({ resolve, reject }, queryStatement, connection, transaction, singleReturnItem, allowEmptyResponse, attempt, _connectionHandle, err);
break;
default :
maybeRollbackAndRelease(connection, transaction);
deferred.reject(constants.errors.UNKNOWN(R.prop('message', err)));
_maybeRollbackAndRelease(connection, transaction);
reject(constants.errors.UNKNOWN(R.prop('message', err)));
break;
}
return deferred.promise;

} else {

if (isNilOrEmpty(data)) {

if (allowEmptyResponse) {
if (!transaction) {
connection.release();
}

deferred.resolve(singleReturnItem ? undefined : [])
resolve(singleReturnItem ? undefined : [])
} else {
connection.release();
deferred.reject(constants.errors.NO_QUERY_RESULTS);
reject(constants.errors.NO_QUERY_RESULTS);
}

return deferred.promise;

} else {
const transformedData = R.compose(transformQueryResponse, maybeEnsureSingleItemArray(singleReturnItem))(data);

if (!transaction) {
connection.release();
}

deferred.resolve(transaction ? { data : transformedData, connection } : transformedData);

return deferred.promise;
resolve(transaction ? { data : transformedData, connection } : transformedData);
}
}
};
Expand Down
14 changes: 6 additions & 8 deletions lib/methods/_transaction.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
'use strict';

const R = require('ramda'),
Q = require('q');
const R = require('ramda');

const _connectionHandle = require('./_connectionHandle');

const _transaction = R.curry((connection, singleReturnItem, allowEmptyResponse, dbPool, queryStatement) => {
const deferred = Q.defer(),
transaction = !!connection,
cb = _connectionHandle(deferred, queryStatement, transaction, singleReturnItem, allowEmptyResponse);
return new Promise((resolve, reject) => {
const transaction = !!connection,
cb = _connectionHandle({ resolve, reject }, queryStatement, transaction, singleReturnItem, allowEmptyResponse);

connection ? cb(null, connection) : dbPool.getConnection(cb);

return deferred.promise;
connection ? cb(null, connection) : dbPool.getConnection(cb);
});
});

module.exports = _transaction;
2 changes: 1 addition & 1 deletion lib/methods/addQueryToTransaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const R = require('ramda');

const _transaction = require('./_transaction');
const _transaction = require('./_transaction');

const EXPECT_SINGLE_RETURN_ITEM = false,
ALLOW_EMPTY_RESPONSE = true,
Expand Down
Loading