From ed0265ebde2a587f1dffdb585592cda33dc6a4a3 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 19 Dec 2014 12:01:46 -0600 Subject: [PATCH 01/26] Allow objects that implement toDate to be cast --- lib/waterline/core/typecast.js | 3 +++ test/unit/core/core.cast/cast.date.js | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/waterline/core/typecast.js b/lib/waterline/core/typecast.js index 9e79c710e..4e8c8be10 100644 --- a/lib/waterline/core/typecast.js +++ b/lib/waterline/core/typecast.js @@ -205,6 +205,9 @@ Cast.prototype.date = function date(value) { if (value.__proto__ == Date.prototype) { _value = new Date(value.getTime()); } + else if (typeof value.toDate === 'function') { + _value = value.toDate(); + } else { _value = new Date(Date.parse(value)); } diff --git a/test/unit/core/core.cast/cast.date.js b/test/unit/core/core.cast/cast.date.js index 6db3063e8..906116483 100644 --- a/test/unit/core/core.cast/cast.date.js +++ b/test/unit/core/core.cast/cast.date.js @@ -38,5 +38,15 @@ describe('Core Type Casting', function() { assert(values.name.toUTCString() === 'Wed, 18 Sep 2013 00:00:00 GMT'); }); + it('should objects that implement toDate()', function() { + function Foo() {} + Foo.prototype.toDate = function () { return new Date(1379462400000); }; + var values = person._cast.run({ + name: new Foo() + }); + assert(values.name.constructor.name === 'Date'); + assert(values.name.toUTCString() === 'Wed, 18 Sep 2013 00:00:00 GMT'); + }); + }); }); From 852c566b57243c4c0218b998391a603d78d3233b Mon Sep 17 00:00:00 2001 From: Globegitter Date: Tue, 7 Apr 2015 13:05:08 +0100 Subject: [PATCH 02/26] WIP: Added first code for findAndModify. --- lib/waterline/adapter/aggregateQueries.js | 72 ++++++++++- lib/waterline/query/composite.js | 146 +++++++++++++++++++--- lib/waterline/query/deferred.js | 15 ++- 3 files changed, 214 insertions(+), 19 deletions(-) diff --git a/lib/waterline/adapter/aggregateQueries.js b/lib/waterline/adapter/aggregateQueries.js index 8203fc482..7a2baa4d0 100644 --- a/lib/waterline/adapter/aggregateQueries.js +++ b/lib/waterline/adapter/aggregateQueries.js @@ -88,7 +88,7 @@ module.exports = { // Check that each of the criteria keys match: // build a criteria query var criteria = {}; - + attributesToCheck.forEach(function (attrName) { criteria[attrName] = values[attrName]; }); @@ -105,6 +105,76 @@ module.exports = { if(err) return cb(err); cb(null, models); }); + }, + + // If an optimized findAndModify exists, use it, otherwise use an asynchronous loop with create() +findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { + console.log('In findAndModifyEach in aggregateQueries.js'); + var self = this; + var connName; + var adapter; + + if(typeof options === 'function') { + cb = options; + // set default values + options = { + upsert: false, + new: false, + mergeArrays: false + }; } + // Normalize Arguments + cb = normalize.callback(cb); + + // Clone sensitive data + attributesToCheck = _.clone(attributesToCheck); + valuesList = _.clone(valuesList); + + // Custom user adapter behavior + if(hasOwnProperty(this.dictionary, 'findAndModifyEach')) { + connName = this.dictionary.findAndModifyEach; + adapter = this.connections[connName]._adapter; + + if(hasOwnProperty(adapter, 'findAndModifyEach')) { + return adapter.findAndModifyEach(connName, this.collection, valuesList, options, cb); + } + } + + // Build a list of models + var models = []; + var i = 0; + + async.eachSeries(valuesList, function (values, cb) { + if (!_.isObject(values)) return cb(new Error('findAndModifyEach: Unexpected value in valuesList.')); + + // Check that each of the criteria keys match: + // build a criteria query + var criteria = {}; + + if (_.isObject(attributesToCheck[i])){ + Object.keys(attributesToCheck[i]).forEach(function (attrName) { + criteria[attrName] = values[attrName]; + }); + i++; + } else { + attributesToCheck.forEach(function (attrName) { + criteria[attrName] = values[attrName]; + }); + } + + return self.findAndModify.call(self, criteria, values, options, function (err, model) { + if(err) return cb(err); + + // Add model to list + if(model) models.push(model); + + cb(null, model); + }); + }, function (err) { + if(err) return cb(err); + cb(null, models); + }); +} + }; diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index 0c53f53cd..fa3d2df26 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -1,25 +1,25 @@ /** - * Composite Queries - */ +* Composite Queries +*/ var async = require('async'), - _ = require('lodash'), - usageError = require('../utils/usageError'), - utils = require('../utils/helpers'), - normalize = require('../utils/normalize'), - Deferred = require('./deferred'), - hasOwnProperty = utils.object.hasOwnProperty; +_ = require('lodash'), +usageError = require('../utils/usageError'), +utils = require('../utils/helpers'), +normalize = require('../utils/normalize'), +Deferred = require('./deferred'), +hasOwnProperty = utils.object.hasOwnProperty; module.exports = { /** - * Find or Create a New Record - * - * @param {Object} search criteria - * @param {Object} values to create if no record found - * @param {Function} callback - * @return Deferred object if no callback - */ + * Find or Create a New Record + * + * @param {Object} search criteria + * @param {Object} values to create if no record found + * @param {Function} callback + * @return Deferred object if no callback + */ findOrCreate: function(criteria, values, cb) { var self = this; @@ -72,6 +72,122 @@ module.exports = { return cb(null, result); }); }); + }, + + /** + * Finds and Updates a Record. If upsert is passed also creates it when it does + * not find it. + * + * @param {Object} criteria search criteria + * @param {Object} values values to update if record found + * @param {Object} [options] + * @param {Boolean} [options.upsert] If true, creates the object if not found + * @param {Boolean} [options.new] If true returns the newly created object, otherwise + * returns either the model before it was updated/created + * @param {Boolean} [options.mergeArrays] If true, merges any arrays passed in values + * @param {Function} [cb] callback + * @return Deferred object if no callback is given + */ + findAndModify: function(criteria, values, options, cb){ + var self = this; + + if(typeof options === 'function') { + cb = options; + // set default values + options = { + upsert: false, + new: false, + mergeArrays: false + }; + } + + // If no criteria is specified, bail out with a vengeance. + var usage = utils.capitalize(this.identity) + '.findAndModify([criteria], values, upsert, new, callback)'; + if(typeof cb == 'function' && (!criteria || criteria.length === 0)) { + return usageError('No criteria option specified!', usage, cb); + } + + // If no values are specified, bail out with a vengeance. + usage = utils.capitalize(this.identity) + '.findAndModify(criteria, [values], upsert, new, callback)'; + if(typeof cb == 'function' && (!values || values.length === 0)) { + return usageError('No values option specified!', usage, cb); + } + + // Normalize criteria + criteria = normalize.criteria(criteria); + + // Return Deferred or pass to adapter + if(typeof cb !== 'function') { + return new Deferred(this, this.findAndModify, criteria, values, options); + } + + // If an array of length 1 is passed convert, otherwise call findAndModifyEach + if(Array.isArray(criteria) && Array.isArray(values)) { + if (criteria.length > 1 && values.length > 1) { + // return usageError('Passing an array of models is not supported yet!', usage, cb); + return this.findAndModifyEach(criteria, values, options, cb); + } else { + criteria = criteria[0]; + values = values[0]; + } + } + + // if(typeof cb !== 'function') return usageError('Invalid callback specified!', usage, cb); + + // Try a find first. + this.find(criteria).exec(function(err, results) { + if (err) return cb(err); + + if (results && results.length !== 0) { + + if (options.mergeArrays) { + var resultKeys = Object.keys(results); + //Loop over all the results to see if it contains an array + for (var i = 0; i < resultKeys.length; i++) { + //if an array was found check if it is also a given array in values + //before merging them + if (Array.isArray(results)) { + if (resultKeys[i] in values && Array.isArray(values[resultKeys[i]])) { + //now update the values array + values[resultKeys[i]] = results[resultKeys[i]].concat(values[[resultKeys[i]]]); + } + } + } + } + + //Then update + self.update(criteria, values).exec(function(err, updatedResults) { + // if new is given return the model after it has been updated + if (options.new) { + // Unserialize values + results = self._transformer.unserialize(updatedResults[0]); + } else { + // Unserialize values + results = self._transformer.unserialize(results[0]); + } + + + // Return an instance of Model + var model = new self._model(results); + return cb(null, results); + }); + } else if (options.upsert) { + // Create a new record if nothing is found and upsert is true. + self.create(values).exec(function(err, result) { + if(err) return cb(err); + + // if new is given return the model after it has been created + //an empty array otherwise + if (options.new) { + return cb(null, result); + } else { + return cb(null, []); + } + }); + } else { + return cb(null, []); + } + }); } }; diff --git a/lib/waterline/query/deferred.js b/lib/waterline/query/deferred.js index 3042627bc..238ffd530 100644 --- a/lib/waterline/query/deferred.js +++ b/lib/waterline/query/deferred.js @@ -16,7 +16,7 @@ var Promise = require('bluebird'), // that were created using Q Promise.prototype.fail = Promise.prototype.catch; -var Deferred = module.exports = function(context, method, criteria, values) { +var Deferred = module.exports = function(context, method, criteria, values, options) { if(!context) return new Error('Must supply a context to a new Deferred object. Usage: new Deferred(context, method, criteria)'); if(!method) return new Error('Must supply a method to a new Deferred object. Usage: new Deferred(context, method, criteria)'); @@ -25,6 +25,7 @@ var Deferred = module.exports = function(context, method, criteria, values) { this._method = method; this._criteria = criteria; this._values = values || null; + this._options = options || { }; this._deferred = null; // deferred object for promises @@ -494,8 +495,16 @@ Deferred.prototype.exec = function(cb) { cb = normalize.callback(cb); // Set up arguments + callback - var args = [this._criteria, cb]; - if(this._values) args.splice(1, 0, this._values); + var args = [this._criteria]; + + if(this._values) { + args.push(this._values); + } + + if(this._options && Object.keys(this._options).length > 0) { + args.push(this._options); + } + args.push(cb); // Pass control to the adapter with the appropriate arguments. this._method.apply(this._context, args); From 44588d33a03120eec539ee1bcecc8b006f28075d Mon Sep 17 00:00:00 2001 From: Globegitter Date: Tue, 7 Apr 2015 13:05:08 +0100 Subject: [PATCH 03/26] WIP: Added first code for findAndModify. --- lib/waterline/adapter/aggregateQueries.js | 70 +++++++++++ lib/waterline/query/composite.js | 146 +++++++++++++++++++--- lib/waterline/query/deferred.js | 15 ++- 3 files changed, 213 insertions(+), 18 deletions(-) diff --git a/lib/waterline/adapter/aggregateQueries.js b/lib/waterline/adapter/aggregateQueries.js index d286f19f3..5fb97d5e8 100644 --- a/lib/waterline/adapter/aggregateQueries.js +++ b/lib/waterline/adapter/aggregateQueries.js @@ -127,6 +127,76 @@ module.exports = { if(err) return cb(err); cb(null, models); }); + }, + + // If an optimized findAndModify exists, use it, otherwise use an asynchronous loop with create() +findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { + console.log('In findAndModifyEach in aggregateQueries.js'); + var self = this; + var connName; + var adapter; + + if(typeof options === 'function') { + cb = options; + // set default values + options = { + upsert: false, + new: false, + mergeArrays: false + }; } + // Normalize Arguments + cb = normalize.callback(cb); + + // Clone sensitive data + attributesToCheck = _.clone(attributesToCheck); + valuesList = _.clone(valuesList); + + // Custom user adapter behavior + if(hasOwnProperty(this.dictionary, 'findAndModifyEach')) { + connName = this.dictionary.findAndModifyEach; + adapter = this.connections[connName]._adapter; + + if(hasOwnProperty(adapter, 'findAndModifyEach')) { + return adapter.findAndModifyEach(connName, this.collection, valuesList, options, cb); + } + } + + // Build a list of models + var models = []; + var i = 0; + + async.eachSeries(valuesList, function (values, cb) { + if (!_.isObject(values)) return cb(new Error('findAndModifyEach: Unexpected value in valuesList.')); + + // Check that each of the criteria keys match: + // build a criteria query + var criteria = {}; + + if (_.isObject(attributesToCheck[i])){ + Object.keys(attributesToCheck[i]).forEach(function (attrName) { + criteria[attrName] = values[attrName]; + }); + i++; + } else { + attributesToCheck.forEach(function (attrName) { + criteria[attrName] = values[attrName]; + }); + } + + return self.findAndModify.call(self, criteria, values, options, function (err, model) { + if(err) return cb(err); + + // Add model to list + if(model) models.push(model); + + cb(null, model); + }); + }, function (err) { + if(err) return cb(err); + cb(null, models); + }); +} + }; diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index 0c53f53cd..fa3d2df26 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -1,25 +1,25 @@ /** - * Composite Queries - */ +* Composite Queries +*/ var async = require('async'), - _ = require('lodash'), - usageError = require('../utils/usageError'), - utils = require('../utils/helpers'), - normalize = require('../utils/normalize'), - Deferred = require('./deferred'), - hasOwnProperty = utils.object.hasOwnProperty; +_ = require('lodash'), +usageError = require('../utils/usageError'), +utils = require('../utils/helpers'), +normalize = require('../utils/normalize'), +Deferred = require('./deferred'), +hasOwnProperty = utils.object.hasOwnProperty; module.exports = { /** - * Find or Create a New Record - * - * @param {Object} search criteria - * @param {Object} values to create if no record found - * @param {Function} callback - * @return Deferred object if no callback - */ + * Find or Create a New Record + * + * @param {Object} search criteria + * @param {Object} values to create if no record found + * @param {Function} callback + * @return Deferred object if no callback + */ findOrCreate: function(criteria, values, cb) { var self = this; @@ -72,6 +72,122 @@ module.exports = { return cb(null, result); }); }); + }, + + /** + * Finds and Updates a Record. If upsert is passed also creates it when it does + * not find it. + * + * @param {Object} criteria search criteria + * @param {Object} values values to update if record found + * @param {Object} [options] + * @param {Boolean} [options.upsert] If true, creates the object if not found + * @param {Boolean} [options.new] If true returns the newly created object, otherwise + * returns either the model before it was updated/created + * @param {Boolean} [options.mergeArrays] If true, merges any arrays passed in values + * @param {Function} [cb] callback + * @return Deferred object if no callback is given + */ + findAndModify: function(criteria, values, options, cb){ + var self = this; + + if(typeof options === 'function') { + cb = options; + // set default values + options = { + upsert: false, + new: false, + mergeArrays: false + }; + } + + // If no criteria is specified, bail out with a vengeance. + var usage = utils.capitalize(this.identity) + '.findAndModify([criteria], values, upsert, new, callback)'; + if(typeof cb == 'function' && (!criteria || criteria.length === 0)) { + return usageError('No criteria option specified!', usage, cb); + } + + // If no values are specified, bail out with a vengeance. + usage = utils.capitalize(this.identity) + '.findAndModify(criteria, [values], upsert, new, callback)'; + if(typeof cb == 'function' && (!values || values.length === 0)) { + return usageError('No values option specified!', usage, cb); + } + + // Normalize criteria + criteria = normalize.criteria(criteria); + + // Return Deferred or pass to adapter + if(typeof cb !== 'function') { + return new Deferred(this, this.findAndModify, criteria, values, options); + } + + // If an array of length 1 is passed convert, otherwise call findAndModifyEach + if(Array.isArray(criteria) && Array.isArray(values)) { + if (criteria.length > 1 && values.length > 1) { + // return usageError('Passing an array of models is not supported yet!', usage, cb); + return this.findAndModifyEach(criteria, values, options, cb); + } else { + criteria = criteria[0]; + values = values[0]; + } + } + + // if(typeof cb !== 'function') return usageError('Invalid callback specified!', usage, cb); + + // Try a find first. + this.find(criteria).exec(function(err, results) { + if (err) return cb(err); + + if (results && results.length !== 0) { + + if (options.mergeArrays) { + var resultKeys = Object.keys(results); + //Loop over all the results to see if it contains an array + for (var i = 0; i < resultKeys.length; i++) { + //if an array was found check if it is also a given array in values + //before merging them + if (Array.isArray(results)) { + if (resultKeys[i] in values && Array.isArray(values[resultKeys[i]])) { + //now update the values array + values[resultKeys[i]] = results[resultKeys[i]].concat(values[[resultKeys[i]]]); + } + } + } + } + + //Then update + self.update(criteria, values).exec(function(err, updatedResults) { + // if new is given return the model after it has been updated + if (options.new) { + // Unserialize values + results = self._transformer.unserialize(updatedResults[0]); + } else { + // Unserialize values + results = self._transformer.unserialize(results[0]); + } + + + // Return an instance of Model + var model = new self._model(results); + return cb(null, results); + }); + } else if (options.upsert) { + // Create a new record if nothing is found and upsert is true. + self.create(values).exec(function(err, result) { + if(err) return cb(err); + + // if new is given return the model after it has been created + //an empty array otherwise + if (options.new) { + return cb(null, result); + } else { + return cb(null, []); + } + }); + } else { + return cb(null, []); + } + }); } }; diff --git a/lib/waterline/query/deferred.js b/lib/waterline/query/deferred.js index 3042627bc..238ffd530 100644 --- a/lib/waterline/query/deferred.js +++ b/lib/waterline/query/deferred.js @@ -16,7 +16,7 @@ var Promise = require('bluebird'), // that were created using Q Promise.prototype.fail = Promise.prototype.catch; -var Deferred = module.exports = function(context, method, criteria, values) { +var Deferred = module.exports = function(context, method, criteria, values, options) { if(!context) return new Error('Must supply a context to a new Deferred object. Usage: new Deferred(context, method, criteria)'); if(!method) return new Error('Must supply a method to a new Deferred object. Usage: new Deferred(context, method, criteria)'); @@ -25,6 +25,7 @@ var Deferred = module.exports = function(context, method, criteria, values) { this._method = method; this._criteria = criteria; this._values = values || null; + this._options = options || { }; this._deferred = null; // deferred object for promises @@ -494,8 +495,16 @@ Deferred.prototype.exec = function(cb) { cb = normalize.callback(cb); // Set up arguments + callback - var args = [this._criteria, cb]; - if(this._values) args.splice(1, 0, this._values); + var args = [this._criteria]; + + if(this._values) { + args.push(this._values); + } + + if(this._options && Object.keys(this._options).length > 0) { + args.push(this._options); + } + args.push(cb); // Pass control to the adapter with the appropriate arguments. this._method.apply(this._context, args); From ec1bee42423339e8e86be4fbee7f2576c5374985 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Tue, 7 Apr 2015 13:05:08 +0100 Subject: [PATCH 04/26] WIP: Added first code for findAndModify. --- lib/waterline/adapter/aggregateQueries.js | 70 +++++++++++ lib/waterline/query/composite.js | 146 +++++++++++++++++++--- lib/waterline/query/deferred.js | 15 ++- 3 files changed, 213 insertions(+), 18 deletions(-) diff --git a/lib/waterline/adapter/aggregateQueries.js b/lib/waterline/adapter/aggregateQueries.js index d286f19f3..5fb97d5e8 100644 --- a/lib/waterline/adapter/aggregateQueries.js +++ b/lib/waterline/adapter/aggregateQueries.js @@ -127,6 +127,76 @@ module.exports = { if(err) return cb(err); cb(null, models); }); + }, + + // If an optimized findAndModify exists, use it, otherwise use an asynchronous loop with create() +findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { + console.log('In findAndModifyEach in aggregateQueries.js'); + var self = this; + var connName; + var adapter; + + if(typeof options === 'function') { + cb = options; + // set default values + options = { + upsert: false, + new: false, + mergeArrays: false + }; } + // Normalize Arguments + cb = normalize.callback(cb); + + // Clone sensitive data + attributesToCheck = _.clone(attributesToCheck); + valuesList = _.clone(valuesList); + + // Custom user adapter behavior + if(hasOwnProperty(this.dictionary, 'findAndModifyEach')) { + connName = this.dictionary.findAndModifyEach; + adapter = this.connections[connName]._adapter; + + if(hasOwnProperty(adapter, 'findAndModifyEach')) { + return adapter.findAndModifyEach(connName, this.collection, valuesList, options, cb); + } + } + + // Build a list of models + var models = []; + var i = 0; + + async.eachSeries(valuesList, function (values, cb) { + if (!_.isObject(values)) return cb(new Error('findAndModifyEach: Unexpected value in valuesList.')); + + // Check that each of the criteria keys match: + // build a criteria query + var criteria = {}; + + if (_.isObject(attributesToCheck[i])){ + Object.keys(attributesToCheck[i]).forEach(function (attrName) { + criteria[attrName] = values[attrName]; + }); + i++; + } else { + attributesToCheck.forEach(function (attrName) { + criteria[attrName] = values[attrName]; + }); + } + + return self.findAndModify.call(self, criteria, values, options, function (err, model) { + if(err) return cb(err); + + // Add model to list + if(model) models.push(model); + + cb(null, model); + }); + }, function (err) { + if(err) return cb(err); + cb(null, models); + }); +} + }; diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index 0c53f53cd..fa3d2df26 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -1,25 +1,25 @@ /** - * Composite Queries - */ +* Composite Queries +*/ var async = require('async'), - _ = require('lodash'), - usageError = require('../utils/usageError'), - utils = require('../utils/helpers'), - normalize = require('../utils/normalize'), - Deferred = require('./deferred'), - hasOwnProperty = utils.object.hasOwnProperty; +_ = require('lodash'), +usageError = require('../utils/usageError'), +utils = require('../utils/helpers'), +normalize = require('../utils/normalize'), +Deferred = require('./deferred'), +hasOwnProperty = utils.object.hasOwnProperty; module.exports = { /** - * Find or Create a New Record - * - * @param {Object} search criteria - * @param {Object} values to create if no record found - * @param {Function} callback - * @return Deferred object if no callback - */ + * Find or Create a New Record + * + * @param {Object} search criteria + * @param {Object} values to create if no record found + * @param {Function} callback + * @return Deferred object if no callback + */ findOrCreate: function(criteria, values, cb) { var self = this; @@ -72,6 +72,122 @@ module.exports = { return cb(null, result); }); }); + }, + + /** + * Finds and Updates a Record. If upsert is passed also creates it when it does + * not find it. + * + * @param {Object} criteria search criteria + * @param {Object} values values to update if record found + * @param {Object} [options] + * @param {Boolean} [options.upsert] If true, creates the object if not found + * @param {Boolean} [options.new] If true returns the newly created object, otherwise + * returns either the model before it was updated/created + * @param {Boolean} [options.mergeArrays] If true, merges any arrays passed in values + * @param {Function} [cb] callback + * @return Deferred object if no callback is given + */ + findAndModify: function(criteria, values, options, cb){ + var self = this; + + if(typeof options === 'function') { + cb = options; + // set default values + options = { + upsert: false, + new: false, + mergeArrays: false + }; + } + + // If no criteria is specified, bail out with a vengeance. + var usage = utils.capitalize(this.identity) + '.findAndModify([criteria], values, upsert, new, callback)'; + if(typeof cb == 'function' && (!criteria || criteria.length === 0)) { + return usageError('No criteria option specified!', usage, cb); + } + + // If no values are specified, bail out with a vengeance. + usage = utils.capitalize(this.identity) + '.findAndModify(criteria, [values], upsert, new, callback)'; + if(typeof cb == 'function' && (!values || values.length === 0)) { + return usageError('No values option specified!', usage, cb); + } + + // Normalize criteria + criteria = normalize.criteria(criteria); + + // Return Deferred or pass to adapter + if(typeof cb !== 'function') { + return new Deferred(this, this.findAndModify, criteria, values, options); + } + + // If an array of length 1 is passed convert, otherwise call findAndModifyEach + if(Array.isArray(criteria) && Array.isArray(values)) { + if (criteria.length > 1 && values.length > 1) { + // return usageError('Passing an array of models is not supported yet!', usage, cb); + return this.findAndModifyEach(criteria, values, options, cb); + } else { + criteria = criteria[0]; + values = values[0]; + } + } + + // if(typeof cb !== 'function') return usageError('Invalid callback specified!', usage, cb); + + // Try a find first. + this.find(criteria).exec(function(err, results) { + if (err) return cb(err); + + if (results && results.length !== 0) { + + if (options.mergeArrays) { + var resultKeys = Object.keys(results); + //Loop over all the results to see if it contains an array + for (var i = 0; i < resultKeys.length; i++) { + //if an array was found check if it is also a given array in values + //before merging them + if (Array.isArray(results)) { + if (resultKeys[i] in values && Array.isArray(values[resultKeys[i]])) { + //now update the values array + values[resultKeys[i]] = results[resultKeys[i]].concat(values[[resultKeys[i]]]); + } + } + } + } + + //Then update + self.update(criteria, values).exec(function(err, updatedResults) { + // if new is given return the model after it has been updated + if (options.new) { + // Unserialize values + results = self._transformer.unserialize(updatedResults[0]); + } else { + // Unserialize values + results = self._transformer.unserialize(results[0]); + } + + + // Return an instance of Model + var model = new self._model(results); + return cb(null, results); + }); + } else if (options.upsert) { + // Create a new record if nothing is found and upsert is true. + self.create(values).exec(function(err, result) { + if(err) return cb(err); + + // if new is given return the model after it has been created + //an empty array otherwise + if (options.new) { + return cb(null, result); + } else { + return cb(null, []); + } + }); + } else { + return cb(null, []); + } + }); } }; diff --git a/lib/waterline/query/deferred.js b/lib/waterline/query/deferred.js index 3042627bc..238ffd530 100644 --- a/lib/waterline/query/deferred.js +++ b/lib/waterline/query/deferred.js @@ -16,7 +16,7 @@ var Promise = require('bluebird'), // that were created using Q Promise.prototype.fail = Promise.prototype.catch; -var Deferred = module.exports = function(context, method, criteria, values) { +var Deferred = module.exports = function(context, method, criteria, values, options) { if(!context) return new Error('Must supply a context to a new Deferred object. Usage: new Deferred(context, method, criteria)'); if(!method) return new Error('Must supply a method to a new Deferred object. Usage: new Deferred(context, method, criteria)'); @@ -25,6 +25,7 @@ var Deferred = module.exports = function(context, method, criteria, values) { this._method = method; this._criteria = criteria; this._values = values || null; + this._options = options || { }; this._deferred = null; // deferred object for promises @@ -494,8 +495,16 @@ Deferred.prototype.exec = function(cb) { cb = normalize.callback(cb); // Set up arguments + callback - var args = [this._criteria, cb]; - if(this._values) args.splice(1, 0, this._values); + var args = [this._criteria]; + + if(this._values) { + args.push(this._values); + } + + if(this._options && Object.keys(this._options).length > 0) { + args.push(this._options); + } + args.push(cb); // Pass control to the adapter with the appropriate arguments. this._method.apply(this._context, args); From 25e8d5254504043a9f345ad22cff12a0e7ba3d8e Mon Sep 17 00:00:00 2001 From: Globegitter Date: Wed, 8 Apr 2015 15:49:50 +0100 Subject: [PATCH 05/26] Fixed findAndModify. Added unit tests. --- lib/waterline/adapter/aggregateQueries.js | 25 ++- lib/waterline/query/aggregate.js | 200 +++++++++++++++------- test/unit/query/query.findAndModify.js | 164 ++++++++++++++++++ 3 files changed, 319 insertions(+), 70 deletions(-) create mode 100644 test/unit/query/query.findAndModify.js diff --git a/lib/waterline/adapter/aggregateQueries.js b/lib/waterline/adapter/aggregateQueries.js index 5fb97d5e8..8de837787 100644 --- a/lib/waterline/adapter/aggregateQueries.js +++ b/lib/waterline/adapter/aggregateQueries.js @@ -146,6 +146,15 @@ findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { }; } + var isObjectArray = false; + if (_.isObject(attributesToCheck[0])) { + if (attributesToCheck.length > 1 && + attributesToCheck.length !== valuesList.length) { + return cb(new Error('findAndModifyEach: The two passed arrays have to be of the same length.')); + } + isObjectArray = true; + } + // Normalize Arguments cb = normalize.callback(cb); @@ -174,11 +183,17 @@ findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { // build a criteria query var criteria = {}; - if (_.isObject(attributesToCheck[i])){ - Object.keys(attributesToCheck[i]).forEach(function (attrName) { - criteria[attrName] = values[attrName]; - }); - i++; + if (isObjectArray) { + if (_.isObject(attributesToCheck[i])) { + Object.keys(attributesToCheck[i]).forEach(function (attrName) { + criteria[attrName] = values[attrName]; + }); + if (attributesToCheck.length > 1) { + i++; + } + } else { + return cb(new Error('findOrCreateEach: Element ' + i + ' in attributesToCheck is not an object.' )); + } } else { attributesToCheck.forEach(function (attrName) { criteria[attrName] = values[attrName]; diff --git a/lib/waterline/query/aggregate.js b/lib/waterline/query/aggregate.js index f6f44ab49..6463371e4 100644 --- a/lib/waterline/query/aggregate.js +++ b/lib/waterline/query/aggregate.js @@ -89,98 +89,168 @@ module.exports = { valuesList = null; } - // Normalize criteria - criteria = normalize.criteria(criteria); + return _asyncRun('findOrCreateEach', this, criteria, valuesList, null, cb); - // Return Deferred or pass to adapter - if(typeof cb !== 'function') { - return new Deferred(this, this.findOrCreateEach, criteria, valuesList); + }, + /** + * Iterate through a list of objects, trying to find each one + * For any that exist update them. If upsert is true, create all that + * don't exits + * + * @param {Object} criteria search criteria + * @param {Object} values values to update if record found + * @param {Object} [options] + * @param {Boolean} [options.upsert] If true, creates the object if not found + * @param {Boolean} [options.new] If true returns the newly created object, otherwise + * returns either the model before it was updated/created + * @param {Boolean} [options.mergeArrays] If true, merges any arrays passed in values + * @param {Function} [cb] callback + * @return Deferred object if no callback is given + */ + + findAndModifyEach: function(criteria, valuesList, options, cb) { + var self = this; + + if(typeof options === 'function') { + // cb = options; + // set default values + options = { + upsert: false, + new: false, + mergeArrays: false + }; } - // Validate Params - var usage = utils.capitalize(this.identity) + '.findOrCreateEach(criteria, valuesList, callback)'; + return _asyncRun('findAndModifyEach', this, criteria, valuesList, options, cb); + } +}; - if(typeof cb !== 'function') return usageError('Invalid callback specified!', usage, cb); - if(!criteria) return usageError('No criteria specified!', usage, cb); - if(!Array.isArray(criteria)) return usageError('No criteria specified!', usage, cb); - if(!valuesList) return usageError('No valuesList specified!', usage, cb); - if(!Array.isArray(valuesList)) return usageError('Invalid valuesList specified (should be an array!)', usage, cb); +/** + * Runs given queryType. Right now that is either findAndModifyEach or findOrCreateEach + * Essentially just a helper function for shared code. + * + * @param {String} queryType + * @param {Array} criteria + * @param {Array} valuesList + * @param {Object} [options] depends on which queryType calls, if given or not + * @param {Function} [cb] + * @return {String} + * @api private + */ - var errStr = _validateValues(valuesList); - if(errStr) return usageError(errStr, usage, cb); +function _asyncRun(queryType, self, criteria, valuesList, options, cb) { + // Normalize criteria + criteria = normalize.criteria(criteria); - // Validate each record in the array and if all are valid - // pass the array to the adapter's findOrCreateEach method - var validateItem = function(item, next) { - _validate.call(self, item, next); - } + if (typeof options === 'function') { + cb = options; + options = null; + } + var optionsString = ''; + if (options) { + optionsString = ', options' + } - async.each(valuesList, validateItem, function(err) { - if(err) return cb(err); + // Return Deferred or pass to adapter + if(typeof cb !== 'function') { + return new Deferred(this, this[queryType], criteria, valuesList, options); + } - // Transform Values - var transformedValues = []; + // Validate Params + var usage = utils.capitalize(this.identity) + '.' + queryType + '(criteria, valuesList ' + optionsString + ', callback)'; - valuesList.forEach(function(value) { + if(typeof cb !== 'function') return usageError('Invalid callback specified!', usage, cb); + if(!criteria) return usageError('No criteria specified!', usage, cb); + if(!Array.isArray(criteria)) return usageError('No criteria specified!', usage, cb); + if(!valuesList) return usageError('No valuesList specified!', usage, cb); + if(!Array.isArray(valuesList)) return usageError('Invalid valuesList specified (should be an array!)', usage, cb); - // Transform values - value = self._transformer.serialize(value); + var errStr = _validateValues(valuesList); + if(errStr) return usageError(errStr, usage, cb); - // Clean attributes - value = self._schema.cleanValues(value); - transformedValues.push(value); - }); + // Validate each record in the array and if all are valid + // pass the array to the adapter's findOrCreateEach method + var validateItem = function(item, next) { + _validate.call(self, item, next); + } - // Set values array to the transformed array - valuesList = transformedValues; - // Transform Search Criteria - var transformedCriteria = []; + async.each(valuesList, validateItem, function(err) { + if(err) return cb(err); - criteria.forEach(function(value) { - value = self._transformer.serialize(value); - transformedCriteria.push(value); - }); + // Transform Values + var transformedValues = []; - // Set criteria array to the transformed array - criteria = transformedCriteria; + valuesList.forEach(function(value) { - // Pass criteria and attributes to adapter definition - self.adapter.findOrCreateEach(criteria, valuesList, function(err, values) { - if(err) return cb(err); + // Transform values + value = self._transformer.serialize(value); - // Unserialize Values - var unserializedValues = []; + // Clean attributes + value = self._schema.cleanValues(value); + transformedValues.push(value); + }); - values.forEach(function(value) { - value = self._transformer.unserialize(value); - unserializedValues.push(value); - }); + // Set values array to the transformed array + valuesList = transformedValues; + + // Transform Search Criteria + var transformedCriteria = []; + + criteria.forEach(function(value) { + value = self._transformer.serialize(value); + transformedCriteria.push(value); + }); + + // Set criteria array to the transformed array + criteria = transformedCriteria; + + var cbFunction = function(err, values) { + if(err) return cb(err); - // Set values array to the transformed array - values = unserializedValues; + // Unserialize Values + var unserializedValues = []; - // Run AfterCreate Callbacks - async.each(values, function(item, next) { - callbacks.afterCreate(self, item, next); - }, function(err) { - if(err) return cb(err); + values.forEach(function(value) { + value = self._transformer.unserialize(value); + unserializedValues.push(value); + }); + + // Set values array to the transformed array + values = unserializedValues; - var models = []; + // Run AfterCreate Callbacks + async.each(values, function(item, next) { + callbacks.afterCreate(self, item, next); + }, function(err) { + if(err) return cb(err); - // Make each result an instance of model - values.forEach(function(value) { - models.push(new self._model(value)); - }); + var models = []; - cb(null, models); + // Make each result an instance of model + values.forEach(function(value) { + models.push(new self._model(value)); }); + + cb(null, models); }); - }); - } -}; + }; + var params = [criteria, valuesList]; + + //Currently only the findAndModifyEach function has an options parameter + if (queryType === 'findAndModifyEach') { + params.push(options); + } + + params.push(cbFunction); + console.log('calling ' + queryType + 'function from _asyncRun'); + // Pass criteria and attributes to adapter definition + //the first argument makes sure the function has access to the right 'this' + self.adapter[queryType].apply(self.adapter, params); + }); +} /** * Validate valuesList diff --git a/test/unit/query/query.findAndModify.js b/test/unit/query/query.findAndModify.js new file mode 100644 index 000000000..ecf5684a5 --- /dev/null +++ b/test/unit/query/query.findAndModify.js @@ -0,0 +1,164 @@ +var Waterline = require('../../../lib/waterline'), + assert = require('assert'); + +describe('Collection Query', function() { + + describe('.findAndModify()', function() { + + describe('with proper values', function() { + var query; + + before(function(done) { + + var waterline = new Waterline(); + var Model = Waterline.Collection.extend({ + identity: 'user', + connection: 'foo', + attributes: { + name: { + type: 'string', + defaultsTo: 'Foo Bar' + }, + doSomething: function() {} + } + }); + + waterline.loadCollection(Model); + + // Fixture Adapter Def + var adapterDef = { + find: function(con, col, criteria, cb) { return cb(null, []); }, + create: function(con, col, values, cb) { return cb(null, values); } + }; + + var connections = { + 'foo': { + adapter: 'foobar' + } + }; + + waterline.initialize({ adapters: { foobar: adapterDef }, connections: connections }, function(err, colls) { + if(err) return done(err); + query = colls.collections.user; + done(); + }); + }); + + it('should get empty array without new and upsert flag', function(done) { + query.findAndModify({ }, { name: 'Foo Bar' }, function(err, status) { + assert(status.length === 0); + done(); + }); + }); + + it('should get empty array with exec', function(done) { + query.findAndModify({ }, { name: 'Foo Bar' }).exec(function(err, status) { + assert(status.length === 0); + done(); + }); + }); + + it('should get an empty array if model does not exist and without upsert flag', function(done) { + query.findAndModify({ }, { name: 'Bar Foo'}, { new: true }).exec(function(err, status) { + assert(status.length === 0); + done(); + }); + }); + + it('should work with upsert and new options', function(done) { + query.findAndModify({ }, { name: 'Bar Foo'}, { upsert: true, new: true }).exec(function(err, status) { + assert(status.name === 'Bar Foo'); + done(); + }); + }); + + it('should work with multiple objects', function(done) { + query.findAndModify({ }, [{ name: 'Bar Foo'}, { name: 'Makis' }], { upsert: true, new: true }).exec(function(err, status) { + assert(status[0].name === 'Bar Foo'); + assert(status[1].name === 'Makis'); + done(); + }); + }); + + it('should add timestamps', function(done) { + query.findAndModify({ }, { }, { upsert: true, new: true }, function(err, status) { + assert(status.createdAt); + assert(status.updatedAt); + done(); + }); + }); + + it('should strip values that don\'t belong to the schema', function(done) { + query.findAndModify({ }, { foo: 'bar' }, { upsert: true, new: true }, function(err, values) { + assert(!values.foo); + done(); + }); + }); + + it('should return an instance of Model', function(done) { + query.findAndModify({ }, { name: 'Rice' }, { upsert: true, new: true }, function(err, status) { + assert(typeof status.doSomething === 'function'); + done(); + }); + }); + + it('should allow a query to be built using deferreds', function(done) { + query.findAndModify(null, null, { upsert: true, new: true }) + .where({ }) + .set({ name: 'bob' }) + .exec(function(err, result) { + assert(!err); + assert(result); + assert(result.name === 'bob'); + done(); + }); + }); + }); + + describe('casting values', function() { + var query; + + before(function(done) { + + var waterline = new Waterline(); + var Model = Waterline.Collection.extend({ + identity: 'user', + connection: 'foo', + attributes: { + name: 'string', + age: 'integer' + } + }); + + waterline.loadCollection(Model); + + // Fixture Adapter Def + var adapterDef = { + find: function(con, col, criteria, cb) { return cb(null, []); }, + create: function(con, col, values, cb) { return cb(null, values); } + }; + + var connections = { + 'foo': { + adapter: 'foobar' + } + }; + + waterline.initialize({ adapters: { foobar: adapterDef }, connections: connections }, function(err, colls) { + if(err) return done(err); + query = colls.collections.user; + done(); + }); + }); + + it('should cast values before sending to adapter', function(done) { + query.findAndModify({ }, { name: 'foo', age: '27' }, { upsert: true, new: true }, function(err, values) { + assert(values.name === 'foo'); + assert(values.age === 27); + done(); + }); + }); + }); + + }); +}); From 202a84f8b5dd84364d864184fff0a70e88aac97b Mon Sep 17 00:00:00 2001 From: Globegitter Date: Wed, 8 Apr 2015 18:59:44 +0100 Subject: [PATCH 06/26] Fixed unit tests. --- lib/waterline/adapter/aggregateQueries.js | 3 +- lib/waterline/adapter/compoundQueries.js | 82 +++++++++++++++++++++++ lib/waterline/query/aggregate.js | 5 +- lib/waterline/query/composite.js | 15 ++++- test/unit/query/query.findAndModify.js | 10 +-- 5 files changed, 103 insertions(+), 12 deletions(-) diff --git a/lib/waterline/adapter/aggregateQueries.js b/lib/waterline/adapter/aggregateQueries.js index 8de837787..250960f3d 100644 --- a/lib/waterline/adapter/aggregateQueries.js +++ b/lib/waterline/adapter/aggregateQueries.js @@ -131,7 +131,6 @@ module.exports = { // If an optimized findAndModify exists, use it, otherwise use an asynchronous loop with create() findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { - console.log('In findAndModifyEach in aggregateQueries.js'); var self = this; var connName; var adapter; @@ -141,7 +140,7 @@ findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { // set default values options = { upsert: false, - new: false, + new: true, mergeArrays: false }; } diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index cf6b36d99..8f999438c 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -41,6 +41,88 @@ module.exports = { self.create(values, cb); }); + }, + + findAndModify: function(criteria, values, options, cb) { + console.log('compoundQueries.js'); + var self = this; + var connName; + var adapter; + + if(typeof options === 'function') { + cb = options; + // set default values + options = { + upsert: false, + new: false, + mergeArrays: false + }; + } + + if (typeof values === 'function') { + cb = values; + values = null + } + + options = options || { }; + + //new: true is the default value + if (!('new' in options)) { + options.new = true; + } + + // // If no values were specified, use criteria + // if (!values) values = criteria.where ? criteria.where : criteria; + + // Normalize Arguments + criteria = normalize.criteria(criteria); + cb = normalize.callback(cb); + + // Build Default Error Message + var err = "No find() or create() method defined in adapter!"; + + // Custom user adapter behavior + if(hasOwnProperty(this.dictionary, 'findAndModify')) { + connName = this.dictionary.findOrCreate; + adapter = this.connections[connName]._adapter; + + if(hasOwnProperty(adapter, 'findAndModify')) { + return adapter.findAndModify(connName, this.collection, values, options, cb); + } + } + + // Default behavior + // WARNING: Not transactional! (unless your data adapter is) + this.findOne(criteria, function(err, result) { + if(err) return cb(err); + if(result) { + + self.update(criteria, values, function(err, updatedResults) { + // if new is given return the model after it has been updated + if (options.new) { + return cb(null, updatedResults); + } else { + // Unserialize values + return cb(null, result); + } + + }); + } else if (options.upsert) { + // Create a new record if nothing is found and upsert is true. + //Note(globegitter): This might now ignore the 'options.new' flag + //so need to find a proper way to test/verify that. + self.create(values, function(err, result) { + if(err) return cb(err); + if (options.new) { + return cb(null, result); + } else { + return cb(null, []); + } + }); + } else { + return cb(null, []); + } + }); } }; diff --git a/lib/waterline/query/aggregate.js b/lib/waterline/query/aggregate.js index 6463371e4..7fbad09c9 100644 --- a/lib/waterline/query/aggregate.js +++ b/lib/waterline/query/aggregate.js @@ -154,11 +154,11 @@ function _asyncRun(queryType, self, criteria, valuesList, options, cb) { // Return Deferred or pass to adapter if(typeof cb !== 'function') { - return new Deferred(this, this[queryType], criteria, valuesList, options); + return new Deferred(self, self[queryType], criteria, valuesList, options); } // Validate Params - var usage = utils.capitalize(this.identity) + '.' + queryType + '(criteria, valuesList ' + optionsString + ', callback)'; + var usage = utils.capitalize(self.identity) + '.' + queryType + '(criteria, valuesList ' + optionsString + ', callback)'; if(typeof cb !== 'function') return usageError('Invalid callback specified!', usage, cb); if(!criteria) return usageError('No criteria specified!', usage, cb); @@ -245,7 +245,6 @@ function _asyncRun(queryType, self, criteria, valuesList, options, cb) { } params.push(cbFunction); - console.log('calling ' + queryType + 'function from _asyncRun'); // Pass criteria and attributes to adapter definition //the first argument makes sure the function has access to the right 'this' self.adapter[queryType].apply(self.adapter, params); diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index fa3d2df26..be1840092 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -83,12 +83,12 @@ module.exports = { * @param {Object} [options] * @param {Boolean} [options.upsert] If true, creates the object if not found * @param {Boolean} [options.new] If true returns the newly created object, otherwise - * returns either the model before it was updated/created + * returns either the model before it was updated/created. Defaults to true. * @param {Boolean} [options.mergeArrays] If true, merges any arrays passed in values * @param {Function} [cb] callback * @return Deferred object if no callback is given */ - findAndModify: function(criteria, values, options, cb){ + findAndModify: function(criteria, values, options, cb) { var self = this; if(typeof options === 'function') { @@ -101,6 +101,17 @@ module.exports = { }; } + if (typeof values === 'function') { + cb = values; + values = null + } + + options = options || { }; + + if (!('new' in options)) { + options.new = true; + } + // If no criteria is specified, bail out with a vengeance. var usage = utils.capitalize(this.identity) + '.findAndModify([criteria], values, upsert, new, callback)'; if(typeof cb == 'function' && (!criteria || criteria.length === 0)) { diff --git a/test/unit/query/query.findAndModify.js b/test/unit/query/query.findAndModify.js index ecf5684a5..02d21c282 100644 --- a/test/unit/query/query.findAndModify.js +++ b/test/unit/query/query.findAndModify.js @@ -44,7 +44,7 @@ describe('Collection Query', function() { }); }); - it('should get empty array without new and upsert flag', function(done) { + it('should get empty array without upsert flag', function(done) { query.findAndModify({ }, { name: 'Foo Bar' }, function(err, status) { assert(status.length === 0); done(); @@ -58,15 +58,15 @@ describe('Collection Query', function() { }); }); - it('should get an empty array if model does not exist and without upsert flag', function(done) { - query.findAndModify({ }, { name: 'Bar Foo'}, { new: true }).exec(function(err, status) { + it('should return empty model, before it got created, with new: false', function(done) { + query.findAndModify({ }, { name: 'Bar Foo'}, { upsert: true, new: false }).exec(function(err, status) { assert(status.length === 0); done(); }); }); - it('should work with upsert and new options', function(done) { - query.findAndModify({ }, { name: 'Bar Foo'}, { upsert: true, new: true }).exec(function(err, status) { + it('should return created model with upsert: true option', function(done) { + query.findAndModify({ }, { name: 'Bar Foo'}, { upsert: true }).exec(function(err, status) { assert(status.name === 'Bar Foo'); done(); }); From 2afa42851d01f31428c2e92eb0059d44aaf77245 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Wed, 8 Apr 2015 19:12:52 +0100 Subject: [PATCH 07/26] Removed consoe.log --- lib/waterline/adapter/compoundQueries.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index 8f999438c..48a880316 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -44,7 +44,6 @@ module.exports = { }, findAndModify: function(criteria, values, options, cb) { - console.log('compoundQueries.js'); var self = this; var connName; var adapter; From e420e6c402a5f0fd434315b561aa511a66794575 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Wed, 8 Apr 2015 22:54:48 +0100 Subject: [PATCH 08/26] Return error on update. Fixed some criteria checks. --- lib/waterline/adapter/compoundQueries.js | 3 +++ lib/waterline/query/composite.js | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index 48a880316..e6e8e2337 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -97,6 +97,9 @@ module.exports = { if(result) { self.update(criteria, values, function(err, updatedResults) { + if (err) { + return cb(err); + } // if new is given return the model after it has been updated if (options.new) { return cb(null, updatedResults); diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index 7d19a53bc..257d8d4aa 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -135,10 +135,10 @@ module.exports = { // If an array of length 1 is passed convert, otherwise call findAndModifyEach if(Array.isArray(criteria) && Array.isArray(values)) { - if (criteria.length > 1 && values.length > 1) { + if (criteria.length > 1 || values.length > 1) { // return usageError('Passing an array of models is not supported yet!', usage, cb); return this.findAndModifyEach(criteria, values, options, cb); - } else { + } else if (criteria.length === 1 && values.length === 1){ criteria = criteria[0]; values = values[0]; } @@ -169,6 +169,9 @@ module.exports = { //Then update self.update(criteria, values).exec(function(err, updatedResults) { + if (err) { + return cb(err); + } // if new is given return the model after it has been updated if (options.new) { // Unserialize values From 32d41269625af98b87ab58284f9132ec0c19e422 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Thu, 9 Apr 2015 10:15:25 +0100 Subject: [PATCH 09/26] Get first working implementation of mergeArrays. --- lib/waterline/adapter/compoundQueries.js | 16 ++++++++++++++++ lib/waterline/query/composite.js | 24 +++++++++++++++--------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index e6e8e2337..bedddcb0c 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -96,6 +96,22 @@ module.exports = { if(err) return cb(err); if(result) { + //merging any arrays in the result with any matching arrays in the values + if (options.mergeArrays) { + var resultKeys = Object.keys(result); + //Loop over all the results to see if it contains an array + for (var i = 0; i < resultKeys.length; i++) { + //if an array was found check if it is also a given array in values + //before merging them + if (Array.isArray(result[resultKeys[i]])) { + if (resultKeys[i] in values && Array.isArray(values[resultKeys[i]])) { + //now update the values array + values[resultKeys[i]] = result[resultKeys[i]].concat(values[[resultKeys[i]]]); + } + } + } + } + self.update(criteria, values, function(err, updatedResults) { if (err) { return cb(err); diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index 257d8d4aa..91129a110 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -152,16 +152,22 @@ module.exports = { if (results && results.length !== 0) { + // merging together any passed type: array values with all the found ones + // you should normally only use mergeArrays if you are searching for a unique + // indexed element. if (options.mergeArrays) { - var resultKeys = Object.keys(results); - //Loop over all the results to see if it contains an array - for (var i = 0; i < resultKeys.length; i++) { - //if an array was found check if it is also a given array in values - //before merging them - if (Array.isArray(results)) { - if (resultKeys[i] in values && Array.isArray(values[resultKeys[i]])) { - //now update the values array - values[resultKeys[i]] = results[resultKeys[i]].concat(values[[resultKeys[i]]]); + for (var i = 0; i < results.length; i++) { + var result = results[i]; + var resultKeys = Object.keys(result); + //Loop over all the results to see if it contains an array + for (var j = 0; j < resultKeys.length; j++) { + //if an array was found check if it is also a given array in values + //before merging them + if (Array.isArray(result[resultKeys[j]])) { + if (resultKeys[j] in values && Array.isArray(values[resultKeys[j]])) { + //now update the values object + values[resultKeys[j]] = result[resultKeys[j]].concat(values[[resultKeys[j]]]); + } } } } From def9b0f653d7ddef496834ec0070d8765b4eb1e9 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Thu, 9 Apr 2015 10:37:23 +0100 Subject: [PATCH 10/26] Slightly optimised and simplified mergeArrays implementation. --- lib/waterline/adapter/compoundQueries.js | 20 ++++++++++---------- lib/waterline/query/composite.js | 23 +++++++++++------------ 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index bedddcb0c..def310b19 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -97,17 +97,17 @@ module.exports = { if(result) { //merging any arrays in the result with any matching arrays in the values + // Note: At this point values should just be an object if (options.mergeArrays) { - var resultKeys = Object.keys(result); - //Loop over all the results to see if it contains an array - for (var i = 0; i < resultKeys.length; i++) { - //if an array was found check if it is also a given array in values - //before merging them - if (Array.isArray(result[resultKeys[i]])) { - if (resultKeys[i] in values && Array.isArray(values[resultKeys[i]])) { - //now update the values array - values[resultKeys[i]] = result[resultKeys[i]].concat(values[[resultKeys[i]]]); - } + var valueKeys = Object.keys(values); + // Loop over all the results to see if it contains an array + for (var i = 0; i < valueKeys.length; i++) { + // Check if both properties are an array, if one isn't just ignore + // Note: We do not have to explicitly check if the property exists in the result + // because isArray then just returns false + if (Array.isArray(values[resultKeys[i]]) && Array.isArray(result[resultKeys[i]])) { + //now concatenate the arrays + values[resultKeys[i]] = result[resultKeys[i]].concat(values[[resultKeys[i]]]); } } } diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index 91129a110..d97fe7b40 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -152,22 +152,21 @@ module.exports = { if (results && results.length !== 0) { - // merging together any passed type: array values with all the found ones - // you should normally only use mergeArrays if you are searching for a unique + // merging together any passed 'type: array' values with all the found ones + // you should usually use mergeArrays only if you are searching for a unique // indexed element. if (options.mergeArrays) { for (var i = 0; i < results.length; i++) { var result = results[i]; - var resultKeys = Object.keys(result); - //Loop over all the results to see if it contains an array - for (var j = 0; j < resultKeys.length; j++) { - //if an array was found check if it is also a given array in values - //before merging them - if (Array.isArray(result[resultKeys[j]])) { - if (resultKeys[j] in values && Array.isArray(values[resultKeys[j]])) { - //now update the values object - values[resultKeys[j]] = result[resultKeys[j]].concat(values[[resultKeys[j]]]); - } + var valueKeys = Object.keys(values); + //Loop over all the value properties to see if it contains an array + for (var j = 0; j < valueKeys.length; j++) { + // Check if both properties are an array, if one isn't just ignore + // Note: We do not have to explicitly check if the property exists in the result + // because isArray then just returns false + if (Array.isArray(values[resultKeys[i]]) && Array.isArray(result[resultKeys[i]])) { + //now concatenate the arrays + values[resultKeys[i]] = result[resultKeys[i]].concat(values[[resultKeys[i]]]); } } } From bf7f206b75887ab11d659f879612d4990ea650b8 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Thu, 9 Apr 2015 11:59:21 +0100 Subject: [PATCH 11/26] Forgot to rename to valueKeys. --- lib/waterline/query/composite.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index d97fe7b40..d4e0116b1 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -164,9 +164,9 @@ module.exports = { // Check if both properties are an array, if one isn't just ignore // Note: We do not have to explicitly check if the property exists in the result // because isArray then just returns false - if (Array.isArray(values[resultKeys[i]]) && Array.isArray(result[resultKeys[i]])) { + if (Array.isArray(values[valueKeys[i]]) && Array.isArray(result[valueKeys[i]])) { //now concatenate the arrays - values[resultKeys[i]] = result[resultKeys[i]].concat(values[[resultKeys[i]]]); + values[valueKeys[i]] = result[valueKeys[i]].concat(values[[valueKeys[i]]]); } } } From 6e7a3dddb09369468d8063d40ee3bcd2500c1cb5 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Thu, 9 Apr 2015 12:07:52 +0100 Subject: [PATCH 12/26] Of course also forgot in compoundQueries. --- lib/waterline/adapter/compoundQueries.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index def310b19..77144654b 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -105,9 +105,9 @@ module.exports = { // Check if both properties are an array, if one isn't just ignore // Note: We do not have to explicitly check if the property exists in the result // because isArray then just returns false - if (Array.isArray(values[resultKeys[i]]) && Array.isArray(result[resultKeys[i]])) { + if (Array.isArray(values[valueKeys[i]]) && Array.isArray(result[valueKeys[i]])) { //now concatenate the arrays - values[resultKeys[i]] = result[resultKeys[i]].concat(values[[resultKeys[i]]]); + values[valueKeys[i]] = result[valueKeys[i]].concat(values[[valueKeys[i]]]); } } } From 43a096ff9253fc36de3cf0b70992e64394bd1178 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Thu, 9 Apr 2015 13:51:02 +0100 Subject: [PATCH 13/26] Take union of arrays rather than simply concatenating them. --- lib/waterline/adapter/compoundQueries.js | 4 ++-- lib/waterline/query/composite.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index 77144654b..7b494fef3 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -106,8 +106,8 @@ module.exports = { // Note: We do not have to explicitly check if the property exists in the result // because isArray then just returns false if (Array.isArray(values[valueKeys[i]]) && Array.isArray(result[valueKeys[i]])) { - //now concatenate the arrays - values[valueKeys[i]] = result[valueKeys[i]].concat(values[[valueKeys[i]]]); + //now take the union of the arrays + values[valueKeys[i]] = _.union(result[valueKeys[i]], values[[valueKeys[i]]]); } } } diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index d4e0116b1..fc51a8f9e 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -165,8 +165,8 @@ module.exports = { // Note: We do not have to explicitly check if the property exists in the result // because isArray then just returns false if (Array.isArray(values[valueKeys[i]]) && Array.isArray(result[valueKeys[i]])) { - //now concatenate the arrays - values[valueKeys[i]] = result[valueKeys[i]].concat(values[[valueKeys[i]]]); + //now take the union of the arrays + values[valueKeys[i]] = _.union(result[valueKeys[i]], values[[valueKeys[i]]]); } } } From a1b4cf2a98e72eb52907da447ba38a360ae38b33 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Tue, 7 Apr 2015 13:05:08 +0100 Subject: [PATCH 14/26] WIP: Added first code for findAndModify. --- lib/waterline/adapter/aggregateQueries.js | 70 +++++++++++ lib/waterline/query/composite.js | 146 +++++++++++++++++++--- lib/waterline/query/deferred.js | 15 ++- 3 files changed, 213 insertions(+), 18 deletions(-) diff --git a/lib/waterline/adapter/aggregateQueries.js b/lib/waterline/adapter/aggregateQueries.js index d286f19f3..5fb97d5e8 100644 --- a/lib/waterline/adapter/aggregateQueries.js +++ b/lib/waterline/adapter/aggregateQueries.js @@ -127,6 +127,76 @@ module.exports = { if(err) return cb(err); cb(null, models); }); + }, + + // If an optimized findAndModify exists, use it, otherwise use an asynchronous loop with create() +findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { + console.log('In findAndModifyEach in aggregateQueries.js'); + var self = this; + var connName; + var adapter; + + if(typeof options === 'function') { + cb = options; + // set default values + options = { + upsert: false, + new: false, + mergeArrays: false + }; } + // Normalize Arguments + cb = normalize.callback(cb); + + // Clone sensitive data + attributesToCheck = _.clone(attributesToCheck); + valuesList = _.clone(valuesList); + + // Custom user adapter behavior + if(hasOwnProperty(this.dictionary, 'findAndModifyEach')) { + connName = this.dictionary.findAndModifyEach; + adapter = this.connections[connName]._adapter; + + if(hasOwnProperty(adapter, 'findAndModifyEach')) { + return adapter.findAndModifyEach(connName, this.collection, valuesList, options, cb); + } + } + + // Build a list of models + var models = []; + var i = 0; + + async.eachSeries(valuesList, function (values, cb) { + if (!_.isObject(values)) return cb(new Error('findAndModifyEach: Unexpected value in valuesList.')); + + // Check that each of the criteria keys match: + // build a criteria query + var criteria = {}; + + if (_.isObject(attributesToCheck[i])){ + Object.keys(attributesToCheck[i]).forEach(function (attrName) { + criteria[attrName] = values[attrName]; + }); + i++; + } else { + attributesToCheck.forEach(function (attrName) { + criteria[attrName] = values[attrName]; + }); + } + + return self.findAndModify.call(self, criteria, values, options, function (err, model) { + if(err) return cb(err); + + // Add model to list + if(model) models.push(model); + + cb(null, model); + }); + }, function (err) { + if(err) return cb(err); + cb(null, models); + }); +} + }; diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index 0c53f53cd..fa3d2df26 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -1,25 +1,25 @@ /** - * Composite Queries - */ +* Composite Queries +*/ var async = require('async'), - _ = require('lodash'), - usageError = require('../utils/usageError'), - utils = require('../utils/helpers'), - normalize = require('../utils/normalize'), - Deferred = require('./deferred'), - hasOwnProperty = utils.object.hasOwnProperty; +_ = require('lodash'), +usageError = require('../utils/usageError'), +utils = require('../utils/helpers'), +normalize = require('../utils/normalize'), +Deferred = require('./deferred'), +hasOwnProperty = utils.object.hasOwnProperty; module.exports = { /** - * Find or Create a New Record - * - * @param {Object} search criteria - * @param {Object} values to create if no record found - * @param {Function} callback - * @return Deferred object if no callback - */ + * Find or Create a New Record + * + * @param {Object} search criteria + * @param {Object} values to create if no record found + * @param {Function} callback + * @return Deferred object if no callback + */ findOrCreate: function(criteria, values, cb) { var self = this; @@ -72,6 +72,122 @@ module.exports = { return cb(null, result); }); }); + }, + + /** + * Finds and Updates a Record. If upsert is passed also creates it when it does + * not find it. + * + * @param {Object} criteria search criteria + * @param {Object} values values to update if record found + * @param {Object} [options] + * @param {Boolean} [options.upsert] If true, creates the object if not found + * @param {Boolean} [options.new] If true returns the newly created object, otherwise + * returns either the model before it was updated/created + * @param {Boolean} [options.mergeArrays] If true, merges any arrays passed in values + * @param {Function} [cb] callback + * @return Deferred object if no callback is given + */ + findAndModify: function(criteria, values, options, cb){ + var self = this; + + if(typeof options === 'function') { + cb = options; + // set default values + options = { + upsert: false, + new: false, + mergeArrays: false + }; + } + + // If no criteria is specified, bail out with a vengeance. + var usage = utils.capitalize(this.identity) + '.findAndModify([criteria], values, upsert, new, callback)'; + if(typeof cb == 'function' && (!criteria || criteria.length === 0)) { + return usageError('No criteria option specified!', usage, cb); + } + + // If no values are specified, bail out with a vengeance. + usage = utils.capitalize(this.identity) + '.findAndModify(criteria, [values], upsert, new, callback)'; + if(typeof cb == 'function' && (!values || values.length === 0)) { + return usageError('No values option specified!', usage, cb); + } + + // Normalize criteria + criteria = normalize.criteria(criteria); + + // Return Deferred or pass to adapter + if(typeof cb !== 'function') { + return new Deferred(this, this.findAndModify, criteria, values, options); + } + + // If an array of length 1 is passed convert, otherwise call findAndModifyEach + if(Array.isArray(criteria) && Array.isArray(values)) { + if (criteria.length > 1 && values.length > 1) { + // return usageError('Passing an array of models is not supported yet!', usage, cb); + return this.findAndModifyEach(criteria, values, options, cb); + } else { + criteria = criteria[0]; + values = values[0]; + } + } + + // if(typeof cb !== 'function') return usageError('Invalid callback specified!', usage, cb); + + // Try a find first. + this.find(criteria).exec(function(err, results) { + if (err) return cb(err); + + if (results && results.length !== 0) { + + if (options.mergeArrays) { + var resultKeys = Object.keys(results); + //Loop over all the results to see if it contains an array + for (var i = 0; i < resultKeys.length; i++) { + //if an array was found check if it is also a given array in values + //before merging them + if (Array.isArray(results)) { + if (resultKeys[i] in values && Array.isArray(values[resultKeys[i]])) { + //now update the values array + values[resultKeys[i]] = results[resultKeys[i]].concat(values[[resultKeys[i]]]); + } + } + } + } + + //Then update + self.update(criteria, values).exec(function(err, updatedResults) { + // if new is given return the model after it has been updated + if (options.new) { + // Unserialize values + results = self._transformer.unserialize(updatedResults[0]); + } else { + // Unserialize values + results = self._transformer.unserialize(results[0]); + } + + + // Return an instance of Model + var model = new self._model(results); + return cb(null, results); + }); + } else if (options.upsert) { + // Create a new record if nothing is found and upsert is true. + self.create(values).exec(function(err, result) { + if(err) return cb(err); + + // if new is given return the model after it has been created + //an empty array otherwise + if (options.new) { + return cb(null, result); + } else { + return cb(null, []); + } + }); + } else { + return cb(null, []); + } + }); } }; diff --git a/lib/waterline/query/deferred.js b/lib/waterline/query/deferred.js index 3042627bc..238ffd530 100644 --- a/lib/waterline/query/deferred.js +++ b/lib/waterline/query/deferred.js @@ -16,7 +16,7 @@ var Promise = require('bluebird'), // that were created using Q Promise.prototype.fail = Promise.prototype.catch; -var Deferred = module.exports = function(context, method, criteria, values) { +var Deferred = module.exports = function(context, method, criteria, values, options) { if(!context) return new Error('Must supply a context to a new Deferred object. Usage: new Deferred(context, method, criteria)'); if(!method) return new Error('Must supply a method to a new Deferred object. Usage: new Deferred(context, method, criteria)'); @@ -25,6 +25,7 @@ var Deferred = module.exports = function(context, method, criteria, values) { this._method = method; this._criteria = criteria; this._values = values || null; + this._options = options || { }; this._deferred = null; // deferred object for promises @@ -494,8 +495,16 @@ Deferred.prototype.exec = function(cb) { cb = normalize.callback(cb); // Set up arguments + callback - var args = [this._criteria, cb]; - if(this._values) args.splice(1, 0, this._values); + var args = [this._criteria]; + + if(this._values) { + args.push(this._values); + } + + if(this._options && Object.keys(this._options).length > 0) { + args.push(this._options); + } + args.push(cb); // Pass control to the adapter with the appropriate arguments. this._method.apply(this._context, args); From 6df2f24766bf97c10e62202dd63da14ecbbf7738 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Wed, 8 Apr 2015 15:49:50 +0100 Subject: [PATCH 15/26] Fixed findAndModify. Added unit tests. --- lib/waterline/adapter/aggregateQueries.js | 25 ++- lib/waterline/query/aggregate.js | 200 +++++++++++++++------- test/unit/query/query.findAndModify.js | 164 ++++++++++++++++++ 3 files changed, 319 insertions(+), 70 deletions(-) create mode 100644 test/unit/query/query.findAndModify.js diff --git a/lib/waterline/adapter/aggregateQueries.js b/lib/waterline/adapter/aggregateQueries.js index 5fb97d5e8..8de837787 100644 --- a/lib/waterline/adapter/aggregateQueries.js +++ b/lib/waterline/adapter/aggregateQueries.js @@ -146,6 +146,15 @@ findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { }; } + var isObjectArray = false; + if (_.isObject(attributesToCheck[0])) { + if (attributesToCheck.length > 1 && + attributesToCheck.length !== valuesList.length) { + return cb(new Error('findAndModifyEach: The two passed arrays have to be of the same length.')); + } + isObjectArray = true; + } + // Normalize Arguments cb = normalize.callback(cb); @@ -174,11 +183,17 @@ findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { // build a criteria query var criteria = {}; - if (_.isObject(attributesToCheck[i])){ - Object.keys(attributesToCheck[i]).forEach(function (attrName) { - criteria[attrName] = values[attrName]; - }); - i++; + if (isObjectArray) { + if (_.isObject(attributesToCheck[i])) { + Object.keys(attributesToCheck[i]).forEach(function (attrName) { + criteria[attrName] = values[attrName]; + }); + if (attributesToCheck.length > 1) { + i++; + } + } else { + return cb(new Error('findOrCreateEach: Element ' + i + ' in attributesToCheck is not an object.' )); + } } else { attributesToCheck.forEach(function (attrName) { criteria[attrName] = values[attrName]; diff --git a/lib/waterline/query/aggregate.js b/lib/waterline/query/aggregate.js index f6f44ab49..6463371e4 100644 --- a/lib/waterline/query/aggregate.js +++ b/lib/waterline/query/aggregate.js @@ -89,98 +89,168 @@ module.exports = { valuesList = null; } - // Normalize criteria - criteria = normalize.criteria(criteria); + return _asyncRun('findOrCreateEach', this, criteria, valuesList, null, cb); - // Return Deferred or pass to adapter - if(typeof cb !== 'function') { - return new Deferred(this, this.findOrCreateEach, criteria, valuesList); + }, + /** + * Iterate through a list of objects, trying to find each one + * For any that exist update them. If upsert is true, create all that + * don't exits + * + * @param {Object} criteria search criteria + * @param {Object} values values to update if record found + * @param {Object} [options] + * @param {Boolean} [options.upsert] If true, creates the object if not found + * @param {Boolean} [options.new] If true returns the newly created object, otherwise + * returns either the model before it was updated/created + * @param {Boolean} [options.mergeArrays] If true, merges any arrays passed in values + * @param {Function} [cb] callback + * @return Deferred object if no callback is given + */ + + findAndModifyEach: function(criteria, valuesList, options, cb) { + var self = this; + + if(typeof options === 'function') { + // cb = options; + // set default values + options = { + upsert: false, + new: false, + mergeArrays: false + }; } - // Validate Params - var usage = utils.capitalize(this.identity) + '.findOrCreateEach(criteria, valuesList, callback)'; + return _asyncRun('findAndModifyEach', this, criteria, valuesList, options, cb); + } +}; - if(typeof cb !== 'function') return usageError('Invalid callback specified!', usage, cb); - if(!criteria) return usageError('No criteria specified!', usage, cb); - if(!Array.isArray(criteria)) return usageError('No criteria specified!', usage, cb); - if(!valuesList) return usageError('No valuesList specified!', usage, cb); - if(!Array.isArray(valuesList)) return usageError('Invalid valuesList specified (should be an array!)', usage, cb); +/** + * Runs given queryType. Right now that is either findAndModifyEach or findOrCreateEach + * Essentially just a helper function for shared code. + * + * @param {String} queryType + * @param {Array} criteria + * @param {Array} valuesList + * @param {Object} [options] depends on which queryType calls, if given or not + * @param {Function} [cb] + * @return {String} + * @api private + */ - var errStr = _validateValues(valuesList); - if(errStr) return usageError(errStr, usage, cb); +function _asyncRun(queryType, self, criteria, valuesList, options, cb) { + // Normalize criteria + criteria = normalize.criteria(criteria); - // Validate each record in the array and if all are valid - // pass the array to the adapter's findOrCreateEach method - var validateItem = function(item, next) { - _validate.call(self, item, next); - } + if (typeof options === 'function') { + cb = options; + options = null; + } + var optionsString = ''; + if (options) { + optionsString = ', options' + } - async.each(valuesList, validateItem, function(err) { - if(err) return cb(err); + // Return Deferred or pass to adapter + if(typeof cb !== 'function') { + return new Deferred(this, this[queryType], criteria, valuesList, options); + } - // Transform Values - var transformedValues = []; + // Validate Params + var usage = utils.capitalize(this.identity) + '.' + queryType + '(criteria, valuesList ' + optionsString + ', callback)'; - valuesList.forEach(function(value) { + if(typeof cb !== 'function') return usageError('Invalid callback specified!', usage, cb); + if(!criteria) return usageError('No criteria specified!', usage, cb); + if(!Array.isArray(criteria)) return usageError('No criteria specified!', usage, cb); + if(!valuesList) return usageError('No valuesList specified!', usage, cb); + if(!Array.isArray(valuesList)) return usageError('Invalid valuesList specified (should be an array!)', usage, cb); - // Transform values - value = self._transformer.serialize(value); + var errStr = _validateValues(valuesList); + if(errStr) return usageError(errStr, usage, cb); - // Clean attributes - value = self._schema.cleanValues(value); - transformedValues.push(value); - }); + // Validate each record in the array and if all are valid + // pass the array to the adapter's findOrCreateEach method + var validateItem = function(item, next) { + _validate.call(self, item, next); + } - // Set values array to the transformed array - valuesList = transformedValues; - // Transform Search Criteria - var transformedCriteria = []; + async.each(valuesList, validateItem, function(err) { + if(err) return cb(err); - criteria.forEach(function(value) { - value = self._transformer.serialize(value); - transformedCriteria.push(value); - }); + // Transform Values + var transformedValues = []; - // Set criteria array to the transformed array - criteria = transformedCriteria; + valuesList.forEach(function(value) { - // Pass criteria and attributes to adapter definition - self.adapter.findOrCreateEach(criteria, valuesList, function(err, values) { - if(err) return cb(err); + // Transform values + value = self._transformer.serialize(value); - // Unserialize Values - var unserializedValues = []; + // Clean attributes + value = self._schema.cleanValues(value); + transformedValues.push(value); + }); - values.forEach(function(value) { - value = self._transformer.unserialize(value); - unserializedValues.push(value); - }); + // Set values array to the transformed array + valuesList = transformedValues; + + // Transform Search Criteria + var transformedCriteria = []; + + criteria.forEach(function(value) { + value = self._transformer.serialize(value); + transformedCriteria.push(value); + }); + + // Set criteria array to the transformed array + criteria = transformedCriteria; + + var cbFunction = function(err, values) { + if(err) return cb(err); - // Set values array to the transformed array - values = unserializedValues; + // Unserialize Values + var unserializedValues = []; - // Run AfterCreate Callbacks - async.each(values, function(item, next) { - callbacks.afterCreate(self, item, next); - }, function(err) { - if(err) return cb(err); + values.forEach(function(value) { + value = self._transformer.unserialize(value); + unserializedValues.push(value); + }); + + // Set values array to the transformed array + values = unserializedValues; - var models = []; + // Run AfterCreate Callbacks + async.each(values, function(item, next) { + callbacks.afterCreate(self, item, next); + }, function(err) { + if(err) return cb(err); - // Make each result an instance of model - values.forEach(function(value) { - models.push(new self._model(value)); - }); + var models = []; - cb(null, models); + // Make each result an instance of model + values.forEach(function(value) { + models.push(new self._model(value)); }); + + cb(null, models); }); - }); - } -}; + }; + var params = [criteria, valuesList]; + + //Currently only the findAndModifyEach function has an options parameter + if (queryType === 'findAndModifyEach') { + params.push(options); + } + + params.push(cbFunction); + console.log('calling ' + queryType + 'function from _asyncRun'); + // Pass criteria and attributes to adapter definition + //the first argument makes sure the function has access to the right 'this' + self.adapter[queryType].apply(self.adapter, params); + }); +} /** * Validate valuesList diff --git a/test/unit/query/query.findAndModify.js b/test/unit/query/query.findAndModify.js new file mode 100644 index 000000000..ecf5684a5 --- /dev/null +++ b/test/unit/query/query.findAndModify.js @@ -0,0 +1,164 @@ +var Waterline = require('../../../lib/waterline'), + assert = require('assert'); + +describe('Collection Query', function() { + + describe('.findAndModify()', function() { + + describe('with proper values', function() { + var query; + + before(function(done) { + + var waterline = new Waterline(); + var Model = Waterline.Collection.extend({ + identity: 'user', + connection: 'foo', + attributes: { + name: { + type: 'string', + defaultsTo: 'Foo Bar' + }, + doSomething: function() {} + } + }); + + waterline.loadCollection(Model); + + // Fixture Adapter Def + var adapterDef = { + find: function(con, col, criteria, cb) { return cb(null, []); }, + create: function(con, col, values, cb) { return cb(null, values); } + }; + + var connections = { + 'foo': { + adapter: 'foobar' + } + }; + + waterline.initialize({ adapters: { foobar: adapterDef }, connections: connections }, function(err, colls) { + if(err) return done(err); + query = colls.collections.user; + done(); + }); + }); + + it('should get empty array without new and upsert flag', function(done) { + query.findAndModify({ }, { name: 'Foo Bar' }, function(err, status) { + assert(status.length === 0); + done(); + }); + }); + + it('should get empty array with exec', function(done) { + query.findAndModify({ }, { name: 'Foo Bar' }).exec(function(err, status) { + assert(status.length === 0); + done(); + }); + }); + + it('should get an empty array if model does not exist and without upsert flag', function(done) { + query.findAndModify({ }, { name: 'Bar Foo'}, { new: true }).exec(function(err, status) { + assert(status.length === 0); + done(); + }); + }); + + it('should work with upsert and new options', function(done) { + query.findAndModify({ }, { name: 'Bar Foo'}, { upsert: true, new: true }).exec(function(err, status) { + assert(status.name === 'Bar Foo'); + done(); + }); + }); + + it('should work with multiple objects', function(done) { + query.findAndModify({ }, [{ name: 'Bar Foo'}, { name: 'Makis' }], { upsert: true, new: true }).exec(function(err, status) { + assert(status[0].name === 'Bar Foo'); + assert(status[1].name === 'Makis'); + done(); + }); + }); + + it('should add timestamps', function(done) { + query.findAndModify({ }, { }, { upsert: true, new: true }, function(err, status) { + assert(status.createdAt); + assert(status.updatedAt); + done(); + }); + }); + + it('should strip values that don\'t belong to the schema', function(done) { + query.findAndModify({ }, { foo: 'bar' }, { upsert: true, new: true }, function(err, values) { + assert(!values.foo); + done(); + }); + }); + + it('should return an instance of Model', function(done) { + query.findAndModify({ }, { name: 'Rice' }, { upsert: true, new: true }, function(err, status) { + assert(typeof status.doSomething === 'function'); + done(); + }); + }); + + it('should allow a query to be built using deferreds', function(done) { + query.findAndModify(null, null, { upsert: true, new: true }) + .where({ }) + .set({ name: 'bob' }) + .exec(function(err, result) { + assert(!err); + assert(result); + assert(result.name === 'bob'); + done(); + }); + }); + }); + + describe('casting values', function() { + var query; + + before(function(done) { + + var waterline = new Waterline(); + var Model = Waterline.Collection.extend({ + identity: 'user', + connection: 'foo', + attributes: { + name: 'string', + age: 'integer' + } + }); + + waterline.loadCollection(Model); + + // Fixture Adapter Def + var adapterDef = { + find: function(con, col, criteria, cb) { return cb(null, []); }, + create: function(con, col, values, cb) { return cb(null, values); } + }; + + var connections = { + 'foo': { + adapter: 'foobar' + } + }; + + waterline.initialize({ adapters: { foobar: adapterDef }, connections: connections }, function(err, colls) { + if(err) return done(err); + query = colls.collections.user; + done(); + }); + }); + + it('should cast values before sending to adapter', function(done) { + query.findAndModify({ }, { name: 'foo', age: '27' }, { upsert: true, new: true }, function(err, values) { + assert(values.name === 'foo'); + assert(values.age === 27); + done(); + }); + }); + }); + + }); +}); From c517bb5c42c208346575cabe90ae1919a35d1d25 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Wed, 8 Apr 2015 18:59:44 +0100 Subject: [PATCH 16/26] Fixed unit tests. --- lib/waterline/adapter/aggregateQueries.js | 3 +- lib/waterline/adapter/compoundQueries.js | 82 +++++++++++++++++++++++ lib/waterline/query/aggregate.js | 5 +- lib/waterline/query/composite.js | 15 ++++- test/unit/query/query.findAndModify.js | 10 +-- 5 files changed, 103 insertions(+), 12 deletions(-) diff --git a/lib/waterline/adapter/aggregateQueries.js b/lib/waterline/adapter/aggregateQueries.js index 8de837787..250960f3d 100644 --- a/lib/waterline/adapter/aggregateQueries.js +++ b/lib/waterline/adapter/aggregateQueries.js @@ -131,7 +131,6 @@ module.exports = { // If an optimized findAndModify exists, use it, otherwise use an asynchronous loop with create() findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { - console.log('In findAndModifyEach in aggregateQueries.js'); var self = this; var connName; var adapter; @@ -141,7 +140,7 @@ findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { // set default values options = { upsert: false, - new: false, + new: true, mergeArrays: false }; } diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index cf6b36d99..8f999438c 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -41,6 +41,88 @@ module.exports = { self.create(values, cb); }); + }, + + findAndModify: function(criteria, values, options, cb) { + console.log('compoundQueries.js'); + var self = this; + var connName; + var adapter; + + if(typeof options === 'function') { + cb = options; + // set default values + options = { + upsert: false, + new: false, + mergeArrays: false + }; + } + + if (typeof values === 'function') { + cb = values; + values = null + } + + options = options || { }; + + //new: true is the default value + if (!('new' in options)) { + options.new = true; + } + + // // If no values were specified, use criteria + // if (!values) values = criteria.where ? criteria.where : criteria; + + // Normalize Arguments + criteria = normalize.criteria(criteria); + cb = normalize.callback(cb); + + // Build Default Error Message + var err = "No find() or create() method defined in adapter!"; + + // Custom user adapter behavior + if(hasOwnProperty(this.dictionary, 'findAndModify')) { + connName = this.dictionary.findOrCreate; + adapter = this.connections[connName]._adapter; + + if(hasOwnProperty(adapter, 'findAndModify')) { + return adapter.findAndModify(connName, this.collection, values, options, cb); + } + } + + // Default behavior + // WARNING: Not transactional! (unless your data adapter is) + this.findOne(criteria, function(err, result) { + if(err) return cb(err); + if(result) { + + self.update(criteria, values, function(err, updatedResults) { + // if new is given return the model after it has been updated + if (options.new) { + return cb(null, updatedResults); + } else { + // Unserialize values + return cb(null, result); + } + + }); + } else if (options.upsert) { + // Create a new record if nothing is found and upsert is true. + //Note(globegitter): This might now ignore the 'options.new' flag + //so need to find a proper way to test/verify that. + self.create(values, function(err, result) { + if(err) return cb(err); + if (options.new) { + return cb(null, result); + } else { + return cb(null, []); + } + }); + } else { + return cb(null, []); + } + }); } }; diff --git a/lib/waterline/query/aggregate.js b/lib/waterline/query/aggregate.js index 6463371e4..7fbad09c9 100644 --- a/lib/waterline/query/aggregate.js +++ b/lib/waterline/query/aggregate.js @@ -154,11 +154,11 @@ function _asyncRun(queryType, self, criteria, valuesList, options, cb) { // Return Deferred or pass to adapter if(typeof cb !== 'function') { - return new Deferred(this, this[queryType], criteria, valuesList, options); + return new Deferred(self, self[queryType], criteria, valuesList, options); } // Validate Params - var usage = utils.capitalize(this.identity) + '.' + queryType + '(criteria, valuesList ' + optionsString + ', callback)'; + var usage = utils.capitalize(self.identity) + '.' + queryType + '(criteria, valuesList ' + optionsString + ', callback)'; if(typeof cb !== 'function') return usageError('Invalid callback specified!', usage, cb); if(!criteria) return usageError('No criteria specified!', usage, cb); @@ -245,7 +245,6 @@ function _asyncRun(queryType, self, criteria, valuesList, options, cb) { } params.push(cbFunction); - console.log('calling ' + queryType + 'function from _asyncRun'); // Pass criteria and attributes to adapter definition //the first argument makes sure the function has access to the right 'this' self.adapter[queryType].apply(self.adapter, params); diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index fa3d2df26..be1840092 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -83,12 +83,12 @@ module.exports = { * @param {Object} [options] * @param {Boolean} [options.upsert] If true, creates the object if not found * @param {Boolean} [options.new] If true returns the newly created object, otherwise - * returns either the model before it was updated/created + * returns either the model before it was updated/created. Defaults to true. * @param {Boolean} [options.mergeArrays] If true, merges any arrays passed in values * @param {Function} [cb] callback * @return Deferred object if no callback is given */ - findAndModify: function(criteria, values, options, cb){ + findAndModify: function(criteria, values, options, cb) { var self = this; if(typeof options === 'function') { @@ -101,6 +101,17 @@ module.exports = { }; } + if (typeof values === 'function') { + cb = values; + values = null + } + + options = options || { }; + + if (!('new' in options)) { + options.new = true; + } + // If no criteria is specified, bail out with a vengeance. var usage = utils.capitalize(this.identity) + '.findAndModify([criteria], values, upsert, new, callback)'; if(typeof cb == 'function' && (!criteria || criteria.length === 0)) { diff --git a/test/unit/query/query.findAndModify.js b/test/unit/query/query.findAndModify.js index ecf5684a5..02d21c282 100644 --- a/test/unit/query/query.findAndModify.js +++ b/test/unit/query/query.findAndModify.js @@ -44,7 +44,7 @@ describe('Collection Query', function() { }); }); - it('should get empty array without new and upsert flag', function(done) { + it('should get empty array without upsert flag', function(done) { query.findAndModify({ }, { name: 'Foo Bar' }, function(err, status) { assert(status.length === 0); done(); @@ -58,15 +58,15 @@ describe('Collection Query', function() { }); }); - it('should get an empty array if model does not exist and without upsert flag', function(done) { - query.findAndModify({ }, { name: 'Bar Foo'}, { new: true }).exec(function(err, status) { + it('should return empty model, before it got created, with new: false', function(done) { + query.findAndModify({ }, { name: 'Bar Foo'}, { upsert: true, new: false }).exec(function(err, status) { assert(status.length === 0); done(); }); }); - it('should work with upsert and new options', function(done) { - query.findAndModify({ }, { name: 'Bar Foo'}, { upsert: true, new: true }).exec(function(err, status) { + it('should return created model with upsert: true option', function(done) { + query.findAndModify({ }, { name: 'Bar Foo'}, { upsert: true }).exec(function(err, status) { assert(status.name === 'Bar Foo'); done(); }); From 2a47c9b142205bd394cecea7d98d8db75ae48fcb Mon Sep 17 00:00:00 2001 From: Globegitter Date: Wed, 8 Apr 2015 19:12:52 +0100 Subject: [PATCH 17/26] Removed consoe.log --- lib/waterline/adapter/compoundQueries.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index 8f999438c..48a880316 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -44,7 +44,6 @@ module.exports = { }, findAndModify: function(criteria, values, options, cb) { - console.log('compoundQueries.js'); var self = this; var connName; var adapter; From a63c7a835febe3d29428a3b9b6c18e08a8f678d0 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Wed, 8 Apr 2015 22:54:48 +0100 Subject: [PATCH 18/26] Return error on update. Fixed some criteria checks. --- lib/waterline/adapter/compoundQueries.js | 3 +++ lib/waterline/query/composite.js | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index 48a880316..e6e8e2337 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -97,6 +97,9 @@ module.exports = { if(result) { self.update(criteria, values, function(err, updatedResults) { + if (err) { + return cb(err); + } // if new is given return the model after it has been updated if (options.new) { return cb(null, updatedResults); diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index be1840092..7edde2747 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -134,10 +134,10 @@ module.exports = { // If an array of length 1 is passed convert, otherwise call findAndModifyEach if(Array.isArray(criteria) && Array.isArray(values)) { - if (criteria.length > 1 && values.length > 1) { + if (criteria.length > 1 || values.length > 1) { // return usageError('Passing an array of models is not supported yet!', usage, cb); return this.findAndModifyEach(criteria, values, options, cb); - } else { + } else if (criteria.length === 1 && values.length === 1){ criteria = criteria[0]; values = values[0]; } @@ -168,6 +168,9 @@ module.exports = { //Then update self.update(criteria, values).exec(function(err, updatedResults) { + if (err) { + return cb(err); + } // if new is given return the model after it has been updated if (options.new) { // Unserialize values From 1767213d4e1f97195e545ee5cd0d50cb4ea7b60e Mon Sep 17 00:00:00 2001 From: Globegitter Date: Thu, 9 Apr 2015 10:15:25 +0100 Subject: [PATCH 19/26] Get first working implementation of mergeArrays. --- lib/waterline/adapter/compoundQueries.js | 16 ++++++++++++++++ lib/waterline/query/composite.js | 24 +++++++++++++++--------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index e6e8e2337..bedddcb0c 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -96,6 +96,22 @@ module.exports = { if(err) return cb(err); if(result) { + //merging any arrays in the result with any matching arrays in the values + if (options.mergeArrays) { + var resultKeys = Object.keys(result); + //Loop over all the results to see if it contains an array + for (var i = 0; i < resultKeys.length; i++) { + //if an array was found check if it is also a given array in values + //before merging them + if (Array.isArray(result[resultKeys[i]])) { + if (resultKeys[i] in values && Array.isArray(values[resultKeys[i]])) { + //now update the values array + values[resultKeys[i]] = result[resultKeys[i]].concat(values[[resultKeys[i]]]); + } + } + } + } + self.update(criteria, values, function(err, updatedResults) { if (err) { return cb(err); diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index 7edde2747..5893e2bf6 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -151,16 +151,22 @@ module.exports = { if (results && results.length !== 0) { + // merging together any passed type: array values with all the found ones + // you should normally only use mergeArrays if you are searching for a unique + // indexed element. if (options.mergeArrays) { - var resultKeys = Object.keys(results); - //Loop over all the results to see if it contains an array - for (var i = 0; i < resultKeys.length; i++) { - //if an array was found check if it is also a given array in values - //before merging them - if (Array.isArray(results)) { - if (resultKeys[i] in values && Array.isArray(values[resultKeys[i]])) { - //now update the values array - values[resultKeys[i]] = results[resultKeys[i]].concat(values[[resultKeys[i]]]); + for (var i = 0; i < results.length; i++) { + var result = results[i]; + var resultKeys = Object.keys(result); + //Loop over all the results to see if it contains an array + for (var j = 0; j < resultKeys.length; j++) { + //if an array was found check if it is also a given array in values + //before merging them + if (Array.isArray(result[resultKeys[j]])) { + if (resultKeys[j] in values && Array.isArray(values[resultKeys[j]])) { + //now update the values object + values[resultKeys[j]] = result[resultKeys[j]].concat(values[[resultKeys[j]]]); + } } } } From 3ad6a824e43c3bdfae19d1779f9f8e594549be33 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Thu, 9 Apr 2015 10:37:23 +0100 Subject: [PATCH 20/26] Slightly optimised and simplified mergeArrays implementation. --- lib/waterline/adapter/compoundQueries.js | 20 ++++++++++---------- lib/waterline/query/composite.js | 23 +++++++++++------------ 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index bedddcb0c..def310b19 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -97,17 +97,17 @@ module.exports = { if(result) { //merging any arrays in the result with any matching arrays in the values + // Note: At this point values should just be an object if (options.mergeArrays) { - var resultKeys = Object.keys(result); - //Loop over all the results to see if it contains an array - for (var i = 0; i < resultKeys.length; i++) { - //if an array was found check if it is also a given array in values - //before merging them - if (Array.isArray(result[resultKeys[i]])) { - if (resultKeys[i] in values && Array.isArray(values[resultKeys[i]])) { - //now update the values array - values[resultKeys[i]] = result[resultKeys[i]].concat(values[[resultKeys[i]]]); - } + var valueKeys = Object.keys(values); + // Loop over all the results to see if it contains an array + for (var i = 0; i < valueKeys.length; i++) { + // Check if both properties are an array, if one isn't just ignore + // Note: We do not have to explicitly check if the property exists in the result + // because isArray then just returns false + if (Array.isArray(values[resultKeys[i]]) && Array.isArray(result[resultKeys[i]])) { + //now concatenate the arrays + values[resultKeys[i]] = result[resultKeys[i]].concat(values[[resultKeys[i]]]); } } } diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index 5893e2bf6..4caa8faab 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -151,22 +151,21 @@ module.exports = { if (results && results.length !== 0) { - // merging together any passed type: array values with all the found ones - // you should normally only use mergeArrays if you are searching for a unique + // merging together any passed 'type: array' values with all the found ones + // you should usually use mergeArrays only if you are searching for a unique // indexed element. if (options.mergeArrays) { for (var i = 0; i < results.length; i++) { var result = results[i]; - var resultKeys = Object.keys(result); - //Loop over all the results to see if it contains an array - for (var j = 0; j < resultKeys.length; j++) { - //if an array was found check if it is also a given array in values - //before merging them - if (Array.isArray(result[resultKeys[j]])) { - if (resultKeys[j] in values && Array.isArray(values[resultKeys[j]])) { - //now update the values object - values[resultKeys[j]] = result[resultKeys[j]].concat(values[[resultKeys[j]]]); - } + var valueKeys = Object.keys(values); + //Loop over all the value properties to see if it contains an array + for (var j = 0; j < valueKeys.length; j++) { + // Check if both properties are an array, if one isn't just ignore + // Note: We do not have to explicitly check if the property exists in the result + // because isArray then just returns false + if (Array.isArray(values[resultKeys[i]]) && Array.isArray(result[resultKeys[i]])) { + //now concatenate the arrays + values[resultKeys[i]] = result[resultKeys[i]].concat(values[[resultKeys[i]]]); } } } From aeb1dd177e71d95edbe7244f32f1e372e637359f Mon Sep 17 00:00:00 2001 From: Globegitter Date: Thu, 9 Apr 2015 11:59:21 +0100 Subject: [PATCH 21/26] Forgot to rename to valueKeys. --- lib/waterline/query/composite.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index 4caa8faab..c6a3154e2 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -163,9 +163,9 @@ module.exports = { // Check if both properties are an array, if one isn't just ignore // Note: We do not have to explicitly check if the property exists in the result // because isArray then just returns false - if (Array.isArray(values[resultKeys[i]]) && Array.isArray(result[resultKeys[i]])) { + if (Array.isArray(values[valueKeys[i]]) && Array.isArray(result[valueKeys[i]])) { //now concatenate the arrays - values[resultKeys[i]] = result[resultKeys[i]].concat(values[[resultKeys[i]]]); + values[valueKeys[i]] = result[valueKeys[i]].concat(values[[valueKeys[i]]]); } } } From 941042e64a9300a457d83d6aed81b6c54fbfd602 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Thu, 9 Apr 2015 12:07:52 +0100 Subject: [PATCH 22/26] Of course also forgot in compoundQueries. --- lib/waterline/adapter/compoundQueries.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index def310b19..77144654b 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -105,9 +105,9 @@ module.exports = { // Check if both properties are an array, if one isn't just ignore // Note: We do not have to explicitly check if the property exists in the result // because isArray then just returns false - if (Array.isArray(values[resultKeys[i]]) && Array.isArray(result[resultKeys[i]])) { + if (Array.isArray(values[valueKeys[i]]) && Array.isArray(result[valueKeys[i]])) { //now concatenate the arrays - values[resultKeys[i]] = result[resultKeys[i]].concat(values[[resultKeys[i]]]); + values[valueKeys[i]] = result[valueKeys[i]].concat(values[[valueKeys[i]]]); } } } From 5817a94eb453bbf4008bb992cb57e0f17190eb74 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Thu, 9 Apr 2015 13:51:02 +0100 Subject: [PATCH 23/26] Take union of arrays rather than simply concatenating them. --- lib/waterline/adapter/compoundQueries.js | 4 ++-- lib/waterline/query/composite.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/waterline/adapter/compoundQueries.js b/lib/waterline/adapter/compoundQueries.js index 77144654b..7b494fef3 100644 --- a/lib/waterline/adapter/compoundQueries.js +++ b/lib/waterline/adapter/compoundQueries.js @@ -106,8 +106,8 @@ module.exports = { // Note: We do not have to explicitly check if the property exists in the result // because isArray then just returns false if (Array.isArray(values[valueKeys[i]]) && Array.isArray(result[valueKeys[i]])) { - //now concatenate the arrays - values[valueKeys[i]] = result[valueKeys[i]].concat(values[[valueKeys[i]]]); + //now take the union of the arrays + values[valueKeys[i]] = _.union(result[valueKeys[i]], values[[valueKeys[i]]]); } } } diff --git a/lib/waterline/query/composite.js b/lib/waterline/query/composite.js index c6a3154e2..c6407f03e 100644 --- a/lib/waterline/query/composite.js +++ b/lib/waterline/query/composite.js @@ -164,8 +164,8 @@ module.exports = { // Note: We do not have to explicitly check if the property exists in the result // because isArray then just returns false if (Array.isArray(values[valueKeys[i]]) && Array.isArray(result[valueKeys[i]])) { - //now concatenate the arrays - values[valueKeys[i]] = result[valueKeys[i]].concat(values[[valueKeys[i]]]); + //now take the union of the arrays + values[valueKeys[i]] = _.union(result[valueKeys[i]], values[[valueKeys[i]]]); } } } From cb1e3377937775d767bb2f548ea64e780085eb24 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Fri, 10 Apr 2015 16:50:42 +0100 Subject: [PATCH 24/26] Removed remaining HEAD in aggregatQueries --- lib/waterline/adapter/aggregateQueries.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/waterline/adapter/aggregateQueries.js b/lib/waterline/adapter/aggregateQueries.js index 4825b3a47..779bbb434 100644 --- a/lib/waterline/adapter/aggregateQueries.js +++ b/lib/waterline/adapter/aggregateQueries.js @@ -130,7 +130,6 @@ module.exports = { }, // If an optimized findAndModify exists, use it, otherwise use an asynchronous loop with create() -<<<<<<< HEAD findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { var self = this; var connName; From 2b5b4039f9b2922a539b861e47d9655a8536c8f5 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Mon, 20 Apr 2015 09:53:21 +0100 Subject: [PATCH 25/26] Fix findAndModifyEach. --- lib/waterline/adapter/aggregateQueries.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/waterline/adapter/aggregateQueries.js b/lib/waterline/adapter/aggregateQueries.js index 779bbb434..75ed4e674 100644 --- a/lib/waterline/adapter/aggregateQueries.js +++ b/lib/waterline/adapter/aggregateQueries.js @@ -202,8 +202,15 @@ findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { return self.findAndModify.call(self, criteria, values, options, function (err, model) { if(err) return cb(err); - // Add model to list - if(model) models.push(model); + // if returned models are an array push each result + if (Array.isArray(model)) { + for (var i = 0; i < model.length; i++) { + models.push(model); + }; + } else if (model) { + // Add model to list + models.push(model); + } cb(null, model); }); From d9a3bd371c14a36ddd4ddbd302aa5bd0a6ff10b3 Mon Sep 17 00:00:00 2001 From: Globegitter Date: Mon, 20 Apr 2015 10:39:09 +0100 Subject: [PATCH 26/26] Push array element, not the whole array. --- lib/waterline/adapter/aggregateQueries.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/waterline/adapter/aggregateQueries.js b/lib/waterline/adapter/aggregateQueries.js index 75ed4e674..0848feb9b 100644 --- a/lib/waterline/adapter/aggregateQueries.js +++ b/lib/waterline/adapter/aggregateQueries.js @@ -205,7 +205,7 @@ findAndModifyEach: function(attributesToCheck, valuesList, options, cb) { // if returned models are an array push each result if (Array.isArray(model)) { for (var i = 0; i < model.length; i++) { - models.push(model); + models.push(model[i]); }; } else if (model) { // Add model to list