diff --git a/lib/include.js b/lib/include.js index 2e685bb1..2b106250 100644 --- a/lib/include.js +++ b/lib/include.js @@ -3,6 +3,7 @@ var utils = require('./utils'); var List = require('./list'); var isPlainObject = utils.isPlainObject; var defineCachedRelations = utils.defineCachedRelations; +var uniq = utils.uniq; /*! * Normalize the include to be an array @@ -308,7 +309,7 @@ Inclusion.include = function (objects, include, options, cb) { where: {} }; throughFilter.where[relation.keyTo] = { - inq: sourceIds + inq: uniq(sourceIds) }; if (polymorphic) { //handle polymorphic hasMany (reverse) in which case we need to filter @@ -351,7 +352,7 @@ Inclusion.include = function (objects, include, options, cb) { //Polymorphic relation does not have idKey of modelTo. Find it manually var modelToIdName = idName(relation.modelTo); filter.where[modelToIdName] = { - inq: targetIds + inq: uniq(targetIds) }; //make sure that the modelToIdName is included if fields are specified @@ -432,7 +433,7 @@ Inclusion.include = function (objects, include, options, cb) { obj.__cachedRelations[relationName] = []; } filter.where[relation.keyTo] = { - inq: allTargetIds + inq: uniq(allTargetIds) }; relation.applyScope(null, filter); /** @@ -496,7 +497,7 @@ Inclusion.include = function (objects, include, options, cb) { obj.__cachedRelations[relationName] = []; } filter.where[relation.keyTo] = { - inq: sourceIds + inq: uniq(sourceIds) }; relation.applyScope(null, filter); relation.modelTo.find(filter, targetFetchHandler); @@ -574,7 +575,7 @@ Inclusion.include = function (objects, include, options, cb) { utils.mergeQuery(typeFilter, filter); var targetIds = targetIdsByType[modelType]; typeFilter.where[relation.keyTo] = { - inq: targetIds + inq: uniq(targetIds) }; var Model = lookupModel(relation.modelFrom.dataSource.modelBuilder. models, modelType); @@ -650,7 +651,7 @@ Inclusion.include = function (objects, include, options, cb) { obj.__cachedRelations[relationName] = null; } filter.where[relation.keyTo] = { - inq: targetIds + inq: uniq(targetIds) }; relation.applyScope(null, filter); relation.modelTo.find(filter, targetFetchHandler); diff --git a/lib/utils.js b/lib/utils.js index cb702cec..3d0c5467 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -10,9 +10,11 @@ exports.sortObjectsByIds = sortObjectsByIds; exports.setScopeValuesFromWhere = setScopeValuesFromWhere; exports.mergeQuery = mergeQuery; exports.mergeIncludes = mergeIncludes; -exports.createPromiseCallback = createPromiseCallback +exports.createPromiseCallback = createPromiseCallback; +exports.uniq = uniq; var traverse = require('traverse'); +var assert = require('assert'); function safeRequire(module) { try { @@ -475,3 +477,22 @@ function throwPromiseNotDefined() { 'Your Node runtime does support ES6 Promises. ' + 'Set "global.Promise" to your preferred implementation of promises.'); } + +/** + * Dedupe an array + * @param {Array} an array + * @returns {Array} an array with unique items + */ +function uniq(a) { + var uniqArray = []; + if (!a) { + return uniqArray; + } + assert(Array.isArray(a), 'array argument is required'); + for (var i = 0, n = a.length; i < n; i++) { + if (a.indexOf(a[i]) === i) { + uniqArray.push(a[i]); + } + } + return uniqArray; +} diff --git a/test/include.test.js b/test/include.test.js index 9b52371d..9c1504fb 100644 --- a/test/include.test.js +++ b/test/include.test.js @@ -160,6 +160,26 @@ describe('include', function () { }); }); + it('should support limit', function(done) { + Passport.find({ + include: { + owner: { + relation: 'posts', scope: { + fields: ['title'], include: ['author'], + order: 'title DESC', + limit: 2 + } + } + }, + limit: 1 + }, function(err, passports) { + if (err) return done(err); + passports.length.should.equal(1); + passports[0].toJSON().owner.posts.length.should.equal(2); + done(); + }); + }); + it('should fetch Users with include scope on Posts - belongsTo', function (done) { Post.find({ include: { relation: 'author', scope:{ fields: ['name'] }} diff --git a/test/util.test.js b/test/util.test.js index 369ae297..c4e7eba6 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -5,6 +5,7 @@ var removeUndefined = utils.removeUndefined; var mergeSettings = utils.mergeSettings; var mergeIncludes = utils.mergeIncludes; var sortObjectsByIds = utils.sortObjectsByIds; +var uniq = utils.uniq; describe('util.fieldsToArray', function () { function sample(fields, excludeUnknown) { @@ -242,7 +243,7 @@ describe('util.mergeIncludes', function () { 'Merged include should match the expectation'); } - it('Merge string values to object', function () { + it('Merge string values to object', function() { var baseInclude = 'relation1'; var updateInclude = 'relation2'; var expectedInclude = [ @@ -252,7 +253,7 @@ describe('util.mergeIncludes', function () { checkInputOutput(baseInclude, updateInclude, expectedInclude); }); - it('Merge string & array values to object', function () { + it('Merge string & array values to object', function() { var baseInclude = 'relation1'; var updateInclude = ['relation2']; var expectedInclude = [ @@ -262,7 +263,7 @@ describe('util.mergeIncludes', function () { checkInputOutput(baseInclude, updateInclude, expectedInclude); }); - it('Merge string & object values to object', function () { + it('Merge string & object values to object', function() { var baseInclude = ['relation1']; var updateInclude = {relation2: 'relation2Include'}; var expectedInclude = [ @@ -272,7 +273,7 @@ describe('util.mergeIncludes', function () { checkInputOutput(baseInclude, updateInclude, expectedInclude); }); - it('Merge array & array values to object', function () { + it('Merge array & array values to object', function() { var baseInclude = ['relation1']; var updateInclude = ['relation2']; var expectedInclude = [ @@ -282,7 +283,7 @@ describe('util.mergeIncludes', function () { checkInputOutput(baseInclude, updateInclude, expectedInclude); }); - it('Merge array & object values to object', function () { + it('Merge array & object values to object', function() { var baseInclude = ['relation1']; var updateInclude = {relation2: 'relation2Include'}; var expectedInclude = [ @@ -292,7 +293,7 @@ describe('util.mergeIncludes', function () { checkInputOutput(baseInclude, updateInclude, expectedInclude); }); - it('Merge object & object values to object', function () { + it('Merge object & object values to object', function() { var baseInclude = {relation1: 'relation1Include'}; var updateInclude = {relation2: 'relation2Include'}; var expectedInclude = [ @@ -302,7 +303,7 @@ describe('util.mergeIncludes', function () { checkInputOutput(baseInclude, updateInclude, expectedInclude); }); - it('Override property collision with update value', function () { + it('Override property collision with update value', function() { var baseInclude = {relation1: 'baseValue'}; var updateInclude = {relation1: 'updateValue'}; var expectedInclude = [ @@ -312,7 +313,7 @@ describe('util.mergeIncludes', function () { }); it('Merge string includes & include with relation syntax properly', - function () { + function() { var baseInclude = 'relation1'; var updateInclude = {relation: 'relation1'}; var expectedInclude = [ @@ -321,7 +322,7 @@ describe('util.mergeIncludes', function () { checkInputOutput(baseInclude, updateInclude, expectedInclude); }); - it('Merge string includes & include with scope properly', function () { + it('Merge string includes & include with scope properly', function() { var baseInclude = 'relation1'; var updateInclude = { relation: 'relation1', @@ -334,7 +335,7 @@ describe('util.mergeIncludes', function () { }); it('Merge includes with and without relation syntax properly', - function () { + function() { //w & w/o relation syntax - no collision var baseInclude = ['relation2']; var updateInclude = { @@ -361,7 +362,7 @@ describe('util.mergeIncludes', function () { checkInputOutput(baseInclude, updateInclude, expectedInclude); }); - it('Merge includes with mixture of strings, arrays & objects properly', function () { + it('Merge includes with mixture of strings, arrays & objects properly', function() { var baseInclude = ['relation1', {relation2: true}, {relation: 'relation3', scope: {where: {id: 'some id'}}}, {relation: 'relation5', scope: {where: {id: 'some id'}}} @@ -374,5 +375,48 @@ describe('util.mergeIncludes', function () { {relation: 'relation5', scope: {where: {id: 'some id'}}}]; checkInputOutput(baseInclude, updateInclude, expectedInclude); }); +}); + +describe('util.uniq', function() { + + it('should dedupe an array with duplicate number entries', function() { + var a = [1, 2, 1, 3]; + var b = uniq(a); + b.should.eql([1, 2, 3]); + }); + + it('should dedupe an array with duplicate string entries', function() { + var a = ['a', 'a', 'b', 'a']; + var b = uniq(a); + b.should.eql(['a', 'b']); + }); + + it('should dedupe an array without duplicate number entries', function() { + var a = [1, 3, 2]; + var b = uniq(a); + b.should.eql([1, 3, 2]); + }); + + it('should dedupe an array without duplicate string entries', function() { + var a = ['a', 'c', 'b']; + var b = uniq(a); + b.should.eql(['a', 'c', 'b']); + }); + + it('should allow null/undefined array', function() { + var a = null; + var b = uniq(a); + b.should.eql([]); + }); + + it('should report error for non-array arg', function() { + var a = '1'; + try { + var b = uniq(a); + throw new Error('The test should have thrown an error'); + } catch (err) { + err.should.be.instanceof(Error); + } + }); });