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
# 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
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
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",
"deepcopy": "^0.6.1",
"express": "^4.13.4",
"gcloud": "^0.28.0",
"mailgun-js": "^0.7.7",
"mime": "^1.3.4",
"mongodb": "~2.1.0",

View File

@@ -3,44 +3,45 @@ var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
var ParsePushAdapter = require("../src/Adapters/Push/ParsePushAdapter");
var S3Adapter = require("../src/Adapters/Files/S3Adapter").default;
var GCSAdapter = require("../src/Adapters/Files/GCSAdapter").default;
describe("AdapterLoader", ()=>{
it("should instantiate an adapter from string in object", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter");
var adapter = loadAdapter({
adapter: adapterPath,
options: {
key: "value",
key: "value",
foo: "bar"
}
});
expect(adapter instanceof Object).toBe(true);
expect(adapter.options.key).toBe("value");
expect(adapter.options.foo).toBe("bar");
done();
});
it("should instantiate an adapter from string", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter");
var adapter = loadAdapter(adapterPath);
expect(adapter instanceof Object).toBe(true);
done();
});
it("should instantiate an adapter from string that is module", (done) => {
var adapterPath = require('path').resolve("./src/Adapters/Files/FilesAdapter");
var adapter = loadAdapter({
adapter: adapterPath
});
expect(adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should instantiate an adapter from function/Class", (done) => {
var adapter = loadAdapter({
adapter: FilesAdapter
@@ -48,27 +49,27 @@ describe("AdapterLoader", ()=>{
expect(adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should instantiate the default adapter from Class", (done) => {
var adapter = loadAdapter(null, FilesAdapter);
expect(adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should use the default adapter", (done) => {
var defaultAdapter = new FilesAdapter();
var adapter = loadAdapter(null, defaultAdapter);
expect(adapter instanceof FilesAdapter).toBe(true);
done();
});
it("should use the provided adapter", (done) => {
var originalAdapter = new FilesAdapter();
var adapter = loadAdapter(originalAdapter);
expect(adapter).toBe(originalAdapter);
done();
});
it("should fail loading an improperly configured adapter", (done) => {
var Adapter = function(options) {
if (!options.foo) {
@@ -79,14 +80,14 @@ describe("AdapterLoader", ()=>{
param: "key",
doSomething: function() {}
};
expect(() => {
var adapter = loadAdapter(adapterOptions, Adapter);
expect(adapter).toEqual(adapterOptions);
}).not.toThrow("foo is required for that adapter");
done();
});
it("should load push adapter from options", (done) => {
var options = {
ios: {
@@ -100,7 +101,7 @@ describe("AdapterLoader", ()=>{
}).not.toThrow();
done();
});
it("should load S3Adapter from direct passing", (done) => {
var s3Adapter = new S3Adapter("key", "secret", "bucket")
expect(() => {
@@ -109,4 +110,13 @@ describe("AdapterLoader", ()=>{
}).not.toThrow();
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 GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
var S3Adapter = require("../src/Adapters/Files/S3Adapter").S3Adapter;
var GCSAdapter = require("../src/Adapters/Files/GCSAdapter").GCSAdapter;
var Config = require("../src/Config");
var FCTestFactory = require("./FilesControllerTestFactory");
@@ -8,26 +9,44 @@ var FCTestFactory = require("./FilesControllerTestFactory");
// Small additional tests to improve overall coverage
describe("FilesController",()=>{
// Test the grid store adapter
var gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse');
FCTestFactory.testAdapter("GridStoreAdapter", gridStoreAdapter);
if (process.env.S3_ACCESS_KEY && process.env.S3_SECRET_KEY) {
// Test the S3 Adapter
var s3Adapter = new S3Adapter(process.env.S3_ACCESS_KEY, process.env.S3_SECRET_KEY, 'parse.server.tests');
FCTestFactory.testAdapter("S3Adapter",s3Adapter);
// Test S3 with direct access
var s3DirectAccessAdapter = new S3Adapter(process.env.S3_ACCESS_KEY, process.env.S3_SECRET_KEY, 'parse.server.tests', {
directAccess: true
});
FCTestFactory.testAdapter("S3AdapterDirect", s3DirectAccessAdapter);
} else if (!process.env.TRAVIS) {
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,35 +1,34 @@
var FilesController = require('../src/Controllers/FilesController').FilesController;
var Config = require("../src/Config");
var testAdapter = function(name, adapter) {
// Small additional tests to improve overall coverage
var config = new Config(Parse.applicationId);
var filesController = new FilesController(adapter);
describe("FilesController with "+name,()=>{
it("should properly expand objects", (done) => {
var result = filesController.expandFilesInObject(config, function(){});
expect(result).toBeUndefined();
var fullFile = {
type: '__type',
url: "http://an.url"
}
var anObject = {
aFile: fullFile
}
filesController.expandFilesInObject(config, anObject);
expect(anObject.aFile.url).toEqual("http://an.url");
done();
})
})
it("should properly create, read, delete files", (done) => {
var filename;
filesController.createFile(config, "file.txt", "hello world").then( (result) => {
@@ -51,14 +50,14 @@ var testAdapter = function(name, adapter) {
console.error(err);
done();
}).then((result) => {
filesController.getFileData(config, filename).then((res) => {
fail("the file should be deleted");
done();
}, (err) => {
done();
done();
});
}, (err) => {
fail("The adapter should delete the file");
console.error(err);

View File

@@ -24,7 +24,11 @@ app.get("/301", function(req, res){
app.post('/echo', function(req, res){
res.json(req.body);
})
});
app.get('/qs', function(req, res){
res.json(req.query);
});
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 request = require('request');
const Parse = require("parse/node");
describe('miscellaneous', function() {
it('create a GameScore object', function(done) {
@@ -372,8 +373,8 @@ describe('miscellaneous', function() {
done();
});
});
it('test cloud function shoud echo keys', function(done) {
it('test cloud function should echo keys', function(done) {
Parse.Cloud.run('echoKeys').then((result) => {
expect(result.applicationId).toEqual(Parse.applicationId);
expect(result.masterKey).toEqual(Parse.masterKey);
@@ -399,7 +400,7 @@ describe('miscellaneous', function() {
expect(results.length).toEqual(1);
expect(results[0]['foo']).toEqual('bar');
done();
}).fail( err => {
}).fail(err => {
fail(err);
done();
})
@@ -415,9 +416,9 @@ describe('miscellaneous', function() {
// Make sure the required mock for all tests is unset.
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
done();
});
it('object is set on create and update', done => {
});
it('object is set on create and update', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.beforeSave('GameScore', (req, res) => {
@@ -683,7 +684,7 @@ describe('miscellaneous', function() {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
// Clear mock afterSave
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
done();
}, function(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) => {
// Register a function which will fail
Parse.Cloud.define('willFail', (req, res) => {

View File

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

View File

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

View File

@@ -2,6 +2,9 @@
// hungry/js/test/parse_query_test.js
//
// Some new tests are added.
'use strict';
const Parse = require('parse/node');
describe('Parse.Query testing', () => {
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) {
var TestObject = Parse.Object.extend("TestObject");
var obj = new TestObject();
@@ -2088,7 +2114,7 @@ describe('Parse.Query testing', () => {
console.log(error);
});
});
// #371
it('should properly interpret a query', (done) => {
var query = new Parse.Query("C1");
@@ -2104,7 +2130,7 @@ describe('Parse.Query testing', () => {
done();
})
});
it('should properly interpret a query', (done) => {
var user = new Parse.User();
user.set("username", "foo");
@@ -2112,22 +2138,22 @@ describe('Parse.Query testing', () => {
return user.save().then( (user) => {
var objIdQuery = new Parse.Query("_User").equalTo("objectId", user.id);
var blockedUserQuery = user.relation("blockedUsers").query();
var aResponseQuery = new Parse.Query("MatchRelationshipActivityResponse");
aResponseQuery.equalTo("userA", user);
aResponseQuery.equalTo("userAResponse", 1);
var bResponseQuery = new Parse.Query("MatchRelationshipActivityResponse");
bResponseQuery.equalTo("userB", user);
bResponseQuery.equalTo("userBResponse", 1);
var matchOr = Parse.Query.or(aResponseQuery, bResponseQuery);
var matchRelationshipA = new Parse.Query("_User");
matchRelationshipA.matchesKeyInQuery("objectId", "userAObjectId", matchOr);
var matchRelationshipB = new Parse.Query("_User");
matchRelationshipB.matchesKeyInQuery("objectId", "userBObjectId", matchOr);
var orQuery = Parse.Query.or(objIdQuery, blockedUserQuery, matchRelationshipA, matchRelationshipB);
var query = new Parse.Query("_User");
query.doesNotMatchQuery("objectId", orQuery);
@@ -2140,8 +2166,8 @@ describe('Parse.Query testing', () => {
fail("should not fail");
done();
});
});
});

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) => {
var ChildObject = Parse.Object.extend("ChildObject");
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');
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) => {
// Make query condition
var where = {
@@ -132,10 +107,10 @@ describe('PushController', () => {
it('properly increment badges', (done) => {
var payload = {
var payload = {data:{
alert: "Hello World!",
badge: "Increment",
}
}}
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
@@ -157,7 +132,7 @@ describe('PushController', () => {
var pushAdapter = {
send: function(body, installations) {
var badge = body.badge;
var badge = body.data.badge;
installations.forEach((installation) => {
if (installation.deviceType == "ios") {
expect(installation.badge).toEqual(badge);
@@ -196,10 +171,10 @@ describe('PushController', () => {
it('properly set badges to 1', (done) => {
var payload = {
var payload = {data: {
alert: "Hello World!",
badge: 1,
}
}}
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
@@ -213,7 +188,7 @@ describe('PushController', () => {
var pushAdapter = {
send: function(body, installations) {
var badge = body.badge;
var badge = body.data.badge;
installations.forEach((installation) => {
expect(installation.badge).toEqual(badge);
expect(1).toEqual(installation.badge);
@@ -244,6 +219,6 @@ describe('PushController', () => {
done();
});
})
});
});

View File

@@ -2,40 +2,6 @@ var PushRouter = require('../src/Routers/PushRouter').PushRouter;
var request = require('request');
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) => {
// Make mock request
var request = {

View File

@@ -703,4 +703,33 @@ describe('Schema', () => {
});
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,6 +1,5 @@
export function loadAdapter(adapter, defaultAdapter, options) {
if (!adapter)
if (!adapter)
{
if (!defaultAdapter) {
return options;
@@ -20,7 +19,7 @@ export function loadAdapter(adapter, defaultAdapter, options) {
if (adapter.default) {
adapter = adapter.default;
}
return loadAdapter(adapter, undefined, options);
} else if (adapter.module) {
return loadAdapter(adapter.module, undefined, adapter.options);
@@ -30,7 +29,7 @@ export function loadAdapter(adapter, defaultAdapter, options) {
return loadAdapter(adapter.adapter, undefined, adapter.options);
}
// return the adapter as provided
return adapter;
return adapter;
}
export default loadAdapter;

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 Collection = mongodb.Collection;
@@ -18,8 +17,7 @@ export default class MongoCollection {
return this._rawFind(query, { skip, limit, sort })
.catch(error => {
// Check for "no geoindex" error
if (error.code != 17007 ||
!error.message.match(/unable to find index for .geoNear/)) {
if (error.code != 17007 || !error.message.match(/unable to find index for .geoNear/)) {
throw error;
}
// 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 => {
// Value is the object where mongo returns multiple fields.
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.
@@ -70,6 +87,14 @@ export default class MongoCollection {
});
}
deleteOne(query) {
return this._mongoCollection.deleteOne(query);
}
deleteMany(query) {
return this._mongoCollection.deleteMany(query);
}
drop() {
return this._mongoCollection.drop();
}

View File

@@ -2,7 +2,7 @@
AdaptableController.js
AdaptableController is the base class for all controllers
that support adapter,
that support adapter,
The super class takes care of creating the right instance for the adapter
based on the parameters passed
@@ -28,30 +28,30 @@ export class AdaptableController {
this.validateAdapter(adapter);
this[_adapter] = adapter;
}
get adapter() {
return this[_adapter];
}
get config() {
return new Config(this.appId);
}
expectedAdapterType() {
throw new Error("Subclasses should implement expectedAdapterType()");
}
validateAdapter(adapter) {
if (!adapter) {
throw new Error(this.constructor.name+" requires an adapter");
}
let Type = this.expectedAdapterType();
// Allow skipping for testing
if (!Type) {
if (!Type) {
return;
}
// Makes sure the prototype matches
let mismatches = Object.getOwnPropertyNames(Type.prototype).reduce( (obj, key) => {
const adapterType = typeof adapter[key];
@@ -64,7 +64,7 @@ export class AdaptableController {
}
return obj;
}, {});
if (Object.keys(mismatches).length > 0) {
throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches);
}

View File

@@ -6,6 +6,7 @@ var Parse = require('parse/node').Parse;
var Schema = require('./../Schema');
var transform = require('./../transform');
const deepcopy = require('deepcopy');
// options can contain:
// collectionPrefix: the string to put in front of every collection name.
@@ -28,16 +29,6 @@ DatabaseController.prototype.connect = function() {
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) {
return this.adapter.adaptiveCollection(this.collectionPrefix + className);
};
@@ -46,10 +37,6 @@ DatabaseController.prototype.collectionExists = function(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) {
return this.adapter.dropCollection(this.collectionPrefix + className);
};
@@ -58,15 +45,23 @@ function returnsTrue() {
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.
// 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.
DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) {
if (!this.schemaPromise) {
this.schemaPromise = this.collection('_SCHEMA').then((coll) => {
this.schemaPromise = this.adaptiveCollection('_SCHEMA').then(collection => {
delete this.schemaPromise;
return Schema.load(coll);
return Schema.load(collection);
});
return this.schemaPromise;
}
@@ -75,9 +70,9 @@ DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) {
if (acceptor(schema)) {
return schema;
}
this.schemaPromise = this.collection('_SCHEMA').then((coll) => {
this.schemaPromise = this.adaptiveCollection('_SCHEMA').then(collection => {
delete this.schemaPromise;
return Schema.load(coll);
return Schema.load(collection);
});
return this.schemaPromise;
});
@@ -136,6 +131,9 @@ DatabaseController.prototype.untransformObject = function(
// one of the provided strings must provide the caller with
// write permissions.
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) {
return schema.hasKeys(className, Object.keys(query));
};
@@ -234,30 +232,28 @@ DatabaseController.prototype.handleRelationUpdates = function(className,
// Adds a relation.
// Returns a promise that resolves successfully iff the add was successful.
DatabaseController.prototype.addRelation = function(key, fromClassName,
fromId, toId) {
var doc = {
DatabaseController.prototype.addRelation = function(key, fromClassName, fromId, toId) {
let doc = {
relatedId: toId,
owningId: fromId
owningId : fromId
};
var className = '_Join:' + key + ':' + fromClassName;
return this.collection(className).then((coll) => {
return coll.update(doc, doc, {upsert: true});
let className = `_Join:${key}:${fromClassName}`;
return this.adaptiveCollection(className).then((coll) => {
return coll.upsertOne(doc, doc);
});
};
// Removes a relation.
// Returns a promise that resolves successfully iff the remove was
// successful.
DatabaseController.prototype.removeRelation = function(key, fromClassName,
fromId, toId) {
DatabaseController.prototype.removeRelation = function(key, fromClassName, fromId, toId) {
var doc = {
relatedId: toId,
owningId: fromId
};
var className = '_Join:' + key + ':' + fromClassName;
return this.collection(className).then((coll) => {
return coll.remove(doc);
let className = `_Join:${key}:${fromClassName}`;
return this.adaptiveCollection(className).then(coll => {
return coll.deleteOne(doc);
});
};
@@ -273,64 +269,63 @@ DatabaseController.prototype.destroy = function(className, query, options = {})
var aclGroup = options.acl || [];
var schema;
return this.loadSchema().then((s) => {
schema = s;
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]}});
return this.loadSchema()
.then(s => {
schema = s;
if (!isMaster) {
return schema.validatePermission(className, aclGroup, 'delete');
}
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);
}).then((resp) => {
//Check _Session to avoid changing password failed without any session.
if (resp.result.n === 0 && className !== "_Session") {
return Promise.reject(
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found.'));
}
}, (error) => {
throw error;
});
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 collection.deleteMany(mongoWhere);
})
.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.
// Returns a promise that resolves successfully iff the object saved.
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 isMaster = !('acl' in options);
var aclGroup = options.acl || [];
return this.loadSchema().then((s) => {
schema = s;
if (!isMaster) {
return schema.validatePermission(className, aclGroup, 'create');
}
return Promise.resolve();
}).then(() => {
return this.handleRelationUpdates(className, null, object);
}).then(() => {
return this.collection(className);
}).then((coll) => {
var mongoObject = transform.transformCreate(schema, className, object);
return coll.insert([mongoObject]);
});
return this.validateClassName(className)
.then(() => this.loadSchema())
.then(s => {
schema = s;
if (!isMaster) {
return schema.validatePermission(className, aclGroup, 'create');
}
return Promise.resolve();
})
.then(() => this.handleRelationUpdates(className, null, object))
.then(() => this.adaptiveCollection(className))
.then(coll => {
var mongoObject = transform.transformCreate(schema, className, object);
return coll.insertOne(mongoObject);
});
};
// Runs a mongo query on the database.
@@ -390,14 +385,14 @@ DatabaseController.prototype.owningIds = function(className, key, relatedIds) {
// equal-to-pointer constraints on relation fields.
// Returns a promise that resolves when query is mutated
DatabaseController.prototype.reduceInRelation = function(className, query, schema) {
// Search for an in-relation or equal-to-relation
// Make it sequential for now, not sure of paralleization side effects
if (query['$or']) {
let ors = query['$or'];
return Promise.all(ors.map((aQuery, index) => {
return this.reduceInRelation(className, aQuery, schema).then((aQuery) => {
query['$or'][index] = aQuery;
query['$or'][index] = aQuery;
})
}));
}
@@ -417,14 +412,14 @@ DatabaseController.prototype.reduceInRelation = function(className, query, schem
relatedIds = [query[key].objectId];
}
return this.owningIds(className, key, relatedIds).then((ids) => {
delete query[key];
delete query[key];
this.addInObjectIdsIds(ids, query);
return Promise.resolve(query);
});
}
return Promise.resolve(query);
})
return Promise.all(promises).then(() => {
return Promise.resolve(query);
})
@@ -433,13 +428,13 @@ DatabaseController.prototype.reduceInRelation = function(className, query, schem
// Modifies query so that it no longer has $relatedTo
// Returns a promise that resolves when query is mutated
DatabaseController.prototype.reduceRelationKeys = function(className, query) {
if (query['$or']) {
return Promise.all(query['$or'].map((aQuery) => {
return this.reduceRelationKeys(className, aQuery);
}));
}
var relatedTo = query['$relatedTo'];
if (relatedTo) {
return this.relatedIds(
@@ -455,13 +450,18 @@ DatabaseController.prototype.reduceRelationKeys = function(className, query) {
DatabaseController.prototype.addInObjectIdsIds = function(ids, query) {
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 || {};
let queryIn = [].concat(query.objectId['$in'] || [], ids || []);
// make a set and spread to remove duplicates
query.objectId = {'$in': [...new Set(queryIn)]};
return query;
query.objectId = query.objectId || {};
let queryIn = [].concat(query.objectId['$in'] || [], ids || []);
// make a set and spread to remove duplicates
// replace the $in operator as other constraints
// may be set
query.objectId['$in'] = [...new Set(queryIn)];
return query;
}
// Runs a query on the database.

View File

@@ -13,11 +13,11 @@ export class FilesController extends AdaptableController {
}
createFile(config, filename, data, contentType) {
let extname = path.extname(filename);
const hasExtension = extname.length > 0;
if (!hasExtension && contentType && mime.extension(contentType)) {
filename = filename + '.' + mime.extension(contentType);
} else if (hasExtension && !contentType) {

View File

@@ -8,104 +8,91 @@ import * as request from "request";
const DefaultHooksCollectionName = "_Hooks";
export class HooksController {
_applicationId: string;
_collectionPrefix: string;
_applicationId:string;
_collectionPrefix:string;
_collection;
constructor(applicationId: string, collectionPrefix: string = '') {
constructor(applicationId:string, collectionPrefix:string = '') {
this._applicationId = applicationId;
this._collectionPrefix = collectionPrefix;
}
database() {
return DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix);
load() {
return this._getHooks().then(hooks => {
hooks = hooks || [];
hooks.forEach((hook) => {
this.addHookToTriggers(hook);
});
});
}
collection() {
getCollection() {
if (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;
return collection;
});
}
getFunction(functionName) {
return this.getOne({functionName: functionName})
return this._getHooks({ functionName: functionName }, 1).then(results => results[0]);
}
getFunctions() {
return this.get({functionName: { $exists: true }})
return this._getHooks({ functionName: { $exists: true } });
}
getTrigger(className, triggerName) {
return this.getOne({className: className, triggerName: triggerName })
return this._getHooks({ className: className, triggerName: triggerName }, 1).then(results => results[0]);
}
getTriggers() {
return this.get({className: { $exists: true }, triggerName: { $exists: true }})
return this._getHooks({ className: { $exists: true }, triggerName: { $exists: true } });
}
deleteFunction(functionName) {
triggers.removeFunction(functionName, this._applicationId);
return this.delete({functionName: functionName});
return this._removeHooks({ functionName: functionName });
}
deleteTrigger(className, triggerName) {
triggers.removeTrigger(triggerName, className, this._applicationId);
return this.delete({className: className, triggerName: triggerName});
return this._removeHooks({ className: className, triggerName: triggerName });
}
delete(query) {
return this.collection().then((collection) => {
return collection.remove(query)
}).then( (res) => {
_getHooks(query, limit) {
let options = limit ? { limit: limit } : undefined;
return this.getCollection().then(collection => collection.find(query, options));
}
_removeHooks(query) {
return this.getCollection().then(collection => {
return collection.deleteMany(query);
}).then(() => {
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) {
var query;
if (hook.functionName && hook.url) {
query = {functionName: hook.functionName }
query = { functionName: hook.functionName }
} else if (hook.triggerName && hook.className && hook.url) {
query = { className: hook.className, triggerName: hook.triggerName }
} else {
throw new Parse.Error(143, "invalid hook declaration");
}
return this.collection().then((collection) => {
return collection.update(query, hook, {upsert: true})
}).then(function(res){
return hook;
})
return this.getCollection()
.then(collection => collection.upsertOne(query, hook))
.then(() => {
return hook;
});
}
addHookToTriggers(hook) {
var wrappedFunction = wrapToHTTPRequest(hook);
wrappedFunction.url = hook.url;
@@ -114,13 +101,13 @@ export class HooksController {
} else {
triggers.addFunction(hook.functionName, wrappedFunction, null, this._applicationId);
}
}
}
addHook(hook) {
this.addHookToTriggers(hook);
return this.saveHook(hook);
}
createOrUpdateHook(aHook) {
var hook;
if (aHook && aHook.functionName && aHook.url) {
@@ -132,19 +119,19 @@ export class HooksController {
hook.className = aHook.className;
hook.url = aHook.url;
hook.triggerName = aHook.triggerName;
} else {
throw new Parse.Error(143, "invalid hook declaration");
}
}
return this.addHook(hook);
};
createHook(aHook) {
if (aHook.functionName) {
return this.getFunction(aHook.functionName).then((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 {
return this.createOrUpdateHook(aHook);
}
@@ -152,49 +139,39 @@ export class HooksController {
} else if (aHook.className && aHook.triggerName) {
return this.getTrigger(aHook.className, aHook.triggerName).then((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);
});
}
throw new Parse.Error(143, "invalid hook declaration");
};
updateHook(aHook) {
if (aHook.functionName) {
return this.getFunction(aHook.functionName).then((result) => {
if (result) {
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) {
return this.getTrigger(aHook.className, aHook.triggerName).then((result) => {
if (result) {
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");
};
load() {
return this.getHooks().then((hooks) => {
hooks = hooks || [];
hooks.forEach((hook) => {
this.addHookToTriggers(hook);
});
});
}
}
function wrapToHTTPRequest(hook) {
return function(req, res) {
var jsonBody = {};
for(var i in req) {
return (req, res) => {
let jsonBody = {};
for (var i in req) {
jsonBody[i] = req[i];
}
if (req.object) {
@@ -205,30 +182,31 @@ function wrapToHTTPRequest(hook) {
jsonBody.original = req.original.toJSON();
jsonBody.original.className = req.original.className;
}
var jsonRequest = {};
jsonRequest.headers = {
'Content-Type': 'application/json'
}
jsonRequest.body = JSON.stringify(jsonBody);
request.post(hook.url, jsonRequest, function(err, httpResponse, body){
let jsonRequest = {
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonBody)
};
request.post(hook.url, jsonRequest, function (err, httpResponse, body) {
var result;
if (body) {
if (typeof body == "string") {
try {
body = JSON.parse(body);
} catch(e) {
err = {error: "Malformed response", code: -1};
} catch (e) {
err = { error: "Malformed response", code: -1 };
}
}
if (!err) {
result = body.success;
err = body.error;
err = body.error;
}
}
if (err) {
return res.error(err);
} else {
} else {
return res.success(result);
}
});

View File

@@ -36,17 +36,6 @@ 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) {
var pushAdapter = this.adapter;
@@ -54,65 +43,65 @@ export class PushController extends AdaptableController {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push adapter is not available');
}
PushController.validateMasterKey(auth);
PushController.validatePushType(where, pushAdapter.getValidPushTypes());
// Replace the expiration_time with a valid Unix epoch milliseconds time
body['expiration_time'] = PushController.getExpirationTime(body);
// 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.
let badgeUpdate = Promise.resolve();
let badgeUpdate = () => {
return Promise.resolve();
}
if (body.badge) {
var op = {};
if (body.badge == "Increment") {
op = {'$inc': {'badge': 1}}
} else if (Number(body.badge)) {
op = {'$set': {'badge': body.badge } }
if (body.data && body.data.badge) {
let badge = body.data.badge;
let op = {};
if (badge == "Increment") {
op = { $inc: { badge: 1 } }
} else if (Number(badge)) {
op = { $set: { badge: badge } }
} else {
throw "Invalid value for badge, expected number or 'Increment'";
}
let updateWhere = deepcopy(where);
updateWhere.deviceType = 'ios'; // Only on iOS!
// Only on iOS!
updateWhere.deviceType = 'ios';
// TODO: @nlutsenko replace with better thing
badgeUpdate = config.database.rawCollection("_Installation").then((coll) => {
return coll.update(updateWhere, op, { multi: true });
});
badgeUpdate = () => {
return config.database.adaptiveCollection("_Installation")
.then(coll => coll.updateMany(updateWhere, op));
}
}
return badgeUpdate.then(() => {
return rest.find(config, auth, '_Installation', where)
return badgeUpdate().then(() => {
return rest.find(config, auth, '_Installation', where);
}).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
let badgeInstallationsMap = response.results.reduce((map, installation) => {
let badge = installation.badge;
if (installation.deviceType != "ios") {
badge = UNSUPPORTED_BADGE_KEY;
}
map[badge] = map[badge] || [];
map[badge].push(installation);
map[badge+''] = map[badge+''] || [];
map[badge+''].push(installation);
return map;
}, {});
// Map the on the badges count and return the send result
let promises = Object.keys(badgeInstallationsMap).map((badge) => {
let payload = deepcopy(body);
if (badge == UNSUPPORTED_BADGE_KEY) {
delete payload.badge;
delete payload.data.badge;
} else {
payload.badge = parseInt(badge);
payload.data.badge = parseInt(badge);
}
return pushAdapter.send(payload, badgeInstallationsMap[badge]);
return pushAdapter.send(payload, badgeInstallationsMap[badge]);
});
return Promise.all(promises);
}
return pushAdapter.send(body, response.results);
});
}
/**
* Get expiration time from the request body.
* @param {Object} request A request object
@@ -144,6 +133,6 @@ export class PushController extends AdaptableController {
expectedAdapterType() {
return PushAdapter;
}
};
}
export default PushController;

View File

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

View File

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

View File

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

View File

@@ -1,57 +1,42 @@
import PushController from '../Controllers/PushController'
import PromiseRouter from '../PromiseRouter';
import * as middleware from "../middlewares";
import { Parse } from "parse/node";
export class PushRouter extends PromiseRouter {
mountRoutes() {
this.route("POST", "/push", req => { return this.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');
}
this.route("POST", "/push", middleware.promiseEnforceMasterKeyAccess, PushRouter.handlePOST);
}
handlePOST(req) {
// TODO: move to middlewares when support for Promise middlewares
PushRouter.validateMasterKey(req);
static handlePOST(req) {
const pushController = req.config.pushController;
if (!pushController) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push controller is not set');
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Push controller is not set');
}
var where = PushRouter.getQueryCondition(req);
let where = PushRouter.getQueryCondition(req);
pushController.sendPush(req.body, where, req.config, req.auth);
return Promise.resolve({
response: {
'result': true
}
response: {
'result': true
}
});
}
/**
/**
* 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
*/
static getQueryCondition(req) {
var body = req.body || {};
var hasWhere = typeof body.where !== 'undefined';
var hasChannels = typeof body.channels !== 'undefined';
let body = req.body || {};
let hasWhere = typeof body.where !== 'undefined';
let hasChannels = typeof body.channels !== 'undefined';
var where;
let where;
if (hasWhere && hasChannels) {
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) {
where = body.where;
} else if (hasChannels) {
@@ -62,11 +47,10 @@ export class PushRouter extends PromiseRouter {
}
} else {
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;
}
}
export default PushRouter;

View File

@@ -4,7 +4,7 @@ var express = require('express'),
Parse = require('parse/node').Parse,
Schema = require('../Schema');
import PromiseRouter from '../PromiseRouter';
import PromiseRouter from '../PromiseRouter';
import * as middleware from "../middlewares";
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) {
return req.config.database.adaptiveCollection('_SCHEMA')
.then(collection => collection.find({}))
.then(schemas => schemas.map(mongoSchemaToSchemaAPIResponse))
.then(schemas => ({ response: { results: schemas }}));
.then(schemas => schemas.map(Schema.mongoSchemaToSchemaAPIResponse))
.then(schemas => ({ response: { results: schemas } }));
}
function getOneSchema(req) {
@@ -51,7 +31,7 @@ function getOneSchema(req) {
}
return results[0];
})
.then(schema => ({ response: mongoSchemaToSchemaAPIResponse(schema) }));
.then(schema => ({ response: Schema.mongoSchemaToSchemaAPIResponse(schema) }));
}
function createSchema(req) {
@@ -68,7 +48,7 @@ function createSchema(req) {
return req.config.database.loadSchema()
.then(schema => schema.addClassIfNotExists(className, req.body.fields))
.then(result => ({ response: mongoSchemaToSchemaAPIResponse(result) }));
.then(result => ({ response: Schema.mongoSchemaToSchemaAPIResponse(result) }));
}
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.`);
}
let existingFields = Object.assign(schema.data[className], {_id: className});
let existingFields = Object.assign(schema.data[className], { _id: className });
Object.keys(submittedFields).forEach(name => {
let field = submittedFields[name];
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.
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
let deletionPromises = [];
Object.keys(submittedFields).forEach(submittedFieldName => {
if (submittedFields[submittedFieldName].__op === 'Delete') {
let promise = schema.deleteField(submittedFieldName, className, req.config.database);
deletionPromises.push(promise);
// Do all deletions first, then add fields to avoid duplicate geopoint error.
let deletePromises = [];
let insertedFields = [];
Object.keys(submittedFields).forEach(fieldName => {
if (submittedFields[fieldName].__op === 'Delete') {
const promise = schema.deleteField(fieldName, className, req.config.database);
deletePromises.push(promise);
} else {
insertedFields.push(fieldName);
}
});
return Promise.all(deletionPromises)
.then(() => new Promise((resolve, reject) => {
schema.collection.update({_id: className}, mongoObject.result, {w: 1}, (err, docs) => {
if (err) {
reject(err);
}
resolve({ response: mongoSchemaToSchemaAPIResponse(mongoObject.result)});
})
}));
return Promise.all(deletePromises) // Delete Everything
.then(() => schema.reloadData()) // Reload our Schema, so we have all the new values
.then(() => {
let promises = insertedFields.map(fieldName => {
const mongoType = mongoObject.result[fieldName];
return schema.validateField(className, fieldName, mongoType);
});
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
// and clear the _Join collections
return req.config.database.adaptiveCollection('_SCHEMA')
.then(coll => coll.findOneAndDelete({_id: req.params.className}))
.then(coll => coll.findOneAndDelete({ _id: req.params.className }))
.then(document => {
if (document === null) {
//tried to delete non-existent class

View File

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

View File

@@ -1,4 +1,5 @@
var request = require("request"),
querystring = require('querystring'),
Parse = require('parse/node').Parse;
var encodeBody = function(body, headers = {}) {
@@ -34,7 +35,13 @@ module.exports = function(options) {
options.body = encodeBody(options.body, options.headers);
// set follow redirects to false by default
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) => {
if (error) {
if (callbacks.error) {

View File

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

View File

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