diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 759f4f551a..036b106424 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -23,6 +23,105 @@ describe('Parse.Query testing', () => { }); }); + it("notEqualTo with Relation is working", function(done) { + var user = new Parse.User(); + user.setPassword("asdf"); + user.setUsername("zxcv"); + + var user1 = new Parse.User(); + user1.setPassword("asdf"); + user1.setUsername("qwerty"); + + var user2 = new Parse.User(); + user2.setPassword("asdf"); + user2.setUsername("asdf"); + + var Cake = Parse.Object.extend("Cake"); + var cake1 = new Cake(); + var cake2 = new Cake(); + var cake3 = new Cake(); + + + user.signUp().then(function(){ + return user1.signUp(); + }).then(function(){ + return user2.signUp(); + }).then(function(){ + var relLike1 = cake1.relation("liker"); + relLike1.add([user, user1]); + + var relDislike1 = cake1.relation("hater"); + relDislike1.add(user2); + return cake1.save(); + }).then(function(){ + var rellike2 = cake2.relation("liker"); + rellike2.add([user, user1]); + + var relDislike2 = cake2.relation("hater"); + relDislike2.add(user2); + + return cake2.save(); + }).then(function(){ + var rellike3 = cake3.relation("liker"); + rellike3.add(user); + + var relDislike3 = cake3.relation("hater"); + relDislike3.add([user1, user2]); + return cake3.save(); + }).then(function(){ + var query = new Parse.Query(Cake); + // User2 likes nothing so we should receive 0 + query.equalTo("liker", user2); + return query.find().then(function(results){ + equal(results.length, 0); + }); + }).then(function(){ + var query = new Parse.Query(Cake); + // User1 likes two of three cakes + query.equalTo("liker", user1); + return query.find().then(function(results){ + // It should return 2 -> cake 1 and cake 2 + equal(results.length, 2); + }); + }).then(function(){ + var query = new Parse.Query(Cake); + // We want to know which cake the user1 is not appreciating -> cake3 + query.notEqualTo("liker", user1); + return query.find().then(function(results){ + // Should return 1 -> the cake 3 + equal(results.length, 1); + }); + }).then(function(){ + var query = new Parse.Query(Cake); + // User2 is a hater of everything so we should receive 0 + query.notEqualTo("hater", user2); + return query.find().then(function(results){ + equal(results.length, 0); + }); + }).then(function(){ + var query = new Parse.Query(Cake); + // Only cake3 is liked by user + query.notContainedIn("liker", [user1]); + return query.find().then(function(results){ + equal(results.length, 1); + }); + }).then(function(){ + var query = new Parse.Query(Cake); + // All the users + query.containedIn("liker", [user, user1, user2]); + // Exclude user 1 + query.notEqualTo("liker", user1); + // Only cake3 is liked only by user1 + return query.find().then(function(results){ + equal(results.length, 1); + let cake = results[0]; + expect(cake.id).toBe(cake3.id); + }); + }).then(function(){ + done(); + }) + }); + it("query with limit", function(done) { var baz = new TestObject({ foo: 'baz' }); var qux = new TestObject({ foo: 'qux' }); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index e98f857600..2e94a228c3 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -444,26 +444,60 @@ DatabaseController.prototype.reduceInRelation = function(className, query, schem } let promises = Object.keys(query).map((key) => { - if (query[key] && (query[key]['$in'] || query[key].__type == 'Pointer')) { + if (query[key] && (query[key]['$in'] || query[key]['$ne'] || query[key]['$nin'] || query[key].__type == 'Pointer')) { let t = schema.getExpectedType(className, key); let match = t ? t.match(/^relation<(.*)>$/) : false; if (!match) { return Promise.resolve(query); } let relatedClassName = match[1]; - let relatedIds; - if (query[key]['$in']) { - relatedIds = query[key]['$in'].map(r => r.objectId); - } else { - relatedIds = [query[key].objectId]; - } - return this.owningIds(className, key, relatedIds).then((ids) => { - delete query[key]; - this.addInObjectIdsIds(ids, query); - return Promise.resolve(query); + // Build the list of queries + let queries = Object.keys(query[key]).map((constraintKey) => { + let relatedIds; + let isNegation = false; + if (constraintKey === 'objectId') { + relatedIds = [query[key].objectId]; + } else if (constraintKey == '$in') { + relatedIds = query[key]['$in'].map(r => r.objectId); + } else if (constraintKey == '$nin') { + isNegation = true; + relatedIds = query[key]['$nin'].map(r => r.objectId); + } else if (constraintKey == '$ne') { + isNegation = true; + relatedIds = [query[key]['$ne'].objectId]; + } else { + return; + } + return { + isNegation, + relatedIds + } + }); + + // remove the current queryKey as we don,t need it anymore + delete query[key]; + // execute each query independnently to build the list of + // $in / $nin + let promises = queries.map((q) => { + if (!q) { + return Promise.resolve(); + } + return this.owningIds(className, key, q.relatedIds).then((ids) => { + if (q.isNegation) { + this.addNotInObjectIdsIds(ids, query); + } else { + this.addInObjectIdsIds(ids, query); + } + return Promise.resolve(); + }); }); + + return Promise.all(promises).then(() => { + return Promise.resolve(); + }) + } - return Promise.resolve(query); + return Promise.resolve(); }) return Promise.all(promises).then(() => { @@ -520,6 +554,29 @@ DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) { return query; } +DatabaseController.prototype.addNotInObjectIdsIds = function(ids = null, query) { + let idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : null; + let allIds = [idsFromNin, ids].filter(list => list !== null); + let totalLength = allIds.reduce((memo, list) => memo + list.length, 0); + + let idsIntersection = []; + if (totalLength > 125) { + idsIntersection = intersect.big(allIds); + } else { + idsIntersection = intersect(allIds); + } + + // Need to make sure we don't clobber existing $lt or other constraints on objectId. + // Clobbering $eq, $in and shorthand $eq (query.objectId === 'string') constraints + // is expected though. + if (!('objectId' in query) || typeof query.objectId === 'string') { + query.objectId = {}; + } + query.objectId['$nin'] = idsIntersection; + + return query; +} + // Runs a query on the database. // Returns a promise that resolves to a list of items. // Options: