From 3dccd612225b46a9759f0f3c3ad79d240a2a0c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Luiz=20Cardoso?= Date: Tue, 29 Mar 2016 11:28:45 -0300 Subject: [PATCH 01/18] Matching queries with doesNotExist constraint --- spec/QueryTools.spec.js | 14 ++++++++++++++ src/LiveQuery/QueryTools.js | 4 +++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/QueryTools.spec.js b/spec/QueryTools.spec.js index 8c187829..50433f58 100644 --- a/spec/QueryTools.spec.js +++ b/spec/QueryTools.spec.js @@ -112,6 +112,20 @@ describe('matchesQuery', function() { expect(matchesQuery(obj, q)).toBe(false); }); + it('matches queries with doesNotExist constraint', function() { + var obj = { + id: new Id('Item', 'O1'), + count: 15 + }; + var q = new Parse.Query('Item'); + q.doesNotExist('name'); + expect(matchesQuery(obj, q)).toBe(true); + + q = new Parse.Query('Item'); + q.doesNotExist('count'); + expect(matchesQuery(obj, q)).toBe(false); + }); + it('matches on equality queries', function() { var day = new Date(); var location = new Parse.GeoPoint({ diff --git a/src/LiveQuery/QueryTools.js b/src/LiveQuery/QueryTools.js index 5710c8c9..adbc4dee 100644 --- a/src/LiveQuery/QueryTools.js +++ b/src/LiveQuery/QueryTools.js @@ -206,7 +206,9 @@ function matchesKeyConstraints(object, key, constraints) { } break; case '$exists': - if (typeof object[key] === 'undefined') { + let propertyExists = typeof object[key] !== 'undefined'; + let existenceIsRequired = constraints['$exists']; + if ((!propertyExists && existenceIsRequired) || (propertyExists && !existenceIsRequired)) { return false; } break; From a0e77395269842f42d9e80c9b5024f147638392a Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 30 Mar 2016 11:43:12 -0700 Subject: [PATCH 02/18] Add a test to repro #701 --- spec/ParseACL.spec.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index 3fe5656e..778da899 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -1,5 +1,9 @@ // This is a port of the test suite: // hungry/js/test/parse_acl_test.js +var rest = require('../src/rest'); +var Config = require('../src/Config'); +var config = new Config('test'); +var auth = require('../src/Auth'); describe('Parse.ACL', () => { it("acl must be valid", (done) => { @@ -1158,4 +1162,34 @@ describe('Parse.ACL', () => { }); }); + it('regression test #701', done => { + var anonUser = { + authData: { + anonymous: { + id: '00000000-0000-0000-0000-000000000001' + } + } + }; + + Parse.Cloud.afterSave(Parse.User, req => { + if (!req.object.existed()) { + var user = req.object; + var acl = new Parse.ACL(user); + user.setACL(acl); + user.save(null, {useMasterKey: true}).then(user => { + new Parse.Query('_User').get(user.objectId).then(user => { + fail('should not have fetched user without public read enabled'); + done(); + }, error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className); + done(); + }); + }); + } + }); + + rest.create(config, auth.nobody(config), '_User', anonUser) + }) + }); From 97d3deb73b815ac757567ae2f9a30e218d19aba4 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 30 Mar 2016 13:23:21 -0700 Subject: [PATCH 03/18] Regression test for #871 --- spec/ParseRelation.spec.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/spec/ParseRelation.spec.js b/spec/ParseRelation.spec.js index fa409f2f..8b38a8e3 100644 --- a/spec/ParseRelation.spec.js +++ b/spec/ParseRelation.spec.js @@ -652,4 +652,31 @@ describe('Parse.Relation testing', () => { })); }); }); + + it('relations are not bidirectional (regression test for #871)', done => { + let PersonObject = Parse.Object.extend("Person"); + let p1 = new PersonObject(); + let p2 = new PersonObject(); + Parse.Object.saveAll([p1, p2]).then(results => { + let p1 = results[0]; + let p2 = results[1]; + let relation = p1.relation('relation'); + relation.add(p2); + p1.save().then(() => { + let query = new Parse.Query(PersonObject); + query.equalTo('relation', p1); + query.find().then(results => { + expect(results.length).toEqual(0); + + let query = new Parse.Query(PersonObject); + query.equalTo('relation', p2); + query.find().then(results => { + expect(results.length).toEqual(1); + expect(results[0].objectId).toEqual(p1.objectId); + done(); + }); + }); + }) + }); + }); }); From 632c8054dae3269760eb69ec4b52f2c2168f1a2e Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 30 Mar 2016 16:45:26 -0700 Subject: [PATCH 04/18] Regression test for #1259 --- spec/ParseQuery.spec.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 70d14aff..08d5ed46 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -2209,4 +2209,23 @@ describe('Parse.Query testing', () => { }) }) + it('query with two OR subqueries (regression test #1259)', done => { + let relatedObject = new Parse.Object('Class2'); + relatedObject.save().then(relatedObject => { + let anObject = new Parse.Object('Class1'); + let relation = anObject.relation('relation'); + relation.add(relatedObject); + return anObject.save(); + }).then(anObject => { + let q1 = anObject.relation('relation').query(); + q1.doesNotExist('nonExistantKey1'); + let q2 = anObject.relation('relation').query(); + q2.doesNotExist('nonExistantKey2'); + let orQuery = Parse.Query.or(q1, q2).find().then(results => { + expect(results.length).toEqual(1); + expect(results[0].objectId).toEqual(q1.objectId); + done(); + }); + }); + }); }); From ab1858616be21bc8b84734786a99cff0bda68237 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Wed, 30 Mar 2016 19:50:08 -0400 Subject: [PATCH 05/18] Adds ability to override mount with publicServerURL for production uses --- spec/index.spec.js | 34 ++++++++++++++++++++++++++++++++++ src/Config.js | 31 +++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index 821f5d16..b4ca4a8c 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -2,6 +2,7 @@ var request = require('request'); var parseServerPackage = require('../package.json'); var MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); var ParseServer = require("../src/index"); +var Config = require('../src/Config'); var express = require('express'); describe('server', () => { @@ -246,4 +247,37 @@ describe('server', () => { expect(ParseServer.FileSystemAdapter).toThrow(); done(); }); + + it('properly gives publicServerURL when set', done => { + setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + masterKey: 'test', + publicServerURL: 'https://myserver.com/1' + }); + var config = new Config('test', 'http://localhost:8378/1'); + expect(config.mount).toEqual('https://myserver.com/1'); + done(); + }); + + it('properly removes trailing slash in mount', done => { + setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + masterKey: 'test' + }); + var config = new Config('test', 'http://localhost:8378/1/'); + expect(config.mount).toEqual('http://localhost:8378/1'); + done(); + }); + + it('should throw when getting invalid mount', done => { + expect(() => setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + masterKey: 'test', + publicServerURL: 'blabla:/some' + }) ).toThrow("publicServerURL should be a valid HTTPS URL starting with https://"); + done(); + }); }); diff --git a/src/Config.js b/src/Config.js index 4e599bde..b9f0d007 100644 --- a/src/Config.js +++ b/src/Config.js @@ -4,6 +4,16 @@ import cache from './cache'; +function removeTrailingSlash(str) { + if (!str) { + return str; + } + if (str.endsWith("/")) { + str = str.substr(0, str.length-1); + } + return str; +} + export class Config { constructor(applicationId: string, mount: string) { let DatabaseAdapter = require('./DatabaseAdapter'); @@ -24,7 +34,7 @@ export class Config { this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix); this.serverURL = cacheInfo.serverURL; - this.publicServerURL = cacheInfo.publicServerURL; + this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL); this.verifyUserEmails = cacheInfo.verifyUserEmails; this.appName = cacheInfo.appName; @@ -35,7 +45,7 @@ export class Config { this.userController = cacheInfo.userController; this.authDataManager = cacheInfo.authDataManager; this.customPages = cacheInfo.customPages || {}; - this.mount = mount; + this.mount = removeTrailingSlash(mount); this.liveQueryController = cacheInfo.liveQueryController; } @@ -43,6 +53,11 @@ export class Config { this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails, appName: options.appName, publicServerURL: options.publicServerURL}) + if (options.publicServerURL) { + if (!options.publicServerURL.startsWith("http://") && !options.publicServerURL.startsWith("https://")) { + throw "publicServerURL should be a valid HTTPS URL starting with https://" + } + } } static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) { @@ -56,6 +71,18 @@ export class Config { } } + get mount() { + var mount = this._mount; + if (this.publicServerURL) { + mount = this.publicServerURL; + } + return mount; + } + + set mount(newValue) { + this._mount = newValue; + } + get invalidLinkURL() { return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`; } From c5636e1b7722bdcddd8cca9e4e1fccbf49fa5dc3 Mon Sep 17 00:00:00 2001 From: Drew Date: Wed, 30 Mar 2016 17:09:11 -0700 Subject: [PATCH 06/18] Point to #1271 as how to write a good issue report --- .github/ISSUE_TEMPLATE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index f738032d..3f33e522 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,5 @@ +Check out [this issue](https://github.com/ParsePlatform/parse-server/issues/1271) for an ideal bug report. The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly! + For implementation related questions or technical support, please refer to the [Stack Overflow](http://stackoverflow.com/questions/tagged/parse.com) and [Server Fault](https://serverfault.com/tags/parse) communities. Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server! From 5d99075663b4d044e98d6d3bcaaaa058b9c4a4d6 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Wed, 30 Mar 2016 20:27:12 -0400 Subject: [PATCH 07/18] Properly let masterKey add fields --- spec/schemas.spec.js | 82 ++++++++++++++++----------- src/Controllers/DatabaseController.js | 7 ++- 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index e9195615..3edd2e58 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -981,7 +981,7 @@ describe('schemas', () => { }); }); }); - + it('should not be able to add a field', done => { request.post({ url: 'http://localhost:8378/1/schemas/AClass', @@ -1010,7 +1010,7 @@ describe('schemas', () => { }) }) }); - + it('should not be able to add a field', done => { request.post({ url: 'http://localhost:8378/1/schemas/AClass', @@ -1038,7 +1038,7 @@ describe('schemas', () => { }) }) }); - + it('should throw with invalid userId (>10 chars)', done => { request.post({ url: 'http://localhost:8378/1/schemas/AClass', @@ -1056,7 +1056,7 @@ describe('schemas', () => { done(); }) }); - + it('should throw with invalid userId (<10 chars)', done => { request.post({ url: 'http://localhost:8378/1/schemas/AClass', @@ -1074,7 +1074,7 @@ describe('schemas', () => { done(); }) }); - + it('should throw with invalid userId (invalid char)', done => { request.post({ url: 'http://localhost:8378/1/schemas/AClass', @@ -1092,7 +1092,7 @@ describe('schemas', () => { done(); }) }); - + it('should throw with invalid * (spaces)', done => { request.post({ url: 'http://localhost:8378/1/schemas/AClass', @@ -1110,7 +1110,7 @@ describe('schemas', () => { done(); }) }); - + it('should throw with invalid * (spaces)', done => { request.post({ url: 'http://localhost:8378/1/schemas/AClass', @@ -1128,7 +1128,7 @@ describe('schemas', () => { done(); }) }); - + it('should throw with invalid value', done => { request.post({ url: 'http://localhost:8378/1/schemas/AClass', @@ -1146,7 +1146,7 @@ describe('schemas', () => { done(); }) }); - + it('should throw with invalid value', done => { request.post({ url: 'http://localhost:8378/1/schemas/AClass', @@ -1164,10 +1164,10 @@ describe('schemas', () => { done(); }) }); - + function setPermissionsOnClass(className, permissions, doPut) { let op = request.post; - if (doPut) + if (doPut) { op = request.put; } @@ -1190,18 +1190,18 @@ describe('schemas', () => { }) }); } - + it('validate CLP 1', done => { let user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); - + let admin = new Parse.User(); admin.setUsername('admin'); admin.setPassword('admin'); - + let role = new Parse.Role('admin', new Parse.ACL()); - + setPermissionsOnClass('AClass', { 'find': { 'role:admin': true @@ -1239,18 +1239,18 @@ describe('schemas', () => { done(); }) }); - + it('validate CLP 2', done => { let user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); - + let admin = new Parse.User(); admin.setUsername('admin'); admin.setPassword('admin'); - + let role = new Parse.Role('admin', new Parse.ACL()); - + setPermissionsOnClass('AClass', { 'find': { 'role:admin': true @@ -1304,18 +1304,18 @@ describe('schemas', () => { done(); }) }); - + it('validate CLP 3', done => { let user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); - + let admin = new Parse.User(); admin.setUsername('admin'); admin.setPassword('admin'); - + let role = new Parse.Role('admin', new Parse.ACL()); - + setPermissionsOnClass('AClass', { 'find': { 'role:admin': true @@ -1362,18 +1362,18 @@ describe('schemas', () => { done(); }); }); - + it('validate CLP 4', done => { let user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); - + let admin = new Parse.User(); admin.setUsername('admin'); admin.setPassword('admin'); - + let role = new Parse.Role('admin', new Parse.ACL()); - + setPermissionsOnClass('AClass', { 'find': { 'role:admin': true @@ -1400,7 +1400,7 @@ describe('schemas', () => { // borked CLP should not affec security return setPermissionsOnClass('AClass', { 'found': { - 'role:admin': true + 'role:admin': true } }, true).then(() => { fail("Should not be able to save a borked CLP"); @@ -1430,21 +1430,21 @@ describe('schemas', () => { done(); }) }); - + it('validate CLP 5', done => { let user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); - + let user2 = new Parse.User(); user2.setUsername('user2'); user2.setPassword('user2'); let admin = new Parse.User(); admin.setUsername('admin'); admin.setPassword('admin'); - + let role = new Parse.Role('admin', new Parse.ACL()); - + Promise.resolve().then(() => { return Parse.Object.saveAll([user, user2, admin, role], {useMasterKey: true}); }).then(()=> { @@ -1495,5 +1495,21 @@ describe('schemas', () => { }).then(() => { done(); }); - }); + }); + + it('can add field as master (issue #1257)', (done) => { + setPermissionsOnClass('AClass', { + 'addField': {} + }).then(() => { + var obj = new Parse.Object('AClass'); + obj.set('key', 'value'); + return obj.save(null, {useMasterKey: true}) + }).then((obj) => { + expect(obj.get('key')).toEqual('value'); + done(); + }, (err) => { + fail('should not fail'); + done(); + }); + }) }); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 7494cc88..25139114 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -103,9 +103,14 @@ DatabaseController.prototype.redirectClassNameForKey = function(className, key) // batch request, that could confuse other users of the schema. DatabaseController.prototype.validateObject = function(className, object, query, options) { let schema; + let isMaster = !('acl' in options); + var aclGroup = options.acl || []; return this.loadSchema().then(s => { schema = s; - return this.canAddField(schema, className, object, options.acl || []); + if (isMaster) { + return Promise.resolve(); + } + return this.canAddField(schema, className, object, aclGroup); }).then(() => { return schema.validateObject(className, object, query); }); From 6311c9578577a29e5bc163d399b44ae4fd3b1c70 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 30 Mar 2016 19:35:54 -0700 Subject: [PATCH 08/18] Fixes #1271 --- package.json | 1 + spec/ParseQuery.spec.js | 18 +++++++ spec/ParseRelation.spec.js | 66 ++++++++++++----------- src/Controllers/DatabaseController.js | 76 ++++++++++++++------------- src/transform.js | 9 ++-- 5 files changed, 98 insertions(+), 72 deletions(-) diff --git a/package.json b/package.json index 20963097..302ce6e8 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "commander": "^2.9.0", "deepcopy": "^0.6.1", "express": "^4.13.4", + "intersect": "^1.0.1", "lru-cache": "^4.0.0", "mailgun-js": "^0.7.7", "mime": "^1.3.4", diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 08d5ed46..334ac1b6 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -2228,4 +2228,22 @@ describe('Parse.Query testing', () => { }); }); }); + + it('objectId containedIn with multiple large array', done => { + let obj = new Parse.Object('MyClass'); + obj.save().then(obj => { + let longListOfStrings = []; + for (let i = 0; i < 130; i++) { + longListOfStrings.push(i.toString()); + } + longListOfStrings.push(obj.id); + let q = new Parse.Query('MyClass'); + q.containedIn('objectId', longListOfStrings); + q.containedIn('objectId', longListOfStrings); + return q.find(); + }).then(results => { + expect(results.length).toEqual(1); + done(); + }); + }); }); diff --git a/spec/ParseRelation.spec.js b/spec/ParseRelation.spec.js index 8b38a8e3..fbb2b1d3 100644 --- a/spec/ParseRelation.spec.js +++ b/spec/ParseRelation.spec.js @@ -248,46 +248,50 @@ describe('Parse.Relation testing', () => { }); }); - it("queries on relation fields with multiple ins", (done) => { - var ChildObject = Parse.Object.extend("ChildObject"); - var childObjects = []; - for (var i = 0; i < 10; i++) { + it("queries on relation fields with multiple containedIn (regression test for #1271)", (done) => { + let ChildObject = Parse.Object.extend("ChildObject"); + let childObjects = []; + for (let i = 0; i < 10; i++) { childObjects.push(new ChildObject({x: i})); } Parse.Object.saveAll(childObjects).then(() => { - var ParentObject = Parse.Object.extend("ParentObject"); - var parent = new ParentObject(); + let ParentObject = Parse.Object.extend("ParentObject"); + let parent = new ParentObject(); parent.set("x", 4); - var relation = parent.relation("child"); - relation.add(childObjects[0]); - relation.add(childObjects[1]); - relation.add(childObjects[2]); - var parent2 = new ParentObject(); + let parent1Children = parent.relation("child"); + parent1Children.add(childObjects[0]); + parent1Children.add(childObjects[1]); + parent1Children.add(childObjects[2]); + let parent2 = new ParentObject(); parent2.set("x", 3); - var relation2 = parent2.relation("child"); - relation2.add(childObjects[4]); - relation2.add(childObjects[5]); - relation2.add(childObjects[6]); + let parent2Children = parent2.relation("child"); + parent2Children.add(childObjects[4]); + parent2Children.add(childObjects[5]); + parent2Children.add(childObjects[6]); - var otherChild2 = parent2.relation("otherChild"); - otherChild2.add(childObjects[0]); - otherChild2.add(childObjects[1]); - otherChild2.add(childObjects[2]); + let parent2OtherChildren = parent2.relation("otherChild"); + parent2OtherChildren.add(childObjects[0]); + parent2OtherChildren.add(childObjects[1]); + parent2OtherChildren.add(childObjects[2]); - var parents = []; - parents.push(parent); - parents.push(parent2); - return Parse.Object.saveAll(parents); + return Parse.Object.saveAll([parent, parent2]); }).then(() => { - var query = new Parse.Query(ParentObject); - var objects = []; - objects.push(childObjects[0]); - query.containedIn("child", objects); - query.containedIn("otherChild", [childObjects[0]]); - return query.find(); - }).then((list) => { - equal(list.length, 2, "There should be 2 results"); + let objectsWithChild0InBothChildren = new Parse.Query(ParentObject); + objectsWithChild0InBothChildren.containedIn("child", [childObjects[0]]); + objectsWithChild0InBothChildren.containedIn("otherChild", [childObjects[0]]); + return objectsWithChild0InBothChildren.find(); + }).then(objectsWithChild0InBothChildren => { + //No parent has child 0 in both it's "child" and "otherChild" field; + expect(objectsWithChild0InBothChildren.length).toEqual(0); + }).then(() => { + let objectsWithChild4andOtherChild1 = new Parse.Query(ParentObject); + objectsWithChild4andOtherChild1.containedIn("child", [childObjects[4]]); + objectsWithChild4andOtherChild1.containedIn("otherChild", [childObjects[1]]); + return objectsWithChild4andOtherChild1.find(); + }).then(objects => { + // parent2 has child 4 and otherChild 1 + expect(objects.length).toEqual(1); done(); }); }); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 7494cc88..cf3e2bf6 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1,6 +1,8 @@ // A database adapter that works with data exported from the hosted // Parse database. +import intersect from 'intersect'; + var mongodb = require('mongodb'); var Parse = require('parse/node').Parse; @@ -487,18 +489,26 @@ DatabaseController.prototype.reduceRelationKeys = function(className, query) { } }; -DatabaseController.prototype.addInObjectIdsIds = function(ids, query) { - if (typeof query.objectId == 'string') { - // Add equality op as we are sure - // we had a constraint on that one - query.objectId = {'$eq': query.objectId}; +DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) { + let idsFromString = typeof query.objectId === 'string' ? [query.objectId] : null; + let idsFromEq = query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null; + let idsFromIn = query.objectId && query.objectId['$in'] ? query.objectId['$in'] : null; + + let allIds = [idsFromString, idsFromEq, idsFromIn, 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); } - query.objectId = query.objectId || {}; - let queryIn = [].concat(query.objectId['$in'] || [], ids || []); - // make a set and spread to remove duplicates - // replace the $in operator as other constraints - // may be set - query.objectId['$in'] = [...new Set(queryIn)]; + + // Need to make sure we don't clobber existing $lt or other constraints on objectId + if (!('objectId' in query) || typeof query.objectId === 'string') { + query.objectId = {}; + } + query.objectId['$in'] = idsIntersection; return query; } @@ -518,7 +528,7 @@ DatabaseController.prototype.addInObjectIdsIds = function(ids, query) { // anything about users, ideally. Then, improve the format of the ACL // arg to work like the others. DatabaseController.prototype.find = function(className, query, options = {}) { - var mongoOptions = {}; + let mongoOptions = {}; if (options.skip) { mongoOptions.skip = options.skip; } @@ -526,45 +536,39 @@ DatabaseController.prototype.find = function(className, query, options = {}) { mongoOptions.limit = options.limit; } - var isMaster = !('acl' in options); - var aclGroup = options.acl || []; - var acceptor = function(schema) { - return schema.hasKeys(className, keysForQuery(query)); - }; - var schema; - return this.loadSchema(acceptor).then((s) => { + let isMaster = !('acl' in options); + let aclGroup = options.acl || []; + let acceptor = schema => schema.hasKeys(className, keysForQuery(query)) + let schema = null; + return this.loadSchema(acceptor).then(s => { schema = s; if (options.sort) { mongoOptions.sort = {}; - for (var key in options.sort) { - var mongoKey = transform.transformKey(schema, className, key); + for (let key in options.sort) { + let mongoKey = transform.transformKey(schema, className, key); mongoOptions.sort[mongoKey] = options.sort[key]; } } if (!isMaster) { - var op = 'find'; - var k = Object.keys(query); - if (k.length == 1 && typeof query.objectId == 'string') { - op = 'get'; - } + let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? + 'get' : + 'find'; return schema.validatePermission(className, aclGroup, op); } return Promise.resolve(); - }).then(() => { - return this.reduceRelationKeys(className, query); - }).then(() => { - return this.reduceInRelation(className, query, schema); - }).then(() => { - return this.adaptiveCollection(className); - }).then(collection => { - var mongoWhere = transform.transformWhere(schema, className, query); + }) + .then(() => this.reduceRelationKeys(className, query)) + .then(() => this.reduceInRelation(className, query, schema)) + .then(() => this.adaptiveCollection(className)) + .then(collection => { + let mongoWhere = transform.transformWhere(schema, className, query); if (!isMaster) { - var orParts = [ + let orParts = [ {"_rperm" : { "$exists": false }}, {"_rperm" : { "$in" : ["*"]}} ]; - for (var acl of aclGroup) { + for (let acl of aclGroup) { orParts.push({"_rperm" : { "$in" : [acl]}}); } mongoWhere = {'$and': [mongoWhere, {'$or': orParts}]}; diff --git a/src/transform.js b/src/transform.js index 3ca9739b..6c1b85ec 100644 --- a/src/transform.js +++ b/src/transform.js @@ -187,13 +187,12 @@ export function transformKeyValue(schema, className, restKey, restValue, options // Returns the mongo form of the query. // Throws a Parse.Error if the input query is invalid. function transformWhere(schema, className, restWhere) { - var mongoWhere = {}; + let mongoWhere = {}; if (restWhere['ACL']) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, - 'Cannot query on ACL.'); + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); } - for (var restKey in restWhere) { - var out = transformKeyValue(schema, className, restKey, restWhere[restKey], + for (let restKey in restWhere) { + let out = transformKeyValue(schema, className, restKey, restWhere[restKey], {query: true, validate: true}); mongoWhere[out.key] = out.value; } From 73bca3b64ca082cb970ff2dfc9d2e50a5e6c8810 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 30 Mar 2016 19:48:33 -0700 Subject: [PATCH 09/18] Improve comments --- src/Controllers/DatabaseController.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index cf3e2bf6..8faa05b7 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -504,7 +504,9 @@ DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) { idsIntersection = intersect(allIds); } - // Need to make sure we don't clobber existing $lt or other constraints on objectId + // 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 = {}; } From eeb33311675672be484c73149e4d3c053c2f3502 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Thu, 31 Mar 2016 00:19:42 -0700 Subject: [PATCH 10/18] Update error message --- src/Routers/PushRouter.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js index c3af0d28..babbeb27 100644 --- a/src/Routers/PushRouter.js +++ b/src/Routers/PushRouter.js @@ -1,6 +1,6 @@ -import PromiseRouter from '../PromiseRouter'; +import PromiseRouter from '../PromiseRouter'; import * as middleware from "../middlewares"; -import { Parse } from "parse/node"; +import { Parse } from "parse/node"; export class PushRouter extends PromiseRouter { @@ -46,8 +46,7 @@ export class PushRouter extends PromiseRouter { } } } else { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Channels and query should be set at least one.'); + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Sending a push requires either "channels" or a "where" query.'); } return where; } From 09279d1987f0947f1cf107c7eafbc59259e1a042 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 31 Mar 2016 11:49:15 -0400 Subject: [PATCH 11/18] Allows loading cli adapters from module path --- src/cli/cli-definitions.js | 83 ++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/src/cli/cli-definitions.js b/src/cli/cli-definitions.js index 2343f112..839d0b3d 100644 --- a/src/cli/cli-definitions.js +++ b/src/cli/cli-definitions.js @@ -1,10 +1,10 @@ export default { - "appId": { + "appId": { env: "PARSE_SERVER_APPLICATION_ID", help: "Your Parse Application ID", required: true }, - "masterKey": { + "masterKey": { env: "PARSE_SERVER_MASTER_KEY", help: "Your Parse Master Key", required: true @@ -21,53 +21,59 @@ export default { return opt; } }, - "databaseURI": { + "databaseURI": { env: "PARSE_SERVER_DATABASE_URI", help: "The full URI to your mongodb database" }, - "serverURL": { + "serverURL": { env: "PARSE_SERVER_URL", help: "URL to your parse server with http:// or https://.", }, - "clientKey": { + "clientKey": { env: "PARSE_SERVER_CLIENT_KEY", help: "Key for iOS, MacOS, tvOS clients" }, - "javascriptKey": { + "javascriptKey": { env: "PARSE_SERVER_JAVASCRIPT_KEY", help: "Key for the Javascript SDK" - }, - "restAPIKey": { + }, + "restAPIKey": { env: "PARSE_SERVER_REST_API_KEY", help: "Key for REST calls" - }, - "dotNetKey": { + }, + "dotNetKey": { env: "PARSE_SERVER_DOT_NET_KEY", help: "Key for Unity and .Net SDK" - }, - "cloud": { + }, + "cloud": { env: "PARSE_SERVER_CLOUD_CODE_MAIN", help: "Full path to your cloud code main.js" - }, + }, "push": { env: "PARSE_SERVER_PUSH", help: "Configuration for push, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Push", action: function(opt) { + if (typeof opt == 'object') { + return opt; + } return JSON.parse(opt) } }, - "oauth": { + "oauth": { env: "PARSE_SERVER_OAUTH_PROVIDERS", help: "Configuration for your oAuth providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth", action: function(opt) { + if (typeof opt == 'object') { + return opt; + } return JSON.parse(opt) } }, - "fileKey": { + "fileKey": { env: "PARSE_SERVER_FILE_KEY", help: "Key for your files", - }, - "facebookAppIds": { + }, + "facebookAppIds": { env: "PARSE_SERVER_FACEBOOK_APP_IDS", help: "Comma separated list for your facebook app Ids", type: "list", @@ -81,7 +87,7 @@ export default { action: function(opt) { if (opt == "true" || opt == "1") { return true; - } + } return false; } }, @@ -95,22 +101,49 @@ export default { return false; } }, - "mountPath": { + "mountPath": { env: "PARSE_SERVER_MOUNT_PATH", help: "Mount path for the server, defaults to /parse", default: "/parse" }, - "databaseAdapter": { - env: "PARSE_SERVER_DATABASE_ADAPTER", - help: "Adapter module for the database sub-system" - }, "filesAdapter": { env: "PARSE_SERVER_FILES_ADAPTER", - help: "Adapter module for the files sub-system" + help: "Adapter module for the files sub-system", + action: function action(opt) { + if (typeof opt == 'object') { + return opt; + } + try { + return JSON.parse(opt); + } catch(e) {} + return opt; + } + }, + "emailAdapter": { + env: "PARSE_SERVER_EMAIL_ADAPTER", + help: "Adapter module for the email sending", + action: function action(opt) { + if (typeof opt == 'object') { + return opt; + } + try { + return JSON.parse(opt); + } catch(e) {} + return opt; + } }, "loggerAdapter": { env: "PARSE_SERVER_LOGGER_ADAPTER", - help: "Adapter module for the logging sub-system" + help: "Adapter module for the logging sub-system", + action: function action(opt) { + if (typeof opt == 'object') { + return opt; + } + try { + return JSON.parse(opt); + } catch(e) {} + return opt; + } }, "maxUploadSize": { env: "PARSE_SERVER_MAX_UPLOAD_SIZE", From b5625bc2e56727101809ec456de98c04ee0691ee Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 31 Mar 2016 11:53:25 -0400 Subject: [PATCH 12/18] more configuration options in the CLI --- src/cli/cli-definitions.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/cli/cli-definitions.js b/src/cli/cli-definitions.js index 839d0b3d..e3740a13 100644 --- a/src/cli/cli-definitions.js +++ b/src/cli/cli-definitions.js @@ -29,6 +29,10 @@ export default { env: "PARSE_SERVER_URL", help: "URL to your parse server with http:// or https://.", }, + "publicServerURL": { + env: "PARSE_PUBLIC_SERVER_URL", + help: "Public URL to your parse server with http:// or https://.", + }, "clientKey": { env: "PARSE_SERVER_CLIENT_KEY", help: "Key for iOS, MacOS, tvOS clients" @@ -145,6 +149,26 @@ export default { return opt; } }, + "liveQuery": { + env: "PARSE_SERVER_LIVE_QUERY_OPTIONS", + help: "liveQuery options", + action: function action(opt) { + if (typeof opt == 'object') { + return opt; + } + return JSON.parse(opt); + } + }, + "customPages": { + env: "PARSE_SERVER_CUSTOM_PAGES", + help: "custom pages for pasword validation and reset", + action: function action(opt) { + if (typeof opt == 'object') { + return opt; + } + return JSON.parse(opt); + } + }, "maxUploadSize": { env: "PARSE_SERVER_MAX_UPLOAD_SIZE", help: "Max file size for uploads.", From 2b3bf7c2b76ab16a77815305c3ad0b528691110c Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 31 Mar 2016 19:01:42 -0400 Subject: [PATCH 13/18] :zap: troubleshoot #1293 --- spec/ParseQuery.spec.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 334ac1b6..50cbebae 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -1422,6 +1422,40 @@ describe('Parse.Query testing', () => { }); }); + it('properly includes array', (done) => { + let objects = []; + let total = 0; + while(objects.length != 5) { + let object = new Parse.Object('AnObject'); + object.set('key', objects.length); + total += objects.length; + objects.push(object); + } + Parse.Object.saveAll(objects).then(() => { + let object = new Parse.Object("AContainer"); + object.set('objects', objects); + return object.save(); + }).then(() => { + let query = new Parse.Query('AContainer'); + query.include('objects'); + return query.find() + }).then((results) => { + expect(results.length).toBe(1); + let res = results[0]; + let objects = res.get('objects'); + expect(objects.length).toBe(5); + objects.forEach((object) => { + total -= object.get('key'); + }); + expect(total).toBe(0); + done() + }, () => { + fail('should not fail'); + console.error(err); + done(); + }) + }) + it("result object creation uses current extension", function(done) { var ParentObject = Parse.Object.extend({ className: "ParentObject" }); // Add a foo() method to ChildObject. From 9c528c6fe886e1a27709221130ec21c64b2100d5 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 31 Mar 2016 19:10:59 -0400 Subject: [PATCH 14/18] :tada: regression test for #1298 --- spec/ParseQuery.spec.js | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 50cbebae..a2340e9e 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -1451,7 +1451,45 @@ describe('Parse.Query testing', () => { done() }, () => { fail('should not fail'); - console.error(err); + done(); + }) + }); + + it('properly includes array of mixed objects', (done) => { + let objects = []; + let total = 0; + while(objects.length != 5) { + let object = new Parse.Object('AnObject'); + object.set('key', objects.length); + total += objects.length; + objects.push(object); + } + while(objects.length != 10) { + let object = new Parse.Object('AnotherObject'); + object.set('key', objects.length); + total += objects.length; + objects.push(object); + } + Parse.Object.saveAll(objects).then(() => { + let object = new Parse.Object("AContainer"); + object.set('objects', objects); + return object.save(); + }).then(() => { + let query = new Parse.Query('AContainer'); + query.include('objects'); + return query.find() + }).then((results) => { + expect(results.length).toBe(1); + let res = results[0]; + let objects = res.get('objects'); + expect(objects.length).toBe(10); + objects.forEach((object) => { + total -= object.get('key'); + }); + expect(total).toBe(0); + done() + }, (err) => { + fail('should not fail'); done(); }) }) From ca7d8580e3b9d061f4b72d6c34f9fdbb6961127a Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 31 Mar 2016 19:32:24 -0400 Subject: [PATCH 15/18] :+1: fixes #1298 --- src/RestQuery.js | 49 +++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/RestQuery.js b/src/RestQuery.js index 8cfd26df..f2636b54 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -449,39 +449,42 @@ function includePath(config, auth, response, path) { if (pointers.length == 0) { return response; } + let pointersHash = {}; var className = null; var objectIds = {}; for (var pointer of pointers) { - if (className === null) { - className = pointer.className; - } else { - if (className != pointer.className) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'inconsistent type data for include'); - } + let className = pointer.className; + // only include the good pointers + if (className) { + pointersHash[className] = pointersHash[className] || []; + pointersHash[className].push(pointer.objectId); } - objectIds[pointer.objectId] = true; - } - if (!className) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'bad pointers'); } + let queryPromises = Object.keys(pointersHash).map((className) => { + var where = {'objectId': {'$in': pointersHash[className]}}; + var query = new RestQuery(config, auth, className, where); + return query.execute().then((results) => { + results.className = className; + return Promise.resolve(results); + }) + }) + // Get the objects for all these object ids - var where = {'objectId': {'$in': Object.keys(objectIds)}}; - var query = new RestQuery(config, auth, className, where); - return query.execute().then((includeResponse) => { - var replace = {}; - for (var obj of includeResponse.results) { - obj.__type = 'Object'; - obj.className = className; + return Promise.all(queryPromises).then((responses) => { + var replace = responses.reduce((replace, includeResponse) => { + for (var obj of includeResponse.results) { + obj.__type = 'Object'; + obj.className = includeResponse.className; - if(className == "_User"){ - delete obj.sessionToken; + if(className == "_User"){ + delete obj.sessionToken; + } + replace[obj.objectId] = obj; } + return replace; + }, {}) - replace[obj.objectId] = obj; - } var resp = { results: replacePointers(response.results, path, replace) }; From 431d864ac35c1ed66475c5992eca15f278af75a5 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 31 Mar 2016 20:38:34 -0400 Subject: [PATCH 16/18] :zap: reproduces #1302 --- spec/ParseQuery.spec.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index a2340e9e..206fe7f6 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -1494,6 +1494,36 @@ describe('Parse.Query testing', () => { }) }) + it('properly fetches nested pointers', (done) =>  { + let color = new Parse.Object('Color'); + color.set('hex','#133733'); + let circle = new Parse.Object('Circle'); + circle.set('radius', 1337); + + Parse.Object.saveAll([color, circle]).then(() => { + circle.set('color', color); + let badCircle = new Parse.Object('Circle'); + badCircle.id = 'badId'; + let complexFigure = new Parse.Object('ComplexFigure'); + complexFigure.set('consistsOf', [circle, badCircle]); + return complexFigure.save(); + }).then(() => { + let q = new Parse.Query('ComplexFigure'); + q.include('consistsOf.color'); + return q.find() + }).then((results) => { + expect(results.length).toBe(1); + let figure = results[0]; + expect(figure.get('consistsOf').length).toBe(1); + expect(figure.get('consistsOf')[0].get('color').get('hex')).toBe('#133733'); + done(); + }, (err) => { + fail('should not fail'); + done(); + }) + + }); + it("result object creation uses current extension", function(done) { var ParentObject = Parse.Object.extend({ className: "ParentObject" }); // Add a foo() method to ChildObject. From edfa4092c0b4cc4b326acdb64468be68eef04ec5 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 31 Mar 2016 20:24:18 -0400 Subject: [PATCH 17/18] :sunglasses: fixes #1302 - when including elements from an array of pointers, filters unaccessible/missing objects --- spec/ParseQuery.spec.js | 47 ++++++++++++++++++++++++++++++++++++++++- src/RestQuery.js | 3 ++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 206fe7f6..4537a073 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -1492,7 +1492,52 @@ describe('Parse.Query testing', () => { fail('should not fail'); done(); }) - }) + }); + + it('properly nested array of mixed objects with bad ids', (done) => { + let objects = []; + let total = 0; + while(objects.length != 5) { + let object = new Parse.Object('AnObject'); + object.set('key', objects.length); + objects.push(object); + } + while(objects.length != 10) { + let object = new Parse.Object('AnotherObject'); + object.set('key', objects.length); + objects.push(object); + } + Parse.Object.saveAll(objects).then(() => { + let object = new Parse.Object("AContainer"); + for (var i=0; i { + let query = new Parse.Query('AContainer'); + query.include('objects'); + return query.find() + }).then((results) => { + expect(results.length).toBe(1); + let res = results[0]; + let objects = res.get('objects'); + expect(objects.length).toBe(5); + objects.forEach((object) => { + total -= object.get('key'); + }); + expect(total).toBe(0); + done() + }, (err) => { + console.error(err); + fail('should not fail'); + done(); + }) + }); it('properly fetches nested pointers', (done) =>  { let color = new Parse.Object('Color'); diff --git a/src/RestQuery.js b/src/RestQuery.js index f2636b54..e825a544 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -537,7 +537,8 @@ function findPointers(object, path) { // pointers inflated. function replacePointers(object, path, replace) { if (object instanceof Array) { - return object.map((obj) => replacePointers(obj, path, replace)); + return object.map((obj) => replacePointers(obj, path, replace)) + .filter((obj) => obj != null && obj != undefined); } if (typeof object !== 'object') { From 01af755d18decb5669aa37bddb7a090569286553 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Thu, 31 Mar 2016 18:49:58 -0700 Subject: [PATCH 18/18] Accept only bool for $exists in LiveQuery --- src/LiveQuery/QueryTools.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/LiveQuery/QueryTools.js b/src/LiveQuery/QueryTools.js index adbc4dee..8a47f25c 100644 --- a/src/LiveQuery/QueryTools.js +++ b/src/LiveQuery/QueryTools.js @@ -208,6 +208,11 @@ function matchesKeyConstraints(object, key, constraints) { case '$exists': let propertyExists = typeof object[key] !== 'undefined'; let existenceIsRequired = constraints['$exists']; + if (typeof constraints['$exists'] !== 'boolean') { + // The SDK will never submit a non-boolean for $exists, but if someone + // tries to submit a non-boolean for $exits outside the SDKs, just ignore it. + break; + } if ((!propertyExists && existenceIsRequired) || (propertyExists && !existenceIsRequired)) { return false; }