Compare commits

..
62 Commits
Author SHA1 Message Date
Fosco Marotto 213bfea62d Merge pull request #1379 from ParsePlatform/fosco.226
Changelog and update to 2.2.6
2016-04-05 10:22:31 -07:00
Fosco Marotto a17e54c397 Changelog and update to 2.2.6 2016-04-05 10:20:48 -07:00
Florent Vilmart 5773143973 Disables find on installation from clients
- fixes #1372
2016-04-05 13:09:40 -04:00
Florent Vilmart 406f237119 Adds missing options to the CLI 2016-04-05 08:53:35 -04:00
Florent Vilmart bc384e97e1 Let travis build all branches 2016-04-05 08:53:17 -04:00
Blayne Chard 18906f17b4 Auth._loadRoles should not query the same role twice. 2016-04-04 21:32:41 -04:00
Fosco Marotto 442c723350 Merge pull request #1364 from ParsePlatform/fosco.225
Changelog and update to 2.2.5
2016-04-04 18:01:27 -07:00
Fosco Marotto 9053b941ee Changelog and update to 2.2.5 2016-04-04 17:47:20 -07:00
Fosco Marotto 7e52c18893 Merge pull request #1363 from ParsePlatform/issue/1319
Improves config loading and tests
2016-04-04 17:30:59 -07:00
Florent Vilmart 8c92b80b8f Improves config loading and tests 2016-04-04 20:02:34 -04:00
Fosco Marotto 27dd2ebf5d Merge pull request #1361 from ParsePlatform/fosco.npm
Adds travis configuration to deploy NPM on new version tags
2016-04-04 13:50:50 -07:00
Fosco Marotto c1ef0440b0 Merge pull request #1362 from peterbytes/patch-1
The Heroku link redirects to the wiki index
2016-04-04 13:50:31 -07:00
peterbytes f4889a3393 The Heroku link redirects to the wiki index
Changed the link so that it goes to Heroku Dev Center guide instead
2016-04-04 13:40:47 -07:00
Fosco Marotto 19da14f3df Adds travis configuration to deploy NPM on new version tags 2016-04-04 12:44:22 -07:00
Florent Vilmart 91de7504fe 🎉 fixes #1288
* ⚡ regression test for #1288

* 🎉 fixes #1288
2016-04-04 14:27:11 -04:00
Florent Vilmart ec6a39718d Adds console transport when testing with VERBOSE=1 2016-04-04 11:07:25 -07:00
Florent Vilmart 0e3636d880 Make notEqual work on relations
* ⚡ regression tests for #1349

* 🎉 fixes #1349

* Adds support for multiple constraints on the same key
2016-04-04 11:05:03 -07:00
Drew 89df778ac8 Merge pull request #1357 from ParsePlatform/schema-defaults
🚧 Inject the default schemas properties when loading it
2016-04-04 08:56:41 -07:00
Tommy Goode acc23d0828 Fix for #1334: using relative cloud code files broken
* Adding tests for absolute and relative cloud code file loading.

* Fixes #1334 by resolving relative cloud code file paths to the process' working directory.
2016-04-04 08:43:06 -04:00
Florent Vilmart 3a28352c96 🎉 fixes #1355
- injects default schema upon schema load
2016-04-04 08:36:40 -04:00
Jeremy May f99b5588ab Added session length option for session tokens to server configuration 2016-04-02 11:36:47 -04:00
Fosco Marotto 51664c8f33 Merge pull request #1268 from ParsePlatform/flovilmart.noLogsWhileTesting
Removes all stdout/err logs while testing
2016-04-01 13:42:50 -07:00
Fosco Marotto 8c12f80d28 Merge pull request #1269 from ParsePlatform/flovilmart.reportPushError
Improves report for Push error in logs and _PushStatus
2016-04-01 13:42:01 -07:00
Drew c346e15a18 Merge pull request #1280 from jeremyjackson89/master
Single object queries to use include and keys
2016-04-01 09:06:54 -07:00
Jeremy 4fd67a51fb Put the allowed query keys for get in a constant. 2016-04-01 11:51:42 -04:00
Jeremy fffc83c0d9 Added tests for include and key query parameters on a specific object. 2016-04-01 11:35:40 -04:00
Jeremy 04e8e3e233 Merge remote-tracking branch 'upstream/master' 2016-04-01 11:33:11 -04:00
Florent Vilmart 02af61dc9a Merge pull request #1315 from drew-gross/bool-only
Accept only bool for $exists in LiveQuery
2016-04-01 10:57:00 -04:00
Drew 57156cf52b Merge pull request #1314 from ParsePlatform/flovilmart.issue1302
Fixes issue #1302
2016-03-31 19:00:55 -07:00
Drew Gross 01af755d18 Accept only bool for $exists in LiveQuery 2016-03-31 18:49:58 -07:00
Drew 3f8a1aae02 Merge pull request #1250 from andrecardoso/live-query-server-matching-does-not-exist-queries
Matching queries with doesNotExist constraint
2016-03-31 18:42:17 -07:00
Florent Vilmart edfa4092c0 😎 fixes #1302
- when including elements from an array of pointers, filters unaccessible/missing objects
2016-03-31 20:39:09 -04:00
Florent Vilmart 431d864ac3 ⚡ reproduces #1302 2016-03-31 20:38:45 -04:00
Drew 647987534f Merge pull request #1312 from ParsePlatform/flovilmart.includesArray
Fixes bug related to include in queries
2016-03-31 16:49:15 -07:00
Florent Vilmart ca7d8580e3 👍 fixes #1298 2016-03-31 19:32:24 -04:00
Florent Vilmart 9c528c6fe8 🎉 regression test for #1298 2016-03-31 19:11:42 -04:00
Florent Vilmart 2b3bf7c2b7 ⚡ troubleshoot #1293 2016-03-31 19:01:42 -04:00
Florent Vilmart 6540ad1483 Merge pull request #1305 from ParsePlatform/flovilmart.emailAdapterConfig
Adds more options when using CLI/config
2016-03-31 16:20:44 -04:00
Florent Vilmart b5625bc2e5 more configuration options in the CLI 2016-03-31 11:53:25 -04:00
Florent Vilmart 09279d1987 Allows loading cli adapters from module path 2016-03-31 11:49:15 -04:00
Florent Vilmart dbf64a6d83 Merge pull request #1297 from drew-gross/error-copy
Update error message
2016-03-31 09:16:55 -04:00
Drew Gross eeb3331167 Update error message 2016-03-31 00:21:21 -07:00
Drew 8a6903aa8d Merge pull request #1287 from ParsePlatform/flovilmart.dynamicConfigMount
Adds ability to override mount with publicServerURL for production uses
2016-03-30 20:29:37 -07:00
Drew 434ac91129 Merge branch 'master' into flovilmart.dynamicConfigMount 2016-03-30 20:29:30 -07:00
Florent Vilmart f0ebb7bae6 Merge pull request #1295 from drew-gross/test-1259
Fixes #1271
2016-03-30 23:14:17 -04:00
Drew 781e81d36b Merge pull request #1291 from ParsePlatform/flovilmart.issue1257
Properly let masterKey add fields
2016-03-30 19:54:38 -07:00
Drew Gross 73bca3b64c Improve comments 2016-03-30 19:48:33 -07:00
Drew Gross 6311c95785 Fixes #1271 2016-03-30 19:35:54 -07:00
Florent Vilmart efa7366250 Merge pull request #1286 from drew-gross/test-1259
Regression test for #1259
2016-03-30 20:31:05 -04:00
Florent Vilmart 6b6e684d55 Merge pull request #1290 from ParsePlatform/drew-gross-patch-2
Point to #1271 as how to write a good issue report
2016-03-30 20:27:43 -04:00
Florent Vilmart 5d99075663 Properly let masterKey add fields 2016-03-30 20:27:12 -04:00
Drew c5636e1b77 Point to #1271 as how to write a good issue report 2016-03-30 17:09:11 -07:00
Florent Vilmart ab1858616b Adds ability to override mount with publicServerURL for production uses 2016-03-30 20:09:10 -04:00
Drew Gross 632c8054da Regression test for #1259 2016-03-30 16:46:08 -07:00
Florent Vilmart 7afc08abe4 Merge pull request #1283 from drew-gross/test-871
Regression test for #871
2016-03-30 19:17:17 -04:00
Florent Vilmart 33c9730be2 Merge pull request #1281 from drew-gross/test-701
Add a test to repro #701
2016-03-30 19:10:20 -04:00
Drew Gross 97d3deb73b Regression test for #871 2016-03-30 16:04:37 -07:00
Drew Gross a0e7739526 Add a test to repro #701 2016-03-30 11:43:47 -07:00
Jeremy 7064f1d3ed Allow queries on specific objects to user the include and keys query parameters. 2016-03-30 13:50:36 -04:00
Florent Vilmart 4774e90937 Removes all stdout/err logs while testing 2016-03-29 22:52:00 -04:00
Florent Vilmart 6055f2a552 Improves report for Push error in logs and _PushStatus 2016-03-29 22:42:37 -04:00
André Luiz Cardoso 3dccd61222 Matching queries with doesNotExist constraint 2016-03-29 11:28:45 -03:00
44 changed files with 1556 additions and 350 deletions
+2
View File
@@ -1,3 +1,5 @@
Check out [this issue](https://github.com/ParsePlatform/parse-server/issues/1271) for an ideal bug report. The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly!
For implementation related questions or technical support, please refer to the [Stack Overflow](http://stackoverflow.com/questions/tagged/parse.com) and [Server Fault](https://serverfault.com/tags/parse) communities.
Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!
+16 -10
View File
@@ -1,16 +1,22 @@
branches:
only:
- master
language: node_js
node_js:
- "4.3"
- '4.3'
env:
global:
- COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**'
global:
- COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**'
matrix:
- MONGODB_VERSION=2.6.11
- MONGODB_VERSION=3.0.8
- MONGODB_VERSION=2.6.11
- MONGODB_VERSION=3.0.8
cache:
directories:
- $HOME/.mongodb/versions/downloads
after_success: ./node_modules/.bin/codecov
- "$HOME/.mongodb/versions/downloads"
after_success: "./node_modules/.bin/codecov"
deploy:
provider: npm
email:
secure: T3LwSJFcu632SDfCoavcKL39NktWLEOOFzonAjeHAglmMyDp9hvU8xHwQ4JJy2CRA03c+ezAe2gH3Va+jfxvh1JtFUan+p88vaCHHKuNwPSp4WZBPt1ZTDmG+ACG6j+ZWHK+eP7hLvtlCi/M9/v4/bVojpm7v01LLiM+MRhnE9E7eLemQC4zb6RVtf2oGQ3mX51kMeya218kEm5bsQPpRJElI9jwletFa8qYK5AFgaCHpll059RRHQTTE9MTNcN58P/Kf6Qb3KGpKNoaHTXoOD4U6LcXLWvdHONVB6AzGefxg2b/dvtaO4nd5lDooMBR3u9kWpopXkyAuf+fO/8je9JTxO6CDUtDjHRcR2mCEXWX6rculIAbNXOv1ccRYCTQ8aFtYnFlMSb1+QIAdyT6HHmeT5ktk3+XJRuEv0PJJvqHTo+j7tPngjiv5sPNutgGFlOFO90omTbzEkIT/D/LsgXbWm2QKYWejbLiuSINg+FiFiZN90doCf4aCpm2y1Q/9GzvK+eXcpqzPCGxOykE7EuPZK3+S/ZU2oaWL2uSpbKPtC0qxebrneR307aVEa7C85HCkTMMBzE4tjUr8h5HhLqtWvqmDcnBo3PfQhf9cEO6jQLd3qhEVJmcwKz/yOCfgPXlCbRqiKk7j94perpNCIalXtMI9ySYjJNjHsB4600=
api_key:
secure: WLjhHVAdUkDy6UdNWveTpZqphw9olN0alCpyPpU0cJjlN/hk7YxPP+YHOSVnMZaRZoHM0LL11jPKLf970nymBfvJHDWxKk71c/5xyTX8pBtSxmGmGy23a5g2VrHVMd2JxsI7NEAH500tlFz+01t4E3Steo0NpAkHR3Q51kj01gQy8IumsksfhRc9uvjZ7zjm99Yk4L+cxyei79v4esfpx7Bgm0HTfAAAG/19F+r0hvvFiV517SREDU/YtcX/rIjVepEy1uiLKEohnmYtVIRSA3Hjh6VlHVhdj2WDd/dYrxi/Ioysq2zOM9pZVLamO9asB1e6JrTEipWE9jSZXxsbnfFzuNLxcOjKv1wd3OzQKF/7pGKGiWRTv5Xm19D/FrXoE/ULs6bHcC/Ke8Gs2RxPlOCdvAFehCkyf5P4HOPCQdR7o8Yiuvt+5JWKBflElNbSd4nWgwUOo3Yv8vC4Vj53fwmM+Uqfu3IgYFWktYxCm4RWIKMfB/gtmjcF6QYFfjvEMwAvRfBV81kuynCsnubXWzQeBE/b3JOhBfpGciKCjKfy+tS6bZfFjCtQV98hMMiCPre8Y7PahHDYc65wU9Ake+ZE+dDaSbeV3DZ5JeifLJHzKW2J2dWeRANkOaiSwO9VBC9/rht5ulK5qQ1pB2+sKDToAaiKy6RMlB/HgIoYqsU=
on:
tags: true
repo: ParsePlatform/parse-server
+35
View File
@@ -1,5 +1,40 @@
## Parse Server Changelog
### 2.2.6 (4/5/2016)
* Important Fix: Disables find on installation from clients [\#1374](https://github.com/ParsePlatform/parse-server/pull/1374) ([flovilmart](https://github.com/flovilmart))
* Adds missing options to the CLI [\#1368](https://github.com/ParsePlatform/parse-server/pull/1368) ([flovilmart](https://github.com/flovilmart))
* Removes only master on travis [\#1367](https://github.com/ParsePlatform/parse-server/pull/1367) ([flovilmart](https://github.com/flovilmart))
* Auth.\_loadRoles should not query the same role twice. [\#1366](https://github.com/ParsePlatform/parse-server/pull/1366) ([blacha](https://github.com/blacha))
### 2.2.5 (4/4/2016)
* Improves config loading and tests [\#1363](https://github.com/ParsePlatform/parse-server/pull/1363) ([flovilmart](https://github.com/flovilmart))
* Adds travis configuration to deploy NPM on new version tags [\#1361](https://github.com/ParsePlatform/parse-server/pull/1361) ([gfosco](https://github.com/gfosco))
* Inject the default schemas properties when loading it [\#1357](https://github.com/ParsePlatform/parse-server/pull/1357) ([flovilmart](https://github.com/flovilmart))
* Adds console transport when testing with VERBOSE=1 [\#1351](https://github.com/ParsePlatform/parse-server/pull/1351) ([flovilmart](https://github.com/flovilmart))
* Make notEqual work on relations [\#1350](https://github.com/ParsePlatform/parse-server/pull/1350) ([flovilmart](https://github.com/flovilmart))
* Accept only bool for $exists in LiveQuery [\#1315](https://github.com/ParsePlatform/parse-server/pull/1315) ([drew-gross](https://github.com/drew-gross))
* Adds more options when using CLI/config [\#1305](https://github.com/ParsePlatform/parse-server/pull/1305) ([flovilmart](https://github.com/flovilmart))
* Update error message [\#1297](https://github.com/ParsePlatform/parse-server/pull/1297) ([drew-gross](https://github.com/drew-gross))
* Properly let masterKey add fields [\#1291](https://github.com/ParsePlatform/parse-server/pull/1291) ([flovilmart](https://github.com/flovilmart))
* Point to \#1271 as how to write a good issue report [\#1290](https://github.com/ParsePlatform/parse-server/pull/1290) ([drew-gross](https://github.com/drew-gross))
* Adds ability to override mount with publicServerURL for production uses [\#1287](https://github.com/ParsePlatform/parse-server/pull/1287) ([flovilmart](https://github.com/flovilmart))
* Single object queries to use include and keys [\#1280](https://github.com/ParsePlatform/parse-server/pull/1280) ([jeremyjackson89](https://github.com/jeremyjackson89))
* Improves report for Push error in logs and \_PushStatus [\#1269](https://github.com/ParsePlatform/parse-server/pull/1269) ([flovilmart](https://github.com/flovilmart))
* Removes all stdout/err logs while testing [\#1268](https://github.com/ParsePlatform/parse-server/pull/1268) ([flovilmart](https://github.com/flovilmart))
* Matching queries with doesNotExist constraint [\#1250](https://github.com/ParsePlatform/parse-server/pull/1250) ([andrecardoso](https://github.com/andrecardoso))
* Added session length option for session tokens to server configuration [\#997](https://github.com/ParsePlatform/parse-server/pull/997) ([Kenishi](https://github.com/Kenishi))
* Regression test for \#1259 [\#1286](https://github.com/ParsePlatform/parse-server/pull/1286) ([drew-gross](https://github.com/drew-gross))
* Regression test for \#871 [\#1283](https://github.com/ParsePlatform/parse-server/pull/1283) ([drew-gross](https://github.com/drew-gross))
* Add a test to repro \#701 [\#1281](https://github.com/ParsePlatform/parse-server/pull/1281) ([drew-gross](https://github.com/drew-gross))
* Fix for \#1334: using relative cloud code files broken [\#1353](https://github.com/ParsePlatform/parse-server/pull/1353) ([airdrummingfool](https://github.com/airdrummingfool))
* Fix Issue/1288 [\#1346](https://github.com/ParsePlatform/parse-server/pull/1346) ([flovilmart](https://github.com/flovilmart))
* Fixes \#1271 [\#1295](https://github.com/ParsePlatform/parse-server/pull/1295) ([drew-gross](https://github.com/drew-gross))
* Fixes issue \#1302 [\#1314](https://github.com/ParsePlatform/parse-server/pull/1314) ([flovilmart](https://github.com/flovilmart))
* Fixes bug related to include in queries [\#1312](https://github.com/ParsePlatform/parse-server/pull/1312) ([flovilmart](https://github.com/flovilmart))
### 2.2.4 (3/29/2016)
* Hotfix: fixed imports issue for S3Adapter, GCSAdapter, FileSystemAdapter [\#1263](https://github.com/ParsePlatform/parse-server/pull/1263) ([drew-gross](https://github.com/drew-gross)
+2 -2
View File
@@ -104,7 +104,7 @@ Once you have a better understanding of how the project works, please refer to t
We have provided a basic [Node.js application](https://github.com/ParsePlatform/parse-server-example) that uses the Parse Server module on Express and can be easily deployed to various infrastructure providers:
* [Heroku and mLab](https://github.com/ParsePlatform/parse-server/wiki/Deploying-Parse-Server#deploying-to-heroku-and-mLab)
* [Heroku and mLab](https://devcenter.heroku.com/articles/deploying-a-parse-server-to-heroku)
* [AWS and Elastic Beanstalk](http://mobile.awsblog.com/post/TxCD57GZLM2JR/How-to-set-up-Parse-Server-on-AWS-using-AWS-Elastic-Beanstalk)
* [Digital Ocean](https://www.digitalocean.com/community/tutorials/how-to-run-parse-server-on-ubuntu-14-04)
* [NodeChef](https://nodechef.com/blog/post/6/migrate-from-parse-to-nodechef%E2%80%99s-managed-parse-server)
@@ -185,7 +185,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
* `filesAdapter` - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js)).
* `maxUploadSize` - Max file size for uploads. Defaults to 20 MB.
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)).
* `databaseAdapter` - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`). Defaults to `MongoStorageAdapter`.
* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year).
##### Email verification and password reset
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "2.2.4",
"version": "2.2.6",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -26,6 +26,7 @@
"commander": "^2.9.0",
"deepcopy": "^0.6.1",
"express": "^4.13.4",
"intersect": "^1.0.1",
"lru-cache": "^4.0.0",
"mailgun-js": "^0.7.7",
"mime": "^1.3.4",
+60 -8
View File
@@ -1,3 +1,4 @@
'use strict';
var commander = require("../src/cli/utils/commander").default;
var definitions = {
@@ -11,7 +12,7 @@ var definitions = {
action: function(value) {
var value = parseInt(value);
if (!Number.isInteger(value)) {
throw "port is invalid";
throw "arg2 is invalid";
}
return value;
}
@@ -23,7 +24,7 @@ var definitions = {
}
describe("commander additions", () => {
afterEach((done) => {
commander.options = [];
delete commander.arg0;
@@ -33,7 +34,7 @@ describe("commander additions", () => {
delete commander.arg4;
done();
})
it("should load properly definitions from args", (done) => {
commander.loadDefinitions(definitions);
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "--arg1", "arg1Value", "--arg2", "2", "--arg3", "some"]);
@@ -44,7 +45,7 @@ describe("commander additions", () => {
expect(commander.arg4).toEqual("arg4Value");
done();
});
it("should load properly definitions from env", (done) => {
commander.loadDefinitions(definitions);
commander.parse([], {
@@ -58,7 +59,7 @@ describe("commander additions", () => {
expect(commander.arg4).toEqual("arg4Value");
done();
});
it("should load properly use args over env", (done) => {
commander.loadDefinitions(definitions);
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "--arg4", "anotherArg4"], {
@@ -72,7 +73,7 @@ describe("commander additions", () => {
expect(commander.arg4).toEqual("anotherArg4");
done();
});
it("should fail in action as port is invalid", (done) => {
commander.loadDefinitions(definitions);
expect(()=> {
@@ -81,7 +82,58 @@ describe("commander additions", () => {
"PROGRAM_ARG_1": "arg1ENVValue",
"PROGRAM_ARG_2": "hello",
});
}).toThrow("port is invalid");
}).toThrow("arg2 is invalid");
done();
});
});
it("should not override config.json", (done) => {
commander.loadDefinitions(definitions);
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "./spec/configs/CLIConfig.json"], {
"PROGRAM_ARG_0": "arg0ENVValue",
"PROGRAM_ARG_1": "arg1ENVValue",
});
let options = commander.getOptions();
expect(options.arg2).toBe(8888);
expect(options.arg3).toBe("hello"); //config value
expect(options.arg4).toBe('/1');
done();
});
it("should fail with invalid values in JSON", (done) => {
commander.loadDefinitions(definitions);
expect(() => {
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "./spec/configs/CLIConfigFail.json"], {
"PROGRAM_ARG_0": "arg0ENVValue",
"PROGRAM_ARG_1": "arg1ENVValue",
});
}).toThrow("arg2 is invalid")
done();
});
it("should fail when too many apps are set", (done) => {
commander.loadDefinitions(definitions);
expect(() => {
commander.parse(["node","./CLI.spec.js","./spec/configs/CLIConfigFailTooManyApps.json"]);
}).toThrow("Multiple apps are not supported")
done();
});
it("should load config from apps", (done) => {
commander.loadDefinitions(definitions);
commander.parse(["node", "./CLI.spec.js", "./spec/configs/CLIConfigApps.json"]);
let options = commander.getOptions();
expect(options.arg1).toBe("my_app");
expect(options.arg2).toBe(8888);
expect(options.arg3).toBe("hello"); //config value
expect(options.arg4).toBe('/1');
done();
});
it("should fail when passing an invalid arguement", (done) => {
commander.loadDefinitions(definitions);
expect(() => {
commander.parse(["node", "./CLI.spec.js", "./spec/configs/CLIConfigUnknownArg.json"]);
}).toThrow('error: unknown option myArg')
done();
});
});
+34
View File
@@ -1,5 +1,9 @@
// This is a port of the test suite:
// hungry/js/test/parse_acl_test.js
var rest = require('../src/rest');
var Config = require('../src/Config');
var config = new Config('test');
var auth = require('../src/Auth');
describe('Parse.ACL', () => {
it("acl must be valid", (done) => {
@@ -1158,4 +1162,34 @@ describe('Parse.ACL', () => {
});
});
it('regression test #701', done => {
var anonUser = {
authData: {
anonymous: {
id: '00000000-0000-0000-0000-000000000001'
}
}
};
Parse.Cloud.afterSave(Parse.User, req => {
if (!req.object.existed()) {
var user = req.object;
var acl = new Parse.ACL(user);
user.setACL(acl);
user.save(null, {useMasterKey: true}).then(user => {
new Parse.Query('_User').get(user.objectId).then(user => {
fail('should not have fetched user without public read enabled');
done();
}, error => {
expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className);
done();
});
});
}
});
rest.create(config, auth.nobody(config), '_User', anonUser)
})
});
+31
View File
@@ -618,6 +618,37 @@ describe('miscellaneous', function() {
});
});
it('pointer reassign is working properly (#1288)', (done) => {
Parse.Cloud.beforeSave('GameScore', (req, res) => {
var obj = req.object;
if (obj.get('point')) {
return res.success();
}
var TestObject1 = Parse.Object.extend('TestObject1');
var newObj = new TestObject1({'key1': 1});
return newObj.save().then((newObj) => {
obj.set('point' , newObj);
res.success();
});
});
var pointId;
var obj = new Parse.Object('GameScore');
obj.set('foo', 'bar');
obj.save().then(() => {
expect(obj.get('point')).not.toBeUndefined();
pointId = obj.get('point').id;
expect(pointId).not.toBeUndefined();
obj.set('foo', 'baz');
return obj.save();
}).then((obj) => {
expect(obj.get('point').id).toEqual(pointId);
Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
done();
})
});
it('test afterSave get full object on create and update', function(done) {
var triggerTime = 0;
// Register a mock beforeSave hook
+44
View File
@@ -119,6 +119,50 @@ describe('Installations', () => {
}).catch((error) => { console.log(error); });
});
it('should properly fail queying installations', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var device = 'android';
var input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
let query = new Parse.Query(Parse.Installation);
return query.find()
}).then((results) => {
fail('Should not succeed!');
done();
}).catch((error) => {
expect(error.code).toBe(119);
expect(error.message).toBe('Clients aren\'t allowed to perform the find operation on the installation collection.')
done();
});
});
it('should properly queying installations with masterKey', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var device = 'android';
var input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
let query = new Parse.Query(Parse.Installation);
return query.find({useMasterKey: true});
}).then((results) => {
expect(results.length).toEqual(1);
var obj = results[0].toJSON();
expect(obj.installationId).toEqual(installId);
expect(obj.deviceType).toEqual(device);
done();
}).catch((error) => {
fail('Should not fail');
done();
});
});
it('fails with missing ids', (done) => {
var input = {
'deviceType': 'android',
+323
View File
@@ -23,6 +23,105 @@ describe('Parse.Query testing', () => {
});
});
it("notEqualTo with Relation is working", function(done) {
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
var user1 = new Parse.User();
user1.setPassword("asdf");
user1.setUsername("qwerty");
var user2 = new Parse.User();
user2.setPassword("asdf");
user2.setUsername("asdf");
var Cake = Parse.Object.extend("Cake");
var cake1 = new Cake();
var cake2 = new Cake();
var cake3 = new Cake();
user.signUp().then(function(){
return user1.signUp();
}).then(function(){
return user2.signUp();
}).then(function(){
var relLike1 = cake1.relation("liker");
relLike1.add([user, user1]);
var relDislike1 = cake1.relation("hater");
relDislike1.add(user2);
return cake1.save();
}).then(function(){
var rellike2 = cake2.relation("liker");
rellike2.add([user, user1]);
var relDislike2 = cake2.relation("hater");
relDislike2.add(user2);
return cake2.save();
}).then(function(){
var rellike3 = cake3.relation("liker");
rellike3.add(user);
var relDislike3 = cake3.relation("hater");
relDislike3.add([user1, user2]);
return cake3.save();
}).then(function(){
var query = new Parse.Query(Cake);
// User2 likes nothing so we should receive 0
query.equalTo("liker", user2);
return query.find().then(function(results){
equal(results.length, 0);
});
}).then(function(){
var query = new Parse.Query(Cake);
// User1 likes two of three cakes
query.equalTo("liker", user1);
return query.find().then(function(results){
// It should return 2 -> cake 1 and cake 2
equal(results.length, 2);
});
}).then(function(){
var query = new Parse.Query(Cake);
// We want to know which cake the user1 is not appreciating -> cake3
query.notEqualTo("liker", user1);
return query.find().then(function(results){
// Should return 1 -> the cake 3
equal(results.length, 1);
});
}).then(function(){
var query = new Parse.Query(Cake);
// User2 is a hater of everything so we should receive 0
query.notEqualTo("hater", user2);
return query.find().then(function(results){
equal(results.length, 0);
});
}).then(function(){
var query = new Parse.Query(Cake);
// Only cake3 is liked by user
query.notContainedIn("liker", [user1]);
return query.find().then(function(results){
equal(results.length, 1);
});
}).then(function(){
var query = new Parse.Query(Cake);
// All the users
query.containedIn("liker", [user, user1, user2]);
// Exclude user 1
query.notEqualTo("liker", user1);
// Only cake3 is liked only by user1
return query.find().then(function(results){
equal(results.length, 1);
let cake = results[0];
expect(cake.id).toBe(cake3.id);
});
}).then(function(){
done();
})
});
it("query with limit", function(done) {
var baz = new TestObject({ foo: 'baz' });
var qux = new TestObject({ foo: 'qux' });
@@ -1422,6 +1521,153 @@ describe('Parse.Query testing', () => {
});
});
it('properly includes array', (done) => {
let objects = [];
let total = 0;
while(objects.length != 5) {
let object = new Parse.Object('AnObject');
object.set('key', objects.length);
total += objects.length;
objects.push(object);
}
Parse.Object.saveAll(objects).then(() => {
let object = new Parse.Object("AContainer");
object.set('objects', objects);
return object.save();
}).then(() => {
let query = new Parse.Query('AContainer');
query.include('objects');
return query.find()
}).then((results) => {
expect(results.length).toBe(1);
let res = results[0];
let objects = res.get('objects');
expect(objects.length).toBe(5);
objects.forEach((object) => {
total -= object.get('key');
});
expect(total).toBe(0);
done()
}, () => {
fail('should not fail');
done();
})
});
it('properly includes array of mixed objects', (done) => {
let objects = [];
let total = 0;
while(objects.length != 5) {
let object = new Parse.Object('AnObject');
object.set('key', objects.length);
total += objects.length;
objects.push(object);
}
while(objects.length != 10) {
let object = new Parse.Object('AnotherObject');
object.set('key', objects.length);
total += objects.length;
objects.push(object);
}
Parse.Object.saveAll(objects).then(() => {
let object = new Parse.Object("AContainer");
object.set('objects', objects);
return object.save();
}).then(() => {
let query = new Parse.Query('AContainer');
query.include('objects');
return query.find()
}).then((results) => {
expect(results.length).toBe(1);
let res = results[0];
let objects = res.get('objects');
expect(objects.length).toBe(10);
objects.forEach((object) => {
total -= object.get('key');
});
expect(total).toBe(0);
done()
}, (err) => {
fail('should not fail');
done();
})
});
it('properly nested array of mixed objects with bad ids', (done) => {
let objects = [];
let total = 0;
while(objects.length != 5) {
let object = new Parse.Object('AnObject');
object.set('key', objects.length);
objects.push(object);
}
while(objects.length != 10) {
let object = new Parse.Object('AnotherObject');
object.set('key', objects.length);
objects.push(object);
}
Parse.Object.saveAll(objects).then(() => {
let object = new Parse.Object("AContainer");
for (var i=0; i<objects.length; i++) {
if (i%2 == 0) {
objects[i].id = 'randomThing'
} else {
total += objects[i].get('key');
}
}
object.set('objects', objects);
return object.save();
}).then(() => {
let query = new Parse.Query('AContainer');
query.include('objects');
return query.find()
}).then((results) => {
expect(results.length).toBe(1);
let res = results[0];
let objects = res.get('objects');
expect(objects.length).toBe(5);
objects.forEach((object) => {
total -= object.get('key');
});
expect(total).toBe(0);
done()
}, (err) => {
console.error(err);
fail('should not fail');
done();
})
});
it('properly fetches nested pointers', (done) =>  {
let color = new Parse.Object('Color');
color.set('hex','#133733');
let circle = new Parse.Object('Circle');
circle.set('radius', 1337);
Parse.Object.saveAll([color, circle]).then(() => {
circle.set('color', color);
let badCircle = new Parse.Object('Circle');
badCircle.id = 'badId';
let complexFigure = new Parse.Object('ComplexFigure');
complexFigure.set('consistsOf', [circle, badCircle]);
return complexFigure.save();
}).then(() => {
let q = new Parse.Query('ComplexFigure');
q.include('consistsOf.color');
return q.find()
}).then((results) => {
expect(results.length).toBe(1);
let figure = results[0];
expect(figure.get('consistsOf').length).toBe(1);
expect(figure.get('consistsOf')[0].get('color').get('hex')).toBe('#133733');
done();
}, (err) => {
fail('should not fail');
done();
})
});
it("result object creation uses current extension", function(done) {
var ParentObject = Parse.Object.extend({ className: "ParentObject" });
// Add a foo() method to ChildObject.
@@ -2209,4 +2455,81 @@ describe('Parse.Query testing', () => {
})
})
it('query with two OR subqueries (regression test #1259)', done => {
let relatedObject = new Parse.Object('Class2');
relatedObject.save().then(relatedObject => {
let anObject = new Parse.Object('Class1');
let relation = anObject.relation('relation');
relation.add(relatedObject);
return anObject.save();
}).then(anObject => {
let q1 = anObject.relation('relation').query();
q1.doesNotExist('nonExistantKey1');
let q2 = anObject.relation('relation').query();
q2.doesNotExist('nonExistantKey2');
let orQuery = Parse.Query.or(q1, q2).find().then(results => {
expect(results.length).toEqual(1);
expect(results[0].objectId).toEqual(q1.objectId);
done();
});
});
});
it('objectId containedIn with multiple large array', done => {
let obj = new Parse.Object('MyClass');
obj.save().then(obj => {
let longListOfStrings = [];
for (let i = 0; i < 130; i++) {
longListOfStrings.push(i.toString());
}
longListOfStrings.push(obj.id);
let q = new Parse.Query('MyClass');
q.containedIn('objectId', longListOfStrings);
q.containedIn('objectId', longListOfStrings);
return q.find();
}).then(results => {
expect(results.length).toEqual(1);
done();
});
});
it('include for specific object', function(done){
var child = new Parse.Object('Child');
var parent = new Parse.Object('Parent');
child.set('foo', 'bar');
parent.set('child', child);
Parse.Object.saveAll([child, parent], function(response){
var savedParent = response[1];
var parentQuery = new Parse.Query('Parent');
parentQuery.include('child');
parentQuery.get(savedParent.id, {
success: function(parentObj) {
var childPointer = parentObj.get('child');
ok(childPointer);
equal(childPointer.get('foo'), 'bar');
done();
}
});
});
});
it('select keys for specific object', function(done){
var Foobar = new Parse.Object('Foobar');
Foobar.set('foo', 'bar');
Foobar.set('fizz', 'buzz');
Foobar.save({
success: function(savedFoobar){
var foobarQuery = new Parse.Query('Foobar');
foobarQuery.select('fizz');
foobarQuery.get(savedFoobar.id,{
success: function(foobarObj){
equal(foobarObj.get('fizz'), 'buzz');
equal(foobarObj.get('foo'), undefined);
done();
}
});
}
})
});
});
+62 -31
View File
@@ -248,46 +248,50 @@ describe('Parse.Relation testing', () => {
});
});
it("queries on relation fields with multiple ins", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
it("queries on relation fields with multiple containedIn (regression test for #1271)", (done) => {
let ChildObject = Parse.Object.extend("ChildObject");
let childObjects = [];
for (let 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();
let ParentObject = Parse.Object.extend("ParentObject");
let parent = new ParentObject();
parent.set("x", 4);
var relation = parent.relation("child");
relation.add(childObjects[0]);
relation.add(childObjects[1]);
relation.add(childObjects[2]);
var parent2 = new ParentObject();
let parent1Children = parent.relation("child");
parent1Children.add(childObjects[0]);
parent1Children.add(childObjects[1]);
parent1Children.add(childObjects[2]);
let parent2 = new ParentObject();
parent2.set("x", 3);
var relation2 = parent2.relation("child");
relation2.add(childObjects[4]);
relation2.add(childObjects[5]);
relation2.add(childObjects[6]);
let parent2Children = parent2.relation("child");
parent2Children.add(childObjects[4]);
parent2Children.add(childObjects[5]);
parent2Children.add(childObjects[6]);
var otherChild2 = parent2.relation("otherChild");
otherChild2.add(childObjects[0]);
otherChild2.add(childObjects[1]);
otherChild2.add(childObjects[2]);
let parent2OtherChildren = parent2.relation("otherChild");
parent2OtherChildren.add(childObjects[0]);
parent2OtherChildren.add(childObjects[1]);
parent2OtherChildren.add(childObjects[2]);
var parents = [];
parents.push(parent);
parents.push(parent2);
return Parse.Object.saveAll(parents);
return Parse.Object.saveAll([parent, parent2]);
}).then(() => {
var query = new Parse.Query(ParentObject);
var objects = [];
objects.push(childObjects[0]);
query.containedIn("child", objects);
query.containedIn("otherChild", [childObjects[0]]);
return query.find();
}).then((list) => {
equal(list.length, 2, "There should be 2 results");
let objectsWithChild0InBothChildren = new Parse.Query(ParentObject);
objectsWithChild0InBothChildren.containedIn("child", [childObjects[0]]);
objectsWithChild0InBothChildren.containedIn("otherChild", [childObjects[0]]);
return objectsWithChild0InBothChildren.find();
}).then(objectsWithChild0InBothChildren => {
//No parent has child 0 in both it's "child" and "otherChild" field;
expect(objectsWithChild0InBothChildren.length).toEqual(0);
}).then(() => {
let objectsWithChild4andOtherChild1 = new Parse.Query(ParentObject);
objectsWithChild4andOtherChild1.containedIn("child", [childObjects[4]]);
objectsWithChild4andOtherChild1.containedIn("otherChild", [childObjects[1]]);
return objectsWithChild4andOtherChild1.find();
}).then(objects => {
// parent2 has child 4 and otherChild 1
expect(objects.length).toEqual(1);
done();
});
});
@@ -652,4 +656,31 @@ describe('Parse.Relation testing', () => {
}));
});
});
it('relations are not bidirectional (regression test for #871)', done => {
let PersonObject = Parse.Object.extend("Person");
let p1 = new PersonObject();
let p2 = new PersonObject();
Parse.Object.saveAll([p1, p2]).then(results => {
let p1 = results[0];
let p2 = results[1];
let relation = p1.relation('relation');
relation.add(p2);
p1.save().then(() => {
let query = new Parse.Query(PersonObject);
query.equalTo('relation', p1);
query.find().then(results => {
expect(results.length).toEqual(0);
let query = new Parse.Query(PersonObject);
query.equalTo('relation', p2);
query.find().then(results => {
expect(results.length).toEqual(1);
expect(results[0].objectId).toEqual(p1.objectId);
done();
});
});
})
});
});
});
+86 -19
View File
@@ -2,6 +2,7 @@
// Roles are not accessible without the master key, so they are not intended
// for use by clients. We can manually test them using the master key.
var RestQuery = require("../src/RestQuery");
var Auth = require("../src/Auth").Auth;
var Config = require("../src/Config");
@@ -60,21 +61,87 @@ describe('Parse Role testing', () => {
});
it("should recursively load roles", (done) => {
var rolesNames = ["FooRole", "BarRole", "BazRole"];
var createRole = function(name, sibling, user) {
var role = new Parse.Role(name, new Parse.ACL());
if (user) {
var users = role.relation('users');
users.add(user);
}
if (sibling) {
role.relation('roles').add(sibling);
}
return role.save({}, { useMasterKey: true });
var createRole = function(name, sibling, user) {
var role = new Parse.Role(name, new Parse.ACL());
if (user) {
var users = role.relation('users');
users.add(user);
}
if (sibling) {
role.relation('roles').add(sibling);
}
return role.save({}, { useMasterKey: true });
};
it("should not recursively load the same role multiple times", (done) => {
var rootRole = "RootRole";
var roleNames = ["FooRole", "BarRole", "BazRole"];
var allRoles = [rootRole].concat(roleNames);
var roleObjs = {};
var createAllRoles = function(user) {
var promises = allRoles.map(function(roleName) {
return createRole(roleName, null, user)
.then(function(roleObj) {
roleObjs[roleName] = roleObj;
return roleObj;
});
});
return Promise.all(promises);
};
var restExecute = spyOn(RestQuery.prototype, "execute").and.callThrough();
var user,
auth,
getAllRolesSpy;
createTestUser().then( (newUser) => {
user = newUser;
return createAllRoles(user);
}).then ( (roles) => {
var rootRoleObj = roleObjs[rootRole];
roles.forEach(function(role, i) {
// Add all roles to the RootRole
if (role.id !== rootRoleObj.id) {
role.relation("roles").add(rootRoleObj);
}
// Add all "roleNames" roles to the previous role
if (i > 0) {
role.relation("roles").add(roles[i - 1]);
}
});
return Parse.Object.saveAll(roles, { useMasterKey: true });
}).then( () => {
auth = new Auth({config: new Config("test"), isMaster: true, user: user});
getAllRolesSpy = spyOn(auth, "_getAllRoleNamesForId").and.callThrough();
return auth._loadRoles();
}).then ( (roles) => {
expect(roles.length).toEqual(4);
allRoles.forEach(function(name) {
expect(roles.indexOf("role:"+name)).not.toBe(-1);
});
// 1 Query for the initial setup
// 4 Queries for all the specific roles
// 1 Query for the final $in
expect(restExecute.calls.count()).toEqual(6);
// 4 One query for each of the roles
// 3 Queries for the "RootRole"
expect(getAllRolesSpy.calls.count()).toEqual(7);
done()
}).catch( () => {
fail("should succeed");
done();
});
});
it("should recursively load roles", (done) => {
var rolesNames = ["FooRole", "BarRole", "BazRole"];
var roleIds = {};
createTestUser().then( (user) => {
// Put the user on the 1st role
@@ -97,7 +164,7 @@ describe('Parse Role testing', () => {
expect(roles.length).toEqual(3);
rolesNames.forEach( (name) => {
expect(roles.indexOf('role:'+name)).not.toBe(-1);
})
});
done();
}, function(err){
fail("should succeed")
@@ -122,7 +189,7 @@ describe('Parse Role testing', () => {
});
});
});
it("Should properly resolve roles", (done) => {
let admin = new Parse.Role("Admin", new Parse.ACL());
let moderator = new Parse.Role("Moderator", new Parse.ACL());
@@ -134,7 +201,7 @@ describe('Parse Role testing', () => {
moderator.getRoles().add([admin, superModerator]);
superContentManager.getRoles().add(superModerator);
return Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true});
}).then(() => {
}).then(() => {
var auth = new Auth({ config: new Config("test"), isMaster: true });
// For each role, fetch their sibling, what they inherit
// return with result and roleId for later comparison
@@ -147,7 +214,7 @@ describe('Parse Role testing', () => {
});
})
});
return Parse.Promise.when(promises);
}).then((results) => {
results.forEach((result) => {
@@ -174,7 +241,7 @@ describe('Parse Role testing', () => {
console.error(err);
done();
})
});
it('can create role and query empty users', (done)=> {
+41
View File
@@ -26,6 +26,7 @@ function verifyACL(user) {
}
describe('Parse.User testing', () => {
it("user sign up class method", (done) => {
Parse.User.signUp("asdf", "zxcv", null, {
success: function(user) {
@@ -2160,4 +2161,44 @@ describe('Parse.User testing', () => {
});
});
it('should fail to become user with expired token', (done) => {
Parse.User.signUp("auser", "somepass", null, {
success: function(user) {
request.get({
url: 'http://localhost:8378/1/classes/_Session',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
}, (error, response, body) => {
var id = body.results[0].objectId;
var expiresAt = new Date((new Date()).setYear(2015));
var token = body.results[0].sessionToken;
request.put({
url: "http://localhost:8378/1/classes/_Session/" + id,
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
body: {
expiresAt: { __type: "Date", iso: expiresAt.toISOString() },
},
}, (error, response, body) => {
Parse.User.become(token)
.then(() => { fail("Should not have succeded"); })
.fail((err) => {
expect(err.code).toEqual(209);
expect(err.message).toEqual("Session token is expired.");
Parse.User.logOut() // Logout to prevent polluting CLI with messages
.then(done());
});
});
});
}
});
});
});
+39
View File
@@ -317,6 +317,45 @@ describe('PushController', () => {
});
it('should properly report failures in _PushStatus', (done) => {
var pushAdapter = {
send: function(body, installations) {
return installations.map((installation) => {
return Promise.resolve({
deviceType: installation.deviceType
})
})
},
getValidPushTypes: function() {
return ["ios"];
}
}
let where = { 'channels': {
'$ins': ['Giants', 'Mets']
}};
var payload = {data: {
alert: "Hello World!",
badge: 1,
}}
var config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId);
pushController.sendPush(payload, where, config, auth).then(() => {
fail('should not succeed');
done();
}).catch(() => {
let query = new Parse.Query('_PushStatus');
query.find({useMasterKey: true}).then((results) => {
expect(results.length).toBe(1);
let pushStatus = results[0];
expect(pushStatus.get('status')).toBe('failed');
done();
});
})
});
it('should support full RESTQuery for increment', (done) => {
var payload = {data: {
alert: "Hello World!",
+14
View File
@@ -112,6 +112,20 @@ describe('matchesQuery', function() {
expect(matchesQuery(obj, q)).toBe(false);
});
it('matches queries with doesNotExist constraint', function() {
var obj = {
id: new Id('Item', 'O1'),
count: 15
};
var q = new Parse.Query('Item');
q.doesNotExist('name');
expect(matchesQuery(obj, q)).toBe(true);
q = new Parse.Query('Item');
q.doesNotExist('count');
expect(matchesQuery(obj, q)).toBe(false);
});
it('matches on equality queries', function() {
var day = new Date();
var location = new Parse.GeoPoint({
+68
View File
@@ -284,4 +284,72 @@ describe('rest create', () => {
});
});
it("test default session length", (done) => {
var user = {
username: 'asdf',
password: 'zxcv',
foo: 'bar',
};
var now = new Date();
rest.create(config, auth.nobody(config), '_User', user)
.then((r) => {
expect(Object.keys(r.response).length).toEqual(3);
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
return rest.find(config, auth.master(config),
'_Session', {sessionToken: r.response.sessionToken});
})
.then((r) => {
expect(r.results.length).toEqual(1);
var session = r.results[0];
var actual = new Date(session.expiresAt.iso);
var expected = new Date(now.getTime() + (1000 * 3600 * 24 * 365));
expect(actual.getFullYear()).toEqual(expected.getFullYear());
expect(actual.getMonth()).toEqual(expected.getMonth());
expect(actual.getDate()).toEqual(expected.getDate());
expect(actual.getMinutes()).toEqual(expected.getMinutes());
done();
});
});
it("test specified session length", (done) => {
var user = {
username: 'asdf',
password: 'zxcv',
foo: 'bar',
};
var sessionLength = 3600, // 1 Hour ahead
now = new Date(); // For reference later
config.sessionLength = sessionLength;
rest.create(config, auth.nobody(config), '_User', user)
.then((r) => {
expect(Object.keys(r.response).length).toEqual(3);
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
return rest.find(config, auth.master(config),
'_Session', {sessionToken: r.response.sessionToken});
})
.then((r) => {
expect(r.results.length).toEqual(1);
var session = r.results[0];
var actual = new Date(session.expiresAt.iso);
var expected = new Date(now.getTime() + (sessionLength*1000));
expect(actual.getFullYear()).toEqual(expected.getFullYear());
expect(actual.getMonth()).toEqual(expected.getMonth());
expect(actual.getDate()).toEqual(expected.getDate());
expect(actual.getHours()).toEqual(expected.getHours());
expect(actual.getMinutes()).toEqual(expected.getMinutes());
done();
});
});
});
+6
View File
@@ -0,0 +1,6 @@
{
"arg1": "my_app",
"arg2": "8888",
"arg3": "hello",
"arg4": "/1"
}
+9
View File
@@ -0,0 +1,9 @@
{
"apps": [
{
"arg1": "my_app",
"arg2": 8888,
"arg3": "hello",
"arg4": "/1"
}]
}
+6
View File
@@ -0,0 +1,6 @@
{
"arg1": "my_app",
"arg2": "hello",
"arg3": "hello",
"arg4": "/1"
}
@@ -0,0 +1,16 @@
{
"apps": [
{
"arg1": "my_app",
"arg2": "99999",
"arg3": "hello",
"arg4": "/1"
},
{
"arg1": "my_app2",
"arg2": "9999",
"arg3": "hello",
"arg4": "/1"
}
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"arg1": "my_app",
"arg2": "8888",
"arg3": "hello",
"myArg": "/1"
}
+1 -1
View File
@@ -10,7 +10,7 @@ var ParseServer = require('../src/index').ParseServer;
var path = require('path');
var databaseURI = process.env.DATABASE_URI;
var cloudMain = process.env.CLOUD_CODE_MAIN || '../spec/cloud/main.js';
var cloudMain = process.env.CLOUD_CODE_MAIN || './spec/cloud/main.js';
var port = 8378;
// Default server configuration for tests.
+87
View File
@@ -2,6 +2,7 @@ var request = require('request');
var parseServerPackage = require('../package.json');
var MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions');
var ParseServer = require("../src/index");
var Config = require('../src/Config');
var express = require('express');
describe('server', () => {
@@ -174,6 +175,26 @@ describe('server', () => {
})
});
it('can load absolute cloud code file', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
masterKey: 'test',
cloud: __dirname + '/cloud/main.js'
});
done();
});
it('can load relative cloud code file', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
masterKey: 'test',
cloud: './spec/cloud/main.js'
});
done();
});
it('can create a parse-server', done => {
var parseServer = new ParseServer.default({
appId: "aTestApp",
@@ -246,4 +267,70 @@ describe('server', () => {
expect(ParseServer.FileSystemAdapter).toThrow();
done();
});
it('properly gives publicServerURL when set', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
masterKey: 'test',
publicServerURL: 'https://myserver.com/1'
});
var config = new Config('test', 'http://localhost:8378/1');
expect(config.mount).toEqual('https://myserver.com/1');
done();
});
it('properly removes trailing slash in mount', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
masterKey: 'test'
});
var config = new Config('test', 'http://localhost:8378/1/');
expect(config.mount).toEqual('http://localhost:8378/1');
done();
});
it('should throw when getting invalid mount', done => {
expect(() => setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
masterKey: 'test',
publicServerURL: 'blabla:/some'
}) ).toThrow("publicServerURL should be a valid HTTPS URL starting with https://");
done();
});
it('fails if the session length is not a number', (done) => {
expect(() => setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
masterKey: 'test',
sessionLength: 'test'
})).toThrow('Session length must be a valid number.');
done();
});
it('fails if the session length is less than or equal to 0', (done) => {
expect(() => setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
masterKey: 'test',
sessionLength: '-33'
})).toThrow('Session length must be a value greater than 0.');
expect(() => setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
masterKey: 'test',
sessionLength: '0'
})).toThrow('Session length must be a value greater than 0.');
done();
})
});
+64 -33
View File
@@ -981,7 +981,7 @@ describe('schemas', () => {
});
});
});
it('should not be able to add a field', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1010,7 +1010,7 @@ describe('schemas', () => {
})
})
});
it('should not be able to add a field', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1038,7 +1038,7 @@ describe('schemas', () => {
})
})
});
it('should throw with invalid userId (>10 chars)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1056,7 +1056,7 @@ describe('schemas', () => {
done();
})
});
it('should throw with invalid userId (<10 chars)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1074,7 +1074,7 @@ describe('schemas', () => {
done();
})
});
it('should throw with invalid userId (invalid char)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1092,7 +1092,7 @@ describe('schemas', () => {
done();
})
});
it('should throw with invalid * (spaces)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1110,7 +1110,7 @@ describe('schemas', () => {
done();
})
});
it('should throw with invalid * (spaces)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1128,7 +1128,7 @@ describe('schemas', () => {
done();
})
});
it('should throw with invalid value', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1146,7 +1146,7 @@ describe('schemas', () => {
done();
})
});
it('should throw with invalid value', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1164,10 +1164,10 @@ describe('schemas', () => {
done();
})
});
function setPermissionsOnClass(className, permissions, doPut) {
let op = request.post;
if (doPut)
if (doPut)
{
op = request.put;
}
@@ -1190,18 +1190,18 @@ describe('schemas', () => {
})
});
}
it('validate CLP 1', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
@@ -1239,18 +1239,18 @@ describe('schemas', () => {
done();
})
});
it('validate CLP 2', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
@@ -1304,18 +1304,18 @@ describe('schemas', () => {
done();
})
});
it('validate CLP 3', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
@@ -1362,18 +1362,18 @@ describe('schemas', () => {
done();
});
});
it('validate CLP 4', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
@@ -1400,7 +1400,7 @@ describe('schemas', () => {
// borked CLP should not affec security
return setPermissionsOnClass('AClass', {
'found': {
'role:admin': true
'role:admin': true
}
}, true).then(() => {
fail("Should not be able to save a borked CLP");
@@ -1430,21 +1430,21 @@ describe('schemas', () => {
done();
})
});
it('validate CLP 5', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let user2 = new Parse.User();
user2.setUsername('user2');
user2.setPassword('user2');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
Promise.resolve().then(() => {
return Parse.Object.saveAll([user, user2, admin, role], {useMasterKey: true});
}).then(()=> {
@@ -1495,5 +1495,36 @@ describe('schemas', () => {
}).then(() => {
done();
});
});
});
it('can add field as master (issue #1257)', (done) => {
setPermissionsOnClass('AClass', {
'addField': {}
}).then(() => {
var obj = new Parse.Object('AClass');
obj.set('key', 'value');
return obj.save(null, {useMasterKey: true})
}).then((obj) => {
expect(obj.get('key')).toEqual('value');
done();
}, (err) => {
fail('should not fail');
done();
});
});
it('can login when addFields is false (issue #1355)', (done) => {
setPermissionsOnClass('_User', {
'addField': {}
}).then(() => {
return Parse.User.signUp('foo', 'bar');
}).then((user) => {
expect(user.getUsername()).toBe('foo');
done()
}, (err) => {
console.error(err);
fail('should create user');
done();
})
})
});
+17 -5
View File
@@ -62,6 +62,13 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } =
if (results.length !== 1 || !results[0]['user']) {
return nobody(config);
}
var now = new Date(),
expiresAt = new Date(results[0].expiresAt.iso);
if(expiresAt < now) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
'Session token is expired.');
}
var obj = results[0]['user'];
delete obj.password;
obj['className'] = '_User';
@@ -110,8 +117,9 @@ Auth.prototype._loadRoles = function() {
var roleIDs = results.map(r => r.objectId);
var promises = [Promise.resolve(roleIDs)];
var queriedRoles = {};
for (var role of roleIDs) {
promises.push(this._getAllRoleNamesForId(role));
promises.push(this._getAllRoleNamesForId(role, queriedRoles));
}
return Promise.all(promises).then((results) => {
var allIDs = [];
@@ -139,8 +147,12 @@ Auth.prototype._loadRoles = function() {
};
// Given a role object id, get any other roles it is part of
Auth.prototype._getAllRoleNamesForId = function(roleID) {
Auth.prototype._getAllRoleNamesForId = function(roleID, queriedRoles = {}) {
// Don't need to requery this role as it is already being queried for.
if (queriedRoles[roleID] != null) {
return Promise.resolve([]);
}
queriedRoles[roleID] = true;
// As per documentation, a Role inherits AnotherRole
// if this Role is in the roles pointer of this AnotherRole
// Let's find all the roles where this role is in a roles relation
@@ -160,13 +172,13 @@ Auth.prototype._getAllRoleNamesForId = function(roleID) {
return Promise.resolve([]);
}
var roleIDs = results.map(r => r.objectId);
// we found a list of roles where the roleID
// is referenced in the roles relation,
// Get the roles where those found roles are also
// referenced the same way
var parentRolesPromises = roleIDs.map( (roleId) => {
return this._getAllRoleNamesForId(roleId);
return this._getAllRoleNamesForId(roleId, queriedRoles);
});
parentRolesPromises.push(Promise.resolve(roleIDs));
return Promise.all(parentRolesPromises);
+49 -4
View File
@@ -4,6 +4,16 @@
import cache from './cache';
function removeTrailingSlash(str) {
if (!str) {
return str;
}
if (str.endsWith("/")) {
str = str.substr(0, str.length-1);
}
return str;
}
export class Config {
constructor(applicationId: string, mount: string) {
let DatabaseAdapter = require('./DatabaseAdapter');
@@ -24,7 +34,7 @@ export class Config {
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
this.serverURL = cacheInfo.serverURL;
this.publicServerURL = cacheInfo.publicServerURL;
this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL);
this.verifyUserEmails = cacheInfo.verifyUserEmails;
this.appName = cacheInfo.appName;
@@ -35,14 +45,23 @@ export class Config {
this.userController = cacheInfo.userController;
this.authDataManager = cacheInfo.authDataManager;
this.customPages = cacheInfo.customPages || {};
this.mount = mount;
this.mount = removeTrailingSlash(mount);
this.liveQueryController = cacheInfo.liveQueryController;
this.sessionLength = cacheInfo.sessionLength;
this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this);
}
static validate(options) {
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
appName: options.appName,
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
appName: options.appName,
publicServerURL: options.publicServerURL})
if (options.publicServerURL) {
if (!options.publicServerURL.startsWith("http://") && !options.publicServerURL.startsWith("https://")) {
throw "publicServerURL should be a valid HTTPS URL starting with https://"
}
}
this.validateSessionLength(options.sessionLength);
}
static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {
@@ -56,6 +75,32 @@ export class Config {
}
}
get mount() {
var mount = this._mount;
if (this.publicServerURL) {
mount = this.publicServerURL;
}
return mount;
}
set mount(newValue) {
this._mount = newValue;
}
static validateSessionLength(sessionLength) {
if(isNaN(sessionLength)) {
throw 'Session length must be a valid number.';
}
else if(sessionLength <= 0) {
throw 'Session length must be a value greater than 0.'
}
}
generateSessionExpiresAt() {
var now = new Date();
return new Date(now.getTime() + (this.sessionLength*1000));
}
get invalidLinkURL() {
return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
}
+117 -49
View File
@@ -1,6 +1,8 @@
// A database adapter that works with data exported from the hosted
// Parse database.
import intersect from 'intersect';
var mongodb = require('mongodb');
var Parse = require('parse/node').Parse;
@@ -103,9 +105,14 @@ DatabaseController.prototype.redirectClassNameForKey = function(className, key)
// batch request, that could confuse other users of the schema.
DatabaseController.prototype.validateObject = function(className, object, query, options) {
let schema;
let isMaster = !('acl' in options);
var aclGroup = options.acl || [];
return this.loadSchema().then(s => {
schema = s;
return this.canAddField(schema, className, object, options.acl || []);
if (isMaster) {
return Promise.resolve();
}
return this.canAddField(schema, className, object, aclGroup);
}).then(() => {
return schema.validateObject(className, object, query);
});
@@ -437,26 +444,60 @@ DatabaseController.prototype.reduceInRelation = function(className, query, schem
}
let promises = Object.keys(query).map((key) => {
if (query[key] && (query[key]['$in'] || query[key].__type == 'Pointer')) {
if (query[key] && (query[key]['$in'] || query[key]['$ne'] || query[key]['$nin'] || query[key].__type == 'Pointer')) {
let t = schema.getExpectedType(className, key);
let match = t ? t.match(/^relation<(.*)>$/) : false;
if (!match) {
return Promise.resolve(query);
}
let relatedClassName = match[1];
let relatedIds;
if (query[key]['$in']) {
relatedIds = query[key]['$in'].map(r => r.objectId);
} else {
relatedIds = [query[key].objectId];
}
return this.owningIds(className, key, relatedIds).then((ids) => {
delete query[key];
this.addInObjectIdsIds(ids, query);
return Promise.resolve(query);
// Build the list of queries
let queries = Object.keys(query[key]).map((constraintKey) => {
let relatedIds;
let isNegation = false;
if (constraintKey === 'objectId') {
relatedIds = [query[key].objectId];
} else if (constraintKey == '$in') {
relatedIds = query[key]['$in'].map(r => r.objectId);
} else if (constraintKey == '$nin') {
isNegation = true;
relatedIds = query[key]['$nin'].map(r => r.objectId);
} else if (constraintKey == '$ne') {
isNegation = true;
relatedIds = [query[key]['$ne'].objectId];
} else {
return;
}
return {
isNegation,
relatedIds
}
});
// remove the current queryKey as we don,t need it anymore
delete query[key];
// execute each query independnently to build the list of
// $in / $nin
let promises = queries.map((q) => {
if (!q) {
return Promise.resolve();
}
return this.owningIds(className, key, q.relatedIds).then((ids) => {
if (q.isNegation) {
this.addNotInObjectIdsIds(ids, query);
} else {
this.addInObjectIdsIds(ids, query);
}
return Promise.resolve();
});
});
return Promise.all(promises).then(() => {
return Promise.resolve();
})
}
return Promise.resolve(query);
return Promise.resolve();
})
return Promise.all(promises).then(() => {
@@ -487,18 +528,51 @@ DatabaseController.prototype.reduceRelationKeys = function(className, query) {
}
};
DatabaseController.prototype.addInObjectIdsIds = function(ids, query) {
if (typeof query.objectId == 'string') {
// Add equality op as we are sure
// we had a constraint on that one
query.objectId = {'$eq': query.objectId};
DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) {
let idsFromString = typeof query.objectId === 'string' ? [query.objectId] : null;
let idsFromEq = query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null;
let idsFromIn = query.objectId && query.objectId['$in'] ? query.objectId['$in'] : null;
let allIds = [idsFromString, idsFromEq, idsFromIn, ids].filter(list => list !== null);
let totalLength = allIds.reduce((memo, list) => memo + list.length, 0);
let idsIntersection = [];
if (totalLength > 125) {
idsIntersection = intersect.big(allIds);
} else {
idsIntersection = intersect(allIds);
}
query.objectId = query.objectId || {};
let queryIn = [].concat(query.objectId['$in'] || [], ids || []);
// make a set and spread to remove duplicates
// replace the $in operator as other constraints
// may be set
query.objectId['$in'] = [...new Set(queryIn)];
// Need to make sure we don't clobber existing $lt or other constraints on objectId.
// Clobbering $eq, $in and shorthand $eq (query.objectId === 'string') constraints
// is expected though.
if (!('objectId' in query) || typeof query.objectId === 'string') {
query.objectId = {};
}
query.objectId['$in'] = idsIntersection;
return query;
}
DatabaseController.prototype.addNotInObjectIdsIds = function(ids = null, query) {
let idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : null;
let allIds = [idsFromNin, ids].filter(list => list !== null);
let totalLength = allIds.reduce((memo, list) => memo + list.length, 0);
let idsIntersection = [];
if (totalLength > 125) {
idsIntersection = intersect.big(allIds);
} else {
idsIntersection = intersect(allIds);
}
// Need to make sure we don't clobber existing $lt or other constraints on objectId.
// Clobbering $eq, $in and shorthand $eq (query.objectId === 'string') constraints
// is expected though.
if (!('objectId' in query) || typeof query.objectId === 'string') {
query.objectId = {};
}
query.objectId['$nin'] = idsIntersection;
return query;
}
@@ -518,7 +592,7 @@ DatabaseController.prototype.addInObjectIdsIds = function(ids, query) {
// anything about users, ideally. Then, improve the format of the ACL
// arg to work like the others.
DatabaseController.prototype.find = function(className, query, options = {}) {
var mongoOptions = {};
let mongoOptions = {};
if (options.skip) {
mongoOptions.skip = options.skip;
}
@@ -526,45 +600,39 @@ DatabaseController.prototype.find = function(className, query, options = {}) {
mongoOptions.limit = options.limit;
}
var isMaster = !('acl' in options);
var aclGroup = options.acl || [];
var acceptor = function(schema) {
return schema.hasKeys(className, keysForQuery(query));
};
var schema;
return this.loadSchema(acceptor).then((s) => {
let isMaster = !('acl' in options);
let aclGroup = options.acl || [];
let acceptor = schema => schema.hasKeys(className, keysForQuery(query))
let schema = null;
return this.loadSchema(acceptor).then(s => {
schema = s;
if (options.sort) {
mongoOptions.sort = {};
for (var key in options.sort) {
var mongoKey = transform.transformKey(schema, className, key);
for (let key in options.sort) {
let mongoKey = transform.transformKey(schema, className, key);
mongoOptions.sort[mongoKey] = options.sort[key];
}
}
if (!isMaster) {
var op = 'find';
var k = Object.keys(query);
if (k.length == 1 && typeof query.objectId == 'string') {
op = 'get';
}
let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ?
'get' :
'find';
return schema.validatePermission(className, aclGroup, op);
}
return Promise.resolve();
}).then(() => {
return this.reduceRelationKeys(className, query);
}).then(() => {
return this.reduceInRelation(className, query, schema);
}).then(() => {
return this.adaptiveCollection(className);
}).then(collection => {
var mongoWhere = transform.transformWhere(schema, className, query);
})
.then(() => this.reduceRelationKeys(className, query))
.then(() => this.reduceInRelation(className, query, schema))
.then(() => this.adaptiveCollection(className))
.then(collection => {
let mongoWhere = transform.transformWhere(schema, className, query);
if (!isMaster) {
var orParts = [
let orParts = [
{"_rperm" : { "$exists": false }},
{"_rperm" : { "$in" : ["*"]}}
];
for (var acl of aclGroup) {
for (let acl of aclGroup) {
orParts.push({"_rperm" : { "$in" : [acl]}});
}
mongoWhere = {'$and': [mongoWhere, {'$or': orParts}]};
+7 -2
View File
@@ -52,7 +52,6 @@ export class PushController extends AdaptableController {
let badgeUpdate = () => {
return Promise.resolve();
}
if (body.data && body.data.badge) {
let badge = body.data.badge;
let op = {};
@@ -89,10 +88,16 @@ export class PushController extends AdaptableController {
}).then(() => {
return rest.find(config, auth, '_Installation', where);
}).then((response) => {
pushStatus.setRunning();
if (!response.results) {
return Promise.reject({error: 'PushController: no results in query'})
}
pushStatus.setRunning(response.results);
return this.sendToAdapter(body, response.results, pushStatus, config);
}).then((results) => {
return pushStatus.complete(results);
}).catch((err) => {
pushStatus.fail(err);
return Promise.reject(err);
});
}
+8 -1
View File
@@ -206,7 +206,14 @@ function matchesKeyConstraints(object, key, constraints) {
}
break;
case '$exists':
if (typeof object[key] === 'undefined') {
let propertyExists = typeof object[key] !== 'undefined';
let existenceIsRequired = constraints['$exists'];
if (typeof constraints['$exists'] !== 'boolean') {
// The SDK will never submit a non-boolean for $exists, but if someone
// tries to submit a non-boolean for $exits outside the SDKs, just ignore it.
break;
}
if ((!propertyExists && existenceIsRequired) || (propertyExists && !existenceIsRequired)) {
return false;
}
break;
+7 -3
View File
@@ -9,6 +9,7 @@ var batch = require('./batch'),
middlewares = require('./middlewares'),
multer = require('multer'),
Parse = require('parse/node').Parse,
path = require('path'),
authDataManager = require('./authDataManager');
import { logger,
@@ -75,6 +76,7 @@ addParseCloud();
// "restAPIKey": optional key from Parse dashboard
// "javascriptKey": optional key from Parse dashboard
// "push": optional key from configure push
// "sessionLength": optional length in seconds for how long Sessions should be valid for
class ParseServer {
@@ -111,7 +113,8 @@ class ParseServer {
choosePassword: undefined,
passwordResetSuccess: undefined
},
liveQuery = {}
liveQuery = {},
sessionLength = 31536000, // 1 Year in seconds
}) {
// Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
@@ -140,7 +143,7 @@ class ParseServer {
if (typeof cloud === 'function') {
cloud(Parse)
} else if (typeof cloud === 'string') {
require(cloud);
require(path.resolve(process.cwd(), cloud));
} else {
throw "argument 'cloud' must either be a string or a function";
}
@@ -185,7 +188,8 @@ class ParseServer {
publicServerURL: publicServerURL,
customPages: customPages,
maxUploadSize: maxUploadSize,
liveQueryController: liveQueryController
liveQueryController: liveQueryController,
sessionLength : Number(sessionLength),
});
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
+28 -24
View File
@@ -449,39 +449,42 @@ function includePath(config, auth, response, path) {
if (pointers.length == 0) {
return response;
}
let pointersHash = {};
var className = null;
var objectIds = {};
for (var pointer of pointers) {
if (className === null) {
className = pointer.className;
} else {
if (className != pointer.className) {
throw new Parse.Error(Parse.Error.INVALID_JSON,
'inconsistent type data for include');
}
let className = pointer.className;
// only include the good pointers
if (className) {
pointersHash[className] = pointersHash[className] || [];
pointersHash[className].push(pointer.objectId);
}
objectIds[pointer.objectId] = true;
}
if (!className) {
throw new Parse.Error(Parse.Error.INVALID_JSON,
'bad pointers');
}
let queryPromises = Object.keys(pointersHash).map((className) => {
var where = {'objectId': {'$in': pointersHash[className]}};
var query = new RestQuery(config, auth, className, where);
return query.execute().then((results) => {
results.className = className;
return Promise.resolve(results);
})
})
// Get the objects for all these object ids
var where = {'objectId': {'$in': Object.keys(objectIds)}};
var query = new RestQuery(config, auth, className, where);
return query.execute().then((includeResponse) => {
var replace = {};
for (var obj of includeResponse.results) {
obj.__type = 'Object';
obj.className = className;
return Promise.all(queryPromises).then((responses) => {
var replace = responses.reduce((replace, includeResponse) => {
for (var obj of includeResponse.results) {
obj.__type = 'Object';
obj.className = includeResponse.className;
if(className == "_User"){
delete obj.sessionToken;
if(className == "_User"){
delete obj.sessionToken;
}
replace[obj.objectId] = obj;
}
return replace;
}, {})
replace[obj.objectId] = obj;
}
var resp = {
results: replacePointers(response.results, path, replace)
};
@@ -534,7 +537,8 @@ function findPointers(object, path) {
// pointers inflated.
function replacePointers(object, path, replace) {
if (object instanceof Array) {
return object.map((obj) => replacePointers(obj, path, replace));
return object.map((obj) => replacePointers(obj, path, replace))
.filter((obj) => obj != null && obj != undefined);
}
if (typeof object !== 'object') {
+5 -5
View File
@@ -169,6 +169,7 @@ RestWrite.prototype.runBeforeTrigger = function() {
if (this.query && this.query.objectId) {
delete this.data.objectId
}
return this.validateSchema();
}
});
};
@@ -302,7 +303,7 @@ RestWrite.prototype.handleAuthData = function(authData) {
'this auth is already used');
}
}
}
}
return Promise.resolve();
});
}
@@ -319,8 +320,7 @@ RestWrite.prototype.transformUser = function() {
var token = 'r:' + cryptoUtils.newToken();
this.storage['token'] = token;
promise = promise.then(() => {
var expiresAt = new Date();
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
var expiresAt = this.config.generateSessionExpiresAt();
var sessionData = {
sessionToken: token,
user: {
@@ -474,8 +474,7 @@ RestWrite.prototype.handleSession = function() {
if (!this.query && !this.auth.isMaster) {
var token = 'r:' + cryptoUtils.newToken();
var expiresAt = new Date();
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
var expiresAt = this.config.generateSessionExpiresAt();
var sessionData = {
sessionToken: token,
user: {
@@ -739,6 +738,7 @@ RestWrite.prototype.runDatabaseOperation = function() {
ACL['*'] = { read: true, write: false };
this.data.ACL = ACL;
}
// Run a create
return this.config.database.create(this.className, this.data, this.runOptions)
.then((resp) => {
+19 -1
View File
@@ -4,6 +4,8 @@ import rest from '../rest';
import url from 'url';
const ALLOWED_GET_QUERY_KEYS = ['keys', 'include'];
export class ClassesRouter extends PromiseRouter {
handleFind(req) {
@@ -59,7 +61,23 @@ export class ClassesRouter extends PromiseRouter {
// Returns a promise for a {response} object.
handleGet(req) {
return rest.find(req.config, req.auth, req.params.className, {objectId: req.params.objectId})
let body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
let options = {};
for (let key of Object.keys(body)) {
if (ALLOWED_GET_QUERY_KEYS.indexOf(key) === -1) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Improper encode of parameter');
}
}
if (typeof body.keys == 'string') {
options.keys = body.keys;
}
if (body.include) {
options.include = String(body.include);
}
return rest.find(req.config, req.auth, req.params.className, {objectId: req.params.objectId}, options)
.then((response) => {
if (!response.results || response.results.length == 0) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
+3 -4
View File
@@ -1,6 +1,6 @@
import PromiseRouter from '../PromiseRouter';
import PromiseRouter from '../PromiseRouter';
import * as middleware from "../middlewares";
import { Parse } from "parse/node";
import { Parse } from "parse/node";
export class PushRouter extends PromiseRouter {
@@ -46,8 +46,7 @@ export class PushRouter extends PromiseRouter {
}
}
} else {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Channels and query should be set at least one.');
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Sending a push requires either "channels" or a "where" query.');
}
return where;
}
+1 -3
View File
@@ -108,9 +108,7 @@ export class UsersRouter extends ClassesRouter {
req.config.filesController.expandFilesInObject(req.config, user);
let expiresAt = new Date();
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
let expiresAt = req.config.generateSessionExpiresAt();
let sessionData = {
sessionToken: token,
user: {
+10
View File
@@ -263,6 +263,16 @@ class Schema {
}
});
if (className) {
// merge with the default schema
let defaultClassData = Object.assign({}, defaultColumns._Default, defaultColumns[className]);
defaultClassData = Object.keys(defaultClassData).reduce((memo, key) => {
let type = schemaAPITypeToMongoFieldType(defaultClassData[key]).result;
if (type) {
memo[key] = type;
}
return memo;
}, {});
classData = Object.assign({}, defaultClassData, classData);
this.data[className] = classData;
if (permsData) {
this.perms[className] = permsData;
+103 -49
View File
@@ -1,10 +1,44 @@
function numberParser(key) {
return function(opt) {
opt = parseInt(opt);
if (!Number.isInteger(opt)) {
throw new Error(`The ${key} is invalid`);
}
return opt;
}
}
function objectParser(opt) {
if (typeof opt == 'object') {
return opt;
}
return JSON.parse(opt)
}
function moduleOrObjectParser(opt) {
if (typeof opt == 'object') {
return opt;
}
try {
return JSON.parse(opt);
} catch(e) {}
return opt;
}
function booleanParser(opt) {
if (opt == true || opt == "true" || opt == "1") {
return true;
}
return false;
}
export default {
"appId": {
"appId": {
env: "PARSE_SERVER_APPLICATION_ID",
help: "Your Parse Application ID",
required: true
},
"masterKey": {
"masterKey": {
env: "PARSE_SERVER_MASTER_KEY",
help: "Your Parse Master Key",
required: true
@@ -13,61 +47,64 @@ export default {
env: "PORT",
help: "The port to run the ParseServer. defaults to 1337.",
default: 1337,
action: function(opt) {
opt = parseInt(opt);
if (!Number.isInteger(opt)) {
throw new Error("The port is invalid");
}
return opt;
}
action: numberParser("port")
},
"databaseURI": {
"databaseURI": {
env: "PARSE_SERVER_DATABASE_URI",
help: "The full URI to your mongodb database"
},
"serverURL": {
"databaseOptions": {
env: "PARSE_SERVER_DATABASE_OPTIONS",
help: "Options to pass to the mongodb client",
action: objectParser
},
"collectionPrefix": {
env: "PARSE_SERVER_COLLECTION_PREFIX",
help: 'A collection prefix for the classes'
},
"serverURL": {
env: "PARSE_SERVER_URL",
help: "URL to your parse server with http:// or https://.",
},
"clientKey": {
"publicServerURL": {
env: "PARSE_PUBLIC_SERVER_URL",
help: "Public URL to your parse server with http:// or https://.",
},
"clientKey": {
env: "PARSE_SERVER_CLIENT_KEY",
help: "Key for iOS, MacOS, tvOS clients"
},
"javascriptKey": {
"javascriptKey": {
env: "PARSE_SERVER_JAVASCRIPT_KEY",
help: "Key for the Javascript SDK"
},
"restAPIKey": {
},
"restAPIKey": {
env: "PARSE_SERVER_REST_API_KEY",
help: "Key for REST calls"
},
"dotNetKey": {
},
"dotNetKey": {
env: "PARSE_SERVER_DOT_NET_KEY",
help: "Key for Unity and .Net SDK"
},
"cloud": {
},
"cloud": {
env: "PARSE_SERVER_CLOUD_CODE_MAIN",
help: "Full path to your cloud code main.js"
},
},
"push": {
env: "PARSE_SERVER_PUSH",
help: "Configuration for push, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Push",
action: function(opt) {
return JSON.parse(opt)
}
action: objectParser
},
"oauth": {
"oauth": {
env: "PARSE_SERVER_OAUTH_PROVIDERS",
help: "Configuration for your oAuth providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth",
action: function(opt) {
return JSON.parse(opt)
}
action: objectParser
},
"fileKey": {
"fileKey": {
env: "PARSE_SERVER_FILE_KEY",
help: "Key for your files",
},
"facebookAppIds": {
},
"facebookAppIds": {
env: "PARSE_SERVER_FACEBOOK_APP_IDS",
help: "Comma separated list for your facebook app Ids",
type: "list",
@@ -78,43 +115,60 @@ export default {
"enableAnonymousUsers": {
env: "PARSE_SERVER_ENABLE_ANON_USERS",
help: "Enable (or disable) anon users, defaults to true",
action: function(opt) {
if (opt == "true" || opt == "1") {
return true;
}
return false;
}
action: booleanParser
},
"allowClientClassCreation": {
env: "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION",
help: "Enable (or disable) client class creation, defaults to true",
action: function(opt) {
if (opt == "true" || opt == "1") {
return true;
}
return false;
}
action: booleanParser
},
"mountPath": {
"mountPath": {
env: "PARSE_SERVER_MOUNT_PATH",
help: "Mount path for the server, defaults to /parse",
default: "/parse"
},
"databaseAdapter": {
env: "PARSE_SERVER_DATABASE_ADAPTER",
help: "Adapter module for the database sub-system"
},
"filesAdapter": {
env: "PARSE_SERVER_FILES_ADAPTER",
help: "Adapter module for the files sub-system"
help: "Adapter module for the files sub-system",
action: moduleOrObjectParser
},
"emailAdapter": {
env: "PARSE_SERVER_EMAIL_ADAPTER",
help: "Adapter module for the email sending",
action: moduleOrObjectParser
},
"verifyUserEmails": {
env: "PARSE_SERVER_VERIFY_USER_EMAILS",
help: "Enable (or disable) user email validation, defaults to false",
action: booleanParser
},
"appName": {
env: "PARSE_SERVER_APP_NAME",
help: "Sets the app name"
},
"loggerAdapter": {
env: "PARSE_SERVER_LOGGER_ADAPTER",
help: "Adapter module for the logging sub-system"
help: "Adapter module for the logging sub-system",
action: moduleOrObjectParser
},
"liveQuery": {
env: "PARSE_SERVER_LIVE_QUERY_OPTIONS",
help: "liveQuery options",
action: objectParser
},
"customPages": {
env: "PARSE_SERVER_CUSTOM_PAGES",
help: "custom pages for pasword validation and reset",
action: objectParser
},
"maxUploadSize": {
env: "PARSE_SERVER_MAX_UPLOAD_SIZE",
help: "Max file size for uploads.",
default: "20mb"
},
"sessionLength": {
env: "PARSE_SERVER_SESSION_LENGTH",
help: "Session duration, defaults to 1 year",
action: numberParser("sessionLength")
}
};
+2 -22
View File
@@ -3,6 +3,7 @@ import express from 'express';
import { ParseServer } from '../index';
import definitions from './cli-definitions';
import program from './utils/commander';
import { mergeWithOptions } from './utils/commander';
import colors from 'colors';
program.loadDefinitions(definitions);
@@ -34,28 +35,7 @@ program.on('--help', function(){
program.parse(process.argv, process.env);
let options = {};
if (program.args.length > 0 ) {
let jsonPath = program.args[0];
jsonPath = path.resolve(jsonPath);
let jsonConfig = require(jsonPath);
if (jsonConfig.apps) {
if (jsonConfig.apps.length > 1) {
throw 'Multiple apps are not supported';
}
options = jsonConfig.apps[0];
} else {
options = jsonConfig;
}
console.log(`Configuation loaded from ${jsonPath}`)
}
options = Object.keys(definitions).reduce(function (options, key) {
if (typeof program[key] !== 'undefined') {
options[key] = program[key];
}
return options;
}, options);
let options = program.getOptions();
if (!options.serverURL) {
options.serverURL = `http://localhost:${options.port}${options.mountPath}`;
+50 -9
View File
@@ -1,12 +1,12 @@
import { Command } from 'commander';
import path from 'path';
let _definitions;
let _reverseDefinitions;
let _defaults;
Command.prototype.loadDefinitions = function(definitions) {
_definitions = definitions;
Object.keys(definitions).reduce((program, opt) => {
if (typeof definitions[opt] == "object") {
const additionalOptions = definitions[opt];
@@ -18,14 +18,14 @@ Command.prototype.loadDefinitions = function(definitions) {
}
return program.option(`--${opt} [${opt}]`);
}, this);
_defaults = Object.keys(definitions).reduce((defs, opt) => {
if(_definitions[opt].default) {
defs[opt] = _definitions[opt].default;
}
return defs;
}, {});
_reverseDefinitions = Object.keys(definitions).reduce((object, key) => {
let value = definitions[key];
if (typeof value == "object") {
@@ -36,7 +36,7 @@ Command.prototype.loadDefinitions = function(definitions) {
}
return object;
}, {});
/* istanbul ignore next */
this.on('--help', function(){
console.log(' Configure From Environment:');
@@ -58,17 +58,46 @@ function parseEnvironment(env = {}) {
}
options[_reverseDefinitions[key]] = action(env[key]);
}
return options;
return options;
}, {});
}
function parseConfigFile(program) {
let options = {};
if (program.args.length > 0) {
let jsonPath = program.args[0];
jsonPath = path.resolve(jsonPath);
let jsonConfig = require(jsonPath);
if (jsonConfig.apps) {
if (jsonConfig.apps.length > 1) {
throw 'Multiple apps are not supported';
}
options = jsonConfig.apps[0];
} else {
options = jsonConfig;
}
Object.keys(options).forEach((key) => {
let value = options[key];
if (!_definitions[key]) {
throw `error: unknown option ${key}`;
}
let action = _definitions[key].action;
if (action) {
options[key] = action(value);
}
})
console.log(`Configuation loaded from ${jsonPath}`)
}
return options;
}
Command.prototype.setValuesIfNeeded = function(options) {
Object.keys(options).forEach((key) => {
if (!this[key]) {
this[key] = options[key];
}
}
});
}
}
Command.prototype._parse = Command.prototype.parse;
@@ -76,10 +105,22 @@ Command.prototype.parse = function(args, env) {
this._parse(args);
// Parse the environment first
const envOptions = parseEnvironment(env);
const fromFile = parseConfigFile(this);
// Load the env if not passed from command line
this.setValuesIfNeeded(envOptions);
// Load from file to override
this.setValuesIfNeeded(fromFile);
// Last set the defaults
this.setValuesIfNeeded(_defaults);
}
Command.prototype.getOptions = function() {
return Object.keys(_definitions).reduce((options, key) => {
if (typeof this[key] !== 'undefined') {
options[key] = this[key];
}
return options;
}, {});
}
export default new Command();
+36 -46
View File
@@ -10,7 +10,31 @@ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
}
let currentLogsFolder = LOGS_FOLDER;
var currentTransports;
function generateTransports() {
let level = process.env.VERBOSE ? 'verbose': 'info';
let transports = [
new (DailyRotateFile)({
filename: 'parse-server.info',
dirname: currentLogsFolder,
name: 'parse-server',
level: level
}),
new (DailyRotateFile)({
filename: 'parse-server.err',
dirname: currentLogsFolder,
name: 'parse-server-error',
level: 'error'
})
]
if (!process.env.TESTING || process.env.VERBOSE) {
transports = [new (winston.transports.Console)({
colorize: true,
level:level
})].concat(transports);
}
return transports;
}
const logger = new winston.Logger();
@@ -25,27 +49,8 @@ export function configureLogger({logsFolder}) {
}
currentLogsFolder = logsFolder;
currentTransports = [
new (winston.transports.Console)({
colorize: true,
level: process.env.VERBOSE ? 'verbose': 'info'
}),
new (DailyRotateFile)({
filename: 'parse-server.info',
dirname: currentLogsFolder,
name: 'parse-server',
level: process.env.VERBOSE ? 'verbose': 'info'
}),
new (DailyRotateFile)({
filename: 'parse-server.err',
dirname: currentLogsFolder,
name: 'parse-server-error',
level: 'error'
})
]
logger.configure({
transports: currentTransports
transports: generateTransports()
})
}
@@ -53,34 +58,19 @@ configureLogger({logsFolder: LOGS_FOLDER});
export function addGroup(groupName) {
let level = process.env.VERBOSE ? 'verbose': 'info';
let transports = generateTransports().concat(new (DailyRotateFile)({
filename: groupName,
dirname: currentLogsFolder,
name: groupName,
level: level
}));
winston.loggers.add(groupName, {
transports: [
new (winston.transports.Console)({
colorize: true,
level: level
}),
new (DailyRotateFile)({
filename: groupName,
dirname: currentLogsFolder,
name: groupName,
level: level
}),
new (DailyRotateFile)({
filename: 'parse-server.info',
name: 'parse-server',
dirname: currentLogsFolder,
level: level
}),
new (DailyRotateFile)({
filename: 'parse-server.err',
dirname: currentLogsFolder,
name: 'parse-server-error',
level: 'error'
})
]
transports: transports
});
return winston.loggers.get(groupName);
}
export { logger };
export default winston;
export default logger;
+9 -3
View File
@@ -128,9 +128,15 @@ function handleParseHeaders(req, res, next) {
}
})
.catch((error) => {
// TODO: Determine the correct error scenario.
log.error('error getting auth for sessionToken', error);
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
if(error instanceof Parse.Error) {
next(error);
return;
}
else {
// TODO: Determine the correct error scenario.
log.error('error getting auth for sessionToken', error);
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
}
});
}
+22 -5
View File
@@ -1,4 +1,5 @@
import { md5Hash, newObjectId } from './cryptoUtils';
import { logger } from './logger';
export function flatten(array) {
return array.reduce((memo, element) => {
@@ -20,8 +21,9 @@ export default function pushStatusHandler(config) {
return config.database.adaptiveCollection('_PushStatus');
}
let setInitial = function(body, where, options = {source: 'rest'}) {
let setInitial = function(body = {}, where, options = {source: 'rest'}) {
let now = new Date();
let data = body.data || {};
let object = {
objectId: newObjectId(),
pushTime: now.toISOString(),
@@ -33,7 +35,7 @@ export default function pushStatusHandler(config) {
expiry: body.expiration_time,
status: "pending",
numSent: 0,
pushHash: md5Hash(JSON.stringify(body.data)),
pushHash: md5Hash(JSON.stringify(data)),
// lockdown!
_wperm: [],
_rperm: []
@@ -49,7 +51,8 @@ export default function pushStatusHandler(config) {
return initialPromise;
}
let setRunning = function() {
let setRunning = function(installations) {
logger.verbose('sending push to %d installations', installations.length);
return initialPromise.then(() => {
return collection();
}).then((collection) => {
@@ -86,7 +89,7 @@ export default function pushStatusHandler(config) {
return memo;
}, update);
}
logger.verbose('sent push! %d success, %d failures', update.numSent, update.numFailed);
return initialPromise.then(() => {
return collection();
}).then((collection) => {
@@ -94,9 +97,23 @@ export default function pushStatusHandler(config) {
});
}
let fail = function(err) {
let update = {
errorMessage: JSON.stringify(err),
status: 'failed'
}
logger.error('error while sending push', err);
return initialPromise.then(() => {
return collection();
}).then((collection) => {
return collection.updateOne({objectId: pushStatus.objectId}, {$set: update});
});
}
return Object.freeze({
setInitial,
setRunning,
complete
complete,
fail
})
}
+5 -5
View File
@@ -119,11 +119,11 @@ function update(config, auth, className, objectId, restObject) {
// Disallowing access to the _Role collection except by master key
function enforceRoleSecurity(method, className, auth) {
if (method === 'delete' && className === '_Installation' && !auth.isMaster) {
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
'Clients aren\'t allowed to perform the ' +
'delete operation on the installation collection.');
if (className === '_Installation' && !auth.isMaster) {
if (method === 'delete' || method === 'find') {
let error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
}
}
}
+4 -5
View File
@@ -187,13 +187,12 @@ export function transformKeyValue(schema, className, restKey, restValue, options
// Returns the mongo form of the query.
// Throws a Parse.Error if the input query is invalid.
function transformWhere(schema, className, restWhere) {
var mongoWhere = {};
let mongoWhere = {};
if (restWhere['ACL']) {
throw new Parse.Error(Parse.Error.INVALID_QUERY,
'Cannot query on ACL.');
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
}
for (var restKey in restWhere) {
var out = transformKeyValue(schema, className, restKey, restWhere[restKey],
for (let restKey in restWhere) {
let out = transformKeyValue(schema, className, restKey, restWhere[restKey],
{query: true, validate: true});
mongoWhere[out.key] = out.value;
}