Merge branch 'master' into patch-1
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -40,3 +40,6 @@ lib/
|
|||||||
|
|
||||||
# cache folder
|
# cache folder
|
||||||
.cache
|
.cache
|
||||||
|
|
||||||
|
# Mac DS_Store files
|
||||||
|
.DS_Store
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -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).
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
59
spec/Parse.Push.spec.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|||||||
@@ -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();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
})
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
125
src/Adapters/Files/GCSAdapter.js
Normal file
125
src/Adapters/Files/GCSAdapter.js
Normal 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;
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) });
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user