Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Jeremy
2016-04-01 11:33:11 -04:00
15 changed files with 582 additions and 162 deletions

View File

@@ -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. 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! Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!

View File

@@ -26,6 +26,7 @@
"commander": "^2.9.0", "commander": "^2.9.0",
"deepcopy": "^0.6.1", "deepcopy": "^0.6.1",
"express": "^4.13.4", "express": "^4.13.4",
"intersect": "^1.0.1",
"lru-cache": "^4.0.0", "lru-cache": "^4.0.0",
"mailgun-js": "^0.7.7", "mailgun-js": "^0.7.7",
"mime": "^1.3.4", "mime": "^1.3.4",

View File

@@ -1,5 +1,9 @@
// This is a port of the test suite: // This is a port of the test suite:
// hungry/js/test/parse_acl_test.js // 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', () => { describe('Parse.ACL', () => {
it("acl must be valid", (done) => { 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)
})
}); });

View File

@@ -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<objects.length; i++) {
if (i%2 == 0) {
objects[i].id = 'randomThing'
} else {
total += objects[i].get('key');
}
}
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()
}, (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) { it("result object creation uses current extension", function(done) {
var ParentObject = Parse.Object.extend({ className: "ParentObject" }); var ParentObject = Parse.Object.extend({ className: "ParentObject" });
// Add a foo() method to ChildObject. // 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();
});
});
}); });

View File

@@ -248,46 +248,50 @@ describe('Parse.Relation testing', () => {
}); });
}); });
it("queries on relation fields with multiple ins", (done) => { it("queries on relation fields with multiple containedIn (regression test for #1271)", (done) => {
var ChildObject = Parse.Object.extend("ChildObject"); let ChildObject = Parse.Object.extend("ChildObject");
var childObjects = []; let childObjects = [];
for (var i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
childObjects.push(new ChildObject({x: i})); childObjects.push(new ChildObject({x: i}));
} }
Parse.Object.saveAll(childObjects).then(() => { Parse.Object.saveAll(childObjects).then(() => {
var ParentObject = Parse.Object.extend("ParentObject"); let ParentObject = Parse.Object.extend("ParentObject");
var parent = new ParentObject(); let parent = new ParentObject();
parent.set("x", 4); parent.set("x", 4);
var relation = parent.relation("child"); let parent1Children = parent.relation("child");
relation.add(childObjects[0]); parent1Children.add(childObjects[0]);
relation.add(childObjects[1]); parent1Children.add(childObjects[1]);
relation.add(childObjects[2]); parent1Children.add(childObjects[2]);
var parent2 = new ParentObject(); let parent2 = new ParentObject();
parent2.set("x", 3); parent2.set("x", 3);
var relation2 = parent2.relation("child"); let parent2Children = parent2.relation("child");
relation2.add(childObjects[4]); parent2Children.add(childObjects[4]);
relation2.add(childObjects[5]); parent2Children.add(childObjects[5]);
relation2.add(childObjects[6]); parent2Children.add(childObjects[6]);
var otherChild2 = parent2.relation("otherChild"); let parent2OtherChildren = parent2.relation("otherChild");
otherChild2.add(childObjects[0]); parent2OtherChildren.add(childObjects[0]);
otherChild2.add(childObjects[1]); parent2OtherChildren.add(childObjects[1]);
otherChild2.add(childObjects[2]); parent2OtherChildren.add(childObjects[2]);
var parents = []; return Parse.Object.saveAll([parent, parent2]);
parents.push(parent);
parents.push(parent2);
return Parse.Object.saveAll(parents);
}).then(() => { }).then(() => {
var query = new Parse.Query(ParentObject); let objectsWithChild0InBothChildren = new Parse.Query(ParentObject);
var objects = []; objectsWithChild0InBothChildren.containedIn("child", [childObjects[0]]);
objects.push(childObjects[0]); objectsWithChild0InBothChildren.containedIn("otherChild", [childObjects[0]]);
query.containedIn("child", objects); return objectsWithChild0InBothChildren.find();
query.containedIn("otherChild", [childObjects[0]]); }).then(objectsWithChild0InBothChildren => {
return query.find(); //No parent has child 0 in both it's "child" and "otherChild" field;
}).then((list) => { expect(objectsWithChild0InBothChildren.length).toEqual(0);
equal(list.length, 2, "There should be 2 results"); }).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(); 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();
});
});
})
});
});
}); });

View File

@@ -112,6 +112,20 @@ describe('matchesQuery', function() {
expect(matchesQuery(obj, q)).toBe(false); 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() { it('matches on equality queries', function() {
var day = new Date(); var day = new Date();
var location = new Parse.GeoPoint({ var location = new Parse.GeoPoint({

View File

@@ -2,6 +2,7 @@ var request = require('request');
var parseServerPackage = require('../package.json'); var parseServerPackage = require('../package.json');
var MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); var MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions');
var ParseServer = require("../src/index"); var ParseServer = require("../src/index");
var Config = require('../src/Config');
var express = require('express'); var express = require('express');
describe('server', () => { describe('server', () => {
@@ -246,4 +247,37 @@ describe('server', () => {
expect(ParseServer.FileSystemAdapter).toThrow(); expect(ParseServer.FileSystemAdapter).toThrow();
done(); 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();
});
}); });

View File

@@ -981,7 +981,7 @@ describe('schemas', () => {
}); });
}); });
}); });
it('should not be able to add a field', done => { it('should not be able to add a field', done => {
request.post({ request.post({
url: 'http://localhost:8378/1/schemas/AClass', url: 'http://localhost:8378/1/schemas/AClass',
@@ -1010,7 +1010,7 @@ describe('schemas', () => {
}) })
}) })
}); });
it('should not be able to add a field', done => { it('should not be able to add a field', done => {
request.post({ request.post({
url: 'http://localhost:8378/1/schemas/AClass', url: 'http://localhost:8378/1/schemas/AClass',
@@ -1038,7 +1038,7 @@ describe('schemas', () => {
}) })
}) })
}); });
it('should throw with invalid userId (>10 chars)', done => { it('should throw with invalid userId (>10 chars)', done => {
request.post({ request.post({
url: 'http://localhost:8378/1/schemas/AClass', url: 'http://localhost:8378/1/schemas/AClass',
@@ -1056,7 +1056,7 @@ describe('schemas', () => {
done(); done();
}) })
}); });
it('should throw with invalid userId (<10 chars)', done => { it('should throw with invalid userId (<10 chars)', done => {
request.post({ request.post({
url: 'http://localhost:8378/1/schemas/AClass', url: 'http://localhost:8378/1/schemas/AClass',
@@ -1074,7 +1074,7 @@ describe('schemas', () => {
done(); done();
}) })
}); });
it('should throw with invalid userId (invalid char)', done => { it('should throw with invalid userId (invalid char)', done => {
request.post({ request.post({
url: 'http://localhost:8378/1/schemas/AClass', url: 'http://localhost:8378/1/schemas/AClass',
@@ -1092,7 +1092,7 @@ describe('schemas', () => {
done(); done();
}) })
}); });
it('should throw with invalid * (spaces)', done => { it('should throw with invalid * (spaces)', done => {
request.post({ request.post({
url: 'http://localhost:8378/1/schemas/AClass', url: 'http://localhost:8378/1/schemas/AClass',
@@ -1110,7 +1110,7 @@ describe('schemas', () => {
done(); done();
}) })
}); });
it('should throw with invalid * (spaces)', done => { it('should throw with invalid * (spaces)', done => {
request.post({ request.post({
url: 'http://localhost:8378/1/schemas/AClass', url: 'http://localhost:8378/1/schemas/AClass',
@@ -1128,7 +1128,7 @@ describe('schemas', () => {
done(); done();
}) })
}); });
it('should throw with invalid value', done => { it('should throw with invalid value', done => {
request.post({ request.post({
url: 'http://localhost:8378/1/schemas/AClass', url: 'http://localhost:8378/1/schemas/AClass',
@@ -1146,7 +1146,7 @@ describe('schemas', () => {
done(); done();
}) })
}); });
it('should throw with invalid value', done => { it('should throw with invalid value', done => {
request.post({ request.post({
url: 'http://localhost:8378/1/schemas/AClass', url: 'http://localhost:8378/1/schemas/AClass',
@@ -1164,10 +1164,10 @@ describe('schemas', () => {
done(); done();
}) })
}); });
function setPermissionsOnClass(className, permissions, doPut) { function setPermissionsOnClass(className, permissions, doPut) {
let op = request.post; let op = request.post;
if (doPut) if (doPut)
{ {
op = request.put; op = request.put;
} }
@@ -1190,18 +1190,18 @@ describe('schemas', () => {
}) })
}); });
} }
it('validate CLP 1', done => { it('validate CLP 1', done => {
let user = new Parse.User(); let user = new Parse.User();
user.setUsername('user'); user.setUsername('user');
user.setPassword('user'); user.setPassword('user');
let admin = new Parse.User(); let admin = new Parse.User();
admin.setUsername('admin'); admin.setUsername('admin');
admin.setPassword('admin'); admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL()); let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', { setPermissionsOnClass('AClass', {
'find': { 'find': {
'role:admin': true 'role:admin': true
@@ -1239,18 +1239,18 @@ describe('schemas', () => {
done(); done();
}) })
}); });
it('validate CLP 2', done => { it('validate CLP 2', done => {
let user = new Parse.User(); let user = new Parse.User();
user.setUsername('user'); user.setUsername('user');
user.setPassword('user'); user.setPassword('user');
let admin = new Parse.User(); let admin = new Parse.User();
admin.setUsername('admin'); admin.setUsername('admin');
admin.setPassword('admin'); admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL()); let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', { setPermissionsOnClass('AClass', {
'find': { 'find': {
'role:admin': true 'role:admin': true
@@ -1304,18 +1304,18 @@ describe('schemas', () => {
done(); done();
}) })
}); });
it('validate CLP 3', done => { it('validate CLP 3', done => {
let user = new Parse.User(); let user = new Parse.User();
user.setUsername('user'); user.setUsername('user');
user.setPassword('user'); user.setPassword('user');
let admin = new Parse.User(); let admin = new Parse.User();
admin.setUsername('admin'); admin.setUsername('admin');
admin.setPassword('admin'); admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL()); let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', { setPermissionsOnClass('AClass', {
'find': { 'find': {
'role:admin': true 'role:admin': true
@@ -1362,18 +1362,18 @@ describe('schemas', () => {
done(); done();
}); });
}); });
it('validate CLP 4', done => { it('validate CLP 4', done => {
let user = new Parse.User(); let user = new Parse.User();
user.setUsername('user'); user.setUsername('user');
user.setPassword('user'); user.setPassword('user');
let admin = new Parse.User(); let admin = new Parse.User();
admin.setUsername('admin'); admin.setUsername('admin');
admin.setPassword('admin'); admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL()); let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', { setPermissionsOnClass('AClass', {
'find': { 'find': {
'role:admin': true 'role:admin': true
@@ -1400,7 +1400,7 @@ describe('schemas', () => {
// borked CLP should not affec security // borked CLP should not affec security
return setPermissionsOnClass('AClass', { return setPermissionsOnClass('AClass', {
'found': { 'found': {
'role:admin': true 'role:admin': true
} }
}, true).then(() => { }, true).then(() => {
fail("Should not be able to save a borked CLP"); fail("Should not be able to save a borked CLP");
@@ -1430,21 +1430,21 @@ describe('schemas', () => {
done(); done();
}) })
}); });
it('validate CLP 5', done => { it('validate CLP 5', done => {
let user = new Parse.User(); let user = new Parse.User();
user.setUsername('user'); user.setUsername('user');
user.setPassword('user'); user.setPassword('user');
let user2 = new Parse.User(); let user2 = new Parse.User();
user2.setUsername('user2'); user2.setUsername('user2');
user2.setPassword('user2'); user2.setPassword('user2');
let admin = new Parse.User(); let admin = new Parse.User();
admin.setUsername('admin'); admin.setUsername('admin');
admin.setPassword('admin'); admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL()); let role = new Parse.Role('admin', new Parse.ACL());
Promise.resolve().then(() => { Promise.resolve().then(() => {
return Parse.Object.saveAll([user, user2, admin, role], {useMasterKey: true}); return Parse.Object.saveAll([user, user2, admin, role], {useMasterKey: true});
}).then(()=> { }).then(()=> {
@@ -1495,5 +1495,21 @@ describe('schemas', () => {
}).then(() => { }).then(() => {
done(); 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();
});
})
}); });

View File

@@ -4,6 +4,16 @@
import cache from './cache'; 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 { export class Config {
constructor(applicationId: string, mount: string) { constructor(applicationId: string, mount: string) {
let DatabaseAdapter = require('./DatabaseAdapter'); let DatabaseAdapter = require('./DatabaseAdapter');
@@ -24,7 +34,7 @@ export class Config {
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix); this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
this.serverURL = cacheInfo.serverURL; this.serverURL = cacheInfo.serverURL;
this.publicServerURL = cacheInfo.publicServerURL; this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL);
this.verifyUserEmails = cacheInfo.verifyUserEmails; this.verifyUserEmails = cacheInfo.verifyUserEmails;
this.appName = cacheInfo.appName; this.appName = cacheInfo.appName;
@@ -35,7 +45,7 @@ export class Config {
this.userController = cacheInfo.userController; this.userController = cacheInfo.userController;
this.authDataManager = cacheInfo.authDataManager; this.authDataManager = cacheInfo.authDataManager;
this.customPages = cacheInfo.customPages || {}; this.customPages = cacheInfo.customPages || {};
this.mount = mount; this.mount = removeTrailingSlash(mount);
this.liveQueryController = cacheInfo.liveQueryController; this.liveQueryController = cacheInfo.liveQueryController;
} }
@@ -43,6 +53,11 @@ export class Config {
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails, this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
appName: options.appName, appName: options.appName,
publicServerURL: options.publicServerURL}) 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}) { 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() { get invalidLinkURL() {
return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`; return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
} }

View File

@@ -1,6 +1,8 @@
// A database adapter that works with data exported from the hosted // A database adapter that works with data exported from the hosted
// Parse database. // Parse database.
import intersect from 'intersect';
var mongodb = require('mongodb'); var mongodb = require('mongodb');
var Parse = require('parse/node').Parse; 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. // batch request, that could confuse other users of the schema.
DatabaseController.prototype.validateObject = function(className, object, query, options) { DatabaseController.prototype.validateObject = function(className, object, query, options) {
let schema; let schema;
let isMaster = !('acl' in options);
var aclGroup = options.acl || [];
return this.loadSchema().then(s => { return this.loadSchema().then(s => {
schema = s; schema = s;
return this.canAddField(schema, className, object, options.acl || []); if (isMaster) {
return Promise.resolve();
}
return this.canAddField(schema, className, object, aclGroup);
}).then(() => { }).then(() => {
return schema.validateObject(className, object, query); return schema.validateObject(className, object, query);
}); });
@@ -487,18 +494,28 @@ DatabaseController.prototype.reduceRelationKeys = function(className, query) {
} }
}; };
DatabaseController.prototype.addInObjectIdsIds = function(ids, query) { DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) {
if (typeof query.objectId == 'string') { let idsFromString = typeof query.objectId === 'string' ? [query.objectId] : null;
// Add equality op as we are sure let idsFromEq = query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null;
// we had a constraint on that one let idsFromIn = query.objectId && query.objectId['$in'] ? query.objectId['$in'] : null;
query.objectId = {'$eq': query.objectId};
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 || []); // Need to make sure we don't clobber existing $lt or other constraints on objectId.
// make a set and spread to remove duplicates // Clobbering $eq, $in and shorthand $eq (query.objectId === 'string') constraints
// replace the $in operator as other constraints // is expected though.
// may be set if (!('objectId' in query) || typeof query.objectId === 'string') {
query.objectId['$in'] = [...new Set(queryIn)]; query.objectId = {};
}
query.objectId['$in'] = idsIntersection;
return query; return query;
} }
@@ -518,7 +535,7 @@ DatabaseController.prototype.addInObjectIdsIds = function(ids, query) {
// anything about users, ideally. Then, improve the format of the ACL // anything about users, ideally. Then, improve the format of the ACL
// arg to work like the others. // arg to work like the others.
DatabaseController.prototype.find = function(className, query, options = {}) { DatabaseController.prototype.find = function(className, query, options = {}) {
var mongoOptions = {}; let mongoOptions = {};
if (options.skip) { if (options.skip) {
mongoOptions.skip = options.skip; mongoOptions.skip = options.skip;
} }
@@ -526,45 +543,39 @@ DatabaseController.prototype.find = function(className, query, options = {}) {
mongoOptions.limit = options.limit; mongoOptions.limit = options.limit;
} }
var isMaster = !('acl' in options); let isMaster = !('acl' in options);
var aclGroup = options.acl || []; let aclGroup = options.acl || [];
var acceptor = function(schema) { let acceptor = schema => schema.hasKeys(className, keysForQuery(query))
return schema.hasKeys(className, keysForQuery(query)); let schema = null;
}; return this.loadSchema(acceptor).then(s => {
var schema;
return this.loadSchema(acceptor).then((s) => {
schema = s; schema = s;
if (options.sort) { if (options.sort) {
mongoOptions.sort = {}; mongoOptions.sort = {};
for (var key in options.sort) { for (let key in options.sort) {
var mongoKey = transform.transformKey(schema, className, key); let mongoKey = transform.transformKey(schema, className, key);
mongoOptions.sort[mongoKey] = options.sort[key]; mongoOptions.sort[mongoKey] = options.sort[key];
} }
} }
if (!isMaster) { if (!isMaster) {
var op = 'find'; let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ?
var k = Object.keys(query); 'get' :
if (k.length == 1 && typeof query.objectId == 'string') { 'find';
op = 'get';
}
return schema.validatePermission(className, aclGroup, op); return schema.validatePermission(className, aclGroup, op);
} }
return Promise.resolve(); return Promise.resolve();
}).then(() => { })
return this.reduceRelationKeys(className, query); .then(() => this.reduceRelationKeys(className, query))
}).then(() => { .then(() => this.reduceInRelation(className, query, schema))
return this.reduceInRelation(className, query, schema); .then(() => this.adaptiveCollection(className))
}).then(() => { .then(collection => {
return this.adaptiveCollection(className); let mongoWhere = transform.transformWhere(schema, className, query);
}).then(collection => {
var mongoWhere = transform.transformWhere(schema, className, query);
if (!isMaster) { if (!isMaster) {
var orParts = [ let orParts = [
{"_rperm" : { "$exists": false }}, {"_rperm" : { "$exists": false }},
{"_rperm" : { "$in" : ["*"]}} {"_rperm" : { "$in" : ["*"]}}
]; ];
for (var acl of aclGroup) { for (let acl of aclGroup) {
orParts.push({"_rperm" : { "$in" : [acl]}}); orParts.push({"_rperm" : { "$in" : [acl]}});
} }
mongoWhere = {'$and': [mongoWhere, {'$or': orParts}]}; mongoWhere = {'$and': [mongoWhere, {'$or': orParts}]};

View File

@@ -206,7 +206,14 @@ function matchesKeyConstraints(object, key, constraints) {
} }
break; break;
case '$exists': 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; return false;
} }
break; break;

View File

@@ -449,39 +449,42 @@ function includePath(config, auth, response, path) {
if (pointers.length == 0) { if (pointers.length == 0) {
return response; return response;
} }
let pointersHash = {};
var className = null; var className = null;
var objectIds = {}; var objectIds = {};
for (var pointer of pointers) { for (var pointer of pointers) {
if (className === null) { let className = pointer.className;
className = pointer.className; // only include the good pointers
} else { if (className) {
if (className != pointer.className) { pointersHash[className] = pointersHash[className] || [];
throw new Parse.Error(Parse.Error.INVALID_JSON, pointersHash[className].push(pointer.objectId);
'inconsistent type data for include');
}
} }
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 // Get the objects for all these object ids
var where = {'objectId': {'$in': Object.keys(objectIds)}}; return Promise.all(queryPromises).then((responses) => {
var query = new RestQuery(config, auth, className, where); var replace = responses.reduce((replace, includeResponse) => {
return query.execute().then((includeResponse) => { for (var obj of includeResponse.results) {
var replace = {}; obj.__type = 'Object';
for (var obj of includeResponse.results) { obj.className = includeResponse.className;
obj.__type = 'Object';
obj.className = className;
if(className == "_User"){ if(className == "_User"){
delete obj.sessionToken; delete obj.sessionToken;
}
replace[obj.objectId] = obj;
} }
return replace;
}, {})
replace[obj.objectId] = obj;
}
var resp = { var resp = {
results: replacePointers(response.results, path, replace) results: replacePointers(response.results, path, replace)
}; };
@@ -534,7 +537,8 @@ function findPointers(object, path) {
// pointers inflated. // pointers inflated.
function replacePointers(object, path, replace) { function replacePointers(object, path, replace) {
if (object instanceof Array) { 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') { if (typeof object !== 'object') {

View File

@@ -1,6 +1,6 @@
import PromiseRouter from '../PromiseRouter'; import PromiseRouter from '../PromiseRouter';
import * as middleware from "../middlewares"; import * as middleware from "../middlewares";
import { Parse } from "parse/node"; import { Parse } from "parse/node";
export class PushRouter extends PromiseRouter { export class PushRouter extends PromiseRouter {
@@ -46,8 +46,7 @@ export class PushRouter extends PromiseRouter {
} }
} }
} else { } else {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Sending a push requires either "channels" or a "where" query.');
'Channels and query should be set at least one.');
} }
return where; return where;
} }

View File

@@ -1,10 +1,10 @@
export default { export default {
"appId": { "appId": {
env: "PARSE_SERVER_APPLICATION_ID", env: "PARSE_SERVER_APPLICATION_ID",
help: "Your Parse Application ID", help: "Your Parse Application ID",
required: true required: true
}, },
"masterKey": { "masterKey": {
env: "PARSE_SERVER_MASTER_KEY", env: "PARSE_SERVER_MASTER_KEY",
help: "Your Parse Master Key", help: "Your Parse Master Key",
required: true required: true
@@ -21,53 +21,63 @@ export default {
return opt; return opt;
} }
}, },
"databaseURI": { "databaseURI": {
env: "PARSE_SERVER_DATABASE_URI", env: "PARSE_SERVER_DATABASE_URI",
help: "The full URI to your mongodb database" help: "The full URI to your mongodb database"
}, },
"serverURL": { "serverURL": {
env: "PARSE_SERVER_URL", env: "PARSE_SERVER_URL",
help: "URL to your parse server with http:// or https://.", 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", env: "PARSE_SERVER_CLIENT_KEY",
help: "Key for iOS, MacOS, tvOS clients" help: "Key for iOS, MacOS, tvOS clients"
}, },
"javascriptKey": { "javascriptKey": {
env: "PARSE_SERVER_JAVASCRIPT_KEY", env: "PARSE_SERVER_JAVASCRIPT_KEY",
help: "Key for the Javascript SDK" help: "Key for the Javascript SDK"
}, },
"restAPIKey": { "restAPIKey": {
env: "PARSE_SERVER_REST_API_KEY", env: "PARSE_SERVER_REST_API_KEY",
help: "Key for REST calls" help: "Key for REST calls"
}, },
"dotNetKey": { "dotNetKey": {
env: "PARSE_SERVER_DOT_NET_KEY", env: "PARSE_SERVER_DOT_NET_KEY",
help: "Key for Unity and .Net SDK" help: "Key for Unity and .Net SDK"
}, },
"cloud": { "cloud": {
env: "PARSE_SERVER_CLOUD_CODE_MAIN", env: "PARSE_SERVER_CLOUD_CODE_MAIN",
help: "Full path to your cloud code main.js" help: "Full path to your cloud code main.js"
}, },
"push": { "push": {
env: "PARSE_SERVER_PUSH", env: "PARSE_SERVER_PUSH",
help: "Configuration for push, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Push", help: "Configuration for push, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Push",
action: function(opt) { action: function(opt) {
if (typeof opt == 'object') {
return opt;
}
return JSON.parse(opt) return JSON.parse(opt)
} }
}, },
"oauth": { "oauth": {
env: "PARSE_SERVER_OAUTH_PROVIDERS", 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", help: "Configuration for your oAuth providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth",
action: function(opt) { action: function(opt) {
if (typeof opt == 'object') {
return opt;
}
return JSON.parse(opt) return JSON.parse(opt)
} }
}, },
"fileKey": { "fileKey": {
env: "PARSE_SERVER_FILE_KEY", env: "PARSE_SERVER_FILE_KEY",
help: "Key for your files", help: "Key for your files",
}, },
"facebookAppIds": { "facebookAppIds": {
env: "PARSE_SERVER_FACEBOOK_APP_IDS", env: "PARSE_SERVER_FACEBOOK_APP_IDS",
help: "Comma separated list for your facebook app Ids", help: "Comma separated list for your facebook app Ids",
type: "list", type: "list",
@@ -81,7 +91,7 @@ export default {
action: function(opt) { action: function(opt) {
if (opt == "true" || opt == "1") { if (opt == "true" || opt == "1") {
return true; return true;
} }
return false; return false;
} }
}, },
@@ -95,22 +105,69 @@ export default {
return false; return false;
} }
}, },
"mountPath": { "mountPath": {
env: "PARSE_SERVER_MOUNT_PATH", env: "PARSE_SERVER_MOUNT_PATH",
help: "Mount path for the server, defaults to /parse", help: "Mount path for the server, defaults to /parse",
default: "/parse" default: "/parse"
}, },
"databaseAdapter": {
env: "PARSE_SERVER_DATABASE_ADAPTER",
help: "Adapter module for the database sub-system"
},
"filesAdapter": { "filesAdapter": {
env: "PARSE_SERVER_FILES_ADAPTER", 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": { "loggerAdapter": {
env: "PARSE_SERVER_LOGGER_ADAPTER", 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": { "maxUploadSize": {
env: "PARSE_SERVER_MAX_UPLOAD_SIZE", env: "PARSE_SERVER_MAX_UPLOAD_SIZE",

View File

@@ -187,13 +187,12 @@ export function transformKeyValue(schema, className, restKey, restValue, options
// Returns the mongo form of the query. // Returns the mongo form of the query.
// Throws a Parse.Error if the input query is invalid. // Throws a Parse.Error if the input query is invalid.
function transformWhere(schema, className, restWhere) { function transformWhere(schema, className, restWhere) {
var mongoWhere = {}; let mongoWhere = {};
if (restWhere['ACL']) { if (restWhere['ACL']) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
'Cannot query on ACL.');
} }
for (var restKey in restWhere) { for (let restKey in restWhere) {
var out = transformKeyValue(schema, className, restKey, restWhere[restKey], let out = transformKeyValue(schema, className, restKey, restWhere[restKey],
{query: true, validate: true}); {query: true, validate: true});
mongoWhere[out.key] = out.value; mongoWhere[out.key] = out.value;
} }