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! 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/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) + }) + }); diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 70d14aff..4537a073 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -1422,6 +1422,153 @@ 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'); + 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(); + }) + }); + + 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'); + 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. @@ -2209,4 +2356,41 @@ 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(); + }); + }); + }); + + 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 fa409f2f..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(); }); }); @@ -652,4 +656,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(); + }); + }); + }) + }); + }); }); 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/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/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/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`; } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 7494cc88..e98f8576 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; @@ -103,9 +105,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); }); @@ -487,18 +494,28 @@ 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. + // 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['$in'] = idsIntersection; return query; } @@ -518,7 +535,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 +543,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/LiveQuery/QueryTools.js b/src/LiveQuery/QueryTools.js index 5710c8c9..8a47f25c 100644 --- a/src/LiveQuery/QueryTools.js +++ b/src/LiveQuery/QueryTools.js @@ -206,7 +206,14 @@ function matchesKeyConstraints(object, key, constraints) { } break; case '$exists': - if (typeof object[key] === 'undefined') { + 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; } break; diff --git a/src/RestQuery.js b/src/RestQuery.js index 8cfd26df..e825a544 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) }; @@ -534,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') { 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; } diff --git a/src/cli/cli-definitions.js b/src/cli/cli-definitions.js index 2343f112..e3740a13 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,63 @@ 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": { + "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" }, - "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 +91,7 @@ export default { action: function(opt) { if (opt == "true" || opt == "1") { return true; - } + } return false; } }, @@ -95,22 +105,69 @@ 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; + } + }, + "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", 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; }