diff --git a/.editorconfig b/.editorconfig index 0f099897b..ca8eec8a7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,3 +8,6 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore index 80ddc0652..fd992ae2f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules .dist coverage/ npm-debug.log +.idea diff --git a/.travis.yml b/.travis.yml index 96e1a5106..c9a43c8e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,9 +12,6 @@ branches: - master - 0.11.x -after_script: - - npm run coverage && cat ./coverage/lcov.info | ./node_modules/.bin/codeclimate - addons: code_climate: repo_token: 351483555263cf9bcd2416c58b0e0ae6ca1b32438aa51bbab2c833560fb67cc0 diff --git a/Makefile b/Makefile index 67847ff20..bda44b39f 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,23 @@ ROOT=$(shell pwd) +NPMVERSION=$(shell npm --version | cut -f1 -d.) test: test-unit test-integration test-unit: @echo "\nRunning unit tests..." - @NODE_ENV=test mocha test/integration test/structure test/support test/unit --recursive + @NODE_ENV=test node_modules/.bin/mocha test/integration test/structure test/support test/unit --recursive +ifeq "$(NPMVERSION)" "2" test-integration: @echo "\nRunning integration tests..." rm -rf node_modules/waterline-adapter-tests/node_modules/waterline; + mkdir -p node_modules/waterline-adapter-tests/node_modules; ln -s "$(ROOT)" node_modules/waterline-adapter-tests/node_modules/waterline; @NODE_ENV=test node test/adapter/runner.js - -coverage: - @echo "\n\nRunning coverage report..." - rm -rf coverage - @NODE_ENV=test ./node_modules/istanbul/lib/cli.js cover --report none --dir coverage/core ./node_modules/.bin/_mocha \ - test/integration test/structure test/support test/unit -- --recursive - ./node_modules/istanbul/lib/cli.js cover --report none --dir coverage/adapter test/adapter/runner.js - ./node_modules/istanbul/lib/cli.js report +else +test-integration: + @echo "\nRunning integration tests..." + @NODE_ENV=test node test/adapter/runner.js +endif -.PHONY: coverage diff --git a/lib/waterline.js b/lib/waterline.js index 2fc426c2d..5875c7c84 100644 --- a/lib/waterline.js +++ b/lib/waterline.js @@ -77,9 +77,27 @@ Waterline.prototype.initialize = function(options, cb) { var self = this; // Ensure a config object is passed in containing adapters - if (!options) throw new Error('Usage Error: function(options, callback)'); - if (!options.adapters) throw new Error('Options object must contain an adapters object'); - if (!options.connections) throw new Error('Options object must contain a connections object'); + if (!options) { throw new Error('Usage Error: function(options, callback)'); } + if (!options.adapters) { throw new Error('Options object must contain an adapters object'); } + if (!options.connections) { throw new Error('Options object must contain a connections object'); } + + // Check that the given adapter is compatible with Waterline 0.11.x. + try { + _.each(options.adapters, function(adapter) { + // Adapters meant for Waterline >= 0.12 will have an adapterApiVersion property, so if we + // see that then we know the adapter won't work with this version of Waterline. + if (!_.isUndefined(adapter.adapterApiVersion)) { + throw new Error( + '\n-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'+ + 'Cannot initialize Waterline.\n'+ + 'The installed version of adapter `' + adapter.identity + '` is too new!\n' + + 'Please try installing a version < 1.0.\n' + + '-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n'); + } + }); + } catch (e) { + return cb(e); + } // Allow collections to be passed in to the initialize method if (options.collections) { diff --git a/lib/waterline/model/lib/associationMethods/add.js b/lib/waterline/model/lib/associationMethods/add.js index 184c6c349..d7d36d766 100644 --- a/lib/waterline/model/lib/associationMethods/add.js +++ b/lib/waterline/model/lib/associationMethods/add.js @@ -295,7 +295,7 @@ Add.prototype.createManyToMany = function(collection, attribute, pk, key, cb) { // If this is a throughTable, look into the meta data cache for what key to use if (collectionAttributes.throughTable) { - var cacheKey = collectionAttributes.throughTable[attribute.on + '.' + key] || collectionAttributes.throughTable[attribute.via + '.' + key]; + var cacheKey = collectionAttributes.throughTable[self.collection.adapter.identity + '.' + key] || collectionAttributes.throughTable[attribute.via + '.' + key]; if (!cacheKey) { return cb(new Error('Unable to find the proper cache key in the through table definition')); } diff --git a/lib/waterline/model/lib/associationMethods/remove.js b/lib/waterline/model/lib/associationMethods/remove.js index 7f46d856c..9abfd6812 100644 --- a/lib/waterline/model/lib/associationMethods/remove.js +++ b/lib/waterline/model/lib/associationMethods/remove.js @@ -43,7 +43,7 @@ var Remove = module.exports = function(collection, proto, records, cb) { // // In the future when transactions are available this will all be done on a single // connection and can be re-written. - this.removeCollectionAssociations(records, cb); + this.removeCollectionAssociations(records, proto, cb); }; /** @@ -78,11 +78,11 @@ Remove.prototype.findPrimaryKey = function(attributes, values) { * @api private */ -Remove.prototype.removeCollectionAssociations = function(records, cb) { +Remove.prototype.removeCollectionAssociations = function(records, proto, cb) { var self = this; async.eachSeries(_.keys(records), function(associationKey, next) { - self.removeAssociations(associationKey, records[associationKey], next); + self.removeAssociations(associationKey, records[associationKey], proto, next); }, function(err) { @@ -103,7 +103,7 @@ Remove.prototype.removeCollectionAssociations = function(records, cb) { * @api private */ -Remove.prototype.removeAssociations = function(key, records, cb) { +Remove.prototype.removeAssociations = function(key, records, proto, cb) { var self = this; // Grab the collection the attribute references @@ -115,7 +115,7 @@ Remove.prototype.removeAssociations = function(key, records, cb) { // Limit Removes to 10 at a time to prevent the connection pool from being exhausted async.eachLimit(records, 10, function(associationId, next) { - self.removeRecord(associatedCollection, schema, associationId, key, next); + self.removeRecord(associatedCollection, schema, associationId, key, proto, next); }, cb); }; @@ -130,7 +130,7 @@ Remove.prototype.removeAssociations = function(key, records, cb) { * @api private */ -Remove.prototype.removeRecord = function(collection, attribute, associationId, key, cb) { +Remove.prototype.removeRecord = function(collection, attribute, associationId, key, proto, cb) { var self = this; // Validate `values` is a correct primary key format @@ -172,6 +172,7 @@ Remove.prototype.removeRecord = function(collection, attribute, associationId, k var _values = {}; criteria[associationKey] = associationId; + criteria[attribute.on] = proto.id; _values[attribute.on] = null; collection.update(criteria, _values, function(err) { @@ -233,7 +234,7 @@ Remove.prototype.removeManyToMany = function(collection, attribute, pk, key, cb) // If this is a throughTable, look into the meta data cache for what key to use if (collectionAttributes.throughTable) { - var cacheKey = collectionAttributes.throughTable[attribute.on + '.' + key] || collectionAttributes.throughTable[attribute.via + '.' + key]; + var cacheKey = collectionAttributes.throughTable[self.collection.adapter.identity + '.' + key] || collectionAttributes.throughTable[attribute.via + '.' + key]; if (!cacheKey) { return cb(new Error('Unable to find the proper cache key in the through table definition')); } diff --git a/lib/waterline/query/dql/count.js b/lib/waterline/query/dql/count.js index cc628d8a1..f4f768b81 100644 --- a/lib/waterline/query/dql/count.js +++ b/lib/waterline/query/dql/count.js @@ -56,5 +56,15 @@ module.exports = function(criteria, options, cb) { // Transform Search Criteria criteria = this._transformer.serialize(criteria); + // Remove any joins from the count criteria. They won't have any effect on the + // number of results found. + if (_.isArray(criteria.joins)) { + delete criteria.joins; + } + + if (criteria.where && _.isArray(criteria.where.joins)) { + delete criteria.where.joins; + } + this.adapter.count(criteria, cb); }; diff --git a/lib/waterline/query/dql/destroy.js b/lib/waterline/query/dql/destroy.js index f715d6dd0..5aae92d5b 100644 --- a/lib/waterline/query/dql/destroy.js +++ b/lib/waterline/query/dql/destroy.js @@ -86,17 +86,17 @@ module.exports = function(criteria, cb) { function destroyJoinTableRecords(item, next) { var collection = self.waterline.collections[item]; - var refKey; + var refKey = []; Object.keys(collection._attributes).forEach(function(key) { var attr = collection._attributes[key]; if (attr.references !== self.identity) return; - refKey = key; + refKey.push(key); }); // If no refKey return, this could leave orphaned join table values but it's better // than crashing. - if (!refKey) return next(); + if (!refKey.length) return next(); // Make sure we don't return any undefined pks var mappedValues = result.reduce(function(memo, vals) { @@ -109,7 +109,22 @@ module.exports = function(criteria, cb) { var criteria = {}; if (mappedValues.length > 0) { - criteria[refKey] = mappedValues; + // Handle reflexive associations by building up an OR clause. + if (refKey.length > 1) { + var orCriteria = []; + _.each(refKey, function(columnName) { + var where = {}; + where[columnName] = mappedValues; + orCriteria.push(where); + }); + + criteria = { + or: orCriteria + }; + } else { + criteria[_.first(refKey)] = mappedValues; + } + collection.destroy(criteria).exec(next); } else { return next(); @@ -123,9 +138,25 @@ module.exports = function(criteria, cb) { }); function after() { - callbacks.afterDestroy(self, result, function(err) { + + // If no result was returned, default to empty array + if (!result) { + result = []; + } + + // If values is not an array, return an array + if (!Array.isArray(result)) { + result = [result]; + } + + // Unserialize each value + var transformedValues = result.map(function(value) { + return self._transformer.unserialize(value); + }); + + callbacks.afterDestroy(self, transformedValues, function(err) { if (err) return cb(err); - cb(null, result); + cb(null, transformedValues); }); } diff --git a/lib/waterline/query/finders/basic.js b/lib/waterline/query/finders/basic.js index e688693a1..a19e6244c 100644 --- a/lib/waterline/query/finders/basic.js +++ b/lib/waterline/query/finders/basic.js @@ -123,7 +123,6 @@ module.exports = { if (!hasOwnProperty(search, join.alias)) return; delete search[join.alias]; }); - return; } if (!hasOwnProperty(tmpCriteria, join.alias)) return; @@ -308,7 +307,6 @@ module.exports = { if (!hasOwnProperty(search, join.alias)) return; delete search[join.alias]; }); - return; } if (!hasOwnProperty(tmpCriteria, join.alias)) return; diff --git a/lib/waterline/utils/nestedOperations/update.js b/lib/waterline/utils/nestedOperations/update.js index a2266d20e..c247ebd13 100644 --- a/lib/waterline/utils/nestedOperations/update.js +++ b/lib/waterline/utils/nestedOperations/update.js @@ -411,7 +411,7 @@ function buildParentRemoveOperations(parent, operations) { model: child.identity, criteria: searchCriteria, keyName: attribute.on, - nullify: !hop(child, 'junctionTable') + nullify: !hop(child, 'throughTable') }; diff --git a/package.json b/package.json index d3d8f18c2..c6c517719 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "waterline", "description": "An ORM for Node.js and the Sails framework.", - "version": "0.11.6", + "version": "0.11.11", "homepage": "http://waterlinejs.org", "contributors": [ { @@ -30,7 +30,7 @@ "prompt": "0.2.14", "switchback": "2.0.0", "waterline-criteria": "~0.11.2", - "waterline-schema": "0.2.0" + "waterline-schema": "~0.2.1" }, "devDependencies": { "codeclimate-test-reporter": "0.3.1", @@ -40,7 +40,7 @@ "mocha": "2.4.5", "sails-memory": "balderdashy/sails-memory", "should": "8.2.1", - "waterline-adapter-tests": "balderdashy/waterline-adapter-tests#0.11.x" + "waterline-adapter-tests": "~0.11.2" }, "keywords": [ "mvc", @@ -59,8 +59,7 @@ "scripts": { "test": "make test", "prepublish": "npm prune", - "browserify": "rm -rf .dist && mkdir .dist && browserify lib/waterline.js -s Waterline | uglifyjs > .dist/waterline.min.js", - "coverage": "make coverage" + "browserify": "rm -rf .dist && mkdir .dist && browserify lib/waterline.js -s Waterline | uglifyjs > .dist/waterline.min.js" }, "engines": { "node": ">=0.10.0" diff --git a/test/integration/model/association.remove.hasMany.js b/test/integration/model/association.remove.hasMany.js index 3ad1b0aa9..a83005573 100644 --- a/test/integration/model/association.remove.hasMany.js +++ b/test/integration/model/association.remove.hasMany.js @@ -41,15 +41,18 @@ describe('Model', function() { waterline.loadCollection(Preference); var _values = [ - { id: 1, preference: [{ foo: 'bar' }, { foo: 'foobar' }] }, - { id: 2, preference: [{ foo: 'a' }, { foo: 'b' }] }, + { id: 1, preference: [{ id: 10, foo: 'bar' }, { id: 20, foo: 'foobar' }] }, + { id: 2, preference: [{ id: 30, foo: 'a' }, { id: 40, foo: 'b' }] }, ]; var adapterDef = { find: function(con, col, criteria, cb) { return cb(null, _values); }, update: function(con, col, criteria, values, cb) { if(col === 'preference') { - prefValues.push({ id: criteria.where.id, values: values }); + prefValues.push({ + id: criteria.where.id, + assoc: criteria.where.user, + values: values }); } return cb(null, values); @@ -80,16 +83,18 @@ describe('Model', function() { var person = models[0]; - person.preferences.remove(1); - person.preferences.remove(2); + person.preferences.remove(10); + person.preferences.remove(20); person.save(function(err) { if(err) return done(err); assert(prefValues.length === 2); - assert(prefValues[0].id === 1); + assert(prefValues[0].id === 10); + assert(prefValues[0].assoc === 1); assert(prefValues[0].values.user === null); - assert(prefValues[1].id === 2); + assert(prefValues[1].id === 20); + assert(prefValues[1].assoc === 1); assert(prefValues[1].values.user === null); done(); diff --git a/test/integration/model/destroy.js b/test/integration/model/destroy.js index 3e82b34d9..9d15ceca4 100644 --- a/test/integration/model/destroy.js +++ b/test/integration/model/destroy.js @@ -26,7 +26,7 @@ describe('Model', function() { waterline.loadCollection(Collection); - var adapterDef = { destroy: function(con, col, options, cb) { return cb(null, true); }}; + var adapterDef = { destroy: function(con, col, options, cb) { return cb(null, [{ id: 1, first_name: 'foo', last_name: 'bar' }]); }}; var connections = { 'my_foo': { @@ -46,12 +46,12 @@ describe('Model', function() { // TEST METHODS //////////////////////////////////////////////////// - it('should pass status from the adapter destroy method', function(done) { + it('should pass data from the adapter destroy method', function(done) { var person = new collection._model({ id: 1, first_name: 'foo', last_name: 'bar' }); - person.destroy(function(err, status) { + person.destroy(function(err, records) { assert(!err); - assert(status === true); + assert.equal(records[0].last_name, 'bar'); done(); }); }); diff --git a/test/unit/query/query.destroy.js b/test/unit/query/query.destroy.js index 509346d22..8c62d021e 100644 --- a/test/unit/query/query.destroy.js +++ b/test/unit/query/query.destroy.js @@ -95,7 +95,7 @@ describe('Collection Query', function() { waterline.loadCollection(Model); // Fixture Adapter Def - var adapterDef = { destroy: function(con, col, options, cb) { return cb(null, options); }}; + var adapterDef = { destroy: function(con, col, options, cb) { return cb(null, [{ pkColumn: 1, name: 'foo' }]); }}; var connections = { 'foo': { @@ -111,10 +111,10 @@ describe('Collection Query', function() { }); - it('should use the custom primary key when a single value is passed in', function(done) { + it('should transform column names when values are sent back', function(done) { query.destroy(1, function(err, values) { assert(!err); - assert(values.where.pkColumn === 1); + assert.equal(values[0].myPk, 1); done(); }); });