Merge branch 'master' into patch-1

This commit is contained in:
Aneesh Devasthale
2016-03-09 18:41:14 +05:30
35 changed files with 993 additions and 552 deletions

BIN
.DS_Store vendored

Binary file not shown.

3
.gitignore vendored
View File

@@ -40,3 +40,6 @@ lib/
# cache folder # cache folder
.cache .cache
# Mac DS_Store files
.DS_Store

View File

@@ -135,9 +135,14 @@ PARSE_SERVER_MAX_UPLOAD_SIZE
``` ```
##### Configuring S3 Adapter ##### Configuring File Adapters
Parse Server allows developers to choose from several options when hosting files: the `GridStoreAdapter`, which backed by MongoDB; the `S3Adapter`, which is backed by [Amazon S3](https://aws.amazon.com/s3/); or the `GCSAdapter`, which is backed by [Google Cloud Storage](https://cloud.google.com/storage/).
You can use the following environment variable setup the S3 adapter `GridStoreAdapter` is used by default and requires no setup, but if you're interested in using S3 or GCS, additional configuration information is available below.
###### Configuring `S3Adapter`
You can use the following environment variable setup to enable the S3 adapter:
```js ```js
S3_ACCESS_KEY S3_ACCESS_KEY
@@ -149,6 +154,19 @@ S3_DIRECT_ACCESS
``` ```
###### Configuring `GCSAdapter`
You can use the following environment variable setup to enable the GCS adapter:
```js
GCP_PROJECT_ID
GCP_KEYFILE_PATH
GCS_BUCKET
GCS_BUCKET_PREFIX
GCS_DIRECT_ACCESS
```
## Contributing ## Contributing
We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md). We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md).

View File

@@ -28,6 +28,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",
"gcloud": "^0.28.0",
"mailgun-js": "^0.7.7", "mailgun-js": "^0.7.7",
"mime": "^1.3.4", "mime": "^1.3.4",
"mongodb": "~2.1.0", "mongodb": "~2.1.0",

View File

@@ -3,6 +3,7 @@ var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default; var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
var ParsePushAdapter = require("../src/Adapters/Push/ParsePushAdapter"); var ParsePushAdapter = require("../src/Adapters/Push/ParsePushAdapter");
var S3Adapter = require("../src/Adapters/Files/S3Adapter").default; var S3Adapter = require("../src/Adapters/Files/S3Adapter").default;
var GCSAdapter = require("../src/Adapters/Files/GCSAdapter").default;
describe("AdapterLoader", ()=>{ describe("AdapterLoader", ()=>{
@@ -109,4 +110,13 @@ describe("AdapterLoader", ()=>{
}).not.toThrow(); }).not.toThrow();
done(); done();
}) })
it("should load GCSAdapter from direct passing", (done) => {
var gcsAdapter = new GCSAdapter("projectId", "path/to/keyfile", "bucket")
expect(() => {
var adapter = loadAdapter(gcsAdapter, FilesAdapter);
expect(adapter).toBe(gcsAdapter);
}).not.toThrow();
done();
})
}); });

View File

@@ -1,6 +1,7 @@
var FilesController = require('../src/Controllers/FilesController').FilesController; var FilesController = require('../src/Controllers/FilesController').FilesController;
var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter; var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
var S3Adapter = require("../src/Adapters/Files/S3Adapter").S3Adapter; var S3Adapter = require("../src/Adapters/Files/S3Adapter").S3Adapter;
var GCSAdapter = require("../src/Adapters/Files/GCSAdapter").GCSAdapter;
var Config = require("../src/Config"); var Config = require("../src/Config");
var FCTestFactory = require("./FilesControllerTestFactory"); var FCTestFactory = require("./FilesControllerTestFactory");
@@ -30,4 +31,22 @@ describe("FilesController",()=>{
} else if (!process.env.TRAVIS) { } else if (!process.env.TRAVIS) {
console.log("set S3_ACCESS_KEY and S3_SECRET_KEY to test S3Adapter") console.log("set S3_ACCESS_KEY and S3_SECRET_KEY to test S3Adapter")
} }
if (process.env.GCP_PROJECT_ID && process.env.GCP_KEYFILE_PATH && process.env.GCS_BUCKET) {
// Test the GCS Adapter
var gcsAdapter = new GCSAdapter(process.env.GCP_PROJECT_ID, process.env.GCP_KEYFILE_PATH, process.env.GCS_BUCKET);
FCTestFactory.testAdapter("GCSAdapter", gcsAdapter);
// Test GCS with direct access
var gcsDirectAccessAdapter = new GCSAdapter(process.env.GCP_PROJECT_ID, process.env.GCP_KEYFILE_PATH, process.env.GCS_BUCKET, {
directAccess: true
});
FCTestFactory.testAdapter("GCSAdapterDirect", gcsDirectAccessAdapter);
} else if (!process.env.TRAVIS) {
console.log("set GCP_PROJECT_ID, GCP_KEYFILE_PATH, and GCS_BUCKET to test GCSAdapter")
}
}); });

View File

@@ -1,4 +1,3 @@
var FilesController = require('../src/Controllers/FilesController').FilesController; var FilesController = require('../src/Controllers/FilesController').FilesController;
var Config = require("../src/Config"); var Config = require("../src/Config");

View File

@@ -24,7 +24,11 @@ app.get("/301", function(req, res){
app.post('/echo', function(req, res){ app.post('/echo', function(req, res){
res.json(req.body); res.json(req.body);
}) });
app.get('/qs', function(req, res){
res.json(req.query);
});
app.listen(13371); app.listen(13371);
@@ -193,4 +197,35 @@ describe("httpRequest", () => {
} }
}); });
}) })
it("should params object to query string", (done) => {
httpRequest({
url: httpRequestServer+"/qs",
params: {
foo: "bar"
}
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
expect(httpResponse.data).toEqual({foo: "bar"});
done();
}, function(){
fail("should not fail");
done();
})
});
it("should params string to query string", (done) => {
httpRequest({
url: httpRequestServer+"/qs",
params: "foo=bar&foo2=bar2"
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
expect(httpResponse.data).toEqual({foo: "bar", foo2: 'bar2'});
done();
}, function(){
fail("should not fail");
done();
})
});
}); });

59
spec/Parse.Push.spec.js Normal file
View File

@@ -0,0 +1,59 @@
describe('Parse.Push', () => {
it('should properly send push', (done) => {
var pushAdapter = {
send: function(body, installations) {
var badge = body.data.badge;
installations.forEach((installation) => {
if (installation.deviceType == "ios") {
expect(installation.badge).toEqual(badge);
expect(installation.originalBadge+1).toEqual(installation.badge);
} else {
expect(installation.badge).toBeUndefined();
}
});
return Promise.resolve({
body: body,
installations: installations
});
},
getValidPushTypes: function() {
return ["ios", "android"];
}
}
setServerConfiguration({
appId: Parse.applicationId,
masterKey: Parse.masterKey,
serverURL: Parse.serverURL,
push: {
adapter: pushAdapter
}
});
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
Parse.Object.saveAll(installations).then(() => {
return Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'Increment',
alert: 'Hello world!'
}
}, {useMasterKey: true});
})
.then(() => {
done();
}, (err) => {
console.error(err);
done();
});
});
});

View File

@@ -4,6 +4,7 @@
var DatabaseAdapter = require('../src/DatabaseAdapter'); var DatabaseAdapter = require('../src/DatabaseAdapter');
var request = require('request'); var request = require('request');
const Parse = require("parse/node");
describe('miscellaneous', function() { describe('miscellaneous', function() {
it('create a GameScore object', function(done) { it('create a GameScore object', function(done) {
@@ -373,7 +374,7 @@ describe('miscellaneous', function() {
}); });
}); });
it('test cloud function shoud echo keys', function(done) { it('test cloud function should echo keys', function(done) {
Parse.Cloud.run('echoKeys').then((result) => { Parse.Cloud.run('echoKeys').then((result) => {
expect(result.applicationId).toEqual(Parse.applicationId); expect(result.applicationId).toEqual(Parse.applicationId);
expect(result.masterKey).toEqual(Parse.masterKey); expect(result.masterKey).toEqual(Parse.masterKey);
@@ -399,7 +400,7 @@ describe('miscellaneous', function() {
expect(results.length).toEqual(1); expect(results.length).toEqual(1);
expect(results[0]['foo']).toEqual('bar'); expect(results[0]['foo']).toEqual('bar');
done(); done();
}).fail( err => { }).fail(err => {
fail(err); fail(err);
done(); done();
}) })
@@ -415,9 +416,9 @@ describe('miscellaneous', function() {
// Make sure the required mock for all tests is unset. // Make sure the required mock for all tests is unset.
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
done(); done();
}); });
it('object is set on create and update', done => { it('object is set on create and update', done => {
let triggerTime = 0; let triggerTime = 0;
// Register a mock beforeSave hook // Register a mock beforeSave hook
Parse.Cloud.beforeSave('GameScore', (req, res) => { Parse.Cloud.beforeSave('GameScore', (req, res) => {
@@ -683,7 +684,7 @@ describe('miscellaneous', function() {
// Make sure the checking has been triggered // Make sure the checking has been triggered
expect(triggerTime).toBe(2); expect(triggerTime).toBe(2);
// Clear mock afterSave // Clear mock afterSave
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
done(); done();
}, function(error) { }, function(error) {
console.error(error); console.error(error);
@@ -732,6 +733,90 @@ describe('miscellaneous', function() {
}); });
}); });
it('beforeSave receives ACL', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.beforeSave('GameScore', function(req, res) {
let object = req.object;
if (triggerTime == 0) {
let acl = object.getACL();
expect(acl.getPublicReadAccess()).toBeTruthy();
expect(acl.getPublicWriteAccess()).toBeTruthy();
} else if (triggerTime == 1) {
let acl = object.getACL();
expect(acl.getPublicReadAccess()).toBeFalsy();
expect(acl.getPublicWriteAccess()).toBeTruthy();
} else {
res.error();
}
triggerTime++;
res.success();
});
let obj = new Parse.Object('GameScore');
let acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
obj.setACL(acl);
obj.save().then(() => {
acl.setPublicReadAccess(false);
obj.setACL(acl);
return obj.save();
}).then(() => {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
// Clear mock afterSave
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
done();
}, error => {
console.error(error);
fail(error);
done();
});
});
it('afterSave receives ACL', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.afterSave('GameScore', function(req, res) {
let object = req.object;
if (triggerTime == 0) {
let acl = object.getACL();
expect(acl.getPublicReadAccess()).toBeTruthy();
expect(acl.getPublicWriteAccess()).toBeTruthy();
} else if (triggerTime == 1) {
let acl = object.getACL();
expect(acl.getPublicReadAccess()).toBeFalsy();
expect(acl.getPublicWriteAccess()).toBeTruthy();
} else {
res.error();
}
triggerTime++;
res.success();
});
let obj = new Parse.Object('GameScore');
let acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
obj.setACL(acl);
obj.save().then(() => {
acl.setPublicReadAccess(false);
obj.setACL(acl);
return obj.save();
}).then(() => {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
// Clear mock afterSave
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
done();
}, error => {
console.error(error);
fail(error);
done();
});
});
it('test cloud function error handling', (done) => { it('test cloud function error handling', (done) => {
// Register a function which will fail // Register a function which will fail
Parse.Cloud.define('willFail', (req, res) => { Parse.Cloud.define('willFail', (req, res) => {

View File

@@ -39,37 +39,33 @@ describe('Parse.GeoPoint testing', () => {
}); });
}); });
// it('geo line', (done) => {
// This test is disabled, since it's extremely flaky on Travis-CI. var line = [];
// Tracking issue: https://github.com/ParsePlatform/parse-server/issues/572 for (var i = 0; i < 10; ++i) {
// var obj = new TestObject();
// it('geo line', (done) => { var point = new Parse.GeoPoint(i * 4.0 - 12.0, i * 3.2 - 11.0);
// var line = []; obj.set('location', point);
// for (var i = 0; i < 10; ++i) { obj.set('construct', 'line');
// var obj = new TestObject(); obj.set('seq', i);
// var point = new Parse.GeoPoint(i * 4.0 - 12.0, i * 3.2 - 11.0); line.push(obj);
// obj.set('location', point); }
// obj.set('construct', 'line'); Parse.Object.saveAll(line, {
// obj.set('seq', i); success: function() {
// line.push(obj); var query = new Parse.Query(TestObject);
// } var point = new Parse.GeoPoint(24, 19);
// Parse.Object.saveAll(line, { query.equalTo('construct', 'line');
// success: function() { query.withinMiles('location', point, 10000);
// var query = new Parse.Query(TestObject); query.find({
// var point = new Parse.GeoPoint(24, 19); success: function(results) {
// query.equalTo('construct', 'line'); equal(results.length, 10);
// query.withinMiles('location', point, 10000); equal(results[0].get('seq'), 9);
// query.find({ equal(results[3].get('seq'), 6);
// success: function(results) { done();
// equal(results.length, 10); }
// equal(results[0].get('seq'), 9); });
// equal(results[3].get('seq'), 6); }
// done(); });
// } });
// });
// }
// });
// });
it('geo max distance large', (done) => { it('geo max distance large', (done) => {
var objects = []; var objects = [];

View File

@@ -5,21 +5,21 @@ var Parse = require('parse/node').Parse;
let Config = require('../src/Config'); let Config = require('../src/Config');
describe('a GlobalConfig', () => { describe('a GlobalConfig', () => {
beforeEach(function(done) { beforeEach(done => {
let config = new Config('test'); let config = new Config('test');
config.database.rawCollection('_GlobalConfig') config.database.adaptiveCollection('_GlobalConfig')
.then(coll => coll.updateOne({ '_id': 1}, { $set: { params: { companies: ['US', 'DK'] } } }, { upsert: true })) .then(coll => coll.upsertOne({ '_id': 1 }, { $set: { params: { companies: ['US', 'DK'] } } }))
.then(done()); .then(() => { done(); });
}); });
it('can be retrieved', (done) => { it('can be retrieved', (done) => {
request.get({ request.get({
url: 'http://localhost:8378/1/config', url : 'http://localhost:8378/1/config',
json: true, json : true,
headers: { headers: {
'X-Parse-Application-Id': 'test', 'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test', 'X-Parse-Master-Key' : 'test'
}, }
}, (error, response, body) => { }, (error, response, body) => {
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
expect(body.params.companies).toEqual(['US', 'DK']); expect(body.params.companies).toEqual(['US', 'DK']);
@@ -29,13 +29,13 @@ describe('a GlobalConfig', () => {
it('can be updated when a master key exists', (done) => { it('can be updated when a master key exists', (done) => {
request.put({ request.put({
url: 'http://localhost:8378/1/config', url : 'http://localhost:8378/1/config',
json: true, json : true,
body: { params: { companies: ['US', 'DK', 'SE'] } }, body : { params: { companies: ['US', 'DK', 'SE'] } },
headers: { headers: {
'X-Parse-Application-Id': 'test', 'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test' 'X-Parse-Master-Key' : 'test'
}, }
}, (error, response, body) => { }, (error, response, body) => {
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
expect(body.result).toEqual(true); expect(body.result).toEqual(true);
@@ -45,13 +45,13 @@ describe('a GlobalConfig', () => {
it('fail to update if master key is missing', (done) => { it('fail to update if master key is missing', (done) => {
request.put({ request.put({
url: 'http://localhost:8378/1/config', url : 'http://localhost:8378/1/config',
json: true, json : true,
body: { params: { companies: [] } }, body : { params: { companies: [] } },
headers: { headers: {
'X-Parse-Application-Id': 'test', 'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest' 'X-Parse-REST-API-Key' : 'rest'
}, }
}, (error, response, body) => { }, (error, response, body) => {
expect(response.statusCode).toEqual(403); expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized: master key is required'); expect(body.error).toEqual('unauthorized: master key is required');
@@ -61,19 +61,19 @@ describe('a GlobalConfig', () => {
it('failed getting config when it is missing', (done) => { it('failed getting config when it is missing', (done) => {
let config = new Config('test'); let config = new Config('test');
config.database.rawCollection('_GlobalConfig') config.database.adaptiveCollection('_GlobalConfig')
.then(coll => coll.deleteOne({ '_id': 1}, {}, {})) .then(coll => coll.deleteOne({ '_id': 1 }))
.then(_ => { .then(() => {
request.get({ request.get({
url: 'http://localhost:8378/1/config', url : 'http://localhost:8378/1/config',
json: true, json : true,
headers: { headers: {
'X-Parse-Application-Id': 'test', 'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test', 'X-Parse-Master-Key' : 'test'
}, }
}, (error, response, body) => { }, (error, response, body) => {
expect(response.statusCode).toEqual(404); expect(response.statusCode).toEqual(200);
expect(body.code).toEqual(Parse.Error.INVALID_KEY_NAME); expect(body.params).toEqual({});
done(); done();
}); });
}); });

View File

@@ -2,6 +2,9 @@
// hungry/js/test/parse_query_test.js // hungry/js/test/parse_query_test.js
// //
// Some new tests are added. // Some new tests are added.
'use strict';
const Parse = require('parse/node');
describe('Parse.Query testing', () => { describe('Parse.Query testing', () => {
it("basic query", function(done) { it("basic query", function(done) {
@@ -1574,6 +1577,29 @@ describe('Parse.Query testing', () => {
}); });
}); });
it("dontSelect query without conditions", function(done) {
const RestaurantObject = Parse.Object.extend("Restaurant");
const PersonObject = Parse.Object.extend("Person");
const objects = [
new RestaurantObject({ location: "Djibouti" }),
new RestaurantObject({ location: "Ouagadougou" }),
new PersonObject({ name: "Bob", hometown: "Djibouti" }),
new PersonObject({ name: "Tom", hometown: "Yoloblahblahblah" }),
new PersonObject({ name: "Billy", hometown: "Ouagadougou" })
];
Parse.Object.saveAll(objects, function() {
const query = new Parse.Query(RestaurantObject);
const mainQuery = new Parse.Query(PersonObject);
mainQuery.doesNotMatchKeyInQuery("hometown", "location", query);
mainQuery.find().then(results => {
equal(results.length, 1);
equal(results[0].get('name'), 'Tom');
done();
});
});
});
it("object with length", function(done) { it("object with length", function(done) {
var TestObject = Parse.Object.extend("TestObject"); var TestObject = Parse.Object.extend("TestObject");
var obj = new TestObject(); var obj = new TestObject();

View File

@@ -329,6 +329,46 @@ describe('Parse.Relation testing', () => {
}); });
}); });
it("query on pointer and relation fields with equal bis", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var 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();
parent.set("x", 4);
var relation = parent.relation("toChilds");
relation.add(childObjects[0]);
relation.add(childObjects[1]);
relation.add(childObjects[2]);
var parent2 = new ParentObject();
parent2.set("x", 3);
parent2.relation("toChilds").add(childObjects[2]);
var parents = [];
parents.push(parent);
parents.push(parent2);
parents.push(new ParentObject());
return Parse.Object.saveAll(parents).then(() => {
var query = new Parse.Query(ParentObject);
query.equalTo("objectId", parent2.id);
// childObjects[2] is in 2 relations
// before the fix, that woul yield 2 results
query.equalTo("toChilds", childObjects[2]);
return query.find().then((list) => {
equal(list.length, 1, "There should be 1 result");
done();
});
});
});
});
it("or queries on pointer and relation fields", (done) => { it("or queries on pointer and relation fields", (done) => {
var ChildObject = Parse.Object.extend("ChildObject"); var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = []; var childObjects = [];

View File

@@ -197,5 +197,84 @@ describe('Parse Role testing', () => {
}); });
}); });
// Based on various scenarios described in issues #827 and #683,
it('should properly handle role permissions on objects', (done) => {
var user, user2, user3;
var role, role2, role3;
var obj, obj2;
var prACL = new Parse.ACL();
prACL.setPublicReadAccess(true);
var adminACL, superACL, customerACL;
createTestUser().then((x) => {
user = x;
user2 = new Parse.User();
return user2.save({ username: 'user2', password: 'omgbbq' });
}).then((x) => {
user3 = new Parse.User();
return user3.save({ username: 'user3', password: 'omgbbq' });
}).then((x) => {
role = new Parse.Role('Admin', prACL);
role.getUsers().add(user);
return role.save({}, { useMasterKey: true });
}).then(() => {
adminACL = new Parse.ACL();
adminACL.setRoleReadAccess("Admin", true);
adminACL.setRoleWriteAccess("Admin", true);
role2 = new Parse.Role('Super', prACL);
role2.getUsers().add(user2);
return role2.save({}, { useMasterKey: true });
}).then(() => {
superACL = new Parse.ACL();
superACL.setRoleReadAccess("Super", true);
superACL.setRoleWriteAccess("Super", true);
role.getRoles().add(role2);
return role.save({}, { useMasterKey: true });
}).then(() => {
role3 = new Parse.Role('Customer', prACL);
role3.getUsers().add(user3);
role3.getRoles().add(role);
return role3.save({}, { useMasterKey: true });
}).then(() => {
customerACL = new Parse.ACL();
customerACL.setRoleReadAccess("Customer", true);
customerACL.setRoleWriteAccess("Customer", true);
var query = new Parse.Query('_Role');
return query.find({ useMasterKey: true });
}).then((x) => {
expect(x.length).toEqual(3);
obj = new Parse.Object('TestObjectRoles');
obj.set('ACL', customerACL);
return obj.save(null, { useMasterKey: true });
}).then(() => {
// Above, the Admin role was added to the Customer role.
// An object secured by the Customer ACL should be able to be edited by the Admin user.
obj.set('changedByAdmin', true);
return obj.save(null, { sessionToken: user.getSessionToken() });
}).then(() => {
obj2 = new Parse.Object('TestObjectRoles');
obj2.set('ACL', adminACL);
return obj2.save(null, { useMasterKey: true });
}, (e) => {
fail('Admin user should have been able to save.');
done();
}).then(() => {
// An object secured by the Admin ACL should not be able to be edited by a Customer role user.
obj2.set('changedByCustomer', true);
return obj2.save(null, { sessionToken: user3.getSessionToken() });
}).then(() => {
fail('Customer user should not have been able to save.');
done();
}, (e) => {
expect(e.code).toEqual(101);
done();
})
});
}); });

View File

@@ -3,31 +3,6 @@ var PushController = require('../src/Controllers/PushController').PushController
var Config = require('../src/Config'); var Config = require('../src/Config');
describe('PushController', () => { describe('PushController', () => {
it('can check valid master key of request', (done) => {
// Make mock request
var auth = {
isMaster: true
}
expect(() => {
PushController.validateMasterKey(auth);
}).not.toThrow();
done();
});
it('can check invalid master key of request', (done) => {
// Make mock request
var auth = {
isMaster: false
}
expect(() => {
PushController.validateMasterKey(auth);
}).toThrow();
done();
});
it('can validate device type when no device type is set', (done) => { it('can validate device type when no device type is set', (done) => {
// Make query condition // Make query condition
var where = { var where = {
@@ -132,10 +107,10 @@ describe('PushController', () => {
it('properly increment badges', (done) => { it('properly increment badges', (done) => {
var payload = { var payload = {data:{
alert: "Hello World!", alert: "Hello World!",
badge: "Increment", badge: "Increment",
} }}
var installations = []; var installations = [];
while(installations.length != 10) { while(installations.length != 10) {
var installation = new Parse.Object("_Installation"); var installation = new Parse.Object("_Installation");
@@ -157,7 +132,7 @@ describe('PushController', () => {
var pushAdapter = { var pushAdapter = {
send: function(body, installations) { send: function(body, installations) {
var badge = body.badge; var badge = body.data.badge;
installations.forEach((installation) => { installations.forEach((installation) => {
if (installation.deviceType == "ios") { if (installation.deviceType == "ios") {
expect(installation.badge).toEqual(badge); expect(installation.badge).toEqual(badge);
@@ -196,10 +171,10 @@ describe('PushController', () => {
it('properly set badges to 1', (done) => { it('properly set badges to 1', (done) => {
var payload = { var payload = {data: {
alert: "Hello World!", alert: "Hello World!",
badge: 1, badge: 1,
} }}
var installations = []; var installations = [];
while(installations.length != 10) { while(installations.length != 10) {
var installation = new Parse.Object("_Installation"); var installation = new Parse.Object("_Installation");
@@ -213,7 +188,7 @@ describe('PushController', () => {
var pushAdapter = { var pushAdapter = {
send: function(body, installations) { send: function(body, installations) {
var badge = body.badge; var badge = body.data.badge;
installations.forEach((installation) => { installations.forEach((installation) => {
expect(installation.badge).toEqual(badge); expect(installation.badge).toEqual(badge);
expect(1).toEqual(installation.badge); expect(1).toEqual(installation.badge);
@@ -244,6 +219,6 @@ describe('PushController', () => {
done(); done();
}); });
}) });
}); });

View File

@@ -2,40 +2,6 @@ var PushRouter = require('../src/Routers/PushRouter').PushRouter;
var request = require('request'); var request = require('request');
describe('PushRouter', () => { describe('PushRouter', () => {
it('can check valid master key of request', (done) => {
// Make mock request
var request = {
info: {
masterKey: 'masterKey'
},
config: {
masterKey: 'masterKey'
}
}
expect(() => {
PushRouter.validateMasterKey(request);
}).not.toThrow();
done();
});
it('can check invalid master key of request', (done) => {
// Make mock request
var request = {
info: {
masterKey: 'masterKey'
},
config: {
masterKey: 'masterKeyAgain'
}
}
expect(() => {
PushRouter.validateMasterKey(request);
}).toThrow();
done();
});
it('can get query condition when channels is set', (done) => { it('can get query condition when channels is set', (done) => {
// Make mock request // Make mock request
var request = { var request = {

View File

@@ -703,4 +703,33 @@ describe('Schema', () => {
}); });
done(); done();
}); });
it('handles legacy _client_permissions keys without crashing', done => {
Schema.mongoSchemaToSchemaAPIResponse({
"_id":"_Installation",
"_client_permissions":{
"get":true,
"find":true,
"update":true,
"create":true,
"delete":true,
},
"_metadata":{
"class_permissions":{
"get":{"*":true},
"find":{"*":true},
"update":{"*":true},
"create":{"*":true},
"delete":{"*":true},
"addField":{"*":true},
}
},
"installationId":"string",
"deviceToken":"string",
"deviceType":"string",
"channels":"array",
"user":"*_User",
});
done();
});
}); });

View File

@@ -1,5 +1,4 @@
export function loadAdapter(adapter, defaultAdapter, options) { export function loadAdapter(adapter, defaultAdapter, options) {
if (!adapter) if (!adapter)
{ {
if (!defaultAdapter) { if (!defaultAdapter) {

View File

@@ -0,0 +1,125 @@
// GCSAdapter
// Store Parse Files in Google Cloud Storage: https://cloud.google.com/storage
import { storage } from 'gcloud';
import { FilesAdapter } from './FilesAdapter';
import requiredParameter from '../../requiredParameter';
function requiredOrFromEnvironment(env, name) {
let environmentVariable = process.env[env];
if (!environmentVariable) {
requiredParameter(`GCSAdapter requires an ${name}`);
}
return environmentVariable;
}
function fromEnvironmentOrDefault(env, defaultValue) {
let environmentVariable = process.env[env];
if (environmentVariable) {
return environmentVariable;
}
return defaultValue;
}
export class GCSAdapter extends FilesAdapter {
// GCS Project ID and the name of a corresponding Keyfile are required.
// Unlike the S3 adapter, you must create a new Cloud Storage bucket, as this is not created automatically.
// See https://googlecloudplatform.github.io/gcloud-node/#/docs/master/guides/authentication
// for more details.
constructor(
projectId = requiredOrFromEnvironment('GCP_PROJECT_ID', 'projectId'),
keyFilename = requiredOrFromEnvironment('GCP_KEYFILE_PATH', 'keyfile path'),
bucket = requiredOrFromEnvironment('GCS_BUCKET', 'bucket name'),
{ bucketPrefix = fromEnvironmentOrDefault('GCS_BUCKET_PREFIX', ''),
directAccess = fromEnvironmentOrDefault('GCS_DIRECT_ACCESS', false) } = {}) {
super();
this._bucket = bucket;
this._bucketPrefix = bucketPrefix;
this._directAccess = directAccess;
let options = {
projectId: projectId,
keyFilename: keyFilename
};
this._gcsClient = new storage(options);
}
// For a given config object, filename, and data, store a file in GCS.
// Resolves the promise or fails with an error.
createFile(config, filename, data, contentType) {
let params = {
contentType: contentType || 'application/octet-stream'
};
return new Promise((resolve, reject) => {
let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename);
// gcloud supports upload(file) not upload(bytes), so we need to stream.
var uploadStream = file.createWriteStream(params);
uploadStream.on('error', (err) => {
return reject(err);
}).on('finish', () => {
// Second call to set public read ACL after object is uploaded.
if (this._directAccess) {
file.makePublic((err, res) => {
if (err !== null) {
return reject(err);
}
resolve();
});
} else {
resolve();
}
});
uploadStream.write(data);
uploadStream.end();
});
}
// Deletes a file with the given file name.
// Returns a promise that succeeds with the delete response, or fails with an error.
deleteFile(config, filename) {
return new Promise((resolve, reject) => {
let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename);
file.delete((err, res) => {
if(err !== null) {
return reject(err);
}
resolve(res);
});
});
}
// Search for and return a file if found by filename.
// Returns a promise that succeeds with the buffer result from GCS, or fails with an error.
getFileData(config, filename) {
return new Promise((resolve, reject) => {
let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename);
// Check for existence, since gcloud-node seemed to be caching the result
file.exists((err, exists) => {
if (exists) {
file.download((err, data) => {
if (err !== null) {
return reject(err);
}
return resolve(data);
});
} else {
reject(err);
}
});
});
}
// Generates and returns the location of a file stored in GCS for the given request and filename.
// The location is the direct GCS link if the option is set,
// otherwise we serve the file through parse-server.
getFileLocation(config, filename) {
if (this._directAccess) {
return `https://${this._bucket}.storage.googleapis.com/${this._bucketPrefix + filename}`;
}
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
}
}
export default GCSAdapter;

View File

@@ -1,4 +1,3 @@
let mongodb = require('mongodb'); let mongodb = require('mongodb');
let Collection = mongodb.Collection; let Collection = mongodb.Collection;
@@ -18,8 +17,7 @@ export default class MongoCollection {
return this._rawFind(query, { skip, limit, sort }) return this._rawFind(query, { skip, limit, sort })
.catch(error => { .catch(error => {
// Check for "no geoindex" error // Check for "no geoindex" error
if (error.code != 17007 || if (error.code != 17007 || !error.message.match(/unable to find index for .geoNear/)) {
!error.message.match(/unable to find index for .geoNear/)) {
throw error; throw error;
} }
// Figure out what key needs an index // Figure out what key needs an index
@@ -56,7 +54,26 @@ export default class MongoCollection {
return this._mongoCollection.findAndModify(query, [], update, { new: true }).then(document => { return this._mongoCollection.findAndModify(query, [], update, { new: true }).then(document => {
// Value is the object where mongo returns multiple fields. // Value is the object where mongo returns multiple fields.
return document.value; return document.value;
}) });
}
insertOne(object) {
return this._mongoCollection.insertOne(object);
}
// Atomically updates data in the database for a single (first) object that matched the query
// If there is nothing that matches the query - does insert
// Postgres Note: `INSERT ... ON CONFLICT UPDATE` that is available since 9.5.
upsertOne(query, update) {
return this._mongoCollection.update(query, update, { upsert: true });
}
updateOne(query, update) {
return this._mongoCollection.updateOne(query, update);
}
updateMany(query, update) {
return this._mongoCollection.updateMany(query, update);
} }
// Atomically find and delete an object based on query. // Atomically find and delete an object based on query.
@@ -70,6 +87,14 @@ export default class MongoCollection {
}); });
} }
deleteOne(query) {
return this._mongoCollection.deleteOne(query);
}
deleteMany(query) {
return this._mongoCollection.deleteMany(query);
}
drop() { drop() {
return this._mongoCollection.drop(); return this._mongoCollection.drop();
} }

View File

@@ -6,6 +6,7 @@ var Parse = require('parse/node').Parse;
var Schema = require('./../Schema'); var Schema = require('./../Schema');
var transform = require('./../transform'); var transform = require('./../transform');
const deepcopy = require('deepcopy');
// options can contain: // options can contain:
// collectionPrefix: the string to put in front of every collection name. // collectionPrefix: the string to put in front of every collection name.
@@ -28,16 +29,6 @@ DatabaseController.prototype.connect = function() {
return this.adapter.connect(); return this.adapter.connect();
}; };
// Returns a promise for a Mongo collection.
// Generally just for internal use.
DatabaseController.prototype.collection = function(className) {
if (!Schema.classNameIsValid(className)) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME,
'invalid className: ' + className);
}
return this.rawCollection(className);
};
DatabaseController.prototype.adaptiveCollection = function(className) { DatabaseController.prototype.adaptiveCollection = function(className) {
return this.adapter.adaptiveCollection(this.collectionPrefix + className); return this.adapter.adaptiveCollection(this.collectionPrefix + className);
}; };
@@ -46,10 +37,6 @@ DatabaseController.prototype.collectionExists = function(className) {
return this.adapter.collectionExists(this.collectionPrefix + className); return this.adapter.collectionExists(this.collectionPrefix + className);
}; };
DatabaseController.prototype.rawCollection = function(className) {
return this.adapter.collection(this.collectionPrefix + className);
};
DatabaseController.prototype.dropCollection = function(className) { DatabaseController.prototype.dropCollection = function(className) {
return this.adapter.dropCollection(this.collectionPrefix + className); return this.adapter.dropCollection(this.collectionPrefix + className);
}; };
@@ -58,15 +45,23 @@ function returnsTrue() {
return true; return true;
} }
DatabaseController.prototype.validateClassName = function(className) {
if (!Schema.classNameIsValid(className)) {
const error = new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className);
return Promise.reject(error);
}
return Promise.resolve();
};
// Returns a promise for a schema object. // Returns a promise for a schema object.
// If we are provided a acceptor, then we run it on the schema. // If we are provided a acceptor, then we run it on the schema.
// If the schema isn't accepted, we reload it at most once. // If the schema isn't accepted, we reload it at most once.
DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) { DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) {
if (!this.schemaPromise) { if (!this.schemaPromise) {
this.schemaPromise = this.collection('_SCHEMA').then((coll) => { this.schemaPromise = this.adaptiveCollection('_SCHEMA').then(collection => {
delete this.schemaPromise; delete this.schemaPromise;
return Schema.load(coll); return Schema.load(collection);
}); });
return this.schemaPromise; return this.schemaPromise;
} }
@@ -75,9 +70,9 @@ DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) {
if (acceptor(schema)) { if (acceptor(schema)) {
return schema; return schema;
} }
this.schemaPromise = this.collection('_SCHEMA').then((coll) => { this.schemaPromise = this.adaptiveCollection('_SCHEMA').then(collection => {
delete this.schemaPromise; delete this.schemaPromise;
return Schema.load(coll); return Schema.load(collection);
}); });
return this.schemaPromise; return this.schemaPromise;
}); });
@@ -136,6 +131,9 @@ DatabaseController.prototype.untransformObject = function(
// one of the provided strings must provide the caller with // one of the provided strings must provide the caller with
// write permissions. // write permissions.
DatabaseController.prototype.update = function(className, query, update, options) { DatabaseController.prototype.update = function(className, query, update, options) {
// Make a copy of the object, so we don't mutate the incoming data.
update = deepcopy(update);
var acceptor = function(schema) { var acceptor = function(schema) {
return schema.hasKeys(className, Object.keys(query)); return schema.hasKeys(className, Object.keys(query));
}; };
@@ -234,30 +232,28 @@ DatabaseController.prototype.handleRelationUpdates = function(className,
// Adds a relation. // Adds a relation.
// Returns a promise that resolves successfully iff the add was successful. // Returns a promise that resolves successfully iff the add was successful.
DatabaseController.prototype.addRelation = function(key, fromClassName, DatabaseController.prototype.addRelation = function(key, fromClassName, fromId, toId) {
fromId, toId) { let doc = {
var doc = {
relatedId: toId, relatedId: toId,
owningId: fromId owningId : fromId
}; };
var className = '_Join:' + key + ':' + fromClassName; let className = `_Join:${key}:${fromClassName}`;
return this.collection(className).then((coll) => { return this.adaptiveCollection(className).then((coll) => {
return coll.update(doc, doc, {upsert: true}); return coll.upsertOne(doc, doc);
}); });
}; };
// Removes a relation. // Removes a relation.
// Returns a promise that resolves successfully iff the remove was // Returns a promise that resolves successfully iff the remove was
// successful. // successful.
DatabaseController.prototype.removeRelation = function(key, fromClassName, DatabaseController.prototype.removeRelation = function(key, fromClassName, fromId, toId) {
fromId, toId) {
var doc = { var doc = {
relatedId: toId, relatedId: toId,
owningId: fromId owningId: fromId
}; };
var className = '_Join:' + key + ':' + fromClassName; let className = `_Join:${key}:${fromClassName}`;
return this.collection(className).then((coll) => { return this.adaptiveCollection(className).then(coll => {
return coll.remove(doc); return coll.deleteOne(doc);
}); });
}; };
@@ -273,64 +269,63 @@ DatabaseController.prototype.destroy = function(className, query, options = {})
var aclGroup = options.acl || []; var aclGroup = options.acl || [];
var schema; var schema;
return this.loadSchema().then((s) => { return this.loadSchema()
schema = s; .then(s => {
if (!isMaster) { schema = s;
return schema.validatePermission(className, aclGroup, 'delete'); if (!isMaster) {
} return schema.validatePermission(className, aclGroup, 'delete');
return Promise.resolve();
}).then(() => {
return this.collection(className);
}).then((coll) => {
var mongoWhere = transform.transformWhere(schema, className, query);
if (options.acl) {
var writePerms = [
{_wperm: {'$exists': false}}
];
for (var entry of options.acl) {
writePerms.push({_wperm: {'$in': [entry]}});
} }
mongoWhere = {'$and': [mongoWhere, {'$or': writePerms}]}; return Promise.resolve();
} })
.then(() => this.adaptiveCollection(className))
.then(collection => {
let mongoWhere = transform.transformWhere(schema, className, query);
return coll.remove(mongoWhere); if (options.acl) {
}).then((resp) => { var writePerms = [
//Check _Session to avoid changing password failed without any session. { _wperm: { '$exists': false } }
if (resp.result.n === 0 && className !== "_Session") { ];
return Promise.reject( for (var entry of options.acl) {
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, writePerms.push({ _wperm: { '$in': [entry] } });
'Object not found.')); }
mongoWhere = { '$and': [mongoWhere, { '$or': writePerms }] };
} }
}, (error) => { return collection.deleteMany(mongoWhere);
throw error; })
}); .then(resp => {
//Check _Session to avoid changing password failed without any session.
// TODO: @nlutsenko Stop relying on `result.n`
if (resp.result.n === 0 && className !== "_Session") {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
}
});
}; };
// Inserts an object into the database. // Inserts an object into the database.
// Returns a promise that resolves successfully iff the object saved. // Returns a promise that resolves successfully iff the object saved.
DatabaseController.prototype.create = function(className, object, options) { DatabaseController.prototype.create = function(className, object, options) {
// Make a copy of the object, so we don't mutate the incoming data.
object = deepcopy(object);
var schema; var schema;
var isMaster = !('acl' in options); var isMaster = !('acl' in options);
var aclGroup = options.acl || []; var aclGroup = options.acl || [];
return this.loadSchema().then((s) => { return this.validateClassName(className)
schema = s; .then(() => this.loadSchema())
if (!isMaster) { .then(s => {
return schema.validatePermission(className, aclGroup, 'create'); schema = s;
} if (!isMaster) {
return Promise.resolve(); return schema.validatePermission(className, aclGroup, 'create');
}).then(() => { }
return Promise.resolve();
return this.handleRelationUpdates(className, null, object); })
}).then(() => { .then(() => this.handleRelationUpdates(className, null, object))
return this.collection(className); .then(() => this.adaptiveCollection(className))
}).then((coll) => { .then(coll => {
var mongoObject = transform.transformCreate(schema, className, object); var mongoObject = transform.transformCreate(schema, className, object);
return coll.insert([mongoObject]); return coll.insertOne(mongoObject);
}); });
}; };
// Runs a mongo query on the database. // Runs a mongo query on the database.
@@ -455,13 +450,18 @@ DatabaseController.prototype.reduceRelationKeys = function(className, query) {
DatabaseController.prototype.addInObjectIdsIds = function(ids, query) { DatabaseController.prototype.addInObjectIdsIds = function(ids, query) {
if (typeof query.objectId == 'string') { if (typeof query.objectId == 'string') {
query.objectId = {'$in': [query.objectId]}; // Add equality op as we are sure
// we had a constraint on that one
query.objectId = {'$eq': query.objectId};
} }
query.objectId = query.objectId || {}; query.objectId = query.objectId || {};
let queryIn = [].concat(query.objectId['$in'] || [], ids || []); let queryIn = [].concat(query.objectId['$in'] || [], ids || []);
// make a set and spread to remove duplicates // make a set and spread to remove duplicates
query.objectId = {'$in': [...new Set(queryIn)]}; // replace the $in operator as other constraints
return query; // may be set
query.objectId['$in'] = [...new Set(queryIn)];
return query;
} }
// Runs a query on the database. // Runs a query on the database.

View File

@@ -8,102 +8,89 @@ import * as request from "request";
const DefaultHooksCollectionName = "_Hooks"; const DefaultHooksCollectionName = "_Hooks";
export class HooksController { export class HooksController {
_applicationId: string; _applicationId:string;
_collectionPrefix: string; _collectionPrefix:string;
_collection; _collection;
constructor(applicationId: string, collectionPrefix: string = '') { constructor(applicationId:string, collectionPrefix:string = '') {
this._applicationId = applicationId; this._applicationId = applicationId;
this._collectionPrefix = collectionPrefix; this._collectionPrefix = collectionPrefix;
} }
database() { load() {
return DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix); return this._getHooks().then(hooks => {
hooks = hooks || [];
hooks.forEach((hook) => {
this.addHookToTriggers(hook);
});
});
} }
collection() { getCollection() {
if (this._collection) { if (this._collection) {
return Promise.resolve(this._collection) return Promise.resolve(this._collection)
} }
return this.database().rawCollection(DefaultHooksCollectionName).then((collection) => {
let database = DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix);
return database.adaptiveCollection(DefaultHooksCollectionName).then(collection => {
this._collection = collection; this._collection = collection;
return collection; return collection;
}); });
} }
getFunction(functionName) { getFunction(functionName) {
return this.getOne({functionName: functionName}) return this._getHooks({ functionName: functionName }, 1).then(results => results[0]);
} }
getFunctions() { getFunctions() {
return this.get({functionName: { $exists: true }}) return this._getHooks({ functionName: { $exists: true } });
} }
getTrigger(className, triggerName) { getTrigger(className, triggerName) {
return this.getOne({className: className, triggerName: triggerName }) return this._getHooks({ className: className, triggerName: triggerName }, 1).then(results => results[0]);
} }
getTriggers() { getTriggers() {
return this.get({className: { $exists: true }, triggerName: { $exists: true }}) return this._getHooks({ className: { $exists: true }, triggerName: { $exists: true } });
} }
deleteFunction(functionName) { deleteFunction(functionName) {
triggers.removeFunction(functionName, this._applicationId); triggers.removeFunction(functionName, this._applicationId);
return this.delete({functionName: functionName}); return this._removeHooks({ functionName: functionName });
} }
deleteTrigger(className, triggerName) { deleteTrigger(className, triggerName) {
triggers.removeTrigger(triggerName, className, this._applicationId); triggers.removeTrigger(triggerName, className, this._applicationId);
return this.delete({className: className, triggerName: triggerName}); return this._removeHooks({ className: className, triggerName: triggerName });
} }
delete(query) { _getHooks(query, limit) {
return this.collection().then((collection) => { let options = limit ? { limit: limit } : undefined;
return collection.remove(query) return this.getCollection().then(collection => collection.find(query, options));
}).then( (res) => { }
_removeHooks(query) {
return this.getCollection().then(collection => {
return collection.deleteMany(query);
}).then(() => {
return {}; return {};
}, 1);
}
getOne(query) {
return this.collection()
.then(coll => coll.findOne(query, {_id: 0}))
.then(hook => {
return hook;
}); });
} }
get(query) {
return this.collection()
.then(coll => coll.find(query, {_id: 0}).toArray())
.then(hooks => {
return hooks;
});
}
getHooks() {
return this.collection()
.then(coll => coll.find({}, {_id: 0}).toArray())
.then(hooks => {
return hooks;
}, () => ([]))
}
saveHook(hook) { saveHook(hook) {
var query; var query;
if (hook.functionName && hook.url) { if (hook.functionName && hook.url) {
query = {functionName: hook.functionName } query = { functionName: hook.functionName }
} else if (hook.triggerName && hook.className && hook.url) { } else if (hook.triggerName && hook.className && hook.url) {
query = { className: hook.className, triggerName: hook.triggerName } query = { className: hook.className, triggerName: hook.triggerName }
} else { } else {
throw new Parse.Error(143, "invalid hook declaration"); throw new Parse.Error(143, "invalid hook declaration");
} }
return this.collection().then((collection) => { return this.getCollection()
return collection.update(query, hook, {upsert: true}) .then(collection => collection.upsertOne(query, hook))
}).then(function(res){ .then(() => {
return hook; return hook;
}) });
} }
addHookToTriggers(hook) { addHookToTriggers(hook) {
@@ -144,7 +131,7 @@ export class HooksController {
if (aHook.functionName) { if (aHook.functionName) {
return this.getFunction(aHook.functionName).then((result) => { return this.getFunction(aHook.functionName).then((result) => {
if (result) { if (result) {
throw new Parse.Error(143,`function name: ${aHook.functionName} already exits`); throw new Parse.Error(143, `function name: ${aHook.functionName} already exits`);
} else { } else {
return this.createOrUpdateHook(aHook); return this.createOrUpdateHook(aHook);
} }
@@ -152,7 +139,7 @@ export class HooksController {
} else if (aHook.className && aHook.triggerName) { } else if (aHook.className && aHook.triggerName) {
return this.getTrigger(aHook.className, aHook.triggerName).then((result) => { return this.getTrigger(aHook.className, aHook.triggerName).then((result) => {
if (result) { if (result) {
throw new Parse.Error(143,`class ${aHook.className} already has trigger ${aHook.triggerName}`); throw new Parse.Error(143, `class ${aHook.className} already has trigger ${aHook.triggerName}`);
} }
return this.createOrUpdateHook(aHook); return this.createOrUpdateHook(aHook);
}); });
@@ -167,34 +154,24 @@ export class HooksController {
if (result) { if (result) {
return this.createOrUpdateHook(aHook); return this.createOrUpdateHook(aHook);
} }
throw new Parse.Error(143,`no function named: ${aHook.functionName} is defined`); throw new Parse.Error(143, `no function named: ${aHook.functionName} is defined`);
}); });
} else if (aHook.className && aHook.triggerName) { } else if (aHook.className && aHook.triggerName) {
return this.getTrigger(aHook.className, aHook.triggerName).then((result) => { return this.getTrigger(aHook.className, aHook.triggerName).then((result) => {
if (result) { if (result) {
return this.createOrUpdateHook(aHook); return this.createOrUpdateHook(aHook);
} }
throw new Parse.Error(143,`class ${aHook.className} does not exist`); throw new Parse.Error(143, `class ${aHook.className} does not exist`);
}); });
} }
throw new Parse.Error(143, "invalid hook declaration"); throw new Parse.Error(143, "invalid hook declaration");
}; };
load() {
return this.getHooks().then((hooks) => {
hooks = hooks || [];
hooks.forEach((hook) => {
this.addHookToTriggers(hook);
});
});
}
} }
function wrapToHTTPRequest(hook) { function wrapToHTTPRequest(hook) {
return function(req, res) { return (req, res) => {
var jsonBody = {}; let jsonBody = {};
for(var i in req) { for (var i in req) {
jsonBody[i] = req[i]; jsonBody[i] = req[i];
} }
if (req.object) { if (req.object) {
@@ -205,25 +182,26 @@ function wrapToHTTPRequest(hook) {
jsonBody.original = req.original.toJSON(); jsonBody.original = req.original.toJSON();
jsonBody.original.className = req.original.className; jsonBody.original.className = req.original.className;
} }
var jsonRequest = {}; let jsonRequest = {
jsonRequest.headers = { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} },
jsonRequest.body = JSON.stringify(jsonBody); body: JSON.stringify(jsonBody)
};
request.post(hook.url, jsonRequest, function(err, httpResponse, body){ request.post(hook.url, jsonRequest, function (err, httpResponse, body) {
var result; var result;
if (body) { if (body) {
if (typeof body == "string") { if (typeof body == "string") {
try { try {
body = JSON.parse(body); body = JSON.parse(body);
} catch(e) { } catch (e) {
err = {error: "Malformed response", code: -1}; err = { error: "Malformed response", code: -1 };
} }
} }
if (!err) { if (!err) {
result = body.success; result = body.success;
err = body.error; err = body.error;
} }
} }
if (err) { if (err) {

View File

@@ -37,63 +37,52 @@ export class PushController extends AdaptableController {
} }
} }
/**
* Check whether the api call has master key or not.
* @param {Object} request A request object
*/
static validateMasterKey(auth = {}) {
if (!auth.isMaster) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Master key is invalid, you should only use master key to send push');
}
}
sendPush(body = {}, where = {}, config, auth) { sendPush(body = {}, where = {}, config, auth) {
var pushAdapter = this.adapter; var pushAdapter = this.adapter;
if (!pushAdapter) { if (!pushAdapter) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push adapter is not available'); 'Push adapter is not available');
} }
PushController.validateMasterKey(auth);
PushController.validatePushType(where, pushAdapter.getValidPushTypes()); PushController.validatePushType(where, pushAdapter.getValidPushTypes());
// Replace the expiration_time with a valid Unix epoch milliseconds time // Replace the expiration_time with a valid Unix epoch milliseconds time
body['expiration_time'] = PushController.getExpirationTime(body); body['expiration_time'] = PushController.getExpirationTime(body);
// TODO: If the req can pass the checking, we return immediately instead of waiting // TODO: If the req can pass the checking, we return immediately instead of waiting
// pushes to be sent. We probably change this behaviour in the future. // pushes to be sent. We probably change this behaviour in the future.
let badgeUpdate = Promise.resolve(); let badgeUpdate = () => {
return Promise.resolve();
}
if (body.badge) { if (body.data && body.data.badge) {
var op = {}; let badge = body.data.badge;
if (body.badge == "Increment") { let op = {};
op = {'$inc': {'badge': 1}} if (badge == "Increment") {
} else if (Number(body.badge)) { op = { $inc: { badge: 1 } }
op = {'$set': {'badge': body.badge } } } else if (Number(badge)) {
op = { $set: { badge: badge } }
} else { } else {
throw "Invalid value for badge, expected number or 'Increment'"; throw "Invalid value for badge, expected number or 'Increment'";
} }
let updateWhere = deepcopy(where); let updateWhere = deepcopy(where);
updateWhere.deviceType = 'ios'; // Only on iOS!
// Only on iOS! badgeUpdate = () => {
updateWhere.deviceType = 'ios'; return config.database.adaptiveCollection("_Installation")
.then(coll => coll.updateMany(updateWhere, op));
// TODO: @nlutsenko replace with better thing }
badgeUpdate = config.database.rawCollection("_Installation").then((coll) => {
return coll.update(updateWhere, op, { multi: true });
});
} }
return badgeUpdate.then(() => { return badgeUpdate().then(() => {
return rest.find(config, auth, '_Installation', where) return rest.find(config, auth, '_Installation', where);
}).then((response) => { }).then((response) => {
if (body.badge && body.badge == "Increment") { if (body.data && body.data.badge && body.data.badge == "Increment") {
// Collect the badges to reduce the # of calls // Collect the badges to reduce the # of calls
let badgeInstallationsMap = response.results.reduce((map, installation) => { let badgeInstallationsMap = response.results.reduce((map, installation) => {
let badge = installation.badge; let badge = installation.badge;
if (installation.deviceType != "ios") { if (installation.deviceType != "ios") {
badge = UNSUPPORTED_BADGE_KEY; badge = UNSUPPORTED_BADGE_KEY;
} }
map[badge] = map[badge] || []; map[badge+''] = map[badge+''] || [];
map[badge].push(installation); map[badge+''].push(installation);
return map; return map;
}, {}); }, {});
@@ -101,9 +90,9 @@ export class PushController extends AdaptableController {
let promises = Object.keys(badgeInstallationsMap).map((badge) => { let promises = Object.keys(badgeInstallationsMap).map((badge) => {
let payload = deepcopy(body); let payload = deepcopy(body);
if (badge == UNSUPPORTED_BADGE_KEY) { if (badge == UNSUPPORTED_BADGE_KEY) {
delete payload.badge; delete payload.data.badge;
} else { } else {
payload.badge = parseInt(badge); payload.data.badge = parseInt(badge);
} }
return pushAdapter.send(payload, badgeInstallationsMap[badge]); return pushAdapter.send(payload, badgeInstallationsMap[badge]);
}); });
@@ -144,6 +133,6 @@ export class PushController extends AdaptableController {
expectedAdapterType() { expectedAdapterType() {
return PushAdapter; return PushAdapter;
} }
}; }
export default PushController; export default PushController;

View File

@@ -168,9 +168,7 @@ RestQuery.prototype.validateClientClassCreation = function() {
let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product']; let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product'];
if (this.config.allowClientClassCreation === false && !this.auth.isMaster if (this.config.allowClientClassCreation === false && !this.auth.isMaster
&& sysClass.indexOf(this.className) === -1) { && sysClass.indexOf(this.className) === -1) {
return this.config.database.loadSchema().then((schema) => { return this.config.database.collectionExists(this.className).then((hasClass) => {
return schema.hasClass(this.className)
}).then((hasClass) => {
if (hasClass === true) { if (hasClass === true) {
return Promise.resolve(); return Promise.resolve();
} }
@@ -326,12 +324,10 @@ RestQuery.prototype.replaceDontSelect = function() {
!dontSelectValue.key || !dontSelectValue.key ||
typeof dontSelectValue.query !== 'object' || typeof dontSelectValue.query !== 'object' ||
!dontSelectValue.query.className || !dontSelectValue.query.className ||
!dontSelectValue.query.where ||
Object.keys(dontSelectValue).length !== 2) { Object.keys(dontSelectValue).length !== 2) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, throw new Parse.Error(Parse.Error.INVALID_QUERY,
'improper usage of $dontSelect'); 'improper usage of $dontSelect');
} }
var subquery = new RestQuery( var subquery = new RestQuery(
this.config, this.auth, dontSelectValue.query.className, this.config, this.auth, dontSelectValue.query.className,
dontSelectValue.query.where); dontSelectValue.query.where);

View File

@@ -112,9 +112,7 @@ RestWrite.prototype.validateClientClassCreation = function() {
let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product']; let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product'];
if (this.config.allowClientClassCreation === false && !this.auth.isMaster if (this.config.allowClientClassCreation === false && !this.auth.isMaster
&& sysClass.indexOf(this.className) === -1) { && sysClass.indexOf(this.className) === -1) {
return this.config.database.loadSchema().then((schema) => { return this.config.database.collectionExists(this.className).then((hasClass) => {
return schema.hasClass(this.className)
}).then((hasClass) => {
if (hasClass === true) { if (hasClass === true) {
return Promise.resolve(); return Promise.resolve();
} }

View File

@@ -1,36 +1,26 @@
// global_config.js // global_config.js
var Parse = require('parse/node').Parse;
import PromiseRouter from '../PromiseRouter'; import PromiseRouter from '../PromiseRouter';
import * as middleware from "../middlewares"; import * as middleware from "../middlewares";
export class GlobalConfigRouter extends PromiseRouter { export class GlobalConfigRouter extends PromiseRouter {
getGlobalConfig(req) { getGlobalConfig(req) {
return req.config.database.rawCollection('_GlobalConfig') return req.config.database.adaptiveCollection('_GlobalConfig')
.then(coll => coll.findOne({'_id': 1})) .then(coll => coll.find({ '_id': 1 }, { limit: 1 }))
.then(globalConfig => ({response: { params: globalConfig.params }})) .then(results => {
.catch(() => ({ if (results.length != 1) {
status: 404, // If there is no config in the database - return empty config.
response: { return { response: { params: {} } };
code: Parse.Error.INVALID_KEY_NAME,
error: 'config does not exist',
} }
})); let globalConfig = results[0];
return { response: { params: globalConfig.params } };
});
} }
updateGlobalConfig(req) { updateGlobalConfig(req) {
return req.config.database.rawCollection('_GlobalConfig') return req.config.database.adaptiveCollection('_GlobalConfig')
.then(coll => coll.findOneAndUpdate({ _id: 1 }, { $set: req.body })) .then(coll => coll.upsertOne({ _id: 1 }, { $set: req.body }))
.then(response => { .then(() => ({ response: { result: true } }));
return { response: { result: true } }
})
.catch(() => ({
status: 404,
response: {
code: Parse.Error.INVALID_KEY_NAME,
error: 'config cannot be updated',
}
}));
} }
mountRoutes() { mountRoutes() {

View File

@@ -1,57 +1,42 @@
import PushController from '../Controllers/PushController'
import PromiseRouter from '../PromiseRouter'; import PromiseRouter from '../PromiseRouter';
import * as middleware from "../middlewares";
import { Parse } from "parse/node";
export class PushRouter extends PromiseRouter { export class PushRouter extends PromiseRouter {
mountRoutes() { mountRoutes() {
this.route("POST", "/push", req => { return this.handlePOST(req); }); this.route("POST", "/push", middleware.promiseEnforceMasterKeyAccess, PushRouter.handlePOST);
} }
/** static handlePOST(req) {
* Check whether the api call has master key or not.
* @param {Object} request A request object
*/
static validateMasterKey(req) {
if (req.info.masterKey !== req.config.masterKey) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Master key is invalid, you should only use master key to send push');
}
}
handlePOST(req) {
// TODO: move to middlewares when support for Promise middlewares
PushRouter.validateMasterKey(req);
const pushController = req.config.pushController; const pushController = req.config.pushController;
if (!pushController) { if (!pushController) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Push controller is not set');
'Push controller is not set');
} }
var where = PushRouter.getQueryCondition(req); let where = PushRouter.getQueryCondition(req);
pushController.sendPush(req.body, where, req.config, req.auth); pushController.sendPush(req.body, where, req.config, req.auth);
return Promise.resolve({ return Promise.resolve({
response: { response: {
'result': true 'result': true
} }
}); });
} }
/** /**
* Get query condition from the request body. * Get query condition from the request body.
* @param {Object} request A request object * @param {Object} req A request object
* @returns {Object} The query condition, the where field in a query api call * @returns {Object} The query condition, the where field in a query api call
*/ */
static getQueryCondition(req) { static getQueryCondition(req) {
var body = req.body || {}; let body = req.body || {};
var hasWhere = typeof body.where !== 'undefined'; let hasWhere = typeof body.where !== 'undefined';
var hasChannels = typeof body.channels !== 'undefined'; let hasChannels = typeof body.channels !== 'undefined';
var where; let where;
if (hasWhere && hasChannels) { if (hasWhere && hasChannels) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Channels and query can not be set at the same time.'); 'Channels and query can not be set at the same time.');
} else if (hasWhere) { } else if (hasWhere) {
where = body.where; where = body.where;
} else if (hasChannels) { } else if (hasChannels) {
@@ -62,11 +47,10 @@ export class PushRouter extends PromiseRouter {
} }
} else { } else {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Channels and query should be set at least one.'); 'Channels and query should be set at least one.');
} }
return where; return where;
} }
} }
export default PushRouter; export default PushRouter;

View File

@@ -4,7 +4,7 @@ var express = require('express'),
Parse = require('parse/node').Parse, Parse = require('parse/node').Parse,
Schema = require('../Schema'); Schema = require('../Schema');
import PromiseRouter from '../PromiseRouter'; import PromiseRouter from '../PromiseRouter';
import * as middleware from "../middlewares"; import * as middleware from "../middlewares";
function classNameMismatchResponse(bodyClass, pathClass) { function classNameMismatchResponse(bodyClass, pathClass) {
@@ -14,31 +14,11 @@ function classNameMismatchResponse(bodyClass, pathClass) {
); );
} }
function mongoSchemaAPIResponseFields(schema) {
var fieldNames = Object.keys(schema).filter(key => key !== '_id' && key !== '_metadata');
var response = fieldNames.reduce((obj, fieldName) => {
obj[fieldName] = Schema.mongoFieldTypeToSchemaAPIType(schema[fieldName])
return obj;
}, {});
response.ACL = {type: 'ACL'};
response.createdAt = {type: 'Date'};
response.updatedAt = {type: 'Date'};
response.objectId = {type: 'String'};
return response;
}
function mongoSchemaToSchemaAPIResponse(schema) {
return {
className: schema._id,
fields: mongoSchemaAPIResponseFields(schema),
};
}
function getAllSchemas(req) { function getAllSchemas(req) {
return req.config.database.adaptiveCollection('_SCHEMA') return req.config.database.adaptiveCollection('_SCHEMA')
.then(collection => collection.find({})) .then(collection => collection.find({}))
.then(schemas => schemas.map(mongoSchemaToSchemaAPIResponse)) .then(schemas => schemas.map(Schema.mongoSchemaToSchemaAPIResponse))
.then(schemas => ({ response: { results: schemas }})); .then(schemas => ({ response: { results: schemas } }));
} }
function getOneSchema(req) { function getOneSchema(req) {
@@ -51,7 +31,7 @@ function getOneSchema(req) {
} }
return results[0]; return results[0];
}) })
.then(schema => ({ response: mongoSchemaToSchemaAPIResponse(schema) })); .then(schema => ({ response: Schema.mongoSchemaToSchemaAPIResponse(schema) }));
} }
function createSchema(req) { function createSchema(req) {
@@ -68,7 +48,7 @@ function createSchema(req) {
return req.config.database.loadSchema() return req.config.database.loadSchema()
.then(schema => schema.addClassIfNotExists(className, req.body.fields)) .then(schema => schema.addClassIfNotExists(className, req.body.fields))
.then(result => ({ response: mongoSchemaToSchemaAPIResponse(result) })); .then(result => ({ response: Schema.mongoSchemaToSchemaAPIResponse(result) }));
} }
function modifySchema(req) { function modifySchema(req) {
@@ -85,7 +65,7 @@ function modifySchema(req) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${req.params.className} does not exist.`); throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${req.params.className} does not exist.`);
} }
let existingFields = Object.assign(schema.data[className], {_id: className}); let existingFields = Object.assign(schema.data[className], { _id: className });
Object.keys(submittedFields).forEach(name => { Object.keys(submittedFields).forEach(name => {
let field = submittedFields[name]; let field = submittedFields[name];
if (existingFields[name] && field.__op !== 'Delete') { if (existingFields[name] && field.__op !== 'Delete') {
@@ -103,24 +83,27 @@ function modifySchema(req) {
} }
// Finally we have checked to make sure the request is valid and we can start deleting fields. // Finally we have checked to make sure the request is valid and we can start deleting fields.
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions. // Do all deletions first, then add fields to avoid duplicate geopoint error.
let deletionPromises = []; let deletePromises = [];
Object.keys(submittedFields).forEach(submittedFieldName => { let insertedFields = [];
if (submittedFields[submittedFieldName].__op === 'Delete') { Object.keys(submittedFields).forEach(fieldName => {
let promise = schema.deleteField(submittedFieldName, className, req.config.database); if (submittedFields[fieldName].__op === 'Delete') {
deletionPromises.push(promise); const promise = schema.deleteField(fieldName, className, req.config.database);
deletePromises.push(promise);
} else {
insertedFields.push(fieldName);
} }
}); });
return Promise.all(deletePromises) // Delete Everything
return Promise.all(deletionPromises) .then(() => schema.reloadData()) // Reload our Schema, so we have all the new values
.then(() => new Promise((resolve, reject) => { .then(() => {
schema.collection.update({_id: className}, mongoObject.result, {w: 1}, (err, docs) => { let promises = insertedFields.map(fieldName => {
if (err) { const mongoType = mongoObject.result[fieldName];
reject(err); return schema.validateField(className, fieldName, mongoType);
} });
resolve({ response: mongoSchemaToSchemaAPIResponse(mongoObject.result)}); return Promise.all(promises);
}) })
})); .then(() => ({ response: Schema.mongoSchemaToSchemaAPIResponse(mongoObject.result) }));
}); });
} }
@@ -160,7 +143,7 @@ function deleteSchema(req) {
// We've dropped the collection now, so delete the item from _SCHEMA // We've dropped the collection now, so delete the item from _SCHEMA
// and clear the _Join collections // and clear the _Join collections
return req.config.database.adaptiveCollection('_SCHEMA') return req.config.database.adaptiveCollection('_SCHEMA')
.then(coll => coll.findOneAndDelete({_id: req.params.className})) .then(coll => coll.findOneAndDelete({ _id: req.params.className }))
.then(document => { .then(document => {
if (document === null) { if (document === null) {
//tried to delete non-existent class //tried to delete non-existent class

View File

@@ -168,12 +168,12 @@ function schemaAPITypeToMongoFieldType(type) {
// '_metadata' is ignored for now // '_metadata' is ignored for now
// Everything else is expected to be a userspace field. // Everything else is expected to be a userspace field.
class Schema { class Schema {
collection; _collection;
data; data;
perms; perms;
constructor(collection) { constructor(collection) {
this.collection = collection; this._collection = collection;
// this.data[className][fieldName] tells you the type of that field // this.data[className][fieldName] tells you the type of that field
this.data = {}; this.data = {};
@@ -184,8 +184,8 @@ class Schema {
reloadData() { reloadData() {
this.data = {}; this.data = {};
this.perms = {}; this.perms = {};
return this.collection.find({}, {}).toArray().then(mongoSchema => { return this._collection.find({}).then(results => {
for (let obj of mongoSchema) { for (let obj of results) {
let className = null; let className = null;
let classData = {}; let classData = {};
let permsData = null; let permsData = null;
@@ -231,7 +231,7 @@ class Schema {
return Promise.reject(mongoObject); return Promise.reject(mongoObject);
} }
return this.collection.insertOne(mongoObject.result) return this._collection.insertOne(mongoObject.result)
.then(result => result.ops[0]) .then(result => result.ops[0])
.catch(error => { .catch(error => {
if (error.code === 11000) { //Mongo's duplicate key error if (error.code === 11000) { //Mongo's duplicate key error
@@ -268,7 +268,7 @@ class Schema {
'schema is frozen, cannot add: ' + className); 'schema is frozen, cannot add: ' + className);
} }
// We don't have this class. Update the schema // We don't have this class. Update the schema
return this.collection.insert([{_id: className}]).then(() => { return this._collection.insertOne({ _id: className }).then(() => {
// The schema update succeeded. Reload the schema // The schema update succeeded. Reload the schema
return this.reloadData(); return this.reloadData();
}, () => { }, () => {
@@ -280,10 +280,9 @@ class Schema {
}).then(() => { }).then(() => {
// Ensure that the schema now validates // Ensure that the schema now validates
return this.validateClassName(className, true); return this.validateClassName(className, true);
}, (error) => { }, () => {
// The schema still doesn't validate. Give up // The schema still doesn't validate. Give up
throw new Parse.Error(Parse.Error.INVALID_JSON, throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate');
'schema class name does not revalidate');
}); });
} }
@@ -296,7 +295,7 @@ class Schema {
} }
}; };
update = {'$set': update}; update = {'$set': update};
return this.collection.findAndModify(query, {}, update, {}).then(() => { return this._collection.updateOne(query, update).then(() => {
// The update succeeded. Reload the schema // The update succeeded. Reload the schema
return this.reloadData(); return this.reloadData();
}); });
@@ -354,12 +353,12 @@ class Schema {
// We don't have this field. Update the schema. // We don't have this field. Update the schema.
// Note that we use the $exists guard and $set to avoid race // Note that we use the $exists guard and $set to avoid race
// conditions in the database. This is important! // conditions in the database. This is important!
var query = {_id: className}; var query = { _id: className };
query[key] = {'$exists': false}; query[key] = { '$exists': false };
var update = {}; var update = {};
update[key] = type; update[key] = type;
update = {'$set': update}; update = {'$set': update};
return this.collection.findAndModify(query, {}, update, {}).then(() => { return this._collection.upsertOne(query, update).then(() => {
// The update succeeded. Reload the schema // The update succeeded. Reload the schema
return this.reloadData(); return this.reloadData();
}, () => { }, () => {
@@ -422,14 +421,14 @@ class Schema {
// for non-relations, remove all the data. // for non-relations, remove all the data.
// This is necessary to ensure that the data is still gone if they add the same field. // This is necessary to ensure that the data is still gone if they add the same field.
return database.collection(className) return database.adaptiveCollection(className)
.then(collection => { .then(collection => {
var mongoFieldName = this.data[className][fieldName].startsWith('*') ? '_p_' + fieldName : fieldName; let mongoFieldName = this.data[className][fieldName].startsWith('*') ? `_p_${fieldName}` : fieldName;
return collection.update({}, { "$unset": { [mongoFieldName] : null } }, { multi: true }); return collection.updateMany({}, { "$unset": { [mongoFieldName]: null } });
}); });
}) })
// Save the _SCHEMA object // Save the _SCHEMA object
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }})); .then(() => this._collection.updateOne({ _id: className }, { $unset: { [fieldName]: null } }));
}); });
} }
@@ -448,9 +447,12 @@ class Schema {
geocount++; geocount++;
} }
if (geocount > 1) { if (geocount > 1) {
throw new Parse.Error( // Make sure all field validation operations run before we return.
Parse.Error.INCORRECT_TYPE, // If not - we are continuing to run logic, but already provided response from the server.
'there can only be one geopoint field in a class'); return promise.then(() => {
return Promise.reject(new Parse.Error(Parse.Error.INCORRECT_TYPE,
'there can only be one geopoint field in a class'));
});
} }
if (!expected) { if (!expected) {
continue; continue;
@@ -760,6 +762,27 @@ function getObjectType(obj) {
return 'object'; return 'object';
} }
const nonFieldSchemaKeys = ['_id', '_metadata', '_client_permissions'];
function mongoSchemaAPIResponseFields(schema) {
var fieldNames = Object.keys(schema).filter(key => nonFieldSchemaKeys.indexOf(key) === -1);
var response = fieldNames.reduce((obj, fieldName) => {
obj[fieldName] = mongoFieldTypeToSchemaAPIType(schema[fieldName])
return obj;
}, {});
response.ACL = {type: 'ACL'};
response.createdAt = {type: 'Date'};
response.updatedAt = {type: 'Date'};
response.objectId = {type: 'String'};
return response;
}
function mongoSchemaToSchemaAPIResponse(schema) {
return {
className: schema._id,
fields: mongoSchemaAPIResponseFields(schema),
};
}
module.exports = { module.exports = {
load: load, load: load,
classNameIsValid: classNameIsValid, classNameIsValid: classNameIsValid,
@@ -768,4 +791,5 @@ module.exports = {
schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType, schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType,
buildMergedSchemaObject: buildMergedSchemaObject, buildMergedSchemaObject: buildMergedSchemaObject,
mongoFieldTypeToSchemaAPIType: mongoFieldTypeToSchemaAPIType, mongoFieldTypeToSchemaAPIType: mongoFieldTypeToSchemaAPIType,
mongoSchemaToSchemaAPIResponse,
}; };

View File

@@ -1,4 +1,5 @@
var request = require("request"), var request = require("request"),
querystring = require('querystring'),
Parse = require('parse/node').Parse; Parse = require('parse/node').Parse;
var encodeBody = function(body, headers = {}) { var encodeBody = function(body, headers = {}) {
@@ -34,6 +35,12 @@ module.exports = function(options) {
options.body = encodeBody(options.body, options.headers); options.body = encodeBody(options.body, options.headers);
// set follow redirects to false by default // set follow redirects to false by default
options.followRedirect = options.followRedirects == true; options.followRedirect = options.followRedirects == true;
// support params options
if (typeof options.params === 'object') {
options.qs = options.params;
} else if (typeof options.params === 'string') {
options.qs = querystring.parse(options.params);
}
request(options, (error, response, body) => { request(options, (error, response, body) => {
if (error) { if (error) {

View File

@@ -24,6 +24,7 @@ import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { FilesController } from './Controllers/FilesController'; import { FilesController } from './Controllers/FilesController';
import { FilesRouter } from './Routers/FilesRouter'; import { FilesRouter } from './Routers/FilesRouter';
import { FunctionsRouter } from './Routers/FunctionsRouter'; import { FunctionsRouter } from './Routers/FunctionsRouter';
import { GCSAdapter } from './Adapters/Files/GCSAdapter';
import { GlobalConfigRouter } from './Routers/GlobalConfigRouter'; import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
import { HooksController } from './Controllers/HooksController'; import { HooksController } from './Controllers/HooksController';
@@ -259,4 +260,5 @@ function addParseCloud() {
module.exports = { module.exports = {
ParseServer: ParseServer, ParseServer: ParseServer,
S3Adapter: S3Adapter, S3Adapter: S3Adapter,
GCSAdapter: GCSAdapter
}; };

View File

@@ -412,6 +412,7 @@ function transformConstraint(constraint, inArray) {
case '$gte': case '$gte':
case '$exists': case '$exists':
case '$ne': case '$ne':
case '$eq':
answer[key] = transformAtom(constraint[key], true, answer[key] = transformAtom(constraint[key], true,
{inArray: inArray}); {inArray: inArray});
break; break;