Skip to content

Commit

Permalink
* Added options to crud functions that did not previously have them
Browse files Browse the repository at this point in the history
 * Added: All crud functions will use options.connection to execute queries if given (supports transactions)
 * Updated docs
 * Added supporting tests
  • Loading branch information
kfitzgerald committed Dec 14, 2017
1 parent 91eb661 commit b7e9bae
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 26 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## v1.2.0
* Added `options` to crud functions that did not previously have them
* Added: All crud functions will use options.connection to execute queries if given (supports transactions)

## v1.1.0
* Added CrudService for MySQL that works just like our MongoService/CrudService
* MySQLService#query now returns the underlying query object
Expand Down
74 changes: 58 additions & 16 deletions CrudService.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Base service that all object CRUD services should inherit
*/
class CrudService {

/**
* Constructor
* @param app
Expand Down Expand Up @@ -64,16 +65,24 @@ class CrudService {
/**
* Creates a new model
* @param {*} data - Record properties
* @param {*} [options] – Query options
* @param {function(err:Error, obj:*)} [callback] – Fired when saved or failed to save
* @param {boolean} [suppressCollisionError] - Option to suppress error reporting on collisions (for quiet retry handling)
* @protected
*/
_create(data, callback, suppressCollisionError) {
_create(data, options, callback, suppressCollisionError) {

if (typeof options === 'function') {
suppressCollisionError = callback;
callback = options;
options = {};
}

let sql = `INSERT INTO \`${this.database}\`.\`${this.table}\` SET ?`;

// Skip the query abstraction and go straight to the pool, cuz we don't want it reporting for us
return this.service.pool.query(sql, data, (err, res) => {
const connection = options.connection || this.service.pool;
return connection.query(sql, data, (err, res) => {
if (err) {
if (!suppressCollisionError || err.errno !== CrudService._collisionErrorCode) {
this.app.report('Failed to create new record!', err, { sql, data, res });
Expand All @@ -90,13 +99,21 @@ class CrudService {
* Creates a new record but calls the objectClosure function before each save attempt
* @param {*} data – Model properties
* @param {function(data:*,attempt:Number)} objectClosure - Called to obtain the object row properties before save
* @param {*} [options] – Query options
* @param {function(err:Error, obj:*)} [callback] – Fired when saved or failed to save
* @param {Number} [attempt] - Internal, used if the save attempt failed due to a collision
* @protected
*/
_createWithRetry(data, objectClosure, callback, attempt) {
_createWithRetry(data, objectClosure, options, callback, attempt) {

if (typeof options === 'function') {
attempt = callback;
callback = options;
options = {};
}

attempt = attempt || 0;
return this._create(objectClosure(data, attempt), (err, doc) => {
return this._create(objectClosure(data, attempt), options, (err, doc) => {
// Only retry if the error matches our known error code
if (err && err.errno === CrudService._collisionErrorCode) {
if (++attempt >= this._createRetryCount) {
Expand All @@ -120,10 +137,16 @@ class CrudService {
* WARNING: this _can_ retrieve dead statuses
*
* @param {string} id - Row identifier
* @param {*} [options] – Query options
* @param {function(err:Error, doc:*, fields:*)} callback – Fired when completed
* @protected
*/
_retrieve(id, callback) {
_retrieve(id, options, callback) {

if (typeof options === 'function') {
callback = options;
options = {};
}

// Only do a query if there's something to query for
if (id !== undefined && id !== null) {
Expand All @@ -139,7 +162,8 @@ class CrudService {

sql += ' LIMIT 1';

return this.service.query(sql, args, (err, res, fields) => {
const connection = options.connection || this.service;
return connection.query(sql, args, (err, res, fields) => {
let row = null;
/* istanbul ignore if: this should be next to impossible to trigger */
if (err) this.app.report('Failed to retrieve record', err, { id, sql, args, res, fields});
Expand Down Expand Up @@ -254,7 +278,8 @@ class CrudService {
args.push(cap.offset, cap.limit);
}

return this.service.query(sql, args, (err, res, fields) => {
const connection = options.connection || this.service;
return connection.query(sql, args, (err, res, fields) => {
/* istanbul ignore if: hopefully you shouldn't throw query errors, and if you do, that's on you */
if (err) {
this.app.report('Failed to find records', err, { sql, args, res, fields, mysqlQuery });
Expand Down Expand Up @@ -375,15 +400,20 @@ class CrudService {
* Update an existing row
* @param {*} doc - row to update
* @param {*} [data] - Data to apply to the row before saving
* @param {*} [options] – Query options
* @param {function(err:Error, obj:*)} callback – Fired when saved or failed to save
* @protected
*/
_update(doc, data, callback) {
_update(doc, data, options, callback) {

// Allow overloading of _update(obj, callback)
if (typeof data === "function") {
callback = data;
options = {};
data = null;
} else if (typeof options === 'function') {
callback = options;
options = {};
}

// Apply any given key updates, if given
Expand All @@ -405,7 +435,8 @@ class CrudService {
let sql = 'UPDATE ??.?? SET ? WHERE ?? = ?';
let args = [this.database, this.table, sets, this.idField, doc[this.idField]];

return this.service.query(sql, args, (err, res, fields) => {
const connection = options.connection || this.service;
return connection.query(sql, args, (err, res, fields) => {
/* istanbul ignore if: hopefully you shouldn't throw query errors, and if you do, that's on you */
if (err) {
this.app.report('Failed to update row', err, { doc, data, sql, args, res, fields });
Expand All @@ -421,7 +452,7 @@ class CrudService {
* Updates all records that match the given criteria with the given properties
* @param {*} criteria – Query criteria (just like _find)
* @param {*} data – Column-value properties to set on each matched record
* @param {{conceal:boolean}} [options] – Additional options
* @param {{connection:*, conceal:boolean}} [options] – Additional options
* @param {function(err:Error, res:*)} callback – Fired when completed
* @protected
*/
Expand Down Expand Up @@ -470,7 +501,8 @@ class CrudService {
this._buildCriteria(criteria, where, args);
if (where.length > 0) sql += ` WHERE ${where.join(' AND ')}`;

return this.service.query(sql, args, (err, res) => {
const connection = options.connection || this.service;
return connection.query(sql, args, (err, res) => {
/* istanbul ignore if: hopefully you shouldn't throw query errors, and if you do, that's on you */
if (err) {
this.app.report('Failed to bulk update rows', err, { criteria, data, sql, args, res, fields });
Expand All @@ -483,12 +515,13 @@ class CrudService {
/**
* Fake-deletes a row from the table (by changing its status to dead and updating the row)
* @param {*} doc - Row to update
* @param {*} [options] – Query options
* @param {function(err:Error, obj:*)} [callback] – Fired when saved or failed to save
* @protected
*/
_delete(doc, callback) {
_delete(doc, options, callback) {
doc.status = this._deletedStatus;
return this._update(doc, (err, doc) => {
return this._update(doc, null, options, (err, doc) => {
callback(err, doc);
});
}
Expand All @@ -514,10 +547,17 @@ class CrudService {
/**
* Permanently removes a row from the table
* @param {*} doc - row to delete
* @param {*} [options] - Query options
* @param {function(err:Error, obj:*)} [callback] - Fired when deleted or failed to delete
* @protected
*/
_deletePermanently(doc, callback) {
_deletePermanently(doc, options, callback) {

if (typeof options === 'function') {
callback = options;
options = {};
}

// Make sure we know what we are deleting!
if (doc[this.idField] === undefined) {
this.app.report('Cannot delete row if id field not provided!', { doc, idField: this.idField });
Expand All @@ -527,7 +567,8 @@ class CrudService {
let sql = 'DELETE FROM ??.?? WHERE ?? = ?';
let args = [this.database, this.table, this.idField, doc[this.idField]];

return this.service.query(sql, args, (err, res, fields) => {
const connection = options.connection || this.service;
return connection.query(sql, args, (err, res, fields) => {
/* istanbul ignore if: out of scope cuz maybe you got FK constrains cramping your style */
if (err) {
this.app.report('Failed to delete row', err, { doc, sql, args, res, fields})
Expand Down Expand Up @@ -587,7 +628,8 @@ class CrudService {
this._buildCriteria(criteria, where, args);
if (where.length > 0) sql += ` WHERE ${where.join(' AND ')}`;

return this.service.query(sql, args, (err, res, fields) => {
const connection = options.connection || this.service;
return connection.query(sql, args, (err, res, fields) => {
/* istanbul ignore if: out of scope cuz maybe you got FK constrains cramping your style? */
if (err) {
this.app.report('Failed to bulk perm-delete rows', err, { criteria, sql, args, res, fields})
Expand Down
35 changes: 26 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,33 +224,39 @@ Creates a new instance. Ideally, you would extend it and call it via `super(app,
* `options.deletedStatus` – (Optional) The status to set docs to when "deleting" them. Defaults to `dead`.
* `options.concealDeadResources` – (Optional) Whether this service should actively prevent "deleted" (status=dead) resources from returning in `_retrieve`, `_find`, `_bulkUpdate`, `_bulkDelete`, and `_bulkDeletePermanently`. Defaults to `true`.

### `_create(data, callback, [suppressCollisionError])`
### `_create(data, [options], callback, [suppressCollisionError])`
Creates a new row.
* `data` – The row object to store
* `options` – (Optional) Query options
* `options.connection` – The connection to execute the query on. Defaults to the service pool.
* `callback(err, doc)` – Function fired when completed
* `err` – Error, if occurred
* `doc` – The row that was created
* `suppressCollisionError` - Internal flag to suppress automatically reporting the error if it is a collision

Returns the underlying MySQL query.

### `_createWithRetry(data, objectClosure, callback, [attempt])`
### `_createWithRetry(data, objectClosure, [options], callback, [attempt])`
Creates a new row after calling the given object closure. This closure is fired again (up to `service._createRetryCount` times) in the event there is a collision.
This is useful when you store rows that have unique fields (e.g. an API key) that you can regenerate in that super rare instance that you collide
* `data` – The row object to store
* `objectClosure(data, attempt)` – Function fired before saving the new row. Set changeable, unique properties here
* `data` – The row object to store
* `attempt` – The attempt number, starting at `0`
* `options` – (Optional) Query options
* `options.connection` – The connection to execute the query on. Defaults to the service pool.
* `callback(err, doc)` – Function fired when completed
* `err` – Error, if occurred
* `doc` – The new row that was created
* `attempt` – The internal attempt number (will increase after collisions)

Returns the underlying MySQL query.

### `_retrieve(id, callback)`
### `_retrieve(id, [options], callback)`
Retrieves a single row from the table.
* `id` – The id of the row.
* `options` – (Optional) Query options
* `options.connection` – The connection to execute the query on. Defaults to the service pool.
* `callback(err, doc)` – Function fired when completed
* `err` – Error, if occurred
* `doc` – The row if found or `null` if not found
Expand All @@ -267,6 +273,7 @@ Finds rows matching the given criteria. Supports pagination, field selection and
* `options.sort` – Sorts the results by the given fields (same syntax as mongo sorts, e.g. `{ field: 1, reverse: -1 }`). Default is unset.
* `options.conceal` – Whether to conceal dead resources. Default is `true`.
* `options.mode` – (Internal) Query mode, used to toggle query modes like SELECT COUNT(*) queries
* `options.connection` – The connection to execute the query on. Defaults to the service pool.
* `callback(err, rows)` – Fired when completed
* `err` – Error, if occurred
* `docs` – The array of rows returned or `[]` if none found.
Expand All @@ -291,14 +298,17 @@ Counts the number of matched records.
* `criteria` – Object with field-value pairs. Supports some special [mongo-like operators](#Special operators)
* `options` – (Optional) Additional query options
* `options.conceal` – Whether to conceal dead resources. Default is `true`.
* `options.connection` – The connection to execute the query on. Defaults to the service pool.
* `callback(err, count)` – Fired when completed
* `err` – Error, if occurred
* `count` – The number of matched rows or `0` if none found.

### `_update(row, [data], callback)`
### `_update(row, [data], [options], callback)`
Updates the given row and optionally applies user-modifiable fields, if service is configured to do so.
* `doc` – The row to update. Must include configured id field.
* `data` – (Optional) Additional pool of key-value fields. Only keys that match `service._modifiableKeys` will be copied if present. Useful for passing in a request payload and copying over pre-validated data as-is.
* `data` – (Optional) Additional pool of key-value fields. Only keys that match `service._modifiableKeys` will be copied if present. Useful for passing in a request payload and copying over pre-validated data as-is.
* `options` – (Optional) Query options
* `options.connection` – The connection to execute the query on. Defaults to the service pool.
* `callback(err, res)` – Fired when completed
* `err` – Error, if occurred
* `res` – The MySQL response. Contains properties like `res.affectedRows` and `res.changedRows`.
Expand All @@ -309,13 +319,16 @@ Updates all rows matching the given criteria with the new column values.
* `data` – Field-value pairs to set on matched rows
* `options` – (Optional) Additional query options
* `options.conceal` – Whether to conceal dead resources. Default is `true`.
* `options.connection` – The connection to execute the query on. Defaults to the service pool.
* `callback(err, res)` – Fired when completed
* `err` – Error, if occurred
* `res` – The MySQL response. Contains properties like `res.affectedRows` and `res.changedRows`.

### `_delete(row, callback)`
### `_delete(row, [options], callback)`
Fake-deletes a row from the table. In reality, it just sets its status to `dead` (or whatever the value of `service._deletedStatus` is).
* `doc` – The row to delete. Must include configured id field.
* `doc` – The row to delete. Must include configured id field.
* `options` – (Optional) Query options
* `options.connection` – The connection to execute the query on. Defaults to the service pool.
* `callback(err, res)` – Fired when completed
* `err` – Error, if occurred
* `res` – The MySQL response. Contains properties like `res.affectedRows` and `res.changedRows`.
Expand All @@ -325,13 +338,16 @@ Fake-deletes all rows matching the given criteria.
* `criteria` – Object with field-value pairs. Supports some special [mongo-like operators](#Special operators)
* `options` – (Optional) Additional query options
* `options.conceal` – Whether to conceal dead resources. Default is `true`.
* `options.connection` – The connection to execute the query on. Defaults to the service pool.
* `callback(err, res)` – Fired when completed
* `err` – Error, if occurred
* `res` – The MySQL response. Contains properties like `res.affectedRows` and `res.changedRows`.

### `_deletePermanently(row, callback)`
### `_deletePermanently(row, [options], callback)`
Permanently deletes a row from the table. This is destructive!
* `doc` – The row to delete. Must include configured id field.
* `doc` – The row to delete. Must include configured id field.
* `options` – (Optional) Query options
* `options.connection` – The connection to execute the query on. Defaults to the service pool.
* `callback(err, res)` – Fired when completed
* `err` – Error, if occurred
* `res` – The MySQL response. Contains properties like `res.affectedRows` and `res.changedRows`.
Expand All @@ -341,6 +357,7 @@ Permanently deletes all rows matching the given criteria.
* `criteria` – Object with field-value pairs. Supports some special [mongo-like operators](#Special operators)
* `options` – (Optional) Additional query options
* `options.conceal` – Whether to conceal dead resources. Default is `true`.
* `options.connection` – The connection to execute the query on. Defaults to the service pool.
* `callback(err, res)` – Fired when completed
* `err` – Error, if occurred
* `res` – The MySQL response. Contains properties like `res.affectedRows` and `res.changedRows`.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "okanjo-app-mysql",
"version": "1.1.0",
"version": "1.2.0",
"description": "Service for interfacing with MySQL",
"main": "MySQLService.js",
"scripts": {
Expand Down

0 comments on commit b7e9bae

Please sign in to comment.