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

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) {
@@ -372,8 +373,8 @@ describe('miscellaneous', function() {
done(); 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) => { 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,35 +45,35 @@ 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');
done(); done();
}); });
}); });
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();
@@ -2088,7 +2114,7 @@ describe('Parse.Query testing', () => {
console.log(error); console.log(error);
}); });
}); });
// #371 // #371
it('should properly interpret a query', (done) => { it('should properly interpret a query', (done) => {
var query = new Parse.Query("C1"); var query = new Parse.Query("C1");
@@ -2104,7 +2130,7 @@ describe('Parse.Query testing', () => {
done(); done();
}) })
}); });
it('should properly interpret a query', (done) => { it('should properly interpret a query', (done) => {
var user = new Parse.User(); var user = new Parse.User();
user.set("username", "foo"); user.set("username", "foo");
@@ -2112,22 +2138,22 @@ describe('Parse.Query testing', () => {
return user.save().then( (user) => { return user.save().then( (user) => {
var objIdQuery = new Parse.Query("_User").equalTo("objectId", user.id); var objIdQuery = new Parse.Query("_User").equalTo("objectId", user.id);
var blockedUserQuery = user.relation("blockedUsers").query(); var blockedUserQuery = user.relation("blockedUsers").query();
var aResponseQuery = new Parse.Query("MatchRelationshipActivityResponse"); var aResponseQuery = new Parse.Query("MatchRelationshipActivityResponse");
aResponseQuery.equalTo("userA", user); aResponseQuery.equalTo("userA", user);
aResponseQuery.equalTo("userAResponse", 1); aResponseQuery.equalTo("userAResponse", 1);
var bResponseQuery = new Parse.Query("MatchRelationshipActivityResponse"); var bResponseQuery = new Parse.Query("MatchRelationshipActivityResponse");
bResponseQuery.equalTo("userB", user); bResponseQuery.equalTo("userB", user);
bResponseQuery.equalTo("userBResponse", 1); bResponseQuery.equalTo("userBResponse", 1);
var matchOr = Parse.Query.or(aResponseQuery, bResponseQuery); var matchOr = Parse.Query.or(aResponseQuery, bResponseQuery);
var matchRelationshipA = new Parse.Query("_User"); var matchRelationshipA = new Parse.Query("_User");
matchRelationshipA.matchesKeyInQuery("objectId", "userAObjectId", matchOr); matchRelationshipA.matchesKeyInQuery("objectId", "userAObjectId", matchOr);
var matchRelationshipB = new Parse.Query("_User"); var matchRelationshipB = new Parse.Query("_User");
matchRelationshipB.matchesKeyInQuery("objectId", "userBObjectId", matchOr); matchRelationshipB.matchesKeyInQuery("objectId", "userBObjectId", matchOr);
var orQuery = Parse.Query.or(objIdQuery, blockedUserQuery, matchRelationshipA, matchRelationshipB); var orQuery = Parse.Query.or(objIdQuery, blockedUserQuery, matchRelationshipA, matchRelationshipB);
var query = new Parse.Query("_User"); var query = new Parse.Query("_User");
query.doesNotMatchQuery("objectId", orQuery); query.doesNotMatchQuery("objectId", orQuery);
@@ -2140,8 +2166,8 @@ describe('Parse.Query testing', () => {
fail("should not fail"); fail("should not fail");
done(); 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) => { 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,6 +1,5 @@
export function loadAdapter(adapter, defaultAdapter, options) { export function loadAdapter(adapter, defaultAdapter, options) {
if (!adapter)
if (!adapter)
{ {
if (!defaultAdapter) { if (!defaultAdapter) {
return options; return options;
@@ -20,7 +19,7 @@ export function loadAdapter(adapter, defaultAdapter, options) {
if (adapter.default) { if (adapter.default) {
adapter = adapter.default; adapter = adapter.default;
} }
return loadAdapter(adapter, undefined, options); return loadAdapter(adapter, undefined, options);
} else if (adapter.module) { } else if (adapter.module) {
return loadAdapter(adapter.module, undefined, adapter.options); 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 loadAdapter(adapter.adapter, undefined, adapter.options);
} }
// return the adapter as provided // return the adapter as provided
return adapter; return adapter;
} }
export default loadAdapter; 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 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

@@ -2,7 +2,7 @@
AdaptableController.js AdaptableController.js
AdaptableController is the base class for all controllers 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 The super class takes care of creating the right instance for the adapter
based on the parameters passed based on the parameters passed
@@ -28,30 +28,30 @@ export class AdaptableController {
this.validateAdapter(adapter); this.validateAdapter(adapter);
this[_adapter] = adapter; this[_adapter] = adapter;
} }
get adapter() { get adapter() {
return this[_adapter]; return this[_adapter];
} }
get config() { get config() {
return new Config(this.appId); return new Config(this.appId);
} }
expectedAdapterType() { expectedAdapterType() {
throw new Error("Subclasses should implement expectedAdapterType()"); throw new Error("Subclasses should implement expectedAdapterType()");
} }
validateAdapter(adapter) { validateAdapter(adapter) {
if (!adapter) { if (!adapter) {
throw new Error(this.constructor.name+" requires an adapter"); throw new Error(this.constructor.name+" requires an adapter");
} }
let Type = this.expectedAdapterType(); let Type = this.expectedAdapterType();
// Allow skipping for testing // Allow skipping for testing
if (!Type) { if (!Type) {
return; return;
} }
// Makes sure the prototype matches // Makes sure the prototype matches
let mismatches = Object.getOwnPropertyNames(Type.prototype).reduce( (obj, key) => { let mismatches = Object.getOwnPropertyNames(Type.prototype).reduce( (obj, key) => {
const adapterType = typeof adapter[key]; const adapterType = typeof adapter[key];
@@ -64,7 +64,7 @@ export class AdaptableController {
} }
return obj; return obj;
}, {}); }, {});
if (Object.keys(mismatches).length > 0) { if (Object.keys(mismatches).length > 0) {
throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches); 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 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.
@@ -390,14 +385,14 @@ DatabaseController.prototype.owningIds = function(className, key, relatedIds) {
// equal-to-pointer constraints on relation fields. // equal-to-pointer constraints on relation fields.
// Returns a promise that resolves when query is mutated // Returns a promise that resolves when query is mutated
DatabaseController.prototype.reduceInRelation = function(className, query, schema) { DatabaseController.prototype.reduceInRelation = function(className, query, schema) {
// Search for an in-relation or equal-to-relation // Search for an in-relation or equal-to-relation
// Make it sequential for now, not sure of paralleization side effects // Make it sequential for now, not sure of paralleization side effects
if (query['$or']) { if (query['$or']) {
let ors = query['$or']; let ors = query['$or'];
return Promise.all(ors.map((aQuery, index) => { return Promise.all(ors.map((aQuery, index) => {
return this.reduceInRelation(className, aQuery, schema).then((aQuery) => { 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]; relatedIds = [query[key].objectId];
} }
return this.owningIds(className, key, relatedIds).then((ids) => { return this.owningIds(className, key, relatedIds).then((ids) => {
delete query[key]; delete query[key];
this.addInObjectIdsIds(ids, query); this.addInObjectIdsIds(ids, query);
return Promise.resolve(query); return Promise.resolve(query);
}); });
} }
return Promise.resolve(query); return Promise.resolve(query);
}) })
return Promise.all(promises).then(() => { return Promise.all(promises).then(() => {
return Promise.resolve(query); return Promise.resolve(query);
}) })
@@ -433,13 +428,13 @@ DatabaseController.prototype.reduceInRelation = function(className, query, schem
// Modifies query so that it no longer has $relatedTo // Modifies query so that it no longer has $relatedTo
// Returns a promise that resolves when query is mutated // Returns a promise that resolves when query is mutated
DatabaseController.prototype.reduceRelationKeys = function(className, query) { DatabaseController.prototype.reduceRelationKeys = function(className, query) {
if (query['$or']) { if (query['$or']) {
return Promise.all(query['$or'].map((aQuery) => { return Promise.all(query['$or'].map((aQuery) => {
return this.reduceRelationKeys(className, aQuery); return this.reduceRelationKeys(className, aQuery);
})); }));
} }
var relatedTo = query['$relatedTo']; var relatedTo = query['$relatedTo'];
if (relatedTo) { if (relatedTo) {
return this.relatedIds( return this.relatedIds(
@@ -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

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

View File

@@ -8,104 +8,91 @@ 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) {
var wrappedFunction = wrapToHTTPRequest(hook); var wrappedFunction = wrapToHTTPRequest(hook);
wrappedFunction.url = hook.url; wrappedFunction.url = hook.url;
@@ -114,13 +101,13 @@ export class HooksController {
} else { } else {
triggers.addFunction(hook.functionName, wrappedFunction, null, this._applicationId); triggers.addFunction(hook.functionName, wrappedFunction, null, this._applicationId);
} }
} }
addHook(hook) { addHook(hook) {
this.addHookToTriggers(hook); this.addHookToTriggers(hook);
return this.saveHook(hook); return this.saveHook(hook);
} }
createOrUpdateHook(aHook) { createOrUpdateHook(aHook) {
var hook; var hook;
if (aHook && aHook.functionName && aHook.url) { if (aHook && aHook.functionName && aHook.url) {
@@ -132,19 +119,19 @@ export class HooksController {
hook.className = aHook.className; hook.className = aHook.className;
hook.url = aHook.url; hook.url = aHook.url;
hook.triggerName = aHook.triggerName; hook.triggerName = aHook.triggerName;
} else { } else {
throw new Parse.Error(143, "invalid hook declaration"); throw new Parse.Error(143, "invalid hook declaration");
} }
return this.addHook(hook); return this.addHook(hook);
}; };
createHook(aHook) { createHook(aHook) {
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,49 +139,39 @@ 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);
}); });
} }
throw new Parse.Error(143, "invalid hook declaration"); throw new Parse.Error(143, "invalid hook declaration");
}; };
updateHook(aHook) { updateHook(aHook) {
if (aHook.functionName) { if (aHook.functionName) {
return this.getFunction(aHook.functionName).then((result) => { return this.getFunction(aHook.functionName).then((result) => {
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,30 +182,31 @@ 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) {
return res.error(err); return res.error(err);
} else { } else {
return res.success(result); 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) { sendPush(body = {}, where = {}, config, auth) {
var pushAdapter = this.adapter; var pushAdapter = this.adapter;
@@ -54,65 +43,65 @@ export class PushController extends AdaptableController {
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;
}, {}); }, {});
// Map the on the badges count and return the send result // Map the on the badges count and return the send result
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]);
}); });
return Promise.all(promises); return Promise.all(promises);
} }
return pushAdapter.send(body, response.results); return pushAdapter.send(body, response.results);
}); });
} }
/** /**
* Get expiration time from the request body. * Get expiration time from the request body.
* @param {Object} request A request object * @param {Object} request A request object
@@ -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,38 +1,28 @@
// 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() {
this.route('GET', '/config', req => { return this.getGlobalConfig(req) }); this.route('GET', '/config', req => { return this.getGlobalConfig(req) });
this.route('PUT', '/config', middleware.promiseEnforceMasterKeyAccess, req => { return this.updateGlobalConfig(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 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);
}
/**
* 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) { static 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,7 +35,13 @@ 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) {
if (callbacks.error) { if (callbacks.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;