mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32ed52f04 | ||
|
|
3e413683f7 | ||
|
|
35b4c063fb | ||
|
|
29ffbabd3d | ||
|
|
57cecd800c | ||
|
|
c36ca0c44e | ||
|
|
a96b0afac5 | ||
|
|
1feedef21d | ||
|
|
006540cda5 | ||
|
|
ab06055369 | ||
|
|
7da4debbe0 | ||
|
|
c37a4ea1a1 | ||
|
|
9d078003a6 | ||
|
|
a685ed8814 | ||
|
|
0ff35e18f0 | ||
|
|
1a75101146 | ||
|
|
627b4164bb | ||
|
|
d1f77a9a98 | ||
|
|
35ed09a7b4 | ||
|
|
2eb861db66 | ||
|
|
ae0ba7b5a9 | ||
|
|
726c866a4d | ||
|
|
f7f5bab75d | ||
|
|
9ed3fcc536 | ||
|
|
8130658583 | ||
|
|
f23d80ec9a | ||
|
|
510a890a03 | ||
|
|
a69a88f3a4 | ||
|
|
f796d69d19 | ||
|
|
452b737be4 | ||
|
|
634d672ad1 | ||
|
|
803b9be97a | ||
|
|
2cafd6919f | ||
|
|
f75c8b3a4d | ||
|
|
693707b33c | ||
|
|
39f11a5134 | ||
|
|
b2a5092627 | ||
|
|
d9f8486790 | ||
|
|
5838a49396 | ||
|
|
63e2ceeb02 | ||
|
|
7c8ad7e071 | ||
|
|
0ec78d478b | ||
|
|
2cc1b0cfa9 | ||
|
|
324cd85775 | ||
|
|
5518edc2a5 | ||
|
|
d559cb2382 | ||
|
|
5baa53d858 | ||
|
|
729a0c101d | ||
|
|
afbc662dcd | ||
|
|
7e868b2dcc | ||
|
|
6415a35433 | ||
|
|
11301d9590 | ||
|
|
a00d795404 | ||
|
|
2e50416cba | ||
|
|
fcd914bdfd | ||
|
|
db76aa376f | ||
|
|
65518cdd64 | ||
|
|
e7e2369132 | ||
|
|
ac705a8da6 | ||
|
|
aeb6880b85 | ||
|
|
bea655e61b | ||
|
|
abeecad907 | ||
|
|
38f02af511 | ||
|
|
c6c9c97b54 | ||
|
|
103839ce60 | ||
|
|
04172eda5a | ||
|
|
5176efbe16 | ||
|
|
3997b1aa5a | ||
|
|
cd525802a6 | ||
|
|
17374eff8d | ||
|
|
181b12771d | ||
|
|
1c162101be | ||
|
|
7d2929abe2 | ||
|
|
1487a73f16 | ||
|
|
50b9ae4803 | ||
|
|
c5e3be8b64 | ||
|
|
dd1d18dbe6 | ||
|
|
2561987d20 | ||
|
|
0850c184d3 | ||
|
|
98f8db36e8 | ||
|
|
c9a138d9ca | ||
|
|
c4499d25e0 | ||
|
|
0896f33824 | ||
|
|
4864518315 | ||
|
|
3ed3c7b62f | ||
|
|
05ae010b91 | ||
|
|
d428041a83 | ||
|
|
a763f7c2fc | ||
|
|
135b0e0254 | ||
|
|
e444ca8425 | ||
|
|
aa072dabff | ||
|
|
c9be5a3aac | ||
|
|
cf0a4b246f | ||
|
|
14938bbe7a | ||
|
|
1ae1d42c32 | ||
|
|
4052470820 | ||
|
|
7dca7e20b0 | ||
|
|
e440046be4 | ||
|
|
f4b1f7b951 | ||
|
|
d944255e4e | ||
|
|
a55b2b6209 | ||
|
|
00de555ecb | ||
|
|
474a893a22 | ||
|
|
fe8160449c | ||
|
|
74ee8613d8 | ||
|
|
c928dcc118 | ||
|
|
9f149e6db5 | ||
|
|
69d5a2f87c | ||
|
|
3a96140103 | ||
|
|
614e1ac8e5 | ||
|
|
88fa7bad92 | ||
|
|
6b28075194 | ||
|
|
8b43d2644f | ||
|
|
361d4a34ce | ||
|
|
e93a1ad7fc | ||
|
|
392102eb97 | ||
|
|
eefa2ccac7 | ||
|
|
fab8cfdfc7 | ||
|
|
9bad87905f | ||
|
|
b2183680be | ||
|
|
fece2a4b47 | ||
|
|
03108e6347 | ||
|
|
c416cad43f | ||
|
|
5165c80570 | ||
|
|
643bdc8227 | ||
|
|
15fc186a51 | ||
|
|
ea09213511 | ||
|
|
559205bc64 | ||
|
|
66b8a8474e | ||
|
|
b24ff151d5 | ||
|
|
0d856c1f23 | ||
|
|
4d4361451c | ||
|
|
d7d4699832 | ||
|
|
8c09c3dae1 | ||
|
|
5d887e18f0 | ||
|
|
40965186c0 | ||
|
|
b40e16647b | ||
|
|
3b4ae2d0a0 | ||
|
|
1854928fe7 | ||
|
|
e4998c256a | ||
|
|
4bfe2c5014 | ||
|
|
d0c3535a39 | ||
|
|
c2cfa14627 | ||
|
|
19e7407f55 | ||
|
|
6cfcb4ddf8 | ||
|
|
10ee229e60 | ||
|
|
1291f70a16 | ||
|
|
26df50924b | ||
|
|
e698f61a58 | ||
|
|
e60901de6a | ||
|
|
b2f36e192a | ||
|
|
10d2988985 | ||
|
|
37c502bed0 | ||
|
|
b00572de65 | ||
|
|
da5b5ccefa | ||
|
|
7c850fc1ad | ||
|
|
90df7841ef | ||
|
|
0b88302bea | ||
|
|
583bb34995 | ||
|
|
71ae7bee94 | ||
|
|
449ca115b1 | ||
|
|
d4bd21fcbc | ||
|
|
a926712951 | ||
|
|
5f564f32f5 | ||
|
|
874d10fc74 | ||
|
|
4371ca164c | ||
|
|
608cba9e8c | ||
|
|
5cbf3eb8dd | ||
|
|
37953d146b | ||
|
|
a3179e43fb | ||
|
|
7b431ad064 | ||
|
|
3d7589c1bc | ||
|
|
ee8a3c94d5 | ||
|
|
e2a3213351 | ||
|
|
5c7b346292 | ||
|
|
0542f860f4 | ||
|
|
e0a9f19895 | ||
|
|
21e73ac276 | ||
|
|
957b5927b1 | ||
|
|
234d0093ff | ||
|
|
d14d451028 | ||
|
|
d33dd68cc5 | ||
|
|
0d094767cf | ||
|
|
ab827e3c2f | ||
|
|
d49d539a81 | ||
|
|
6e9529f81c | ||
|
|
2d94a885d2 | ||
|
|
b7b57f7ea7 | ||
|
|
b185508508 | ||
|
|
7b91ee98ed | ||
|
|
9776362ab2 | ||
|
|
59b4047de8 | ||
|
|
54b21c2f73 | ||
|
|
5d37755d88 | ||
|
|
ddd01181be | ||
|
|
bf47a0b2b6 | ||
|
|
3945e0c680 | ||
|
|
61b62e4e9f | ||
|
|
0708af17d7 | ||
|
|
cecb2a1cb1 | ||
|
|
ab0ea09067 | ||
|
|
414ee6740d | ||
|
|
65bd59d95f | ||
|
|
ac839f75c7 | ||
|
|
c57c6b33b1 | ||
|
|
4c2636e23c | ||
|
|
2f51e751de | ||
|
|
a727e1ccd3 | ||
|
|
f076078bf1 | ||
|
|
3f66c302b7 | ||
|
|
eef51cf9ad | ||
|
|
1023baf20d | ||
|
|
51970fb470 | ||
|
|
d57e384dcb | ||
|
|
34851c0ae5 | ||
|
|
686cc4ab95 | ||
|
|
fc1cdd4408 | ||
|
|
3fb3ce1ab7 | ||
|
|
512251eb58 | ||
|
|
f312f81cbd | ||
|
|
af30f669cc | ||
|
|
c050a65d49 | ||
|
|
c419106a38 | ||
|
|
99ecbb39f5 | ||
|
|
e9e561f5e8 | ||
|
|
73a3db44ab | ||
|
|
281568edd2 | ||
|
|
b433fb9b4e | ||
|
|
30197a7d84 | ||
|
|
ab6925a5e2 | ||
|
|
a770c4cba8 | ||
|
|
de2a2ee245 | ||
|
|
c22bafb951 | ||
|
|
bc96f0beb1 | ||
|
|
cedac3fda6 | ||
|
|
616ac71623 | ||
|
|
325d09ca4c | ||
|
|
ef92a79818 | ||
|
|
91ace4e718 | ||
|
|
cbbd66964a | ||
|
|
1bd804693c | ||
|
|
1eb2a87f9f | ||
|
|
0b2dc12143 | ||
|
|
213bfea62d | ||
|
|
a17e54c397 | ||
|
|
5773143973 | ||
|
|
406f237119 | ||
|
|
bc384e97e1 | ||
|
|
18906f17b4 | ||
|
|
442c723350 | ||
|
|
9053b941ee | ||
|
|
7e52c18893 | ||
|
|
8c92b80b8f | ||
|
|
27dd2ebf5d | ||
|
|
c1ef0440b0 | ||
|
|
f4889a3393 | ||
|
|
19da14f3df | ||
|
|
91de7504fe | ||
|
|
ec6a39718d | ||
|
|
0e3636d880 | ||
|
|
89df778ac8 | ||
|
|
acc23d0828 | ||
|
|
3a28352c96 | ||
|
|
f99b5588ab | ||
|
|
51664c8f33 | ||
|
|
8c12f80d28 | ||
|
|
c346e15a18 | ||
|
|
4fd67a51fb | ||
|
|
fffc83c0d9 | ||
|
|
04e8e3e233 | ||
|
|
02af61dc9a | ||
|
|
57156cf52b | ||
|
|
01af755d18 | ||
|
|
3f8a1aae02 | ||
|
|
edfa4092c0 | ||
|
|
431d864ac3 | ||
|
|
647987534f | ||
|
|
ca7d8580e3 | ||
|
|
9c528c6fe8 | ||
|
|
2b3bf7c2b7 | ||
|
|
6540ad1483 | ||
|
|
b5625bc2e5 | ||
|
|
09279d1987 | ||
|
|
dbf64a6d83 | ||
|
|
eeb3331167 | ||
|
|
8a6903aa8d | ||
|
|
434ac91129 | ||
|
|
f0ebb7bae6 | ||
|
|
781e81d36b | ||
|
|
73bca3b64c | ||
|
|
6311c95785 | ||
|
|
efa7366250 | ||
|
|
6b6e684d55 | ||
|
|
5d99075663 | ||
|
|
c5636e1b77 | ||
|
|
ab1858616b | ||
|
|
632c8054da | ||
|
|
7afc08abe4 | ||
|
|
33c9730be2 | ||
|
|
97d3deb73b | ||
|
|
a0e7739526 | ||
|
|
7064f1d3ed | ||
|
|
4774e90937 | ||
|
|
6055f2a552 | ||
|
|
b2819df7e5 | ||
|
|
5dc5ef239e | ||
|
|
f25bcc490a | ||
|
|
aa5ebd49f0 | ||
|
|
106ac95622 | ||
|
|
1f4393cd4d | ||
|
|
e361ccf5a5 | ||
|
|
342a3a92fe | ||
|
|
507fa54570 | ||
|
|
1b8e613dfb | ||
|
|
f259a3b17e | ||
|
|
3dccd61222 | ||
|
|
cbf3f3c1be |
@@ -1,17 +1,67 @@
|
||||
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.
|
||||
Please read the following instructions carefully.
|
||||
|
||||
Check out 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!
|
||||
|
||||
Many members of the community use Stack Overflow and Server Fault to ask questions.
|
||||
Read through the existing questions or ask your own!
|
||||
- Stack Overflow: http://stackoverflow.com/questions/tagged/parse.com
|
||||
- Server Fault: https://serverfault.com/tags/parse
|
||||
|
||||
For database migration help, please file a bug report at https://parse.com/help#report
|
||||
|
||||
Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!
|
||||
|
||||
- [ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
|
||||
- [ ] You've met the prerequisites: https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites.
|
||||
|
||||
- [ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
|
||||
- [ ] You're running the latest version of Parse Server: https://github.com/ParsePlatform/parse-server/releases
|
||||
|
||||
- [ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
|
||||
- [ ] You've searched through existing issues: https://github.com/ParsePlatform/Parse-Server/issues?utf8=%E2%9C%93&q=is%3Aissue Chances are that your issue has been reported or resolved before.
|
||||
|
||||
#### Environment Setup
|
||||
- [ ] You have filled out every section below. Issues without sufficient information are more likely to be closed.
|
||||
|
||||
--
|
||||
|
||||
#### Steps to reproduce
|
||||
### Issue Description
|
||||
|
||||
[DELETE EVERYTHING ABOVE THIS LINE BEFORE SUBMITTING YOUR ISSUE]
|
||||
|
||||
#### Logs/Trace
|
||||
Describe your issue in as much detail as possible.
|
||||
|
||||
[FILL THIS OUT]
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
Please include a detailed list of steps that reproduce the issue. Include curl commands when applicable.
|
||||
|
||||
1. [FILL THIS OUT]
|
||||
2. [FILL THIS OUT]
|
||||
3. [FILL THIS OUT]
|
||||
|
||||
#### Expected Results
|
||||
|
||||
[FILL THIS OUT]
|
||||
|
||||
#### Actual Outcome
|
||||
|
||||
[FILL THIS OUT]
|
||||
|
||||
### Environment Setup
|
||||
|
||||
- **Server**
|
||||
- parse-server version: [FILL THIS OUT]
|
||||
- Operating System: [FILL THIS OUT]
|
||||
- Hardware: [FILL THIS OUT]
|
||||
- Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): [FILL THIS OUT]
|
||||
|
||||
- **Database**
|
||||
- MongoDB version: [FILL THIS OUT]
|
||||
- Storage engine: [FILL THIS OUT]
|
||||
- Hardware: [FILL THIS OUT]
|
||||
- Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): [FILL THIS OUT]
|
||||
|
||||
### Logs/Trace
|
||||
|
||||
You can turn on additional logging by configuring VERBOSE=1 in your environment.
|
||||
|
||||
[FILL THIS OUT]
|
||||
|
||||
+33
-10
@@ -1,16 +1,39 @@
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
language: node_js
|
||||
node_js:
|
||||
- "4.3"
|
||||
- '4.3'
|
||||
- '6.1'
|
||||
services:
|
||||
- postgresql
|
||||
addons:
|
||||
postgresql: '9.4'
|
||||
before_script:
|
||||
- psql -c 'create database parse_server_postgres_adapter_test_database;' -U postgres
|
||||
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
|
||||
- PARSE_SERVER_TEST_DB=postgres
|
||||
- MONGODB_VERSION=2.6.11
|
||||
- MONGODB_VERSION=3.0.8
|
||||
- MONGODB_VERSION=3.2.6
|
||||
matrix:
|
||||
fast_finish: true,
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^[0-9]+.[0-9]+.[0-9]+(-.*)?$/
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.mongodb/versions/downloads
|
||||
after_success: ./node_modules/.bin/codecov
|
||||
- "$HOME/.mongodb/versions/downloads"
|
||||
after_script: "./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
|
||||
all_branches: true
|
||||
condition: "$MONGODB_VERSION = '3.0.8'"
|
||||
repo: ParsePlatform/parse-server
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Upgrading Parse Server to version 2.3.0
|
||||
|
||||
Parse Server version 2.3.0 begins using unique indexes to ensure User's username and email are unique. This is not a backwards incompatable change, but it may in some cases cause a significant performance regression until the index finishes building. Building the unique index before upgrading your Parse Server version will eliminate the performance impact, and is a recommended step before upgrading any app to Parse Server 2.3.0. New apps starting with version 2.3.0 do not need to take any steps before beginning their project.
|
||||
|
||||
If you are using MongoDB in Cluster or Replica Set mode, we recommend reading Mongo's [documentation on index building](https://docs.mongodb.com/v3.0/tutorial/build-indexes-on-replica-sets/) first. If you are not using these features, you can execute the following commands from the Mongo shell to build the unique index. You may also want to create a backup first.
|
||||
|
||||
```js
|
||||
// Select the database that your Parse App uses
|
||||
use parse;
|
||||
|
||||
// Select the collection your Parse App uses for users. For migrated apps, this probably includes a collectionPrefix.
|
||||
var coll = db['your_prefix:_User'];
|
||||
|
||||
// You can check if the indexes already exists by running coll.getIndexes()
|
||||
coll.getIndexes();
|
||||
|
||||
// The indexes you want should look like this. If they already exist, you can skip creating them.
|
||||
{
|
||||
"v" : 1,
|
||||
"unique" : true,
|
||||
"key" : {
|
||||
"username" : 1
|
||||
},
|
||||
"name" : "username_1",
|
||||
"ns" : "parse.your_prefix:_User",
|
||||
"background" : true,
|
||||
"sparse" : true
|
||||
}
|
||||
|
||||
{
|
||||
"v" : 1,
|
||||
"unique" : true,
|
||||
"key" : {
|
||||
"email" : 1
|
||||
},
|
||||
"name" : "email_1",
|
||||
"ns" : "parse.your_prefix:_User",
|
||||
"background" : true,
|
||||
"sparse" : true
|
||||
}
|
||||
|
||||
// Create the username index.
|
||||
// "background: true" is mandatory and avoids downtime while the index builds.
|
||||
// "sparse: true" is also mandatory because Parse Server uses sparse indexes.
|
||||
coll.ensureIndex({ username: 1 }, { background: true, unique: true, sparse: true });
|
||||
|
||||
// Create the email index.
|
||||
// "background: true" is still mandatory.
|
||||
// "sparse: true" is also mandatory both because Parse Server uses sparse indexes, and because email addresses are not required by the Parse API.
|
||||
coll.ensureIndex({ email: 1 }, { background: true, unique: true, sparse: true });
|
||||
```
|
||||
|
||||
There are some issues you may run into during this process:
|
||||
|
||||
## Mongo complains that the index already exists, but with different options
|
||||
|
||||
In this case, you will need to remove the incorrect index. If your app relies on the existence of the index in order to be performant, you can create a new index, with "-1" for the direction of the field, so that it counts as different options. Then, drop the conflicting index, and create the unique index.
|
||||
|
||||
## There is already non-unique data in the username or email field
|
||||
|
||||
This is possible if you have explicitly set some user's emails to null. If this is bogus data, and those null fields shoud be unset, you can unset the null emails with this command. If your app relies on the difference between null and unset emails, you will need to upgrade your app to treat null and unset emails the same before building the index and upgrading to Parse Server 2.3.0.
|
||||
|
||||
```js
|
||||
coll.update({ email: { $exists: true, $eq: null } }, { $unset: { email: '' } }, { multi: true })
|
||||
```
|
||||
|
||||
## There is already non-unique data in the username or email field, and it's not nulls
|
||||
|
||||
This is possible due to a race condition in previous versions of Parse Server. If you have this problem, it is unlikely that you have a lot of rows with duplicate data. We recommend you clean up the data manually, by removing or modifying the offending rows.
|
||||
|
||||
This command, can be used to find the duplicate data:
|
||||
|
||||
```js
|
||||
coll.aggregate([
|
||||
{$match: {"username": {"$ne": null}}},
|
||||
{$group: {_id: "$username", uniqueIds: {$addToSet: "$_id"}, count: {$sum: 1}}},
|
||||
{$match: {count: {"$gt": 1}}},
|
||||
{$project: {id: "$uniqueIds", username: "$_id", _id : 0} },
|
||||
{$unwind: "$id" },
|
||||
{$out: '_duplicates'} // Save the list of duplicates to a new, "_duplicates" collection. Remove this line to just output the list.
|
||||
], {allowDiskUse:true})
|
||||
```
|
||||
+154
-1
@@ -1,5 +1,158 @@
|
||||
## Parse Server Changelog
|
||||
|
||||
### 2.2.14 (6/25/2016)
|
||||
|
||||
* Hotfix: Fix Parse.Cloud.HTTPResponse serialization
|
||||
|
||||
### 2.2.13 (6/12/2016)
|
||||
|
||||
* Hotfix: Pin version of deepcopy
|
||||
|
||||
### 2.2.12 (6/9/2016)
|
||||
|
||||
* New: Custom error codes in cloud code response.error, thanks to [Jeremy Pease](https://github.com/JeremyPlease)
|
||||
* Fix: Crash in beforeSave when response is not an object, thanks to [Tyler Brock](https://github.com/TylerBrock)
|
||||
* Fix: Allow "get" on installations
|
||||
* Fix: Fix overly restrictive Class Level Permissions, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Fix: Fix nested date parsing in Cloud Code, thanks to [Marco Cheung](https://github.com/Marco129)
|
||||
* Fix: Support very old file formats from Parse.com
|
||||
|
||||
### 2.2.11 (5/31/2016)
|
||||
|
||||
* Security: Censor user password in logs, thanks to [Marco Cheung](https://github.com/Marco129)
|
||||
* New: Add PARSE_SERVER_LOGS_FOLDER env var for setting log folder, thanks to [KartikeyaRokde](https://github.com/KartikeyaRokde)
|
||||
* New: Webhook key support, thanks to [Tyler Brock](https://github.com/TylerBrock)
|
||||
* Perf: Add cache adapter and default caching of certain objects, thanks to [Blayne Chard](https://github.com/blacha)
|
||||
* Improvement: Better error messages for schema type mismatches, thanks to [Jeremy Pease](https://github.com/JeremyPlease)
|
||||
* Improvement: Better error messages for reset password emails
|
||||
* Improvement: Webhook key support in CLI, thanks to [Tyler Brock](https://github.com/TylerBrock)
|
||||
* Fix: Remove read only fields when using beforeSave, thanks to [Tyler Brock](https://github.com/TylerBrock)
|
||||
* Fix: Use content type provided by JS SDK, thanks to [Blayne Chard](https://github.com/blacha) and [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Fix: Tell the dashboard the stored push data is available, thanks to [Jeremy Pease](https://github.com/JeremyPlease)
|
||||
* Fix: Add support for HTTP Basic Auth, thanks to [Hussam Moqhim](https://github.com/hmoqhim)
|
||||
* Fix: Support for MongoDB version 3.2.6, (note: do not use MongoDB 3.2 with migrated apps that still have traffic on Parse.com), thanks to [Tyler Brock](https://github.com/TylerBrock)
|
||||
* Fix: Prevent `pm2` from crashing when push notifications fail, thanks to [benishak](https://github.com/benishak)
|
||||
* Fix: Add full list of default _Installation fields, thanks to [Jeremy Pease](https://github.com/JeremyPlease)
|
||||
* Fix: Strip objectId out of hooks responses, thanks to [Tyler Brock](https://github.com/TylerBrock)
|
||||
* Fix: Fix external webhook response format, thanks to [Tyler Brock](https://github.com/TylerBrock)
|
||||
* Fix: Fix beforeSave when object is passed to `success`, thanks to [Madhav Bhagat](https://github.com/codebreach)
|
||||
* Fix: Remove use of deprecated APIs, thanks to [Emad Ehsan](https://github.com/emadehsan)
|
||||
* Fix: Crash when multiple Parse Servers on the same machine try to write to the same logs folder, thanks to [Steven Shipton](https://github.com/steven-supersolid)
|
||||
* Fix: Various issues with key names in `Parse.Object`s
|
||||
* Fix: Treat Bytes type properly
|
||||
* Fix: Caching bugs that caused writes by masterKey or other session token to not show up to users reading with a different session token
|
||||
* Fix: Pin mongo driver version, preventing a regression in version 2.1.19
|
||||
* Fix: Various issues with pointer fields not being treated properly
|
||||
* Fix: Issues with pointed getting un-fetched due to changes in beforeSave
|
||||
* Fix: Fixed crash when deleting classes that have CLPs
|
||||
|
||||
### 2.2.10 (5/15/2016)
|
||||
|
||||
* Fix: Write legacy ACLs to Mongo so that clients that still go through Parse.com can read them, thanks to [Tyler Brock](https://github.com/TylerBrock) and [carmenlau](https://github.com/carmenlau)
|
||||
* Fix: Querying installations with limit = 0 and count = 1 now works, thanks to [ssk7833](https://github.com/ssk7833)
|
||||
* Fix: Return correct error when violating unique index, thanks to [Marco Cheung](https://github.com/Marco129)
|
||||
* Fix: Allow unsetting user's email, thanks to [Marco Cheung](https://github.com/Marco129)
|
||||
* New: Support for Node 6.1
|
||||
|
||||
### 2.2.9 (5/9/2016)
|
||||
|
||||
* Fix: Fix a regression that caused Parse Server to crash when a null parameter is passed to a Cloud function
|
||||
|
||||
### 2.2.8 (5/8/2016)
|
||||
|
||||
* New: Support for Pointer Permissions
|
||||
* New: Expose logger in Cloud Code
|
||||
* New: Option to revoke sessions on password reset
|
||||
* New: Option to expire inactive sessions
|
||||
* Perf: Improvements in ACL checking query
|
||||
* Fix: Issues when sending pushes to list of devices that contains invalid values
|
||||
* Fix: Issues caused by using babel-polyfill outside of Parse Server, but in the same express app
|
||||
* Fix: Remove creation of extra session tokens
|
||||
* Fix: Return authData when querying with master key
|
||||
* Fix: Bugs when deleting webhooks
|
||||
* Fix: Ignore _RevocableSession header, which might be sent by the JS SDK
|
||||
* Fix: Issues with querying via URL params
|
||||
* Fix: Properly encode "Date" parameters to cloud code functions
|
||||
|
||||
|
||||
### 2.2.7 (4/15/2016)
|
||||
|
||||
* Adds support for --verbose and verbose option when running ParseServer [\#1414](https://github.com/ParsePlatform/parse-server/pull/1414) ([flovilmart](https://github.com/flovilmart))
|
||||
* Adds limit = 0 as a valid parameter for queries [\#1493](https://github.com/ParsePlatform/parse-server/pull/1493) ([seijiakiyama](https://github.com/seijiakiyama))
|
||||
* Makes sure we preserve Installations when updating a token \(\#1475\) [\#1486](https://github.com/ParsePlatform/parse-server/pull/1486) ([flovilmart](https://github.com/flovilmart))
|
||||
* Hotfix for tests [\#1503](https://github.com/ParsePlatform/parse-server/pull/1503) ([flovilmart](https://github.com/flovilmart))
|
||||
* Enable logs [\#1502](https://github.com/ParsePlatform/parse-server/pull/1502) ([drew-gross](https://github.com/drew-gross))
|
||||
* Do some triple equals for great justice [\#1499](https://github.com/ParsePlatform/parse-server/pull/1499) ([TylerBrock](https://github.com/TylerBrock))
|
||||
* Apply credential stripping to all untransforms for \_User [\#1498](https://github.com/ParsePlatform/parse-server/pull/1498) ([TylerBrock](https://github.com/TylerBrock))
|
||||
* Checking if object has defined key for Pointer constraints in liveQuery [\#1487](https://github.com/ParsePlatform/parse-server/pull/1487) ([simonas-notcat](https://github.com/simonas-notcat))
|
||||
* Remove collection prefix and default mongo URI [\#1479](https://github.com/ParsePlatform/parse-server/pull/1479) ([drew-gross](https://github.com/drew-gross))
|
||||
* Store collection prefix in mongo adapter, and clean up adapter interface [\#1472](https://github.com/ParsePlatform/parse-server/pull/1472) ([drew-gross](https://github.com/drew-gross))
|
||||
* Move field deletion logic into mongo adapter [\#1471](https://github.com/ParsePlatform/parse-server/pull/1471) ([drew-gross](https://github.com/drew-gross))
|
||||
* Adds support for Long and Double mongodb types \(fixes \#1316\) [\#1470](https://github.com/ParsePlatform/parse-server/pull/1470) ([flovilmart](https://github.com/flovilmart))
|
||||
* Schema.js database agnostic [\#1468](https://github.com/ParsePlatform/parse-server/pull/1468) ([flovilmart](https://github.com/flovilmart))
|
||||
* Remove console.log [\#1465](https://github.com/ParsePlatform/parse-server/pull/1465) ([drew-gross](https://github.com/drew-gross))
|
||||
* Push status nits [\#1462](https://github.com/ParsePlatform/parse-server/pull/1462) ([flovilmart](https://github.com/flovilmart))
|
||||
* Fixes \#1444 [\#1451](https://github.com/ParsePlatform/parse-server/pull/1451) ([flovilmart](https://github.com/flovilmart))
|
||||
* Removing sessionToken and authData from \_User objects included in a query [\#1450](https://github.com/ParsePlatform/parse-server/pull/1450) ([simonas-notcat](https://github.com/simonas-notcat))
|
||||
* Move mongo field type logic into mongoadapter [\#1432](https://github.com/ParsePlatform/parse-server/pull/1432) ([drew-gross](https://github.com/drew-gross))
|
||||
* Prevents \_User lock out when setting ACL on signup or afterwards [\#1429](https://github.com/ParsePlatform/parse-server/pull/1429) ([flovilmart](https://github.com/flovilmart))
|
||||
* Update .travis.yml [\#1428](https://github.com/ParsePlatform/parse-server/pull/1428) ([flovilmart](https://github.com/flovilmart))
|
||||
* Adds relation fields to objects [\#1424](https://github.com/ParsePlatform/parse-server/pull/1424) ([flovilmart](https://github.com/flovilmart))
|
||||
* Update .travis.yml [\#1423](https://github.com/ParsePlatform/parse-server/pull/1423) ([flovilmart](https://github.com/flovilmart))
|
||||
* Sets the defaultSchemas keys in the SchemaCollection [\#1421](https://github.com/ParsePlatform/parse-server/pull/1421) ([flovilmart](https://github.com/flovilmart))
|
||||
* Fixes \#1417 [\#1420](https://github.com/ParsePlatform/parse-server/pull/1420) ([drew-gross](https://github.com/drew-gross))
|
||||
* Untransform should treat Array's as nested objects [\#1416](https://github.com/ParsePlatform/parse-server/pull/1416) ([blacha](https://github.com/blacha))
|
||||
* Adds X-Parse-Push-Status-Id header [\#1412](https://github.com/ParsePlatform/parse-server/pull/1412) ([flovilmart](https://github.com/flovilmart))
|
||||
* Schema format cleanup [\#1407](https://github.com/ParsePlatform/parse-server/pull/1407) ([drew-gross](https://github.com/drew-gross))
|
||||
* Updates the publicServerURL option [\#1397](https://github.com/ParsePlatform/parse-server/pull/1397) ([flovilmart](https://github.com/flovilmart))
|
||||
* Fix exception with non-expiring session tokens. [\#1386](https://github.com/ParsePlatform/parse-server/pull/1386) ([0x18B2EE](https://github.com/0x18B2EE))
|
||||
* Move mongo schema format related logic into mongo adapter [\#1385](https://github.com/ParsePlatform/parse-server/pull/1385) ([drew-gross](https://github.com/drew-gross))
|
||||
* WIP: Huge performance improvement on roles queries [\#1383](https://github.com/ParsePlatform/parse-server/pull/1383) ([flovilmart](https://github.com/flovilmart))
|
||||
* Removes GCS Adapter from provided adapters [\#1339](https://github.com/ParsePlatform/parse-server/pull/1339) ([flovilmart](https://github.com/flovilmart))
|
||||
* DBController refactoring [\#1228](https://github.com/ParsePlatform/parse-server/pull/1228) ([flovilmart](https://github.com/flovilmart))
|
||||
* Spotify authentication [\#1226](https://github.com/ParsePlatform/parse-server/pull/1226) ([1nput0utput](https://github.com/1nput0utput))
|
||||
* Expose DatabaseAdapter to simplify application tests [\#1121](https://github.com/ParsePlatform/parse-server/pull/1121) ([steven-supersolid](https://github.com/steven-supersolid))
|
||||
|
||||
### 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)
|
||||
* Fix: Clean null authData values on _User update [\#1199](https://github.com/ParsePlatform/parse-server/pull/1199) ([yuzeh](https://github.com/yuzeh))
|
||||
|
||||
### 2.2.3 (3/29/2016)
|
||||
|
||||
* Fixed bug with invalid email verification link on email update. [\#1253](https://github.com/ParsePlatform/parse-server/pull/1253) ([kzielonka](https://github.com/kzielonka))
|
||||
@@ -20,7 +173,7 @@
|
||||
|
||||
* Important Fix: Mounts createLiveQueryServer, fix babel induced problem [\#1153](https://github.com/ParsePlatform/parse-server/pull/1153) (flovilmart)
|
||||
* Move ParseServer to it's own file [\#1166](https://github.com/ParsePlatform/parse-server/pull/1166) (flovilmart)
|
||||
* Update README.md - remove deploy buttons - replace with community links [\#1139](https://github.com/ParsePlatform/parse-server/pull/1139) (drew-gross)
|
||||
* Update README.md * remove deploy buttons * replace with community links [\#1139](https://github.com/ParsePlatform/parse-server/pull/1139) (drew-gross)
|
||||
* Adds bootstrap.sh [\#1138](https://github.com/ParsePlatform/parse-server/pull/1138) (flovilmart)
|
||||
* Fix: Do not override username [\#1142](https://github.com/ParsePlatform/parse-server/pull/1142) (flovilmart)
|
||||
* Fix: Add pushId back to GCM payload [\#1168](https://github.com/ParsePlatform/parse-server/pull/1168) (wangmengyan95)
|
||||
|
||||
@@ -10,6 +10,8 @@ Parse Server works with the Express web application framework. It can be added t
|
||||
|
||||
# Getting Started
|
||||
|
||||
April 2016 - We created a series of video screencasts, please check them out here: [http://blog.parse.com/learn/parse-server-video-series-april-2016/](http://blog.parse.com/learn/parse-server-video-series-april-2016/)
|
||||
|
||||
The fastest and easiest way to get started is to run MongoDB and Parse Server locally.
|
||||
|
||||
## Running Parse Server locally
|
||||
@@ -104,7 +106,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)
|
||||
@@ -147,7 +149,7 @@ The full documentation for Parse Server is available in the [wiki](https://githu
|
||||
|
||||
## Migrating an Existing Parse App
|
||||
|
||||
The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. There are a few areas where Parse Server does not provide compatibility with the hosted version of Parse. Learn more in the [Migration guide](https://github.com/ParsePlatform/parse-server/wiki/Migrating-an-Existing-Parse-App).
|
||||
The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. There are a few areas where Parse Server does not provide compatibility with the hosted version of Parse. Learn more in the [Migration guide](https://parse.com/migration).
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -159,7 +161,7 @@ For the full list of available options, run `parse-server --help`.
|
||||
|
||||
* `appId` **(required)** - The application id to host with this server instance. You can use any arbitrary string. For migrated apps, this should match your hosted Parse app.
|
||||
* `masterKey` **(required)** - The master key to use for overriding ACL security. You can use any arbitrary string. Keep it secret! For migrated apps, this should match your hosted Parse app.
|
||||
* `databaseURI` **(required)** - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`.
|
||||
* `databaseURI` **(required)** - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`. Be sure to [URL encode your password](https://app.zencoder.com/docs/guides/getting-started/special-characters-in-usernames-and-passwords) if your password has special characters.
|
||||
* `port` - The default port is 1337, specify this parameter to use a different port.
|
||||
* `serverURL` - URL to your Parse Server (don't forget to specify http:// or https://). This URL will be used when making requests to Parse Server from Cloud Code.
|
||||
* `cloud` - The absolute path to your cloud code `main.js` file.
|
||||
@@ -179,13 +181,24 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
||||
* `fileKey` - For migrated apps, this is necessary to provide access to files already hosted on Parse.
|
||||
* `allowClientClassCreation` - Set to false to disable client class creation. Defaults to true.
|
||||
* `enableAnonymousUsers` - Set to false to disable anonymous users. Defaults to true.
|
||||
* `oauth` - Used to configure support for [3rd party authentication](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth).
|
||||
* `oauth` - Used to configure support for [3rd party authentication](https://github.com/ParsePlatform/parse-server/wiki/OAuth).
|
||||
* `facebookAppIds` - An array of valid Facebook application IDs that users may authenticate with.
|
||||
* `mountPath` - Mount path for the server. Defaults to `/parse`.
|
||||
* `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).
|
||||
* `revokeSessionOnPasswordReset` - When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
|
||||
|
||||
##### Logging
|
||||
|
||||
Use the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server` to save your server logfiles to the specified folder.
|
||||
|
||||
Usage:
|
||||
|
||||
```
|
||||
PARSE_SERVER_LOGS_FOLDER='<path-to-logs-folder>' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
|
||||
```
|
||||
|
||||
##### Email verification and password reset
|
||||
|
||||
@@ -198,7 +211,8 @@ var server = ParseServer({
|
||||
verifyUserEmails: true,
|
||||
// The public URL of your app.
|
||||
// This will appear in the link that is used to verify email addresses and reset passwords.
|
||||
publicServerURL: 'https://example.com',
|
||||
// Set the mount path as it is in serverURL
|
||||
publicServerURL: 'https://example.com/parse',
|
||||
// Your apps name. This will appear in the subject and body of the emails that are sent.
|
||||
appName: 'Parse App',
|
||||
// The email adapter
|
||||
@@ -216,7 +230,7 @@ var server = ParseServer({
|
||||
});
|
||||
```
|
||||
|
||||
You can also use other email adapters contributed by the community such as [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter).
|
||||
You can also use other email adapters contributed by the community such as [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter) or [parse-server-mandrill-adapter](https://github.com/back4app/parse-server-mandrill-adapter).
|
||||
|
||||
### Using environment variables to configure Parse Server
|
||||
|
||||
@@ -239,6 +253,9 @@ $ PORT=8080 parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
|
||||
|
||||
For the full list of configurable environment variables, run `parse-server --help`.
|
||||
|
||||
### Available Adapters
|
||||
[Parse Server Modules (Adapters)](https://github.com/parse-server-modules)
|
||||
|
||||
### Configuring File Adapters
|
||||
|
||||
Parse Server allows developers to choose from several options when hosting files:
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ cat > ./config.json << EOF
|
||||
"masterKey": "$MASTER_KEY",
|
||||
"appName": "$APP_NAME",
|
||||
"cloud": "./cloud/main",
|
||||
"mongodbURI": "$MONGODB_URI"
|
||||
"databaseURI": "$MONGODB_URI"
|
||||
}
|
||||
EOF
|
||||
echo "${CHECK} Created config.json"
|
||||
|
||||
+44
-42
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "2.2.3",
|
||||
"version": "2.2.14",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -18,54 +18,56 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"babel-polyfill": "^6.5.0",
|
||||
"babel-runtime": "^6.5.0",
|
||||
"babel-polyfill": "6.8.0",
|
||||
"babel-runtime": "6.6.1",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"body-parser": "^1.14.2",
|
||||
"colors": "^1.1.2",
|
||||
"commander": "^2.9.0",
|
||||
"deepcopy": "^0.6.1",
|
||||
"express": "^4.13.4",
|
||||
"lru-cache": "^4.0.0",
|
||||
"mailgun-js": "^0.7.7",
|
||||
"mime": "^1.3.4",
|
||||
"mongodb": "~2.1.0",
|
||||
"multer": "^1.1.0",
|
||||
"parse": "^1.8.0",
|
||||
"parse-server-fs-adapter": "^1.0.0",
|
||||
"parse-server-gcs-adapter": "^1.0.0",
|
||||
"parse-server-push-adapter": "^1.0.0",
|
||||
"parse-server-s3-adapter": "^1.0.0",
|
||||
"parse-server-simple-mailgun-adapter": "^1.0.0",
|
||||
"redis": "^2.5.0-1",
|
||||
"request": "^2.65.0",
|
||||
"tv4": "^1.2.7",
|
||||
"winston": "^2.1.1",
|
||||
"winston-daily-rotate-file": "^1.0.1",
|
||||
"ws": "^1.0.1"
|
||||
"body-parser": "1.15.2",
|
||||
"colors": "1.1.2",
|
||||
"commander": "2.9.0",
|
||||
"deepcopy": "0.6.3",
|
||||
"express": "4.14.0",
|
||||
"intersect": "1.0.1",
|
||||
"lodash": "4.12.0",
|
||||
"lru-cache": "4.0.1",
|
||||
"mailgun-js": "0.7.10",
|
||||
"mime": "1.3.4",
|
||||
"mongodb": "2.1.18",
|
||||
"multer": "1.1.0",
|
||||
"parse": "1.8.5",
|
||||
"parse-server-fs-adapter": "1.0.0",
|
||||
"parse-server-push-adapter": "1.0.4",
|
||||
"parse-server-s3-adapter": "1.0.3",
|
||||
"parse-server-simple-mailgun-adapter": "1.0.0",
|
||||
"pg-promise": "4.8.0",
|
||||
"redis": "2.6.2",
|
||||
"request": "2.72.0",
|
||||
"request-promise": "3.0.0",
|
||||
"tv4": "1.2.7",
|
||||
"winston": "2.2.0",
|
||||
"winston-daily-rotate-file": "1.0.1",
|
||||
"ws": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.5.1",
|
||||
"babel-core": "^6.5.1",
|
||||
"babel-istanbul": "^0.6.0",
|
||||
"babel-plugin-syntax-flow": "^6.5.0",
|
||||
"babel-plugin-transform-flow-strip-types": "^6.5.0",
|
||||
"babel-preset-es2015": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-register": "^6.5.1",
|
||||
"codecov": "^1.0.1",
|
||||
"cross-env": "^1.0.7",
|
||||
"deep-diff": "^0.3.3",
|
||||
"flow-bin": "^0.22.0",
|
||||
"gaze": "^0.5.2",
|
||||
"jasmine": "^2.3.2",
|
||||
"mongodb-runner": "3.1.15",
|
||||
"nodemon": "^1.8.1"
|
||||
"babel-cli": "6.8.0",
|
||||
"babel-core": "6.10.4",
|
||||
"babel-istanbul": "0.6.1",
|
||||
"babel-plugin-syntax-flow": "6.8.0",
|
||||
"babel-plugin-transform-flow-strip-types": "6.8.0",
|
||||
"babel-preset-es2015": "6.6.0",
|
||||
"babel-preset-stage-0": "6.5.0",
|
||||
"babel-register": "6.8.0",
|
||||
"codecov": "1.0.1",
|
||||
"cross-env": "1.0.7",
|
||||
"deep-diff": "0.3.4",
|
||||
"gaze": "1.0.0",
|
||||
"jasmine": "2.4.1",
|
||||
"mongodb-runner": "3.3.2",
|
||||
"nodemon": "1.9.2"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm run build && node bin/dev",
|
||||
"build": "./node_modules/.bin/babel src/ -d lib/",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./node_modules/.bin/mongodb-runner start",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 ./node_modules/.bin/mongodb-runner start",
|
||||
"test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js",
|
||||
"test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
|
||||
"posttest": "./node_modules/.bin/mongodb-runner stop",
|
||||
|
||||
@@ -10,7 +10,6 @@ MockController.prototype = Object.create(AdaptableController.prototype);
|
||||
MockController.prototype.constructor = AdaptableController;
|
||||
|
||||
describe("AdaptableController", ()=>{
|
||||
|
||||
it("should use the provided adapter", (done) => {
|
||||
var adapter = new FilesAdapter();
|
||||
var controller = new FilesController(adapter);
|
||||
@@ -22,7 +21,7 @@ describe("AdaptableController", ()=>{
|
||||
expect(controller.adapter).toBe(adapter);
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it("should throw when creating a new mock controller", (done) => {
|
||||
var adapter = new FilesAdapter();
|
||||
expect(() => {
|
||||
@@ -30,7 +29,7 @@ describe("AdaptableController", ()=>{
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it("should fail setting the wrong adapter to the controller", (done) => {
|
||||
function WrongAdapter() {};
|
||||
var adapter = new FilesAdapter();
|
||||
@@ -41,7 +40,7 @@ describe("AdaptableController", ()=>{
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it("should fail to instantiate a controller with wrong adapter", (done) => {
|
||||
function WrongAdapter() {};
|
||||
var adapter = new WrongAdapter();
|
||||
@@ -50,14 +49,14 @@ describe("AdaptableController", ()=>{
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it("should fail to instantiate a controller without an adapter", (done) => {
|
||||
expect(() => {
|
||||
new FilesController();
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it("should accept an object adapter", (done) => {
|
||||
var adapter = {
|
||||
createFile: function(config, filename, data) { },
|
||||
@@ -70,18 +69,18 @@ describe("AdaptableController", ()=>{
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it("should accept an object adapter", (done) => {
|
||||
function AGoodAdapter() {};
|
||||
AGoodAdapter.prototype.createFile = function(config, filename, data) { };
|
||||
AGoodAdapter.prototype.deleteFile = function(config, filename) { };
|
||||
AGoodAdapter.prototype.getFileData = function(config, filename) { };
|
||||
AGoodAdapter.prototype.getFileLocation = function(config, filename) { };
|
||||
|
||||
|
||||
var adapter = new AGoodAdapter();
|
||||
expect(() => {
|
||||
new FilesController(adapter);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
|
||||
var FilesAdapter = require("parse-server-fs-adapter").default;
|
||||
var S3Adapter = require("parse-server-s3-adapter").default;
|
||||
var GCSAdapter = require("parse-server-gcs-adapter").default;
|
||||
var ParsePushAdapter = require("parse-server-push-adapter").default;
|
||||
|
||||
describe("AdapterLoader", ()=>{
|
||||
@@ -114,13 +113,4 @@ describe("AdapterLoader", ()=>{
|
||||
}).not.toThrow();
|
||||
done();
|
||||
})
|
||||
|
||||
it("should load GCSAdapter from direct passing", (done) => {
|
||||
var gcsAdapter = new GCSAdapter("projectId", "path/to/keyfile", "bucket")
|
||||
expect(() => {
|
||||
var adapter = loadAdapter(gcsAdapter, FilesAdapter);
|
||||
expect(adapter).toBe(gcsAdapter);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
describe('Auth', () => {
|
||||
var Auth = require('../src/Auth.js').Auth;
|
||||
|
||||
describe('getUserRoles', () => {
|
||||
var auth;
|
||||
var config;
|
||||
var cacheController;
|
||||
var currentRoles = null;
|
||||
var currentUserId = 'userId';
|
||||
|
||||
beforeEach(() => {
|
||||
currentRoles = ['role:userId'];
|
||||
|
||||
config = {
|
||||
cacheController: {
|
||||
role: {
|
||||
get: () => Promise.resolve(currentRoles),
|
||||
set: jasmine.createSpy('set')
|
||||
}
|
||||
}
|
||||
}
|
||||
spyOn(config.cacheController.role, 'get').and.callThrough();
|
||||
|
||||
auth = new Auth({
|
||||
config: config,
|
||||
isMaster: false,
|
||||
user: {
|
||||
id: currentUserId
|
||||
},
|
||||
installationId: 'installationId'
|
||||
});
|
||||
});
|
||||
|
||||
it('should get user roles from the cache', (done) => {
|
||||
auth.getUserRoles()
|
||||
.then((roles) => {
|
||||
var firstSet = config.cacheController.role.set.calls.first();
|
||||
expect(firstSet).toEqual(undefined);
|
||||
|
||||
var firstGet = config.cacheController.role.get.calls.first();
|
||||
expect(firstGet.args[0]).toEqual(currentUserId);
|
||||
expect(roles).toEqual(currentRoles);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should only query the roles once', (done) => {
|
||||
var loadRolesSpy = spyOn(auth, '_loadRoles').and.callThrough();
|
||||
auth.getUserRoles()
|
||||
.then((roles) => {
|
||||
expect(roles).toEqual(currentRoles);
|
||||
return auth.getUserRoles()
|
||||
})
|
||||
.then((roles) => auth.getUserRoles())
|
||||
.then((roles) => auth.getUserRoles())
|
||||
.then((roles) => {
|
||||
// Should only call the cache adapter once.
|
||||
expect(config.cacheController.role.get.calls.count()).toEqual(1);
|
||||
expect(loadRolesSpy.calls.count()).toEqual(1);
|
||||
|
||||
var firstGet = config.cacheController.role.get.calls.first();
|
||||
expect(firstGet.args[0]).toEqual(currentUserId);
|
||||
expect(roles).toEqual(currentRoles);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not have any roles with no user', (done) => {
|
||||
auth.user = null
|
||||
auth.getUserRoles()
|
||||
.then((roles) => expect(roles).toEqual([]))
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
it('should not have any user roles with master', (done) => {
|
||||
auth.isMaster = true
|
||||
auth.getUserRoles()
|
||||
.then((roles) => expect(roles).toEqual([]))
|
||||
.then(() => done());
|
||||
})
|
||||
|
||||
});
|
||||
});
|
||||
+59
-8
@@ -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,6 @@ var definitions = {
|
||||
}
|
||||
|
||||
describe("commander additions", () => {
|
||||
|
||||
afterEach((done) => {
|
||||
commander.options = [];
|
||||
delete commander.arg0;
|
||||
@@ -33,7 +33,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 +44,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 +58,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 +72,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 +81,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
var CacheController = require('../src/Controllers/CacheController.js').default;
|
||||
|
||||
describe('CacheController', function() {
|
||||
var FakeCacheAdapter;
|
||||
var FakeAppID = 'foo';
|
||||
var KEY = 'hello';
|
||||
|
||||
beforeEach(() => {
|
||||
FakeCacheAdapter = {
|
||||
get: () => Promise.resolve(null),
|
||||
put: jasmine.createSpy('put'),
|
||||
del: jasmine.createSpy('del'),
|
||||
clear: jasmine.createSpy('clear')
|
||||
}
|
||||
|
||||
spyOn(FakeCacheAdapter, 'get').and.callThrough();
|
||||
});
|
||||
|
||||
|
||||
it('should expose role and user caches', (done) => {
|
||||
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
|
||||
|
||||
expect(cache.role).not.toEqual(null);
|
||||
expect(cache.role.get).not.toEqual(null);
|
||||
expect(cache.user).not.toEqual(null);
|
||||
expect(cache.user.get).not.toEqual(null);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
['role', 'user'].forEach((cacheName) => {
|
||||
it('should prefix ' + cacheName + ' cache', () => {
|
||||
var cache = new CacheController(FakeCacheAdapter, FakeAppID)[cacheName];
|
||||
|
||||
cache.put(KEY, 'world');
|
||||
var firstPut = FakeCacheAdapter.put.calls.first();
|
||||
expect(firstPut.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
|
||||
|
||||
cache.get(KEY);
|
||||
var firstGet = FakeCacheAdapter.get.calls.first();
|
||||
expect(firstGet.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
|
||||
|
||||
cache.del(KEY);
|
||||
var firstDel = FakeCacheAdapter.del.calls.first();
|
||||
expect(firstDel.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear the entire cache', () => {
|
||||
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
|
||||
|
||||
cache.clear();
|
||||
expect(FakeCacheAdapter.clear.calls.count()).toEqual(1);
|
||||
|
||||
cache.user.clear();
|
||||
expect(FakeCacheAdapter.clear.calls.count()).toEqual(2);
|
||||
|
||||
cache.role.clear();
|
||||
expect(FakeCacheAdapter.clear.calls.count()).toEqual(3);
|
||||
});
|
||||
|
||||
it('should handle cache rejections', (done) => {
|
||||
|
||||
FakeCacheAdapter.get = () => Promise.reject();
|
||||
|
||||
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
|
||||
|
||||
cache.get('foo').then(done, () => {
|
||||
fail('Promise should not be rejected.');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -2,7 +2,6 @@ var Client = require('../src/LiveQuery/Client').Client;
|
||||
var ParseWebSocket = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocket;
|
||||
|
||||
describe('Client', function() {
|
||||
|
||||
it('can be initialized', function() {
|
||||
var parseWebSocket = new ParseWebSocket({});
|
||||
var client = new Client(1, parseWebSocket);
|
||||
|
||||
@@ -0,0 +1,669 @@
|
||||
"use strict"
|
||||
const Parse = require("parse/node");
|
||||
const request = require('request');
|
||||
const rp = require('request-promise');
|
||||
const InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').InMemoryCacheAdapter;
|
||||
|
||||
describe('Cloud Code', () => {
|
||||
it('can load absolute cloud code file', done => {
|
||||
reconfigureServer({ cloud: __dirname + '/cloud/cloudCodeRelativeFile.js' })
|
||||
.then(() => {
|
||||
Parse.Cloud.run('cloudCodeInFile', {}, result => {
|
||||
expect(result).toEqual('It is possible to define cloud code in a file.');
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('can load relative cloud code file', done => {
|
||||
reconfigureServer({ cloud: './spec/cloud/cloudCodeAbsoluteFile.js' })
|
||||
.then(() => {
|
||||
Parse.Cloud.run('cloudCodeInFile', {}, result => {
|
||||
expect(result).toEqual('It is possible to define cloud code in a file.');
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('can create functions', done => {
|
||||
Parse.Cloud.define('hello', (req, res) => {
|
||||
res.success('Hello world!');
|
||||
});
|
||||
|
||||
Parse.Cloud.run('hello', {}, result => {
|
||||
expect(result).toEqual('Hello world!');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('is cleared cleared after the previous test', done => {
|
||||
Parse.Cloud.run('hello', {})
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(141);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('basic beforeSave rejection', function(done) {
|
||||
Parse.Cloud.beforeSave('BeforeSaveFail', function(req, res) {
|
||||
res.error('You shall not pass!');
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('BeforeSaveFail');
|
||||
obj.set('foo', 'bar');
|
||||
obj.save().then(() => {
|
||||
fail('Should not have been able to save BeforeSaveFailure class.');
|
||||
done();
|
||||
}, () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('beforeSave rejection with custom error code', function(done) {
|
||||
Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function (req, res) {
|
||||
res.error(999, 'Nope');
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('BeforeSaveFailWithErrorCode');
|
||||
obj.set('foo', 'bar');
|
||||
obj.save().then(function() {
|
||||
fail('Should not have been able to save BeforeSaveFailWithErrorCode class.');
|
||||
done();
|
||||
}, function(error) {
|
||||
expect(error.code).toEqual(999);
|
||||
expect(error.message).toEqual('Nope');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('basic beforeSave rejection via promise', function(done) {
|
||||
Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', function (req, res) {
|
||||
var query = new Parse.Query('Yolo');
|
||||
query.find().then(() => {
|
||||
res.error('Nope');
|
||||
}, () => {
|
||||
res.success();
|
||||
});
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('BeforeSaveFailWithPromise');
|
||||
obj.set('foo', 'bar');
|
||||
obj.save().then(function() {
|
||||
fail('Should not have been able to save BeforeSaveFailure class.');
|
||||
done();
|
||||
}, function(error) {
|
||||
expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED);
|
||||
expect(error.message).toEqual('Nope');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('test beforeSave changed object success', function(done) {
|
||||
Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) {
|
||||
req.object.set('foo', 'baz');
|
||||
res.success();
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('BeforeSaveChanged');
|
||||
obj.set('foo', 'bar');
|
||||
obj.save().then(function() {
|
||||
var query = new Parse.Query('BeforeSaveChanged');
|
||||
query.get(obj.id).then(function(objAgain) {
|
||||
expect(objAgain.get('foo')).toEqual('baz');
|
||||
done();
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test beforeSave returns value on create and update', (done) => {
|
||||
Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) {
|
||||
req.object.set('foo', 'baz');
|
||||
res.success();
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('BeforeSaveChanged');
|
||||
obj.set('foo', 'bing');
|
||||
obj.save().then(() => {
|
||||
expect(obj.get('foo')).toEqual('baz');
|
||||
obj.set('foo', 'bar');
|
||||
return obj.save().then(() => {
|
||||
expect(obj.get('foo')).toEqual('baz');
|
||||
done();
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
it('test afterSave ran and created an object', function(done) {
|
||||
Parse.Cloud.afterSave('AfterSaveTest', function(req) {
|
||||
var obj = new Parse.Object('AfterSaveProof');
|
||||
obj.set('proof', req.object.id);
|
||||
obj.save();
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('AfterSaveTest');
|
||||
obj.save();
|
||||
|
||||
setTimeout(function() {
|
||||
var query = new Parse.Query('AfterSaveProof');
|
||||
query.equalTo('proof', obj.id);
|
||||
query.find().then(function(results) {
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
it('test beforeSave happens on update', function(done) {
|
||||
Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) {
|
||||
req.object.set('foo', 'baz');
|
||||
res.success();
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('BeforeSaveChanged');
|
||||
obj.set('foo', 'bar');
|
||||
obj.save().then(function() {
|
||||
obj.set('foo', 'bar');
|
||||
return obj.save();
|
||||
}).then(function() {
|
||||
var query = new Parse.Query('BeforeSaveChanged');
|
||||
return query.get(obj.id).then(function(objAgain) {
|
||||
expect(objAgain.get('foo')).toEqual('baz');
|
||||
done();
|
||||
});
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test beforeDelete failure', function(done) {
|
||||
Parse.Cloud.beforeDelete('BeforeDeleteFail', function(req, res) {
|
||||
res.error('Nope');
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('BeforeDeleteFail');
|
||||
var id;
|
||||
obj.set('foo', 'bar');
|
||||
obj.save().then(() => {
|
||||
id = obj.id;
|
||||
return obj.destroy();
|
||||
}).then(() => {
|
||||
fail('obj.destroy() should have failed, but it succeeded');
|
||||
done();
|
||||
}, (error) => {
|
||||
expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED);
|
||||
expect(error.message).toEqual('Nope');
|
||||
|
||||
var objAgain = new Parse.Object('BeforeDeleteFail', {objectId: id});
|
||||
return objAgain.fetch();
|
||||
}).then((objAgain) => {
|
||||
if (objAgain) {
|
||||
expect(objAgain.get('foo')).toEqual('bar');
|
||||
} else {
|
||||
fail("unable to fetch the object ", id);
|
||||
}
|
||||
done();
|
||||
}, (error) => {
|
||||
// We should have been able to fetch the object again
|
||||
fail(error);
|
||||
});
|
||||
});
|
||||
|
||||
it('basic beforeDelete rejection via promise', function(done) {
|
||||
Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', function (req, res) {
|
||||
var query = new Parse.Query('Yolo');
|
||||
query.find().then(() => {
|
||||
res.error('Nope');
|
||||
}, () => {
|
||||
res.success();
|
||||
});
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('BeforeDeleteFailWithPromise');
|
||||
obj.set('foo', 'bar');
|
||||
obj.save().then(function() {
|
||||
fail('Should not have been able to save BeforeSaveFailure class.');
|
||||
done();
|
||||
}, function(error) {
|
||||
expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED);
|
||||
expect(error.message).toEqual('Nope');
|
||||
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('test afterDelete ran and created an object', function(done) {
|
||||
Parse.Cloud.afterDelete('AfterDeleteTest', function(req) {
|
||||
var obj = new Parse.Object('AfterDeleteProof');
|
||||
obj.set('proof', req.object.id);
|
||||
obj.save();
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('AfterDeleteTest');
|
||||
obj.save().then(function() {
|
||||
obj.destroy();
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
var query = new Parse.Query('AfterDeleteProof');
|
||||
query.equalTo('proof', obj.id);
|
||||
query.find().then(function(results) {
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
it('test cloud function return types', function(done) {
|
||||
Parse.Cloud.define('foo', function(req, res) {
|
||||
res.success({
|
||||
object: {
|
||||
__type: 'Object',
|
||||
className: 'Foo',
|
||||
objectId: '123',
|
||||
x: 2,
|
||||
relation: {
|
||||
__type: 'Object',
|
||||
className: 'Bar',
|
||||
objectId: '234',
|
||||
x: 3
|
||||
}
|
||||
},
|
||||
array: [{
|
||||
__type: 'Object',
|
||||
className: 'Bar',
|
||||
objectId: '345',
|
||||
x: 2
|
||||
}],
|
||||
a: 2
|
||||
});
|
||||
});
|
||||
|
||||
Parse.Cloud.run('foo').then((result) => {
|
||||
expect(result.object instanceof Parse.Object).toBeTruthy();
|
||||
if (!result.object) {
|
||||
fail("Unable to run foo");
|
||||
done();
|
||||
return;
|
||||
}
|
||||
expect(result.object.className).toEqual('Foo');
|
||||
expect(result.object.get('x')).toEqual(2);
|
||||
var bar = result.object.get('relation');
|
||||
expect(bar instanceof Parse.Object).toBeTruthy();
|
||||
expect(bar.className).toEqual('Bar');
|
||||
expect(bar.get('x')).toEqual(3);
|
||||
expect(Array.isArray(result.array)).toEqual(true);
|
||||
expect(result.array[0] instanceof Parse.Object).toBeTruthy();
|
||||
expect(result.array[0].get('x')).toEqual(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test cloud function request params types', function(done) {
|
||||
Parse.Cloud.define('params', function(req, res) {
|
||||
expect(req.params.date instanceof Date).toBe(true);
|
||||
expect(req.params.date.getTime()).toBe(1463907600000);
|
||||
expect(req.params.dateList[0] instanceof Date).toBe(true);
|
||||
expect(req.params.dateList[0].getTime()).toBe(1463907600000);
|
||||
expect(req.params.complexStructure.date[0] instanceof Date).toBe(true);
|
||||
expect(req.params.complexStructure.date[0].getTime()).toBe(1463907600000);
|
||||
expect(req.params.complexStructure.deepDate.date[0] instanceof Date).toBe(true);
|
||||
expect(req.params.complexStructure.deepDate.date[0].getTime()).toBe(1463907600000);
|
||||
expect(req.params.complexStructure.deepDate2[0].date instanceof Date).toBe(true);
|
||||
expect(req.params.complexStructure.deepDate2[0].date.getTime()).toBe(1463907600000);
|
||||
return res.success({});
|
||||
});
|
||||
|
||||
let params = {
|
||||
'date': {
|
||||
'__type': 'Date',
|
||||
'iso': '2016-05-22T09:00:00.000Z'
|
||||
},
|
||||
'dateList': [
|
||||
{
|
||||
'__type': 'Date',
|
||||
'iso': '2016-05-22T09:00:00.000Z'
|
||||
}
|
||||
],
|
||||
'lol': 'hello',
|
||||
'complexStructure': {
|
||||
'date': [
|
||||
{
|
||||
'__type': 'Date',
|
||||
'iso': '2016-05-22T09:00:00.000Z'
|
||||
}
|
||||
],
|
||||
'deepDate': {
|
||||
'date': [
|
||||
{
|
||||
'__type': 'Date',
|
||||
'iso': '2016-05-22T09:00:00.000Z'
|
||||
}
|
||||
]
|
||||
},
|
||||
'deepDate2': [
|
||||
{
|
||||
'date': {
|
||||
'__type': 'Date',
|
||||
'iso': '2016-05-22T09:00:00.000Z'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
Parse.Cloud.run('params', params).then((result) => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test cloud function should echo keys', function(done) {
|
||||
Parse.Cloud.define('echoKeys', function(req, res){
|
||||
return res.success({
|
||||
applicationId: Parse.applicationId,
|
||||
masterKey: Parse.masterKey,
|
||||
javascriptKey: Parse.javascriptKey
|
||||
})
|
||||
});
|
||||
|
||||
Parse.Cloud.run('echoKeys').then((result) => {
|
||||
expect(result.applicationId).toEqual(Parse.applicationId);
|
||||
expect(result.masterKey).toEqual(Parse.masterKey);
|
||||
expect(result.javascriptKey).toEqual(Parse.javascriptKey);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly create an object in before save', done => {
|
||||
Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) {
|
||||
req.object.set('foo', 'baz');
|
||||
res.success();
|
||||
});
|
||||
|
||||
Parse.Cloud.define('createBeforeSaveChangedObject', function(req, res){
|
||||
var obj = new Parse.Object('BeforeSaveChanged');
|
||||
obj.save().then(() => {
|
||||
res.success(obj);
|
||||
})
|
||||
})
|
||||
|
||||
Parse.Cloud.run('createBeforeSaveChangedObject').then((res) => {
|
||||
expect(res.get('foo')).toEqual('baz');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('dirtyKeys are set on update', done => {
|
||||
let triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
Parse.Cloud.beforeSave('GameScore', (req, res) => {
|
||||
var object = req.object;
|
||||
expect(object instanceof Parse.Object).toBeTruthy();
|
||||
expect(object.get('fooAgain')).toEqual('barAgain');
|
||||
if (triggerTime == 0) {
|
||||
// Create
|
||||
expect(object.get('foo')).toEqual('bar');
|
||||
} else if (triggerTime == 1) {
|
||||
// Update
|
||||
expect(object.dirtyKeys()).toEqual(['foo']);
|
||||
expect(object.dirty('foo')).toBeTruthy();
|
||||
expect(object.get('foo')).toEqual('baz');
|
||||
} else {
|
||||
res.error();
|
||||
}
|
||||
triggerTime++;
|
||||
res.success();
|
||||
});
|
||||
|
||||
let obj = new Parse.Object('GameScore');
|
||||
obj.set('foo', 'bar');
|
||||
obj.set('fooAgain', 'barAgain');
|
||||
obj.save().then(() => {
|
||||
// We only update foo
|
||||
obj.set('foo', 'baz');
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
// Make sure the checking has been triggered
|
||||
expect(triggerTime).toBe(2);
|
||||
done();
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test beforeSave unchanged success', function(done) {
|
||||
Parse.Cloud.beforeSave('BeforeSaveUnchanged', function(req, res) {
|
||||
res.success();
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('BeforeSaveUnchanged');
|
||||
obj.set('foo', 'bar');
|
||||
obj.save().then(function() {
|
||||
done();
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test beforeDelete success', function(done) {
|
||||
Parse.Cloud.beforeDelete('BeforeDeleteTest', function(req, res) {
|
||||
res.success();
|
||||
});
|
||||
|
||||
var obj = new Parse.Object('BeforeDeleteTest');
|
||||
obj.set('foo', 'bar');
|
||||
obj.save().then(function() {
|
||||
return obj.destroy();
|
||||
}).then(function() {
|
||||
var objAgain = new Parse.Object('BeforeDeleteTest', obj.id);
|
||||
return objAgain.fetch().then(fail, done);
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test save triggers get user', function(done) {
|
||||
Parse.Cloud.beforeSave('SaveTriggerUser', function(req, res) {
|
||||
if (req.user && req.user.id) {
|
||||
res.success();
|
||||
} else {
|
||||
res.error('No user present on request object for beforeSave.');
|
||||
}
|
||||
});
|
||||
|
||||
Parse.Cloud.afterSave('SaveTriggerUser', function(req) {
|
||||
if (!req.user || !req.user.id) {
|
||||
console.log('No user present on request object for afterSave.');
|
||||
}
|
||||
});
|
||||
|
||||
var user = new Parse.User();
|
||||
user.set("password", "asdf");
|
||||
user.set("email", "asdf@example.com");
|
||||
user.set("username", "zxcv");
|
||||
user.signUp(null, {
|
||||
success: function() {
|
||||
var obj = new Parse.Object('SaveTriggerUser');
|
||||
obj.save().then(function() {
|
||||
done();
|
||||
}, function(error) {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('beforeSave change propagates through the save response', (done) => {
|
||||
Parse.Cloud.beforeSave('ChangingObject', function(request, response) {
|
||||
request.object.set('foo', 'baz');
|
||||
response.success();
|
||||
});
|
||||
let obj = new Parse.Object('ChangingObject');
|
||||
obj.save({ foo: 'bar' }).then((objAgain) => {
|
||||
expect(objAgain.get('foo')).toEqual('baz');
|
||||
done();
|
||||
}, (e) => {
|
||||
fail('Should not have failed to save.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test cloud function parameter validation success', (done) => {
|
||||
// Register a function with validation
|
||||
Parse.Cloud.define('functionWithParameterValidation', (req, res) => {
|
||||
res.success('works');
|
||||
}, (request) => {
|
||||
return request.params.success === 100;
|
||||
});
|
||||
|
||||
Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => {
|
||||
done();
|
||||
}, (e) => {
|
||||
fail('Validation should not have failed.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('doesnt receive stale user in cloud code functions after user has been updated with master key (regression test for #1836)', done => {
|
||||
Parse.Cloud.define('testQuery', function(request, response) {
|
||||
response.success(request.user.get('data'));
|
||||
});
|
||||
|
||||
Parse.User.signUp('user', 'pass')
|
||||
.then(user => {
|
||||
user.set('data', 'AAA');
|
||||
return user.save();
|
||||
})
|
||||
.then(() => Parse.Cloud.run('testQuery'))
|
||||
.then(result => {
|
||||
expect(result).toEqual('AAA');
|
||||
Parse.User.current().set('data', 'BBB');
|
||||
return Parse.User.current().save(null, {useMasterKey: true});
|
||||
})
|
||||
.then(() => Parse.Cloud.run('testQuery'))
|
||||
.then(result => {
|
||||
expect(result).toEqual('BBB');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('clears out the user cache for all sessions when the user is changed', done => {
|
||||
let session1;
|
||||
let session2;
|
||||
let user;
|
||||
const cacheAdapter = new InMemoryCacheAdapter({ ttl: 100000000 });
|
||||
reconfigureServer({ cacheAdapter })
|
||||
.then(() => {
|
||||
Parse.Cloud.define('checkStaleUser', (request, response) => {
|
||||
response.success(request.user.get('data'));
|
||||
});
|
||||
|
||||
user = new Parse.User();
|
||||
user.set('username', 'test');
|
||||
user.set('password', 'moon-y');
|
||||
user.set('data', 'first data');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(user => {
|
||||
session1 = user.getSessionToken();
|
||||
return rp({
|
||||
uri: 'http://localhost:8378/1/login?username=test&password=moon-y',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
})
|
||||
})
|
||||
.then(body => {
|
||||
session2 = body.sessionToken;
|
||||
|
||||
//Ensure both session tokens are in the cache
|
||||
return Parse.Cloud.run('checkStaleUser')
|
||||
})
|
||||
.then(() => rp({
|
||||
method: 'POST',
|
||||
uri: 'http://localhost:8378/1/functions/checkStaleUser',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': session2,
|
||||
}
|
||||
}))
|
||||
.then(() => Parse.Promise.all([cacheAdapter.get('test:user:' + session1), cacheAdapter.get('test:user:' + session2)]))
|
||||
.then(cachedVals => {
|
||||
expect(cachedVals[0].objectId).toEqual(user.id);
|
||||
expect(cachedVals[1].objectId).toEqual(user.id);
|
||||
|
||||
//Change with session 1 and then read with session 2.
|
||||
user.set('data', 'second data');
|
||||
return user.save()
|
||||
})
|
||||
.then(() => rp({
|
||||
method: 'POST',
|
||||
uri: 'http://localhost:8378/1/functions/checkStaleUser',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': session2,
|
||||
}
|
||||
}))
|
||||
.then(body => {
|
||||
expect(body.result).toEqual('second data');
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
fail(JSON.stringify(error));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('trivial beforeSave should not affect fetched pointers (regression test for #1238)', done => {
|
||||
Parse.Cloud.beforeSave('BeforeSaveUnchanged', (req, res) => {
|
||||
res.success();
|
||||
});
|
||||
|
||||
var TestObject = Parse.Object.extend("TestObject");
|
||||
var NoBeforeSaveObject = Parse.Object.extend("NoBeforeSave");
|
||||
var BeforeSaveObject = Parse.Object.extend("BeforeSaveUnchanged");
|
||||
|
||||
var aTestObject = new TestObject();
|
||||
aTestObject.set("foo", "bar");
|
||||
aTestObject.save()
|
||||
.then(aTestObject => {
|
||||
var aNoBeforeSaveObj = new NoBeforeSaveObject();
|
||||
aNoBeforeSaveObj.set("aTestObject", aTestObject);
|
||||
expect(aNoBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar");
|
||||
return aNoBeforeSaveObj.save();
|
||||
})
|
||||
.then(aNoBeforeSaveObj => {
|
||||
expect(aNoBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar");
|
||||
|
||||
var aBeforeSaveObj = new BeforeSaveObject();
|
||||
aBeforeSaveObj.set("aTestObject", aTestObject);
|
||||
expect(aBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar");
|
||||
return aBeforeSaveObj.save();
|
||||
})
|
||||
.then(aBeforeSaveObj => {
|
||||
expect(aBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
|
||||
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
|
||||
|
||||
describe("Cloud Code Logger", () => {
|
||||
it("should expose log to functions", (done) => {
|
||||
var logController = new LoggerController(new FileLoggerAdapter());
|
||||
|
||||
Parse.Cloud.define("loggerTest", (req, res) => {
|
||||
req.log.info('logTest', 'info log', {info: 'some log' });
|
||||
req.log.error('logTest','error log', {error: 'there was an error'});
|
||||
res.success({});
|
||||
});
|
||||
|
||||
Parse.Cloud.run('loggerTest').then(() => {
|
||||
return logController.getLogs({from: Date.now() - 500, size: 1000});
|
||||
}).then((res) => {
|
||||
expect(res.length).not.toBe(0);
|
||||
let lastLogs = res.slice(0, 2);
|
||||
let errorMessage = lastLogs[0];
|
||||
let infoMessage = lastLogs[1];
|
||||
expect(errorMessage.level).toBe('error');
|
||||
expect(errorMessage.error).toBe('there was an error');
|
||||
expect(errorMessage.message).toBe('logTest error log');
|
||||
expect(infoMessage.level).toBe('info');
|
||||
expect(infoMessage.info).toBe('some log');
|
||||
expect(infoMessage.message).toBe('logTest info log');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose log to trigger", (done) => {
|
||||
var logController = new LoggerController(new FileLoggerAdapter());
|
||||
|
||||
Parse.Cloud.beforeSave("MyObject", (req, res) => {
|
||||
req.log.info('beforeSave MyObject', 'info log', {info: 'some log' });
|
||||
req.log.error('beforeSave MyObject','error log', {error: 'there was an error'});
|
||||
res.success({});
|
||||
});
|
||||
|
||||
let obj = new Parse.Object('MyObject');
|
||||
obj.save().then(() => {
|
||||
return logController.getLogs({from: Date.now() - 500, size: 1000})
|
||||
}).then((res) => {
|
||||
expect(res.length).not.toBe(0);
|
||||
let lastLogs = res.slice(0, 2);
|
||||
let errorMessage = lastLogs[0];
|
||||
let infoMessage = lastLogs[1];
|
||||
expect(errorMessage.level).toBe('error');
|
||||
expect(errorMessage.error).toBe('there was an error');
|
||||
expect(errorMessage.message).toBe('beforeSave MyObject error log');
|
||||
expect(infoMessage.level).toBe('info');
|
||||
expect(infoMessage.info).toBe('some log');
|
||||
expect(infoMessage.message).toBe('beforeSave MyObject info log');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
let DatabaseAdapter = require('../src/DatabaseAdapter');
|
||||
|
||||
describe('DatabaseAdapter', () => {
|
||||
it('options and URI are available to adapter', done => {
|
||||
DatabaseAdapter.setAppDatabaseURI('optionsTest', 'mongodb://localhost:27017/optionsTest');
|
||||
DatabaseAdapter.setAppDatabaseOptions('optionsTest', {foo: "bar"});
|
||||
let optionsTestDatabaseConnection = DatabaseAdapter.getDatabaseConnection('optionsTest');
|
||||
|
||||
expect(optionsTestDatabaseConnection instanceof Object).toBe(true);
|
||||
expect(optionsTestDatabaseConnection.adapter._options instanceof Object).toBe(true);
|
||||
expect(optionsTestDatabaseConnection.adapter._options.foo).toBe("bar");
|
||||
|
||||
DatabaseAdapter.setAppDatabaseURI('noOptionsTest', 'mongodb://localhost:27017/noOptionsTest');
|
||||
let noOptionsTestDatabaseConnection = DatabaseAdapter.getDatabaseConnection('noOptionsTest');
|
||||
|
||||
expect(noOptionsTestDatabaseConnection instanceof Object).toBe(true);
|
||||
expect(noOptionsTestDatabaseConnection.adapter._options instanceof Object).toBe(false);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
let DatabaseController = require('../src/Controllers/DatabaseController');
|
||||
let MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
|
||||
|
||||
describe('DatabaseController', () => {
|
||||
it('can be constructed', done => {
|
||||
let adapter = new MongoStorageAdapter('mongodb://localhost:27017/test');
|
||||
let databaseController = new DatabaseController(adapter, {
|
||||
collectionPrefix: 'test_'
|
||||
});
|
||||
databaseController.connect().then(done, error => {
|
||||
console.log('error', error.stack);
|
||||
fail();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
|
||||
|
||||
describe('EventEmitterPubSub', function() {
|
||||
|
||||
it('can publish and subscribe', function() {
|
||||
var publisher = EventEmitterPubSub.createPublisher();
|
||||
var subscriber = EventEmitterPubSub.createSubscriber();
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
|
||||
var Parse = require('parse/node').Parse;
|
||||
var request = require('request');
|
||||
|
||||
describe('info logs', () => {
|
||||
|
||||
it("Verify INFO logs", (done) => {
|
||||
var fileLoggerAdapter = new FileLoggerAdapter();
|
||||
fileLoggerAdapter.info('testing info logs', () => {
|
||||
fileLoggerAdapter.query({
|
||||
size: 1,
|
||||
from: new Date(Date.now() - 500),
|
||||
size: 100,
|
||||
level: 'info'
|
||||
}, (results) => {
|
||||
if(results.length == 0) {
|
||||
@@ -23,12 +26,12 @@ describe('info logs', () => {
|
||||
});
|
||||
|
||||
describe('error logs', () => {
|
||||
|
||||
it("Verify ERROR logs", (done) => {
|
||||
var fileLoggerAdapter = new FileLoggerAdapter();
|
||||
fileLoggerAdapter.error('testing error logs', () => {
|
||||
fileLoggerAdapter.query({
|
||||
size: 1,
|
||||
from: new Date(Date.now() - 500),
|
||||
size: 100,
|
||||
level: 'error'
|
||||
}, (results) => {
|
||||
if(results.length == 0) {
|
||||
@@ -43,3 +46,54 @@ describe('error logs', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('verbose logs', () => {
|
||||
it("mask sensitive information in _User class", (done) => {
|
||||
reconfigureServer({ verbose: true })
|
||||
.then(() => createTestUser())
|
||||
.then(() => {
|
||||
let fileLoggerAdapter = new FileLoggerAdapter();
|
||||
return fileLoggerAdapter.query({
|
||||
from: new Date(Date.now() - 500),
|
||||
size: 100,
|
||||
level: 'verbose'
|
||||
});
|
||||
}).then((results) => {
|
||||
expect(results[1].message.includes('"password": "********"')).toEqual(true);
|
||||
var headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
};
|
||||
request.get({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/login?username=test&password=moon-y'
|
||||
}, (error, response, body) => {
|
||||
let fileLoggerAdapter = new FileLoggerAdapter();
|
||||
return fileLoggerAdapter.query({
|
||||
from: new Date(Date.now() - 500),
|
||||
size: 100,
|
||||
level: 'verbose'
|
||||
}).then((results) => {
|
||||
expect(results[1].message.includes('password=********')).toEqual(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should not mask information in non _User class", (done) => {
|
||||
let obj = new Parse.Object('users');
|
||||
obj.set('password', 'pw');
|
||||
obj.save().then(() => {
|
||||
let fileLoggerAdapter = new FileLoggerAdapter();
|
||||
return fileLoggerAdapter.query({
|
||||
from: new Date(Date.now() - 500),
|
||||
size: 100,
|
||||
level: 'verbose'
|
||||
});
|
||||
}).then((results) => {
|
||||
expect(results[1].message.includes('"password": "pw"')).toEqual(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+95
-29
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var httpRequest = require("../src/cloud-code/httpRequest"),
|
||||
HTTPResponse = require('../src/cloud-code/HTTPResponse').default,
|
||||
bodyParser = require('body-parser'),
|
||||
express = require("express");
|
||||
|
||||
@@ -36,7 +37,6 @@ app.listen(13371);
|
||||
|
||||
|
||||
describe("httpRequest", () => {
|
||||
|
||||
it("should do /hello", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/hello"
|
||||
@@ -51,7 +51,7 @@ describe("httpRequest", () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it("should do /hello with callback and promises", (done) => {
|
||||
var calls = 0;
|
||||
httpRequest({
|
||||
@@ -70,7 +70,7 @@ describe("httpRequest", () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it("should do not follow redirects by default", (done) => {
|
||||
|
||||
httpRequest({
|
||||
@@ -83,7 +83,7 @@ describe("httpRequest", () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it("should follow redirects when set", (done) => {
|
||||
|
||||
httpRequest({
|
||||
@@ -100,17 +100,17 @@ describe("httpRequest", () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it("should fail on 404", (done) => {
|
||||
var calls = 0;
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/404",
|
||||
success: function() {
|
||||
success: function() {
|
||||
calls++;
|
||||
fail("should not succeed");
|
||||
done();
|
||||
},
|
||||
error: function(httpResponse) {
|
||||
error: function(httpResponse) {
|
||||
calls++;
|
||||
expect(calls).toBe(1);
|
||||
expect(httpResponse.status).toBe(404);
|
||||
@@ -121,7 +121,7 @@ describe("httpRequest", () => {
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
it("should fail on 404", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/404",
|
||||
@@ -136,7 +136,7 @@ describe("httpRequest", () => {
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
it("should post on echo", (done) => {
|
||||
var calls = 0;
|
||||
httpRequest({
|
||||
@@ -160,27 +160,27 @@ describe("httpRequest", () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it("should encode a query string body by default", (done) => {
|
||||
let options = {
|
||||
body: {"foo": "bar"},
|
||||
body: {"foo": "bar"},
|
||||
}
|
||||
let result = httpRequest.encodeBody(options);
|
||||
expect(result.body).toEqual('foo=bar');
|
||||
expect(result.headers['Content-Type']).toEqual('application/x-www-form-urlencoded');
|
||||
done();
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
it("should encode a JSON body", (done) => {
|
||||
let options = {
|
||||
body: {"foo": "bar"},
|
||||
body: {"foo": "bar"},
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
}
|
||||
let result = httpRequest.encodeBody(options);
|
||||
expect(result.body).toEqual('{"foo":"bar"}');
|
||||
done();
|
||||
|
||||
|
||||
})
|
||||
it("should encode a www-form body", (done) => {
|
||||
let options = {
|
||||
@@ -193,7 +193,7 @@ describe("httpRequest", () => {
|
||||
});
|
||||
it("should not encode a wrong content type", (done) => {
|
||||
let options = {
|
||||
body:{"foo": "bar", "bar": "baz"},
|
||||
body:{"foo": "bar", "bar": "baz"},
|
||||
headers: {'cOntent-tYpe': 'mime/jpeg'}
|
||||
}
|
||||
let result = httpRequest.encodeBody(options);
|
||||
@@ -204,28 +204,17 @@ describe("httpRequest", () => {
|
||||
it("should fail gracefully", (done) => {
|
||||
httpRequest({
|
||||
url: "http://not a good url",
|
||||
success: function() {
|
||||
success: function() {
|
||||
fail("should not succeed");
|
||||
done();
|
||||
},
|
||||
error: function(error) {
|
||||
error: function(error) {
|
||||
expect(error).not.toBeUndefined();
|
||||
expect(error).not.toBeNull();
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a cat image', (done) => {
|
||||
httpRequest({
|
||||
url: 'http://thecatapi.com/api/images/get?format=src&type=jpg',
|
||||
followRedirects: true
|
||||
}).then((res) => {
|
||||
expect(res.buffer).not.toBe(null);
|
||||
expect(res.text).not.toBe(null);
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
it("should params object to query string", (done) => {
|
||||
httpRequest({
|
||||
@@ -257,4 +246,81 @@ describe("httpRequest", () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('should not crash with undefined body', () => {
|
||||
let httpResponse = new HTTPResponse({});
|
||||
expect(httpResponse.body).toBeUndefined();
|
||||
expect(httpResponse.data).toBeUndefined();
|
||||
expect(httpResponse.text).toBeUndefined();
|
||||
expect(httpResponse.buffer).toBeUndefined();
|
||||
});
|
||||
|
||||
it('serialized httpResponse correctly with body string', () => {
|
||||
let httpResponse = new HTTPResponse({}, 'hello');
|
||||
expect(httpResponse.text).toBe('hello');
|
||||
expect(httpResponse.data).toBe(undefined);
|
||||
expect(httpResponse.body).toBe('hello');
|
||||
|
||||
let serialized = JSON.stringify(httpResponse);
|
||||
let result = JSON.parse(serialized);
|
||||
expect(result.text).toBe('hello');
|
||||
expect(result.data).toBe(undefined);
|
||||
expect(result.body).toBe(undefined);
|
||||
});
|
||||
|
||||
it('serialized httpResponse correctly with body object', () => {
|
||||
let httpResponse = new HTTPResponse({}, {foo: "bar"});
|
||||
let encodedResponse = Parse._encode(httpResponse);
|
||||
let serialized = JSON.stringify(httpResponse);
|
||||
let result = JSON.parse(serialized);
|
||||
|
||||
expect(httpResponse.text).toEqual('{"foo":"bar"}');
|
||||
expect(httpResponse.data).toEqual({foo: 'bar'});
|
||||
expect(httpResponse.body).toEqual({foo: 'bar'});
|
||||
|
||||
expect(result.text).toEqual('{"foo":"bar"}');
|
||||
expect(result.data).toEqual({foo: 'bar'});
|
||||
expect(result.body).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('serialized httpResponse correctly with body buffer string', () => {
|
||||
let httpResponse = new HTTPResponse({}, new Buffer('hello'));
|
||||
expect(httpResponse.text).toBe('hello');
|
||||
expect(httpResponse.data).toBe(undefined);
|
||||
|
||||
let serialized = JSON.stringify(httpResponse);
|
||||
let result = JSON.parse(serialized);
|
||||
expect(result.text).toBe('hello');
|
||||
expect(result.data).toBe(undefined);
|
||||
});
|
||||
|
||||
it('serialized httpResponse correctly with body buffer JSON Object', () => {
|
||||
let json = '{"foo":"bar"}';
|
||||
let httpResponse = new HTTPResponse({}, new Buffer(json));
|
||||
let serialized = JSON.stringify(httpResponse);
|
||||
let result = JSON.parse(serialized);
|
||||
expect(result.text).toEqual('{"foo":"bar"}');
|
||||
expect(result.data).toEqual({foo: 'bar'});
|
||||
});
|
||||
|
||||
it('serialized httpResponse with Parse._encode should be allright', () => {
|
||||
let json = '{"foo":"bar"}';
|
||||
let httpResponse = new HTTPResponse({}, new Buffer(json));
|
||||
let encoded = Parse._encode(httpResponse);
|
||||
let foundData, foundText, foundBody = false;
|
||||
for(var key in encoded) {
|
||||
if (key == 'data') {
|
||||
foundData = true;
|
||||
}
|
||||
if (key == 'text') {
|
||||
foundText = true;
|
||||
}
|
||||
if (key == 'body') {
|
||||
foundBody = true;
|
||||
}
|
||||
}
|
||||
expect(foundData).toBe(true);
|
||||
expect(foundText).toBe(true);
|
||||
expect(foundBody).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
const InMemoryCache = require('../src/Adapters/Cache/InMemoryCache').default;
|
||||
|
||||
|
||||
describe('InMemoryCache', function() {
|
||||
var BASE_TTL = {
|
||||
ttl: 10
|
||||
};
|
||||
var NO_EXPIRE_TTL = {
|
||||
ttl: NaN
|
||||
};
|
||||
var KEY = 'hello';
|
||||
var KEY_2 = KEY + '_2';
|
||||
|
||||
var VALUE = 'world';
|
||||
|
||||
|
||||
function wait(sleep) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
setTimeout(resolve, sleep);
|
||||
})
|
||||
}
|
||||
|
||||
it('should destroy a expire items in the cache', (done) => {
|
||||
var cache = new InMemoryCache(BASE_TTL);
|
||||
|
||||
cache.put(KEY, VALUE);
|
||||
|
||||
var value = cache.get(KEY);
|
||||
expect(value).toEqual(VALUE);
|
||||
|
||||
wait(BASE_TTL.ttl * 5).then(() => {
|
||||
value = cache.get(KEY)
|
||||
expect(value).toEqual(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete items', (done) => {
|
||||
var cache = new InMemoryCache(NO_EXPIRE_TTL);
|
||||
cache.put(KEY, VALUE);
|
||||
cache.put(KEY_2, VALUE);
|
||||
expect(cache.get(KEY)).toEqual(VALUE);
|
||||
expect(cache.get(KEY_2)).toEqual(VALUE);
|
||||
|
||||
cache.del(KEY);
|
||||
expect(cache.get(KEY)).toEqual(null);
|
||||
expect(cache.get(KEY_2)).toEqual(VALUE);
|
||||
|
||||
cache.del(KEY_2);
|
||||
expect(cache.get(KEY)).toEqual(null);
|
||||
expect(cache.get(KEY_2)).toEqual(null);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should clear all items', (done) => {
|
||||
var cache = new InMemoryCache(NO_EXPIRE_TTL);
|
||||
cache.put(KEY, VALUE);
|
||||
cache.put(KEY_2, VALUE);
|
||||
|
||||
expect(cache.get(KEY)).toEqual(VALUE);
|
||||
expect(cache.get(KEY_2)).toEqual(VALUE);
|
||||
cache.clear();
|
||||
|
||||
expect(cache.get(KEY)).toEqual(null);
|
||||
expect(cache.get(KEY_2)).toEqual(null);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should deafult TTL to 5 seconds', () => {
|
||||
var cache = new InMemoryCache({});
|
||||
expect(cache.ttl).toEqual(5 * 1000);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
var InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').default;
|
||||
|
||||
describe('InMemoryCacheAdapter', function() {
|
||||
var KEY = 'hello';
|
||||
var VALUE = 'world';
|
||||
|
||||
function wait(sleep) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
setTimeout(resolve, sleep);
|
||||
})
|
||||
}
|
||||
|
||||
it('should expose promisifyed methods', (done) => {
|
||||
var cache = new InMemoryCacheAdapter({
|
||||
ttl: NaN
|
||||
});
|
||||
|
||||
var noop = () => {};
|
||||
|
||||
// Verify all methods return promises.
|
||||
Promise.all([
|
||||
cache.put(KEY, VALUE),
|
||||
cache.del(KEY),
|
||||
cache.get(KEY),
|
||||
cache.clear()
|
||||
]).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get/set/clear', (done) => {
|
||||
var cache = new InMemoryCacheAdapter({
|
||||
ttl: NaN
|
||||
});
|
||||
|
||||
cache.put(KEY, VALUE)
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(VALUE))
|
||||
.then(() => cache.clear())
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(null))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('should expire after ttl', (done) => {
|
||||
var cache = new InMemoryCacheAdapter({
|
||||
ttl: 10
|
||||
});
|
||||
|
||||
cache.put(KEY, VALUE)
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(VALUE))
|
||||
.then(wait.bind(null, 50))
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(null))
|
||||
.then(done);
|
||||
})
|
||||
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
var auth = require('../src/Auth');
|
||||
var Config = require('../src/Config');
|
||||
var rest = require('../src/rest');
|
||||
var InstallationsRouter = require('../src/Routers/InstallationsRouter').InstallationsRouter;
|
||||
|
||||
var config = new Config('test');
|
||||
|
||||
describe('InstallationsRouter', () => {
|
||||
it('uses find condition from request.body', (done) => {
|
||||
var androidDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
|
||||
'deviceType': 'android'
|
||||
};
|
||||
var iosDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
var request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {
|
||||
where: {
|
||||
deviceType: 'android'
|
||||
}
|
||||
},
|
||||
query: {}
|
||||
};
|
||||
|
||||
var router = new InstallationsRouter();
|
||||
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
}).then(() => {
|
||||
return router.handleFind(request);
|
||||
}).then((res) => {
|
||||
var results = res.response.results;
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('uses find condition from request.query', (done) => {
|
||||
var androidDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
|
||||
'deviceType': 'android'
|
||||
};
|
||||
var iosDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
var request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
where: {
|
||||
deviceType: 'android'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var router = new InstallationsRouter();
|
||||
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
}).then(() => {
|
||||
return router.handleFind(request);
|
||||
}).then((res) => {
|
||||
var results = res.response.results;
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('query installations with limit = 0', (done) => {
|
||||
var androidDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
|
||||
'deviceType': 'android'
|
||||
};
|
||||
var iosDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
var request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
limit: 0
|
||||
}
|
||||
};
|
||||
|
||||
var router = new InstallationsRouter();
|
||||
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
}).then(() => {
|
||||
return router.handleFind(request);
|
||||
}).then((res) => {
|
||||
var response = res.response;
|
||||
expect(response.results.length).toEqual(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('query installations with count = 1', done => {
|
||||
var androidDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
|
||||
'deviceType': 'android'
|
||||
};
|
||||
var iosDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
var request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
count: 1
|
||||
}
|
||||
};
|
||||
|
||||
var router = new InstallationsRouter();
|
||||
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest))
|
||||
.then(() => router.handleFind(request))
|
||||
.then((res) => {
|
||||
var response = res.response;
|
||||
expect(response.results.length).toEqual(2);
|
||||
expect(response.count).toEqual(2);
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
fail(JSON.stringify(error));
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('query installations with limit = 0 and count = 1', (done) => {
|
||||
var androidDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
|
||||
'deviceType': 'android'
|
||||
};
|
||||
var iosDeviceRequest = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
var request = {
|
||||
config: config,
|
||||
auth: auth.master(config),
|
||||
body: {},
|
||||
query: {
|
||||
limit: 0,
|
||||
count: 1
|
||||
}
|
||||
};
|
||||
|
||||
var router = new InstallationsRouter();
|
||||
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
|
||||
}).then(() => {
|
||||
return router.handleFind(request);
|
||||
}).then((res) => {
|
||||
var response = res.response;
|
||||
expect(response.results.length).toEqual(0);
|
||||
expect(response.count).toEqual(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
var middlewares = require('../src/middlewares');
|
||||
var AppCache = require('../src/cache').AppCache;
|
||||
|
||||
describe('middlewares', () => {
|
||||
|
||||
var fakeReq, fakeRes;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeReq = {
|
||||
originalUrl: 'http://example.com/parse/',
|
||||
url: 'http://example.com/',
|
||||
body: {
|
||||
_ApplicationId: 'FakeAppId'
|
||||
},
|
||||
headers: {},
|
||||
get: (key) => {
|
||||
return fakeReq.headers[key.toLowerCase()]
|
||||
}
|
||||
};
|
||||
AppCache.put(fakeReq.body._ApplicationId, {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
AppCache.del(fakeReq.body._ApplicationId);
|
||||
});
|
||||
|
||||
it('should use _ContentType if provided', (done) => {
|
||||
expect(fakeReq.headers['content-type']).toEqual(undefined);
|
||||
var contentType = 'image/jpeg';
|
||||
fakeReq.body._ContentType = contentType;
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeReq.headers['content-type']).toEqual(contentType);
|
||||
expect(fakeReq.body._ContentType).toEqual(undefined);
|
||||
done()
|
||||
});
|
||||
});
|
||||
|
||||
const BodyParams = {
|
||||
clientVersion: '_ClientVersion',
|
||||
installationId: '_InstallationId',
|
||||
sessionToken: '_SessionToken',
|
||||
masterKey: '_MasterKey',
|
||||
javascriptKey: '_JavaScriptKey'
|
||||
};
|
||||
|
||||
const BodyKeys = Object.keys(BodyParams);
|
||||
|
||||
BodyKeys.forEach((infoKey) => {
|
||||
const bodyKey = BodyParams[infoKey];
|
||||
const keyValue = 'Fake' + bodyKey;
|
||||
// javascriptKey is the only one that gets defaulted,
|
||||
const otherKeys = BodyKeys.filter((otherKey) => otherKey !== infoKey && otherKey !== 'javascriptKey');
|
||||
|
||||
it(`it should pull ${bodyKey} into req.info`, (done) => {
|
||||
fakeReq.body[bodyKey] = keyValue;
|
||||
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeReq.body[bodyKey]).toEqual(undefined);
|
||||
expect(fakeReq.info[infoKey]).toEqual(keyValue);
|
||||
|
||||
otherKeys.forEach((otherKey) => {
|
||||
expect(fakeReq.info[otherKey]).toEqual(undefined);
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
'use strict';
|
||||
|
||||
const MongoSchemaCollection = require('../src/Adapters/Storage/Mongo/MongoSchemaCollection').default;
|
||||
|
||||
describe('MongoSchemaCollection', () => {
|
||||
it('can transform legacy _client_permissions keys to parse format', done => {
|
||||
expect(MongoSchemaCollection._TESTmongoSchemaToParseSchema({
|
||||
"_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",
|
||||
"pushType":"string",
|
||||
"GCMSenderId":"string",
|
||||
"timeZone":"string",
|
||||
"localeIdentifier":"string",
|
||||
"badge":"number",
|
||||
"appVersion":"string",
|
||||
"appName":"string",
|
||||
"appIdentifier":"string",
|
||||
"parseVersion":"string",
|
||||
})).toEqual({
|
||||
className: '_Installation',
|
||||
fields: {
|
||||
installationId: { type: 'String' },
|
||||
deviceToken: { type: 'String' },
|
||||
deviceType: { type: 'String' },
|
||||
channels: { type: 'Array' },
|
||||
user: { type: 'Pointer', targetClass: '_User' },
|
||||
pushType: { type: 'String' },
|
||||
GCMSenderId: { type: 'String' },
|
||||
timeZone: { type: 'String' },
|
||||
localeIdentifier: { type: 'String' },
|
||||
badge: { type: 'Number' },
|
||||
appVersion: { type: 'String' },
|
||||
appName: { type: 'String' },
|
||||
appIdentifier: { type: 'String' },
|
||||
parseVersion: { type: 'String' },
|
||||
ACL: { type: 'ACL' },
|
||||
createdAt: { type: 'Date' },
|
||||
updatedAt: { type: 'Date' },
|
||||
objectId: { type: 'String' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: { '*': true },
|
||||
}
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -2,12 +2,22 @@
|
||||
|
||||
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
|
||||
const MongoClient = require('mongodb').MongoClient;
|
||||
const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||
|
||||
// These tests are specific to the mongo storage adapter + mongo storage format
|
||||
// and will eventually be moved into their own repo
|
||||
describe('MongoStorageAdapter', () => {
|
||||
beforeEach(done => {
|
||||
new MongoStorageAdapter({ uri: databaseURI })
|
||||
.deleteAllClasses()
|
||||
.then(done, fail);
|
||||
});
|
||||
|
||||
it('auto-escapes symbols in auth information', () => {
|
||||
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
|
||||
new MongoStorageAdapter('mongodb://user!with@+ symbols:password!with@+ symbols@localhost:1234/parse', {})
|
||||
.connect();
|
||||
new MongoStorageAdapter({
|
||||
uri: 'mongodb://user!with@+ symbols:password!with@+ symbols@localhost:1234/parse'
|
||||
}).connect();
|
||||
expect(MongoClient.connect).toHaveBeenCalledWith(
|
||||
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
|
||||
jasmine.any(Object)
|
||||
@@ -16,8 +26,9 @@ describe('MongoStorageAdapter', () => {
|
||||
|
||||
it("doesn't double escape already URI-encoded information", () => {
|
||||
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
|
||||
new MongoStorageAdapter('mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse', {})
|
||||
.connect();
|
||||
new MongoStorageAdapter({
|
||||
uri: 'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse'
|
||||
}).connect();
|
||||
expect(MongoClient.connect).toHaveBeenCalledWith(
|
||||
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
|
||||
jasmine.any(Object)
|
||||
@@ -27,11 +38,118 @@ describe('MongoStorageAdapter', () => {
|
||||
// https://github.com/ParsePlatform/parse-server/pull/148#issuecomment-180407057
|
||||
it('preserves replica sets', () => {
|
||||
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
|
||||
new MongoStorageAdapter('mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415', {})
|
||||
.connect();
|
||||
new MongoStorageAdapter({
|
||||
uri: 'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415'
|
||||
}).connect();
|
||||
expect(MongoClient.connect).toHaveBeenCalledWith(
|
||||
'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415',
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('stores objectId in _id', done => {
|
||||
let adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
adapter.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
|
||||
.then(() => adapter._rawFind('Foo', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
var obj = results[0];
|
||||
expect(obj._id).toEqual('abcde');
|
||||
expect(obj.objectId).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('stores pointers with a _p_ prefix', (done) => {
|
||||
let obj = {
|
||||
objectId: 'bar',
|
||||
aPointer: {
|
||||
__type: 'Pointer',
|
||||
className: 'JustThePointer',
|
||||
objectId: 'qwerty'
|
||||
}
|
||||
};
|
||||
let adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
adapter.createObject('APointerDarkly', { fields: {
|
||||
objectId: { type: 'String' },
|
||||
aPointer: { type: 'Pointer', targetClass: 'JustThePointer' },
|
||||
}}, obj)
|
||||
.then(() => adapter._rawFind('APointerDarkly', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
let output = results[0];
|
||||
expect(typeof output._id).toEqual('string');
|
||||
expect(typeof output._p_aPointer).toEqual('string');
|
||||
expect(output._p_aPointer).toEqual('JustThePointer$qwerty');
|
||||
expect(output.aPointer).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles object and subdocument', done => {
|
||||
let adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
let schema = { fields : { subdoc: { type: 'Object' } } };
|
||||
let obj = { subdoc: {foo: 'bar', wu: 'tan'} };
|
||||
adapter.createObject('MyClass', schema, obj)
|
||||
.then(() => adapter._rawFind('MyClass', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
let mob = results[0];
|
||||
expect(typeof mob.subdoc).toBe('object');
|
||||
expect(mob.subdoc.foo).toBe('bar');
|
||||
expect(mob.subdoc.wu).toBe('tan');
|
||||
let obj = { 'subdoc.wu': 'clan' };
|
||||
return adapter.findOneAndUpdate('MyClass', schema, {}, obj);
|
||||
})
|
||||
.then(() => adapter._rawFind('MyClass', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
let mob = results[0];
|
||||
expect(typeof mob.subdoc).toBe('object');
|
||||
expect(mob.subdoc.foo).toBe('bar');
|
||||
expect(mob.subdoc.wu).toBe('clan');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles array, object, date', (done) => {
|
||||
let adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
let obj = {
|
||||
array: [1, 2, 3],
|
||||
object: {foo: 'bar'},
|
||||
date: {
|
||||
__type: 'Date',
|
||||
iso: '2016-05-26T20:55:01.154Z',
|
||||
},
|
||||
};
|
||||
let schema = { fields: {
|
||||
array: { type: 'Array' },
|
||||
object: { type: 'Object' },
|
||||
date: { type: 'Date' },
|
||||
} };
|
||||
adapter.createObject('MyClass', schema, obj)
|
||||
.then(() => adapter._rawFind('MyClass', {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
let mob = results[0];
|
||||
expect(mob.array instanceof Array).toBe(true);
|
||||
expect(typeof mob.object).toBe('object');
|
||||
expect(mob.date instanceof Date).toBe(true);
|
||||
return adapter.find('MyClass', schema, {}, {});
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
let mob = results[0];
|
||||
expect(mob.array instanceof Array).toBe(true);
|
||||
expect(typeof mob.object).toBe('object');
|
||||
expect(mob.date.__type).toBe('Date');
|
||||
expect(mob.date.iso).toBe('2016-05-26T20:55:01.154Z');
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
fail();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
// These tests are unit tests designed to only test transform.js.
|
||||
"use strict";
|
||||
|
||||
let transform = require('../src/Adapters/Storage/Mongo/MongoTransform');
|
||||
let dd = require('deep-diff');
|
||||
let mongodb = require('mongodb');
|
||||
|
||||
describe('parseObjectToMongoObjectForCreate', () => {
|
||||
it('a basic number', (done) => {
|
||||
var input = {five: 5};
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: {five: {type: 'Number'}}
|
||||
});
|
||||
jequal(input, output);
|
||||
done();
|
||||
});
|
||||
|
||||
it('an object with null values', (done) => {
|
||||
var input = {objectWithNullValues: {isNull: null, notNull: 3}};
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: {objectWithNullValues: {type: 'object'}}
|
||||
});
|
||||
jequal(input, output);
|
||||
done();
|
||||
});
|
||||
|
||||
it('built-in timestamps', (done) => {
|
||||
var input = {
|
||||
createdAt: "2015-10-06T21:24:50.332Z",
|
||||
updatedAt: "2015-10-06T21:24:50.332Z"
|
||||
};
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
|
||||
expect(output._created_at instanceof Date).toBe(true);
|
||||
expect(output._updated_at instanceof Date).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it('array of pointers', (done) => {
|
||||
var pointer = {
|
||||
__type: 'Pointer',
|
||||
objectId: 'myId',
|
||||
className: 'Blah',
|
||||
};
|
||||
var out = transform.parseObjectToMongoObjectForCreate(null, {pointers: [pointer]},{
|
||||
fields: {pointers: {type: 'Array'}}
|
||||
});
|
||||
jequal([pointer], out.pointers);
|
||||
done();
|
||||
});
|
||||
|
||||
//TODO: object creation requests shouldn't be seeing __op delete, it makes no sense to
|
||||
//have __op delete in a new object. Figure out what this should actually be testing.
|
||||
xit('a delete op', (done) => {
|
||||
var input = {deleteMe: {__op: 'Delete'}};
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
|
||||
jequal(output, {});
|
||||
done();
|
||||
});
|
||||
|
||||
it('Doesnt allow ACL, as Parse Server should tranform ACL to _wperm + _rperm', done => {
|
||||
var input = {ACL: {'0123': {'read': true, 'write': true}}};
|
||||
expect(() => transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} })).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('plain', (done) => {
|
||||
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
|
||||
var out = transform.parseObjectToMongoObjectForCreate(null, {location: geoPoint},{
|
||||
fields: {location: {type: 'GeoPoint'}}
|
||||
});
|
||||
expect(out.location).toEqual([180, -180]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('in array', (done) => {
|
||||
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
|
||||
var out = transform.parseObjectToMongoObjectForCreate(null, {locations: [geoPoint, geoPoint]},{
|
||||
fields: {locations: {type: 'Array'}}
|
||||
});
|
||||
expect(out.locations).toEqual([geoPoint, geoPoint]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('in sub-object', (done) => {
|
||||
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
|
||||
var out = transform.parseObjectToMongoObjectForCreate(null, { locations: { start: geoPoint }},{
|
||||
fields: {locations: {type: 'Object'}}
|
||||
});
|
||||
expect(out).toEqual({ locations: { start: geoPoint } });
|
||||
done();
|
||||
});
|
||||
|
||||
it('objectId', (done) => {
|
||||
var out = transform.transformWhere(null, {objectId: 'foo'});
|
||||
expect(out._id).toEqual('foo');
|
||||
done();
|
||||
});
|
||||
|
||||
it('objectId in a list', (done) => {
|
||||
var input = {
|
||||
objectId: {'$in': ['one', 'two', 'three']},
|
||||
};
|
||||
var output = transform.transformWhere(null, input);
|
||||
jequal(input.objectId, output._id);
|
||||
done();
|
||||
});
|
||||
|
||||
it('built-in timestamps', (done) => {
|
||||
var input = {createdAt: new Date(), updatedAt: new Date()};
|
||||
var output = transform.mongoObjectToParseObject(null, input, { fields: {} });
|
||||
expect(typeof output.createdAt).toEqual('string');
|
||||
expect(typeof output.updatedAt).toEqual('string');
|
||||
done();
|
||||
});
|
||||
|
||||
it('pointer', (done) => {
|
||||
var input = {_p_userPointer: '_User$123'};
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { userPointer: { type: 'Pointer', targetClass: '_User' } },
|
||||
});
|
||||
expect(typeof output.userPointer).toEqual('object');
|
||||
expect(output.userPointer).toEqual(
|
||||
{__type: 'Pointer', className: '_User', objectId: '123'}
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
it('null pointer', (done) => {
|
||||
var input = {_p_userPointer: null};
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { userPointer: { type: 'Pointer', targetClass: '_User' } },
|
||||
});
|
||||
expect(output.userPointer).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
|
||||
it('file', (done) => {
|
||||
var input = {picture: 'pic.jpg'};
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { picture: { type: 'File' }},
|
||||
});
|
||||
expect(typeof output.picture).toEqual('object');
|
||||
expect(output.picture).toEqual({__type: 'File', name: 'pic.jpg'});
|
||||
done();
|
||||
});
|
||||
|
||||
it('geopoint', (done) => {
|
||||
var input = {location: [180, -180]};
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { location: { type: 'GeoPoint' }},
|
||||
});
|
||||
expect(typeof output.location).toEqual('object');
|
||||
expect(output.location).toEqual(
|
||||
{__type: 'GeoPoint', longitude: 180, latitude: -180}
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
it('nested array', (done) => {
|
||||
var input = {arr: [{_testKey: 'testValue' }]};
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { arr: { type: 'Array' } },
|
||||
});
|
||||
expect(Array.isArray(output.arr)).toEqual(true);
|
||||
expect(output.arr).toEqual([{ _testKey: 'testValue'}]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('untransforms objects containing nested special keys', done => {
|
||||
let input = {array: [{
|
||||
_id: "Test ID",
|
||||
_hashed_password: "I Don't know why you would name a key this, but if you do it should work",
|
||||
_tombstone: {
|
||||
_updated_at: "I'm sure people will nest keys like this",
|
||||
_acl: 7,
|
||||
_id: { someString: "str", someNumber: 7},
|
||||
regularKey: { moreContents: [1, 2, 3] },
|
||||
},
|
||||
regularKey: "some data",
|
||||
}]}
|
||||
let output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: { array: { type: 'Array' }},
|
||||
});
|
||||
expect(dd(output, input)).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
|
||||
it('changes new pointer key', (done) => {
|
||||
var input = {
|
||||
somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'}
|
||||
};
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: {somePointer: {type: 'Pointer'}}
|
||||
});
|
||||
expect(typeof output._p_somePointer).toEqual('string');
|
||||
expect(output._p_somePointer).toEqual('Micro$oft');
|
||||
done();
|
||||
});
|
||||
|
||||
it('changes existing pointer keys', (done) => {
|
||||
var input = {
|
||||
userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'}
|
||||
};
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
|
||||
fields: {userPointer: {type: 'Pointer'}}
|
||||
});
|
||||
expect(typeof output._p_userPointer).toEqual('string');
|
||||
expect(output._p_userPointer).toEqual('_User$qwerty');
|
||||
done();
|
||||
});
|
||||
|
||||
it('writes the old ACL format in addition to rperm and wperm', (done) => {
|
||||
var input = {
|
||||
_rperm: ['*'],
|
||||
_wperm: ['Kevin'],
|
||||
};
|
||||
|
||||
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
|
||||
expect(typeof output._acl).toEqual('object');
|
||||
expect(output._acl["Kevin"].w).toBeTruthy();
|
||||
expect(output._acl["Kevin"].r).toBeUndefined();
|
||||
done();
|
||||
})
|
||||
|
||||
it('untransforms from _rperm and _wperm to ACL', (done) => {
|
||||
var input = {
|
||||
_rperm: ["*"],
|
||||
_wperm: ["Kevin"]
|
||||
};
|
||||
var output = transform.mongoObjectToParseObject(null, input, { fields: {} });
|
||||
expect(output._rperm).toEqual(['*']);
|
||||
expect(output._wperm).toEqual(['Kevin']);
|
||||
expect(output.ACL).toBeUndefined()
|
||||
done();
|
||||
});
|
||||
|
||||
it('untransforms mongodb number types', (done) => {
|
||||
var input = {
|
||||
long: mongodb.Long.fromNumber(Number.MAX_SAFE_INTEGER),
|
||||
double: new mongodb.Double(Number.MAX_VALUE)
|
||||
}
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields: {
|
||||
long: { type: 'Number' },
|
||||
double: { type: 'Number' },
|
||||
},
|
||||
});
|
||||
expect(output.long).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(output.double).toBe(Number.MAX_VALUE);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
+34
-36
@@ -1,9 +1,9 @@
|
||||
var OAuth = require("../src/authDataManager/OAuth1Client");
|
||||
var request = require('request');
|
||||
var Config = require("../src/Config");
|
||||
var defaultColumns = require('../src/Controllers/SchemaController').defaultColumns;
|
||||
|
||||
describe('OAuth', function() {
|
||||
|
||||
it("Nonce should have right length", (done) => {
|
||||
jequal(OAuth.nonce().length, 30);
|
||||
done();
|
||||
@@ -217,50 +217,47 @@ describe('OAuth', function() {
|
||||
return request.post(options, callback);
|
||||
}
|
||||
|
||||
it("should create user with REST API", (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])("should create user with REST API", done => {
|
||||
createOAuthUser((error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
ok(b.sessionToken);
|
||||
expect(b.objectId).not.toBeNull();
|
||||
expect(b.objectId).not.toBeUndefined();
|
||||
var sessionToken = b.sessionToken;
|
||||
var q = new Parse.Query("_Session");
|
||||
q.equalTo('sessionToken', sessionToken);
|
||||
q.first({useMasterKey: true}).then((res) => {
|
||||
expect(res.get("installationId")).toEqual('yolo');
|
||||
done();
|
||||
}).fail((err) => {
|
||||
fail('should not fail fetching the session');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
ok(b.sessionToken);
|
||||
expect(b.objectId).not.toBeNull();
|
||||
expect(b.objectId).not.toBeUndefined();
|
||||
var sessionToken = b.sessionToken;
|
||||
var q = new Parse.Query("_Session");
|
||||
q.equalTo('sessionToken', sessionToken);
|
||||
q.first({useMasterKey: true}).then((res) => {
|
||||
expect(res.get("installationId")).toEqual('yolo');
|
||||
done();
|
||||
}).fail((err) => {
|
||||
fail('should not fail fetching the session');
|
||||
done();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it("should only create a single user with REST API", (done) => {
|
||||
it_exclude_dbs(['postgres'])("should only create a single user with REST API", (done) => {
|
||||
var objectId;
|
||||
createOAuthUser((error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.objectId).not.toBeNull();
|
||||
expect(b.objectId).not.toBeUndefined();
|
||||
objectId = b.objectId;
|
||||
|
||||
createOAuthUser((error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.objectId).not.toBeNull();
|
||||
expect(b.objectId).not.toBeUndefined();
|
||||
objectId = b.objectId;
|
||||
|
||||
createOAuthUser((error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.objectId).not.toBeNull();
|
||||
expect(b.objectId).not.toBeUndefined();
|
||||
expect(b.objectId).toBe(objectId);
|
||||
done();
|
||||
});
|
||||
expect(b.objectId).toBe(objectId);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it("unlink and link with custom provider", (done) => {
|
||||
it_exclude_dbs(['postgres'])("unlink and link with custom provider", (done) => {
|
||||
var provider = getMockMyOauthProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("myoauth", {
|
||||
@@ -284,9 +281,10 @@ describe('OAuth', function() {
|
||||
"Expiration should be cleared");
|
||||
// make sure the auth data is properly deleted
|
||||
var config = new Config(Parse.applicationId);
|
||||
config.database.mongoFind('_User', {
|
||||
_id: model.id
|
||||
}).then((res) => {
|
||||
config.database.adapter.find('_User', {
|
||||
fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation),
|
||||
}, { objectId: model.id }, {})
|
||||
.then(res => {
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0]._auth_data_myoauth).toBeUndefined();
|
||||
expect(res[0]._auth_data_myoauth).not.toBeNull();
|
||||
|
||||
+90
-16
@@ -1,8 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
let request = require('request');
|
||||
|
||||
describe('Parse.Push', () => {
|
||||
|
||||
var setup = function() {
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
@@ -27,29 +27,30 @@ describe('Parse.Push', () => {
|
||||
}
|
||||
}
|
||||
|
||||
setServerConfiguration({
|
||||
return reconfigureServer({
|
||||
appId: Parse.applicationId,
|
||||
masterKey: Parse.masterKey,
|
||||
serverURL: Parse.serverURL,
|
||||
push: {
|
||||
adapter: pushAdapter
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
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);
|
||||
}
|
||||
return Parse.Object.saveAll(installations);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
return Parse.Object.saveAll(installations);
|
||||
}
|
||||
|
||||
it('should properly send push', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should properly send push', (done) => {
|
||||
return setup().then(() => {
|
||||
return Parse.Push.send({
|
||||
where: {
|
||||
@@ -70,7 +71,7 @@ describe('Parse.Push', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly send push with lowercaseIncrement', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should properly send push with lowercaseIncrement', (done) => {
|
||||
return setup().then(() => {
|
||||
return Parse.Push.send({
|
||||
where: {
|
||||
@@ -89,4 +90,77 @@ describe('Parse.Push', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should not allow clients to query _PushStatus', done => {
|
||||
setup()
|
||||
.then(() => Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true}))
|
||||
.then(() => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/classes/_PushStatus',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
},
|
||||
}, (error, response, body) => {
|
||||
expect(body.error).toEqual('unauthorized');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should allow master key to query _PushStatus', done => {
|
||||
setup()
|
||||
.then(() => Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true}))
|
||||
.then(() => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/classes/_PushStatus',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}, (error, response, body) => {
|
||||
expect(body.results.length).toEqual(1);
|
||||
expect(body.results[0].query).toEqual('{"deviceType":"ios"}');
|
||||
expect(body.results[0].payload).toEqual('{"badge":"increment","alert":"Hello world!"}');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should throw error if missing push configuration', done => {
|
||||
reconfigureServer({push: null})
|
||||
.then(() => {
|
||||
return Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true})
|
||||
}).then((response) => {
|
||||
fail('should not succeed');
|
||||
}, (err) => {
|
||||
expect(err.code).toEqual(Parse.Error.PUSH_MISCONFIGURED);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+622
-550
File diff suppressed because it is too large
Load Diff
+713
-491
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ var ParseCloudCodePublisher = require('../src/LiveQuery/ParseCloudCodePublisher'
|
||||
var Parse = require('parse/node');
|
||||
|
||||
describe('ParseCloudCodePublisher', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
// Mock ParsePubSub
|
||||
var mockParsePubSub = {
|
||||
|
||||
+52
-7
@@ -36,6 +36,31 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('works with _ContentType', done => {
|
||||
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo='
|
||||
})
|
||||
}, (error, response, body) => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.name).toMatch(/_file.html/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
|
||||
request.get(b.url, (error, response, body) => {
|
||||
expect(response.headers['content-type']).toMatch('^text/html');
|
||||
expect(error).toBe(null);
|
||||
expect(body).toEqual('<html></html>\n');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('works without Content-Type', done => {
|
||||
var headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -185,7 +210,7 @@ describe('Parse.File testing', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("save file in object", done => {
|
||||
it_exclude_dbs(['postgres'])("save file in object", done => {
|
||||
var file = new Parse.File("hello.txt", data, "text/plain");
|
||||
ok(!file.url());
|
||||
file.save(expectSuccess({
|
||||
@@ -212,7 +237,7 @@ describe('Parse.File testing', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("save file in object with escaped characters in filename", done => {
|
||||
it_exclude_dbs(['postgres'])("save file in object with escaped characters in filename", done => {
|
||||
var file = new Parse.File("hello . txt", data, "text/plain");
|
||||
ok(!file.url());
|
||||
file.save(expectSuccess({
|
||||
@@ -240,7 +265,7 @@ describe('Parse.File testing', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("autosave file in object", done => {
|
||||
it_exclude_dbs(['postgres'])("autosave file in object", done => {
|
||||
var file = new Parse.File("hello.txt", data, "text/plain");
|
||||
ok(!file.url());
|
||||
var object = new Parse.Object("TestObject");
|
||||
@@ -262,7 +287,7 @@ describe('Parse.File testing', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("autosave file in object in object", done => {
|
||||
it_exclude_dbs(['postgres'])("autosave file in object in object", done => {
|
||||
var file = new Parse.File("hello.txt", data, "text/plain");
|
||||
ok(!file.url());
|
||||
|
||||
@@ -330,7 +355,7 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("file toJSON testing", done => {
|
||||
it_exclude_dbs(['postgres'])("file toJSON testing", done => {
|
||||
var file = new Parse.File("hello.txt", data, "text/plain");
|
||||
ok(!file.url());
|
||||
var object = new Parse.Object("TestObject");
|
||||
@@ -466,7 +491,7 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates correct url for old files hosted on parse', done => {
|
||||
it_exclude_dbs(['postgres'])('creates correct url for old files hosted on files.parsetfss.com', done => {
|
||||
var file = {
|
||||
__type: 'File',
|
||||
url: 'http://irrelevant.elephant/',
|
||||
@@ -486,7 +511,27 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('supports files in objects without urls', done => {
|
||||
it_exclude_dbs(['postgres'])('creates correct url for old files hosted on files.parse.com', done => {
|
||||
var file = {
|
||||
__type: 'File',
|
||||
url: 'http://irrelevant.elephant/',
|
||||
name: 'd6e80979-a128-4c57-a167-302f874700dc-123.txt'
|
||||
};
|
||||
var obj = new Parse.Object('OldFileTest');
|
||||
obj.set('oldfile', file);
|
||||
obj.save().then(() => {
|
||||
var query = new Parse.Query('OldFileTest');
|
||||
return query.first();
|
||||
}).then((result) => {
|
||||
var fileAgain = result.get('oldfile');
|
||||
expect(fileAgain.url()).toEqual(
|
||||
'http://files.parse.com/test/d6e80979-a128-4c57-a167-302f874700dc-123.txt'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('supports files in objects without urls', done => {
|
||||
var file = {
|
||||
__type: 'File',
|
||||
name: '123.txt'
|
||||
|
||||
+17
-17
@@ -4,7 +4,7 @@
|
||||
var TestObject = Parse.Object.extend('TestObject');
|
||||
|
||||
describe('Parse.GeoPoint testing', () => {
|
||||
it('geo point roundtrip', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo point roundtrip', (done) => {
|
||||
var point = new Parse.GeoPoint(44.0, -11.0);
|
||||
var obj = new TestObject();
|
||||
obj.set('location', point);
|
||||
@@ -26,7 +26,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('geo point exception two fields', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo point exception two fields', (done) => {
|
||||
var point = new Parse.GeoPoint(20, 20);
|
||||
var obj = new TestObject();
|
||||
obj.set('locationOne', point);
|
||||
@@ -39,7 +39,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('geo line', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo line', (done) => {
|
||||
var line = [];
|
||||
for (var i = 0; i < 10; ++i) {
|
||||
var obj = new TestObject();
|
||||
@@ -67,7 +67,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('geo max distance large', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo max distance large', (done) => {
|
||||
var objects = [];
|
||||
[0, 1, 2].map(function(i) {
|
||||
var obj = new TestObject();
|
||||
@@ -85,12 +85,12 @@ describe('Parse.GeoPoint testing', () => {
|
||||
equal(results.length, 3);
|
||||
done();
|
||||
}, (err) => {
|
||||
console.log(err);
|
||||
fail();
|
||||
fail("Couldn't query GeoPoint");
|
||||
fail(err)
|
||||
});
|
||||
});
|
||||
|
||||
it('geo max distance medium', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo max distance medium', (done) => {
|
||||
var objects = [];
|
||||
[0, 1, 2].map(function(i) {
|
||||
var obj = new TestObject();
|
||||
@@ -114,7 +114,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('geo max distance small', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo max distance small', (done) => {
|
||||
var objects = [];
|
||||
[0, 1, 2].map(function(i) {
|
||||
var obj = new TestObject();
|
||||
@@ -153,7 +153,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
Parse.Object.saveAll([sacramento, sf, honolulu], callback);
|
||||
};
|
||||
|
||||
it('geo max distance in km everywhere', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo max distance in km everywhere', (done) => {
|
||||
makeSomeGeoPoints(function(list) {
|
||||
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
var query = new Parse.Query(TestObject);
|
||||
@@ -167,7 +167,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('geo max distance in km california', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo max distance in km california', (done) => {
|
||||
makeSomeGeoPoints(function(list) {
|
||||
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
var query = new Parse.Query(TestObject);
|
||||
@@ -183,7 +183,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('geo max distance in km bay area', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo max distance in km bay area', (done) => {
|
||||
makeSomeGeoPoints(function(list) {
|
||||
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
var query = new Parse.Query(TestObject);
|
||||
@@ -198,7 +198,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('geo max distance in km mid peninsula', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo max distance in km mid peninsula', (done) => {
|
||||
makeSomeGeoPoints(function(list) {
|
||||
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
var query = new Parse.Query(TestObject);
|
||||
@@ -212,7 +212,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('geo max distance in miles everywhere', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo max distance in miles everywhere', (done) => {
|
||||
makeSomeGeoPoints(function(list) {
|
||||
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
var query = new Parse.Query(TestObject);
|
||||
@@ -226,7 +226,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('geo max distance in miles california', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo max distance in miles california', (done) => {
|
||||
makeSomeGeoPoints(function(list) {
|
||||
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
var query = new Parse.Query(TestObject);
|
||||
@@ -242,7 +242,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('geo max distance in miles bay area', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo max distance in miles bay area', (done) => {
|
||||
makeSomeGeoPoints(function(list) {
|
||||
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
var query = new Parse.Query(TestObject);
|
||||
@@ -257,7 +257,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('geo max distance in miles mid peninsula', (done) => {
|
||||
it_exclude_dbs(['postgres'])('geo max distance in miles mid peninsula', (done) => {
|
||||
makeSomeGeoPoints(function(list) {
|
||||
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||
var query = new Parse.Query(TestObject);
|
||||
@@ -271,7 +271,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('works with geobox queries', (done) => {
|
||||
it_exclude_dbs(['postgres'])('works with geobox queries', (done) => {
|
||||
var inSF = new Parse.GeoPoint(37.75, -122.4);
|
||||
var southwestOfSF = new Parse.GeoPoint(37.708813, -122.526398);
|
||||
var northeastOfSF = new Parse.GeoPoint(37.822802, -122.373962);
|
||||
|
||||
@@ -7,12 +7,15 @@ let Config = require('../src/Config');
|
||||
describe('a GlobalConfig', () => {
|
||||
beforeEach(done => {
|
||||
let config = new Config('test');
|
||||
config.database.adaptiveCollection('_GlobalConfig')
|
||||
.then(coll => coll.upsertOne({ '_id': 1 }, { $set: { params: { companies: ['US', 'DK'] } } }))
|
||||
.then(() => { done(); });
|
||||
config.database.adapter.upsertOneObject(
|
||||
'_GlobalConfig',
|
||||
{ fields: {} },
|
||||
{ objectId: 1 },
|
||||
{ params: { companies: ['US', 'DK'] } }
|
||||
).then(done);
|
||||
});
|
||||
|
||||
it('can be retrieved', (done) => {
|
||||
it_exclude_dbs(['postgres'])('can be retrieved', (done) => {
|
||||
request.get({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
@@ -27,7 +30,7 @@ describe('a GlobalConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can be updated when a master key exists', (done) => {
|
||||
it_exclude_dbs(['postgres'])('can be updated when a master key exists', (done) => {
|
||||
request.put({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
@@ -43,7 +46,36 @@ describe('a GlobalConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fail to update if master key is missing', (done) => {
|
||||
it_exclude_dbs(['postgres'])('properly handles delete op', (done) => {
|
||||
request.put({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
body : { params: { companies: {__op: 'Delete'}, foo: 'bar' } },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key' : 'test'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(body.result).toEqual(true);
|
||||
request.get({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key' : 'test'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(body.params.companies).toBeUndefined();
|
||||
expect(body.params.foo).toBe('bar');
|
||||
expect(Object.keys(body.params).length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('fail to update if master key is missing', (done) => {
|
||||
request.put({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
@@ -59,24 +91,25 @@ describe('a GlobalConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('failed getting config when it is missing', (done) => {
|
||||
it_exclude_dbs(['postgres'])('failed getting config when it is missing', (done) => {
|
||||
let config = new Config('test');
|
||||
config.database.adaptiveCollection('_GlobalConfig')
|
||||
.then(coll => coll.deleteOne({ '_id': 1 }))
|
||||
.then(() => {
|
||||
request.get({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key' : 'test'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(body.params).toEqual({});
|
||||
done();
|
||||
});
|
||||
config.database.adapter.deleteObjectsByQuery(
|
||||
'_GlobalConfig',
|
||||
{ fields: { params: { __type: 'String' } } },
|
||||
{ objectId: 1 }
|
||||
).then(() => {
|
||||
request.get({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key' : 'test'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(body.params).toEqual({});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
+161
-90
@@ -1,3 +1,4 @@
|
||||
"use strict";
|
||||
/* global describe, it, expect, fail, Parse */
|
||||
var request = require('request');
|
||||
var triggers = require('../src/triggers');
|
||||
@@ -9,15 +10,14 @@ Parse.Hooks = require("../src/cloud-code/Parse.Hooks");
|
||||
|
||||
var port = 12345;
|
||||
var hookServerURL = "http://localhost:"+port;
|
||||
let AppCache = require('../src/cache').AppCache;
|
||||
|
||||
var app = express();
|
||||
app.use(bodyParser.json({ 'type': '*/*' }))
|
||||
app.listen(12345);
|
||||
|
||||
|
||||
describe('Hooks', () => {
|
||||
|
||||
it("should have some hooks registered", (done) => {
|
||||
it_exclude_dbs(['postgres'])("should have no hooks registered", (done) => {
|
||||
Parse.Hooks.getFunctions().then((res) => {
|
||||
expect(res.constructor).toBe(Array.prototype.constructor);
|
||||
done();
|
||||
@@ -26,8 +26,8 @@ describe('Hooks', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should have some triggers registered", (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])("should have no triggers registered", (done) => {
|
||||
Parse.Hooks.getTriggers().then( (res) => {
|
||||
expect(res.constructor).toBe(Array.prototype.constructor);
|
||||
done();
|
||||
@@ -37,52 +37,48 @@ describe('Hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should CRUD a function registration", (done) => {
|
||||
// Create
|
||||
Parse.Hooks.createFunction("My-Test-Function", "http://someurl").then((res) => {
|
||||
expect(res.functionName).toBe("My-Test-Function");
|
||||
expect(res.url).toBe("http://someurl")
|
||||
// Find
|
||||
return Parse.Hooks.getFunction("My-Test-Function");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
expect(res).not.toBe(null);
|
||||
expect(res).not.toBe(undefined);
|
||||
expect(res.url).toBe("http://someurl");
|
||||
// delete
|
||||
return Parse.Hooks.updateFunction("My-Test-Function", "http://anotherurl");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
expect(res.functionName).toBe("My-Test-Function");
|
||||
expect(res.url).toBe("http://anotherurl")
|
||||
|
||||
return Parse.Hooks.deleteFunction("My-Test-Function");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
// Find again! but should be deleted
|
||||
return Parse.Hooks.getFunction("My-Test-Function");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
done();
|
||||
}).then((res) => {
|
||||
fail("Should not succeed")
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err).not.toBe(null);
|
||||
expect(err).not.toBe(undefined);
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe("no function named: My-Test-Function is defined")
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should CRUD a trigger registration", (done) => {
|
||||
it_exclude_dbs(['postgres'])("should CRUD a function registration", (done) => {
|
||||
// Create
|
||||
Parse.Hooks.createFunction("My-Test-Function", "http://someurl")
|
||||
.then(response => {
|
||||
expect(response.functionName).toBe("My-Test-Function");
|
||||
expect(response.url).toBe("http://someurl")
|
||||
// Find
|
||||
return Parse.Hooks.getFunction("My-Test-Function")
|
||||
}).then(response => {
|
||||
expect(response.objectId).toBeUndefined();
|
||||
expect(response.url).toBe("http://someurl");
|
||||
return Parse.Hooks.updateFunction("My-Test-Function", "http://anotherurl");
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res.objectId).toBeUndefined();
|
||||
expect(res.functionName).toBe("My-Test-Function");
|
||||
expect(res.url).toBe("http://anotherurl")
|
||||
// delete
|
||||
return Parse.Hooks.deleteFunction("My-Test-Function")
|
||||
})
|
||||
.then((res) => {
|
||||
// Find again! but should be deleted
|
||||
return Parse.Hooks.getFunction("My-Test-Function")
|
||||
.then(res => {
|
||||
fail("Failed to delete hook")
|
||||
fail(res)
|
||||
done();
|
||||
return Promise.resolve();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.error).toBe("no function named: My-Test-Function is defined")
|
||||
done();
|
||||
return Promise.resolve();
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
fail(error);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])("should CRUD a trigger registration", (done) => {
|
||||
// Create
|
||||
Parse.Hooks.createTrigger("MyClass","beforeDelete", "http://someurl").then((res) => {
|
||||
expect(res.className).toBe("MyClass");
|
||||
@@ -96,6 +92,7 @@ describe('Hooks', () => {
|
||||
}).then((res) => {
|
||||
expect(res).not.toBe(null);
|
||||
expect(res).not.toBe(undefined);
|
||||
expect(res.objectId).toBeUndefined();
|
||||
expect(res.url).toBe("http://someurl");
|
||||
// delete
|
||||
return Parse.Hooks.updateTrigger("MyClass","beforeDelete", "http://anotherurl");
|
||||
@@ -105,7 +102,8 @@ describe('Hooks', () => {
|
||||
}).then((res) => {
|
||||
expect(res.className).toBe("MyClass");
|
||||
expect(res.url).toBe("http://anotherurl")
|
||||
|
||||
expect(res.objectId).toBeUndefined();
|
||||
|
||||
return Parse.Hooks.deleteTrigger("MyClass","beforeDelete");
|
||||
}, (err) => {
|
||||
fail(err);
|
||||
@@ -127,7 +125,7 @@ describe('Hooks', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should fail to register hooks without Master Key", (done) => {
|
||||
request.post(Parse.serverURL+"/hooks/functions", {
|
||||
headers: {
|
||||
@@ -141,8 +139,8 @@ describe('Hooks', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should fail trying to create two times the same function", (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])("should fail trying to create two times the same function", (done) => {
|
||||
Parse.Hooks.createFunction("my_new_function", "http://url.com").then( () => {
|
||||
return Parse.Hooks.createFunction("my_new_function", "http://url.com")
|
||||
}, () => {
|
||||
@@ -162,8 +160,8 @@ describe('Hooks', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should fail trying to create two times the same trigger", (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])("should fail trying to create two times the same trigger", (done) => {
|
||||
Parse.Hooks.createTrigger("MyClass", "beforeSave", "http://url.com").then( () => {
|
||||
return Parse.Hooks.createTrigger("MyClass", "beforeSave", "http://url.com")
|
||||
}, () => {
|
||||
@@ -181,8 +179,8 @@ describe('Hooks', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should fail trying to update a function that don't exist", (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])("should fail trying to update a function that don't exist", (done) => {
|
||||
Parse.Hooks.updateFunction("A_COOL_FUNCTION", "http://url.com").then( () => {
|
||||
fail("Should not succeed")
|
||||
}, (err) => {
|
||||
@@ -198,8 +196,8 @@ describe('Hooks', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail trying to update a trigger that don't exist", (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])("should fail trying to update a trigger that don't exist", (done) => {
|
||||
Parse.Hooks.updateTrigger("AClassName","beforeSave", "http://url.com").then( () => {
|
||||
fail("Should not succeed")
|
||||
}, (err) => {
|
||||
@@ -215,8 +213,8 @@ describe('Hooks', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
it("should fail trying to create a malformed function", (done) => {
|
||||
Parse.Hooks.createFunction("MyFunction").then( (res) => {
|
||||
fail(res);
|
||||
@@ -226,7 +224,7 @@ describe('Hooks', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should fail trying to create a malformed function (REST)", (done) => {
|
||||
request.post(Parse.serverURL+"/hooks/functions", {
|
||||
headers: {
|
||||
@@ -241,16 +239,16 @@ describe('Hooks', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it("should create hooks and properly preload them", (done) => {
|
||||
|
||||
|
||||
|
||||
it_exclude_dbs(['postgres'])("should create hooks and properly preload them", (done) => {
|
||||
|
||||
var promises = [];
|
||||
for (var i = 0; i<5; i++) {
|
||||
promises.push(Parse.Hooks.createTrigger("MyClass"+i, "beforeSave", "http://url.com/beforeSave/"+i));
|
||||
promises.push(Parse.Hooks.createFunction("AFunction"+i, "http://url.com/function"+i));
|
||||
}
|
||||
|
||||
|
||||
Parse.Promise.when(promises).then(function(results){
|
||||
for (var i=0; i<5; i++) {
|
||||
// Delete everything from memory, as the server just started
|
||||
@@ -259,11 +257,11 @@ describe('Hooks', () => {
|
||||
expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).toBeUndefined();
|
||||
expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).toBeUndefined();
|
||||
}
|
||||
const hooksController = new HooksController(Parse.applicationId);
|
||||
const hooksController = new HooksController(Parse.applicationId, AppCache.get('test').databaseController);
|
||||
return hooksController.load()
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail();
|
||||
fail('Should properly create all hooks');
|
||||
done();
|
||||
}).then(function() {
|
||||
for (var i=0; i<5; i++) {
|
||||
@@ -273,17 +271,17 @@ describe('Hooks', () => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail();
|
||||
fail('should properly load all hooks');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should run the function on the test server", (done) => {
|
||||
|
||||
|
||||
it_exclude_dbs(['postgres'])("should run the function on the test server", (done) => {
|
||||
|
||||
app.post("/SomeFunction", function(req, res) {
|
||||
res.json({success:"OK!"});
|
||||
});
|
||||
|
||||
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunction").then(function(){
|
||||
return Parse.Cloud.run("SOME_TEST_FUNCTION")
|
||||
}, (err) => {
|
||||
@@ -297,15 +295,15 @@ describe('Hooks', () => {
|
||||
console.error(err);
|
||||
fail("Should not fail calling a function");
|
||||
done();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it("should run the function on the test server", (done) => {
|
||||
|
||||
|
||||
it_exclude_dbs(['postgres'])("should run the function on the test server", (done) => {
|
||||
|
||||
app.post("/SomeFunctionError", function(req, res) {
|
||||
res.json({error: {code: 1337, error: "hacking that one!"}});
|
||||
});
|
||||
// The function is delete as the DB is dropped between calls
|
||||
// The function is deleted as the DB is dropped between calls
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunctionError").then(function(){
|
||||
return Parse.Cloud.run("SOME_TEST_FUNCTION")
|
||||
}, (err) => {
|
||||
@@ -322,9 +320,62 @@ describe('Hooks', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should run the beforeSave hook on the test server", (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])("should provide X-Parse-Webhook-Key when defined", (done) => {
|
||||
app.post("/ExpectingKey", function(req, res) {
|
||||
if (req.get('X-Parse-Webhook-Key') === 'hook') {
|
||||
res.json({success: "correct key provided"});
|
||||
} else {
|
||||
res.json({error: "incorrect key provided"});
|
||||
}
|
||||
});
|
||||
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/ExpectingKey").then(function(){
|
||||
return Parse.Cloud.run("SOME_TEST_FUNCTION")
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail creating a function");
|
||||
done();
|
||||
}).then(function(res){
|
||||
expect(res).toBe("correct key provided");
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail calling a function");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])("should not pass X-Parse-Webhook-Key if not provided", (done) => {
|
||||
reconfigureServer({ webhookKey: undefined })
|
||||
.then(() => {
|
||||
app.post("/ExpectingKeyAlso", function(req, res) {
|
||||
if (req.get('X-Parse-Webhook-Key') === 'hook') {
|
||||
res.json({success: "correct key provided"});
|
||||
} else {
|
||||
res.json({error: "incorrect key provided"});
|
||||
}
|
||||
});
|
||||
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/ExpectingKeyAlso").then(function(){
|
||||
return Parse.Cloud.run("SOME_TEST_FUNCTION")
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail creating a function");
|
||||
done();
|
||||
}).then(function(res){
|
||||
fail("Should not succeed calling that function");
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(141);
|
||||
expect(err.message).toEqual("incorrect key provided");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it_exclude_dbs(['postgres'])("should run the beforeSave hook on the test server", (done) => {
|
||||
var triggerCount = 0;
|
||||
app.post("/BeforeSaveSome", function(req, res) {
|
||||
triggerCount++;
|
||||
@@ -332,16 +383,16 @@ describe('Hooks', () => {
|
||||
object.hello = "world";
|
||||
// Would need parse cloud express to set much more
|
||||
// But this should override the key upon return
|
||||
res.json({success: {object: object}});
|
||||
res.json({success: object});
|
||||
});
|
||||
// The function is delete as the DB is dropped between calls
|
||||
Parse.Hooks.createTrigger("SomeRandomObject", "beforeSave" ,hookServerURL+"/BeforeSaveSome").then(function(){
|
||||
const obj = new Parse.Object("SomeRandomObject");
|
||||
return obj.save();
|
||||
}).then(function(res){
|
||||
}).then(function(res) {
|
||||
expect(triggerCount).toBe(1);
|
||||
return res.fetch();
|
||||
}).then(function(res){
|
||||
}).then(function(res) {
|
||||
expect(res.get("hello")).toEqual("world");
|
||||
done();
|
||||
}).fail((err) => {
|
||||
@@ -350,8 +401,28 @@ describe('Hooks', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should run the afterSave hook on the test server", (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])("beforeSave hooks should correctly handle responses containing entire object", (done) => {
|
||||
app.post("/BeforeSaveSome2", function(req, res) {
|
||||
var object = Parse.Object.fromJSON(req.body.object);
|
||||
object.set('hello', "world");
|
||||
res.json({success: object});
|
||||
});
|
||||
Parse.Hooks.createTrigger("SomeRandomObject2", "beforeSave" ,hookServerURL+"/BeforeSaveSome2").then(function(){
|
||||
const obj = new Parse.Object("SomeRandomObject2");
|
||||
return obj.save();
|
||||
}).then(function(res) {
|
||||
return res.save();
|
||||
}).then(function(res) {
|
||||
expect(res.get("hello")).toEqual("world");
|
||||
done();
|
||||
}).fail((err) => {
|
||||
fail(`Should not fail: ${JSON.stringify(err)}`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])("should run the afterSave hook on the test server", (done) => {
|
||||
var triggerCount = 0;
|
||||
var newObjectId;
|
||||
app.post("/AfterSaveSome", function(req, res) {
|
||||
@@ -387,4 +458,4 @@ describe('Hooks', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+255
-216
@@ -2,19 +2,21 @@
|
||||
// These tests check the Installations functionality of the REST API.
|
||||
// Ported from installation_collection_test.go
|
||||
|
||||
var auth = require('../src/Auth');
|
||||
var cache = require('../src/cache');
|
||||
var Config = require('../src/Config');
|
||||
var DatabaseAdapter = require('../src/DatabaseAdapter');
|
||||
var Parse = require('parse/node').Parse;
|
||||
var rest = require('../src/rest');
|
||||
let auth = require('../src/Auth');
|
||||
let cache = require('../src/cache');
|
||||
let Config = require('../src/Config');
|
||||
let Parse = require('parse/node').Parse;
|
||||
let rest = require('../src/rest');
|
||||
let request = require("request");
|
||||
|
||||
var config = new Config('test');
|
||||
let database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
|
||||
let config = new Config('test');
|
||||
let database = config.database;
|
||||
let defaultColumns = require('../src/Controllers/SchemaController').defaultColumns;
|
||||
|
||||
const installationSchema = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation) };
|
||||
|
||||
describe('Installations', () => {
|
||||
|
||||
it('creates an android installation with ids', (done) => {
|
||||
it_exclude_dbs(['postgres'])('creates an android installation with ids', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var device = 'android';
|
||||
var input = {
|
||||
@@ -22,9 +24,8 @@ describe('Installations', () => {
|
||||
'deviceType': device
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
var obj = results[0];
|
||||
expect(obj.installationId).toEqual(installId);
|
||||
@@ -33,7 +34,7 @@ describe('Installations', () => {
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
it('creates an ios installation with ids', (done) => {
|
||||
it_exclude_dbs(['postgres'])('creates an ios installation with ids', (done) => {
|
||||
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||
var device = 'ios';
|
||||
var input = {
|
||||
@@ -41,9 +42,8 @@ describe('Installations', () => {
|
||||
'deviceType': device
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
var obj = results[0];
|
||||
expect(obj.deviceToken).toEqual(t);
|
||||
@@ -52,7 +52,7 @@ describe('Installations', () => {
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
it('creates an embedded installation with ids', (done) => {
|
||||
it_exclude_dbs(['postgres'])('creates an embedded installation with ids', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var device = 'embedded';
|
||||
var input = {
|
||||
@@ -60,9 +60,8 @@ describe('Installations', () => {
|
||||
'deviceType': device
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
var obj = results[0];
|
||||
expect(obj.installationId).toEqual(installId);
|
||||
@@ -71,7 +70,7 @@ describe('Installations', () => {
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
it('creates an android installation with all fields', (done) => {
|
||||
it_exclude_dbs(['postgres'])('creates an android installation with all fields', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var device = 'android';
|
||||
var input = {
|
||||
@@ -80,9 +79,8 @@ describe('Installations', () => {
|
||||
'channels': ['foo', 'bar']
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
var obj = results[0];
|
||||
expect(obj.installationId).toEqual(installId);
|
||||
@@ -95,7 +93,7 @@ describe('Installations', () => {
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
it('creates an ios installation with all fields', (done) => {
|
||||
it_exclude_dbs(['postgres'])('creates an ios installation with all fields', (done) => {
|
||||
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||
var device = 'ios';
|
||||
var input = {
|
||||
@@ -104,9 +102,8 @@ describe('Installations', () => {
|
||||
'channels': ['foo', 'bar']
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
var obj = results[0];
|
||||
expect(obj.deviceToken).toEqual(t);
|
||||
@@ -119,6 +116,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_exclude_dbs(['postgres'])('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',
|
||||
@@ -150,7 +191,7 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates an object with custom fields', (done) => {
|
||||
it_exclude_dbs(['postgres'])('creates an object with custom fields', (done) => {
|
||||
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||
var input = {
|
||||
'deviceToken': t,
|
||||
@@ -159,9 +200,8 @@ describe('Installations', () => {
|
||||
'custom': 'allowed'
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
var obj = results[0];
|
||||
expect(obj.custom).toEqual('allowed');
|
||||
@@ -171,7 +211,7 @@ describe('Installations', () => {
|
||||
|
||||
// Note: did not port test 'TestObjectIDForIdentifiers'
|
||||
|
||||
it('merging when installationId already exists', (done) => {
|
||||
it_exclude_dbs(['postgres'])('merging when installationId already exists', (done) => {
|
||||
var installId1 = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||
var installId2 = '12345678-abcd-abcd-abcd-123456789abd';
|
||||
@@ -184,18 +224,17 @@ describe('Installations', () => {
|
||||
var firstObject;
|
||||
var secondObject;
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
firstObject = results[0];
|
||||
delete input.deviceToken;
|
||||
delete input.channels;
|
||||
input['foo'] = 'bar';
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
secondObject = results[0];
|
||||
expect(firstObject._id).toEqual(secondObject._id);
|
||||
@@ -205,7 +244,7 @@ describe('Installations', () => {
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
it('merging when two objects both only have one id', (done) => {
|
||||
it_exclude_dbs(['postgres'])('merging when two objects both only have one id', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
var input1 = {
|
||||
@@ -224,15 +263,14 @@ describe('Installations', () => {
|
||||
var firstObject;
|
||||
var secondObject;
|
||||
rest.create(config, auth.nobody(config), '_Installation', input1)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
firstObject = results[0];
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input2);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(2);
|
||||
if (results[0]['_id'] == firstObject._id) {
|
||||
secondObject = results[1];
|
||||
@@ -240,16 +278,16 @@ describe('Installations', () => {
|
||||
secondObject = results[0];
|
||||
}
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input3);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]['_id']).toEqual(secondObject._id);
|
||||
done();
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
notWorking('creating multiple devices with same device token works', (done) => {
|
||||
xit('creating multiple devices with same device token works', (done) => {
|
||||
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||
var installId3 = '33333333-abcd-abcd-abcd-123456789abc';
|
||||
@@ -266,51 +304,50 @@ describe('Installations', () => {
|
||||
}).then(() => {
|
||||
input.installationId = installId3;
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation',
|
||||
{installationId: installId1}, {});
|
||||
}).then((results) => {
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', {installationId: installId1}, installationSchema, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
return database.mongoFind('_Installation',
|
||||
{installationId: installId2}, {});
|
||||
}).then((results) => {
|
||||
return database.adapter.find('_Installation', {installationId: installId2}, installationSchema, {});
|
||||
}).then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
return database.mongoFind('_Installation',
|
||||
{installationId: installId3}, {});
|
||||
return database.adapter.find('_Installation', {installationId: installId3}, installationSchema, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
done();
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
it('updating with new channels', (done) => {
|
||||
it_exclude_dbs(['postgres'])('updating with new channels', (done) => {
|
||||
var input = {
|
||||
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
|
||||
'deviceType': 'android',
|
||||
'channels': ['foo', 'bar']
|
||||
installationId: '12345678-abcd-abcd-abcd-123456789abc',
|
||||
deviceType: 'android',
|
||||
channels: ['foo', 'bar']
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
var id = results[0]['_id'];
|
||||
var id = results[0].objectId;
|
||||
var update = {
|
||||
'channels': ['baz']
|
||||
};
|
||||
return rest.update(config, auth.nobody(config),
|
||||
'_Installation', id, update);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
return rest.update(config, auth.nobody(config), '_Installation', id, update);
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].channels.length).toEqual(1);
|
||||
expect(results[0].channels[0]).toEqual('baz');
|
||||
done();
|
||||
}).catch((error) => { console.log(error); });
|
||||
}).catch(error => {
|
||||
console.log(error);
|
||||
fail();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('update android fails with new installation id', (done) => {
|
||||
it_exclude_dbs(['postgres'])('update android fails with new installation id', (done) => {
|
||||
var installId1 = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var installId2 = '87654321-abcd-abcd-abcd-123456789abc';
|
||||
var input = {
|
||||
@@ -319,15 +356,11 @@ describe('Installations', () => {
|
||||
'channels': ['foo', 'bar']
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
input = {
|
||||
'installationId': installId2
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
results[0]['_id'], input);
|
||||
input = { 'installationId': installId2 };
|
||||
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
|
||||
}).then(() => {
|
||||
fail('Updating the installation should have failed.');
|
||||
done();
|
||||
@@ -337,7 +370,7 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('update ios fails with new deviceToken and no installationId', (done) => {
|
||||
it_exclude_dbs(['postgres'])('update ios fails with new deviceToken and no installationId', (done) => {
|
||||
var a = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||
var b = '91433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||
var input = {
|
||||
@@ -346,15 +379,11 @@ describe('Installations', () => {
|
||||
'channels': ['foo', 'bar']
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
input = {
|
||||
'deviceToken': b
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
results[0]['_id'], input);
|
||||
input = { 'deviceToken': b };
|
||||
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
|
||||
}).then(() => {
|
||||
fail('Updating the installation should have failed.');
|
||||
}).catch((error) => {
|
||||
@@ -363,7 +392,7 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('update ios updates device token', (done) => {
|
||||
it_exclude_dbs(['postgres'])('update ios updates device token', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||
var u = '91433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||
@@ -374,27 +403,25 @@ describe('Installations', () => {
|
||||
'channels': ['foo', 'bar']
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
input = {
|
||||
'installationId': installId,
|
||||
'deviceToken': u,
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
results[0]['_id'], input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].deviceToken).toEqual(u);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('update fails to change deviceType', (done) => {
|
||||
it_exclude_dbs(['postgres'])('update fails to change deviceType', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var input = {
|
||||
'installationId': installId,
|
||||
@@ -402,15 +429,13 @@ describe('Installations', () => {
|
||||
'channels': ['foo', 'bar']
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
input = {
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
results[0]['_id'], input);
|
||||
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
|
||||
}).then(() => {
|
||||
fail('Should not have been able to update Installation.');
|
||||
done();
|
||||
@@ -420,7 +445,7 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('update android with custom field', (done) => {
|
||||
it_exclude_dbs(['postgres'])('update android with custom field', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var input = {
|
||||
'installationId': installId,
|
||||
@@ -428,25 +453,23 @@ describe('Installations', () => {
|
||||
'channels': ['foo', 'bar']
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
input = {
|
||||
'custom': 'allowed'
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
results[0]['_id'], input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]['custom']).toEqual('allowed');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('update android device token with duplicate device token', (done) => {
|
||||
it_exclude_dbs(['postgres'])('update android device token with duplicate device token', (done) => {
|
||||
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
@@ -458,41 +481,37 @@ describe('Installations', () => {
|
||||
var firstObject;
|
||||
var secondObject;
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
input = {
|
||||
'installationId': installId2,
|
||||
'deviceType': 'android'
|
||||
};
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation',
|
||||
{installationId: installId1}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
.then(() => {
|
||||
input = {
|
||||
'installationId': installId2,
|
||||
'deviceType': 'android'
|
||||
};
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId1}, {}))
|
||||
.then(results => {
|
||||
firstObject = results[0];
|
||||
return database.mongoFind('_Installation',
|
||||
{installationId: installId2}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
return database.adapter.find('_Installation', installationSchema, {installationId: installId2}, {});
|
||||
}).then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
secondObject = results[0];
|
||||
// Update second installation to conflict with first installation
|
||||
input = {
|
||||
'objectId': secondObject._id,
|
||||
'objectId': secondObject.objectId,
|
||||
'deviceToken': t
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
secondObject._id, input);
|
||||
}).then(() => {
|
||||
return rest.update(config, auth.nobody(config), '_Installation', secondObject.objectId, input);
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {objectId: firstObject.objectId}, {}))
|
||||
.then(results => {
|
||||
// The first object should have been deleted
|
||||
return database.mongoFind('_Installation', {_id: firstObject._id}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(0);
|
||||
done();
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
|
||||
it('update ios device token with duplicate device token', (done) => {
|
||||
it_exclude_dbs(['postgres'])('update ios device token with duplicate device token', (done) => {
|
||||
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
@@ -510,15 +529,14 @@ describe('Installations', () => {
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation',
|
||||
{installationId: installId1}, {});
|
||||
}).then((results) => {
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId1}, {}))
|
||||
.then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
firstObject = results[0];
|
||||
return database.mongoFind('_Installation',
|
||||
{installationId: installId2}, {});
|
||||
}).then((results) => {
|
||||
return database.adapter.find('_Installation', installationSchema, {installationId: installId2}, {});
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
secondObject = results[0];
|
||||
// Update second installation to conflict with first installation id
|
||||
@@ -526,18 +544,17 @@ describe('Installations', () => {
|
||||
'installationId': installId2,
|
||||
'deviceToken': t
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
secondObject._id, input);
|
||||
}).then(() => {
|
||||
return rest.update(config, auth.nobody(config), '_Installation', secondObject.objectId, input);
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {objectId: firstObject.objectId}, {}))
|
||||
.then(results => {
|
||||
// The first object should have been deleted
|
||||
return database.mongoFind('_Installation', {_id: firstObject._id}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(0);
|
||||
done();
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
notWorking('update ios device token with duplicate token different app', (done) => {
|
||||
xit('update ios device token with duplicate token different app', (done) => {
|
||||
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
@@ -552,9 +569,9 @@ describe('Installations', () => {
|
||||
input.installationId = installId2;
|
||||
input.appIdentifier = 'bar';
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
// The first object should have been deleted during merge
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].installationId).toEqual(installId2);
|
||||
@@ -562,7 +579,7 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('update ios token and channels', (done) => {
|
||||
it_exclude_dbs(['postgres'])('update ios token and channels', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
var input = {
|
||||
@@ -570,19 +587,17 @@ describe('Installations', () => {
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
input = {
|
||||
'deviceToken': t,
|
||||
'channels': []
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
results[0]['_id'], input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].installationId).toEqual(installId);
|
||||
expect(results[0].deviceToken).toEqual(t);
|
||||
@@ -591,7 +606,7 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('update ios linking two existing objects', (done) => {
|
||||
it_exclude_dbs(['postgres'])('update ios linking two existing objects', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
var input = {
|
||||
@@ -605,21 +620,19 @@ describe('Installations', () => {
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation',
|
||||
{deviceToken: t}, {});
|
||||
}).then((results) => {
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
input = {
|
||||
'deviceToken': t,
|
||||
'installationId': installId,
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
results[0]['_id'], input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].installationId).toEqual(installId);
|
||||
expect(results[0].deviceToken).toEqual(t);
|
||||
@@ -628,7 +641,7 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('update is linking two existing objects w/ increment', (done) => {
|
||||
it_exclude_dbs(['postgres'])('update is linking two existing objects w/ increment', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
var input = {
|
||||
@@ -642,10 +655,9 @@ describe('Installations', () => {
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation',
|
||||
{deviceToken: t}, {});
|
||||
}).then((results) => {
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
input = {
|
||||
'deviceToken': t,
|
||||
@@ -656,11 +668,10 @@ describe('Installations', () => {
|
||||
'amount': 1
|
||||
}
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
results[0]['_id'], input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].installationId).toEqual(installId);
|
||||
expect(results[0].deviceToken).toEqual(t);
|
||||
@@ -670,7 +681,7 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('update is linking two existing with installation id', (done) => {
|
||||
it_exclude_dbs(['postgres'])('update is linking two existing with installation id', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
var input = {
|
||||
@@ -680,9 +691,8 @@ describe('Installations', () => {
|
||||
var installObj;
|
||||
var tokenObj;
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
installObj = results[0];
|
||||
input = {
|
||||
@@ -690,9 +700,9 @@ describe('Installations', () => {
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {deviceToken: t}, {});
|
||||
}).then((results) => {
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
tokenObj = results[0];
|
||||
input = {
|
||||
@@ -700,11 +710,10 @@ describe('Installations', () => {
|
||||
'deviceToken': t,
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
installObj._id, input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {_id: tokenObj._id}, {});
|
||||
}).then((results) => {
|
||||
return rest.update(config, auth.nobody(config), '_Installation', installObj.objectId, input);
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, { objectId: tokenObj.objectId }, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].installationId).toEqual(installId);
|
||||
expect(results[0].deviceToken).toEqual(t);
|
||||
@@ -712,7 +721,7 @@ describe('Installations', () => {
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
it('update is linking two existing with installation id w/ op', (done) => {
|
||||
it_exclude_dbs(['postgres'])('update is linking two existing with installation id w/ op', (done) => {
|
||||
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
var input = {
|
||||
@@ -722,9 +731,8 @@ describe('Installations', () => {
|
||||
var installObj;
|
||||
var tokenObj;
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
installObj = results[0];
|
||||
input = {
|
||||
@@ -732,9 +740,9 @@ describe('Installations', () => {
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {deviceToken: t}, {});
|
||||
}).then((results) => {
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
tokenObj = results[0];
|
||||
input = {
|
||||
@@ -746,11 +754,10 @@ describe('Installations', () => {
|
||||
'amount': 1
|
||||
}
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation',
|
||||
installObj._id, input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {_id: tokenObj._id}, {});
|
||||
}).then((results) => {
|
||||
return rest.update(config, auth.nobody(config), '_Installation', installObj.objectId, input);
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, { objectId: tokenObj.objectId }, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].installationId).toEqual(installId);
|
||||
expect(results[0].deviceToken).toEqual(t);
|
||||
@@ -759,7 +766,7 @@ describe('Installations', () => {
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
it('ios merge existing same token no installation id', (done) => {
|
||||
it_exclude_dbs(['postgres'])('ios merge existing same token no installation id', (done) => {
|
||||
// Test creating installation when there is an existing object with the
|
||||
// same device token but no installation ID. This is possible when
|
||||
// developers import device tokens from another push provider; the import
|
||||
@@ -777,9 +784,8 @@ describe('Installations', () => {
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
input = {
|
||||
'installationId': installId,
|
||||
@@ -787,18 +793,51 @@ describe('Installations', () => {
|
||||
'deviceType': 'ios'
|
||||
};
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
}).then(() => {
|
||||
return database.mongoFind('_Installation', {}, {});
|
||||
}).then((results) => {
|
||||
})
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].deviceToken).toEqual(t);
|
||||
expect(results[0].installationId).toEqual(installId);
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
fail();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allows you to get your own installation (regression test for #1718)', done => {
|
||||
let installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
let device = 'android';
|
||||
let input = {
|
||||
'installationId': installId,
|
||||
'deviceType': device
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(createResult => {
|
||||
let headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
request.get({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/installations/' + createResult.response.objectId,
|
||||
json: true,
|
||||
}, (error, response, body) => {
|
||||
expect(body.objectId).toEqual(createResult.response.objectId);
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
fail('failed');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Look at additional tests from installation_collection_test.go:882
|
||||
// TODO: Do we need to support _tombstone disabling of installations?
|
||||
// TODO: Test deletion, badge increments
|
||||
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ var testUserId = 'userId';
|
||||
var testClassName = 'TestObject';
|
||||
|
||||
describe('ParseLiveQueryServer', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
// Mock ParseWebSocketServer
|
||||
var mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer');
|
||||
|
||||
+26
-26
@@ -49,7 +49,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("save cycle", function(done) {
|
||||
it("save cycle", done => {
|
||||
var a = new Parse.Object("TestObject");
|
||||
var b = new Parse.Object("TestObject");
|
||||
a.set("b", b);
|
||||
@@ -117,7 +117,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("relational fields", function(done) {
|
||||
it_exclude_dbs(['postgres'])("relational fields", function(done) {
|
||||
var item = new Item();
|
||||
item.set("property", "x");
|
||||
var container = new Container();
|
||||
@@ -205,7 +205,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("createdAt doesn't change", function(done) {
|
||||
it_exclude_dbs(['postgres'])("createdAt doesn't change", function(done) {
|
||||
var object = new TestObject({ foo: "bar" });
|
||||
object.save(null, {
|
||||
success: function() {
|
||||
@@ -268,7 +268,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("can set null", function(done) {
|
||||
it_exclude_dbs(['postgres'])("can set null", function(done) {
|
||||
var obj = new Parse.Object("TestObject");
|
||||
obj.set("foo", null);
|
||||
obj.save(null, {
|
||||
@@ -365,7 +365,7 @@ describe('Parse.Object testing', () => {
|
||||
}).then(fail, err => next(0));
|
||||
});
|
||||
|
||||
it("simple field deletion", function(done) {
|
||||
it_exclude_dbs(['postgres'])("simple field deletion", function(done) {
|
||||
var simple = new Parse.Object("SimpleObject");
|
||||
simple.save({
|
||||
foo: "bar"
|
||||
@@ -439,7 +439,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("relation deletion", function(done) {
|
||||
it_exclude_dbs(['postgres'])("relation deletion", function(done) {
|
||||
var simple = new Parse.Object("SimpleObject");
|
||||
var child = new Parse.Object("Child");
|
||||
simple.save({
|
||||
@@ -578,7 +578,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("addUnique", function(done) {
|
||||
it_exclude_dbs(['postgres'])("addUnique", function(done) {
|
||||
var x1 = new Parse.Object('X');
|
||||
x1.set('stuff', [1, 2]);
|
||||
x1.save().then(() => {
|
||||
@@ -600,7 +600,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("addUnique with object", function(done) {
|
||||
it_exclude_dbs(['postgres'])("addUnique with object", function(done) {
|
||||
var x1 = new Parse.Object('X');
|
||||
x1.set('stuff', [ 1, {'hello': 'world'}, {'foo': 'bar'}]);
|
||||
x1.save().then(() => {
|
||||
@@ -622,7 +622,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("removes with object", function(done) {
|
||||
it_exclude_dbs(['postgres'])("removes with object", function(done) {
|
||||
var x1 = new Parse.Object('X');
|
||||
x1.set('stuff', [ 1, {'hello': 'world'}, {'foo': 'bar'}]);
|
||||
x1.save().then(() => {
|
||||
@@ -668,7 +668,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("dirty keys", function(done) {
|
||||
it_exclude_dbs(['postgres'])("dirty keys", function(done) {
|
||||
var object = new Parse.Object("TestObject");
|
||||
object.set("gogo", "good");
|
||||
object.set("sito", "sexy");
|
||||
@@ -763,7 +763,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("old attribute unset then unset", function(done) {
|
||||
it_exclude_dbs(['postgres'])("old attribute unset then unset", function(done) {
|
||||
var TestObject = Parse.Object.extend("TestObject");
|
||||
var obj = new TestObject();
|
||||
obj.set("x", 3);
|
||||
@@ -832,7 +832,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("old attribute unset then clear", function(done) {
|
||||
it_exclude_dbs(['postgres'])("old attribute unset then clear", function(done) {
|
||||
var TestObject = Parse.Object.extend("TestObject");
|
||||
var obj = new TestObject();
|
||||
obj.set("x", 3);
|
||||
@@ -901,7 +901,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("old attribute clear then unset", function(done) {
|
||||
it_exclude_dbs(['postgres'])("old attribute clear then unset", function(done) {
|
||||
var TestObject = Parse.Object.extend("TestObject");
|
||||
var obj = new TestObject();
|
||||
obj.set("x", 3);
|
||||
@@ -970,7 +970,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("old attribute clear then clear", function(done) {
|
||||
it_exclude_dbs(['postgres'])("old attribute clear then clear", function(done) {
|
||||
var TestObject = Parse.Object.extend("TestObject");
|
||||
var obj = new TestObject();
|
||||
obj.set("x", 3);
|
||||
@@ -1039,7 +1039,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("saving children in an array", function(done) {
|
||||
it_exclude_dbs(['postgres'])("saving children in an array", function(done) {
|
||||
var Parent = Parse.Object.extend("Parent");
|
||||
var Child = Parse.Object.extend("Child");
|
||||
|
||||
@@ -1304,7 +1304,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("bytes work", function(done) {
|
||||
it_exclude_dbs(['postgres'])("bytes work", function(done) {
|
||||
Parse.Promise.as().then(function() {
|
||||
var obj = new TestObject();
|
||||
obj.set("bytes", { __type: "Bytes", base64: "ZnJveW8=" });
|
||||
@@ -1342,7 +1342,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("fetchAll", function(done) {
|
||||
it_exclude_dbs(['postgres'])("fetchAll", function(done) {
|
||||
var numItems = 11;
|
||||
var container = new Container();
|
||||
var items = [];
|
||||
@@ -1389,7 +1389,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("fetchAll updates dates", function(done) {
|
||||
it_exclude_dbs(['postgres'])("fetchAll updates dates", function(done) {
|
||||
var updatedObject;
|
||||
var object = new TestObject();
|
||||
object.set("x", 7);
|
||||
@@ -1409,7 +1409,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("fetchAll backbone-style callbacks", function(done) {
|
||||
it_exclude_dbs(['postgres'])("fetchAll backbone-style callbacks", function(done) {
|
||||
var numItems = 11;
|
||||
var container = new Container();
|
||||
var items = [];
|
||||
@@ -1478,7 +1478,7 @@ describe('Parse.Object testing', () => {
|
||||
expectError(Parse.Error.MISSING_OBJECT_ID, done));
|
||||
});
|
||||
|
||||
it("fetchAll error on deleted object", function(done) {
|
||||
it_exclude_dbs(['postgres'])("fetchAll error on deleted object", function(done) {
|
||||
var numItems = 11;
|
||||
var container = new Container();
|
||||
var subContainer = new Container();
|
||||
@@ -1504,7 +1504,7 @@ describe('Parse.Object testing', () => {
|
||||
|
||||
// TODO: Verify that with Sessions, this test is wrong... A fetch on
|
||||
// user should not bring down a session token.
|
||||
notWorking("fetchAll User attributes get merged", function(done) {
|
||||
xit("fetchAll User attributes get merged", function(done) {
|
||||
var sameUser;
|
||||
var user = new Parse.User();
|
||||
user.set("username", "asdf");
|
||||
@@ -1536,7 +1536,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("fetchAllIfNeeded", function(done) {
|
||||
it_exclude_dbs(['postgres'])("fetchAllIfNeeded", function(done) {
|
||||
var numItems = 11;
|
||||
var container = new Container();
|
||||
var items = [];
|
||||
@@ -1574,7 +1574,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("fetchAllIfNeeded backbone-style callbacks", function(done) {
|
||||
it_exclude_dbs(['postgres'])("fetchAllIfNeeded backbone-style callbacks", function(done) {
|
||||
var numItems = 11;
|
||||
var container = new Container();
|
||||
var items = [];
|
||||
@@ -1778,7 +1778,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('dictionary fetched pointers do not lose data on fetch', (done) => {
|
||||
it_exclude_dbs(['postgres'])('dictionary fetched pointers do not lose data on fetch', (done) => {
|
||||
var parent = new Parse.Object('Parent');
|
||||
var dict = {};
|
||||
for (var i = 0; i < 5; i++) {
|
||||
@@ -1838,7 +1838,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have undefined includes when object is missing', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should have undefined includes when object is missing', (done) => {
|
||||
let obj1 = new Parse.Object("AnObject");
|
||||
let obj2 = new Parse.Object("AnObject");
|
||||
|
||||
@@ -1867,7 +1867,7 @@ describe('Parse.Object testing', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('should have undefined includes when object is missing on deeper path', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should have undefined includes when object is missing on deeper path', (done) => {
|
||||
let obj1 = new Parse.Object("AnObject");
|
||||
let obj2 = new Parse.Object("AnObject");
|
||||
let obj3 = new Parse.Object("AnObject");
|
||||
|
||||
+391
-71
@@ -23,6 +23,105 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])("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' });
|
||||
@@ -38,7 +137,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("containedIn object array queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("containedIn object array queries", function(done) {
|
||||
var messageList = [];
|
||||
for (var i = 0; i < 4; ++i) {
|
||||
var message = new TestObject({});
|
||||
@@ -73,7 +172,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("containsAll number array queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("containsAll number array queries", function(done) {
|
||||
var NumberSet = Parse.Object.extend({ className: "NumberSet" });
|
||||
|
||||
var objectsList = [];
|
||||
@@ -96,7 +195,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("containsAll string array queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("containsAll string array queries", function(done) {
|
||||
var StringSet = Parse.Object.extend({ className: "StringSet" });
|
||||
|
||||
var objectsList = [];
|
||||
@@ -115,7 +214,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("containsAll date array queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("containsAll date array queries", function(done) {
|
||||
var DateSet = Parse.Object.extend({ className: "DateSet" });
|
||||
|
||||
function parseDate(iso8601) {
|
||||
@@ -171,7 +270,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("containsAll object array queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("containsAll object array queries", function(done) {
|
||||
|
||||
var MessageSet = Parse.Object.extend({ className: "MessageSet" });
|
||||
|
||||
@@ -213,7 +312,7 @@ describe('Parse.Query testing', () => {
|
||||
className: "BoxedNumber"
|
||||
});
|
||||
|
||||
it("equalTo queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("equalTo queries", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -230,7 +329,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("equalTo undefined", function(done) {
|
||||
it_exclude_dbs(['postgres'])("equalTo undefined", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -247,7 +346,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lessThan queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("lessThan queries", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -264,7 +363,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lessThanOrEqualTo queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("lessThanOrEqualTo queries", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -282,7 +381,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("greaterThan queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("greaterThan queries", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -300,7 +399,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("greaterThanOrEqualTo queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("greaterThanOrEqualTo queries", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -318,7 +417,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lessThanOrEqualTo greaterThanOrEqualTo queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("lessThanOrEqualTo greaterThanOrEqualTo queries", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -337,7 +436,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lessThan greaterThan queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("lessThan greaterThan queries", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -374,7 +473,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("containedIn queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("containedIn queries", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -392,7 +491,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("notContainedIn queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("notContainedIn queries", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -411,7 +510,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
|
||||
|
||||
it("objectId containedIn queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("objectId containedIn queries", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -438,7 +537,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("objectId equalTo queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("objectId equalTo queries", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -461,7 +560,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("find no elements", function(done) {
|
||||
it_exclude_dbs(['postgres'])("find no elements", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -582,7 +681,7 @@ describe('Parse.Query testing', () => {
|
||||
className: "Container"
|
||||
});
|
||||
|
||||
it("notEqualTo object", function(done) {
|
||||
it_exclude_dbs(['postgres'])("notEqualTo object", function(done) {
|
||||
var item1 = new TestObject();
|
||||
var item2 = new TestObject();
|
||||
var container1 = new Container({item: item1});
|
||||
@@ -599,7 +698,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("skip", function(done) {
|
||||
it_exclude_dbs(['postgres'])("skip", function(done) {
|
||||
Parse.Object.saveAll([new TestObject(), new TestObject()], function() {
|
||||
var query = new Parse.Query(TestObject);
|
||||
query.skip(1);
|
||||
@@ -618,7 +717,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("skip doesn't affect count", function(done) {
|
||||
it_exclude_dbs(['postgres'])("skip doesn't affect count", function(done) {
|
||||
Parse.Object.saveAll([new TestObject(), new TestObject()], function() {
|
||||
var query = new Parse.Query(TestObject);
|
||||
query.count({
|
||||
@@ -642,7 +741,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("count", function(done) {
|
||||
it_exclude_dbs(['postgres'])("count", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -660,7 +759,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("order by ascending number", function(done) {
|
||||
it_exclude_dbs(['postgres'])("order by ascending number", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -679,7 +778,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("order by descending number", function(done) {
|
||||
it_exclude_dbs(['postgres'])("order by descending number", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -698,7 +797,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("order by ascending number then descending string", function(done) {
|
||||
it_exclude_dbs(['postgres'])("order by ascending number then descending string", function(done) {
|
||||
var strings = ["a", "b", "c", "d"];
|
||||
var makeBoxedNumber = function(num, i) {
|
||||
return new BoxedNumber({ number: num, string: strings[i] });
|
||||
@@ -725,7 +824,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("order by descending number then ascending string", function(done) {
|
||||
it_exclude_dbs(['postgres'])("order by descending number then ascending string", function(done) {
|
||||
var strings = ["a", "b", "c", "d"];
|
||||
var makeBoxedNumber = function(num, i) {
|
||||
return new BoxedNumber({ number: num, string: strings[i] });
|
||||
@@ -751,7 +850,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("order by descending number and string", function(done) {
|
||||
it_exclude_dbs(['postgres'])("order by descending number and string", function(done) {
|
||||
var strings = ["a", "b", "c", "d"];
|
||||
var makeBoxedNumber = function(num, i) {
|
||||
return new BoxedNumber({ number: num, string: strings[i] });
|
||||
@@ -777,7 +876,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("order by descending number and string, with space", function(done) {
|
||||
it_exclude_dbs(['postgres'])("order by descending number and string, with space", function(done) {
|
||||
var strings = ["a", "b", "c", "d"];
|
||||
var makeBoxedNumber = function(num, i) {
|
||||
return new BoxedNumber({ number: num, string: strings[i] });
|
||||
@@ -803,7 +902,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("order by descending number and string, with array arg", function(done) {
|
||||
it_exclude_dbs(['postgres'])("order by descending number and string, with array arg", function(done) {
|
||||
var strings = ["a", "b", "c", "d"];
|
||||
var makeBoxedNumber = function(num, i) {
|
||||
return new BoxedNumber({ number: num, string: strings[i] });
|
||||
@@ -829,7 +928,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("order by descending number and string, with multiple args", function(done) {
|
||||
it_exclude_dbs(['postgres'])("order by descending number and string, with multiple args", function(done) {
|
||||
var strings = ["a", "b", "c", "d"];
|
||||
var makeBoxedNumber = function(num, i) {
|
||||
return new BoxedNumber({ number: num, string: strings[i] });
|
||||
@@ -855,7 +954,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("can't order by password", function(done) {
|
||||
it_exclude_dbs(['postgres'])("can't order by password", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -894,7 +993,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("order by createdAt", function(done) {
|
||||
it_exclude_dbs(['postgres'])("order by createdAt", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -918,7 +1017,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("order by _updated_at", function(done) {
|
||||
it_exclude_dbs(['postgres'])("order by _updated_at", function(done) {
|
||||
var makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
@@ -947,7 +1046,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("order by updatedAt", function(done) {
|
||||
it_exclude_dbs(['postgres'])("order by updatedAt", function(done) {
|
||||
var makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); };
|
||||
var numbers = [3, 1, 2].map(makeBoxedNumber);
|
||||
numbers[0].save().then(() => {
|
||||
@@ -998,7 +1097,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
}
|
||||
|
||||
it("time equality", function(done) {
|
||||
it_exclude_dbs(['postgres'])("time equality", function(done) {
|
||||
makeThreeTimeObjects().then(function(list) {
|
||||
var query = new Parse.Query(TestObject);
|
||||
query.equalTo("time", list[1].get("time"));
|
||||
@@ -1012,7 +1111,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("time lessThan", function(done) {
|
||||
it_exclude_dbs(['postgres'])("time lessThan", function(done) {
|
||||
makeThreeTimeObjects().then(function(list) {
|
||||
var query = new Parse.Query(TestObject);
|
||||
query.lessThan("time", list[2].get("time"));
|
||||
@@ -1026,7 +1125,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
|
||||
// This test requires Date objects to be consistently stored as a Date.
|
||||
it("time createdAt", function(done) {
|
||||
it_exclude_dbs(['postgres'])("time createdAt", function(done) {
|
||||
makeThreeTimeObjects().then(function(list) {
|
||||
var query = new Parse.Query(TestObject);
|
||||
query.greaterThanOrEqualTo("createdAt", list[0].createdAt);
|
||||
@@ -1039,7 +1138,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("matches string", function(done) {
|
||||
it_exclude_dbs(['postgres'])("matches string", function(done) {
|
||||
var thing1 = new TestObject();
|
||||
thing1.set("myString", "football");
|
||||
var thing2 = new TestObject();
|
||||
@@ -1056,7 +1155,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("matches regex", function(done) {
|
||||
it_exclude_dbs(['postgres'])("matches regex", function(done) {
|
||||
var thing1 = new TestObject();
|
||||
thing1.set("myString", "football");
|
||||
var thing2 = new TestObject();
|
||||
@@ -1073,7 +1172,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("case insensitive regex success", function(done) {
|
||||
it_exclude_dbs(['postgres'])("case insensitive regex success", function(done) {
|
||||
var thing = new TestObject();
|
||||
thing.set("myString", "football");
|
||||
Parse.Object.saveAll([thing], function() {
|
||||
@@ -1087,13 +1186,13 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("regexes with invalid options fail", function(done) {
|
||||
it_exclude_dbs(['postgres'])("regexes with invalid options fail", function(done) {
|
||||
var query = new Parse.Query(TestObject);
|
||||
query.matches("myString", "FootBall", "some invalid option");
|
||||
query.find(expectError(Parse.Error.INVALID_QUERY, done));
|
||||
});
|
||||
|
||||
it("Use a regex that requires all modifiers", function(done) {
|
||||
it_exclude_dbs(['postgres'])("Use a regex that requires all modifiers", function(done) {
|
||||
var thing = new TestObject();
|
||||
thing.set("myString", "PArSe\nCom");
|
||||
Parse.Object.saveAll([thing], function() {
|
||||
@@ -1113,7 +1212,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Regular expression constructor includes modifiers inline", function(done) {
|
||||
it_exclude_dbs(['postgres'])("Regular expression constructor includes modifiers inline", function(done) {
|
||||
var thing = new TestObject();
|
||||
thing.set("myString", "\n\nbuffer\n\nparse.COM");
|
||||
Parse.Object.saveAll([thing], function() {
|
||||
@@ -1131,7 +1230,7 @@ describe('Parse.Query testing', () => {
|
||||
var someAscii = "\\E' !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTU" +
|
||||
"VWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'";
|
||||
|
||||
it("contains", function(done) {
|
||||
it_exclude_dbs(['postgres'])("contains", function(done) {
|
||||
Parse.Object.saveAll([new TestObject({myString: "zax" + someAscii + "qub"}),
|
||||
new TestObject({myString: "start" + someAscii}),
|
||||
new TestObject({myString: someAscii + "end"}),
|
||||
@@ -1147,7 +1246,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("startsWith", function(done) {
|
||||
it_exclude_dbs(['postgres'])("startsWith", function(done) {
|
||||
Parse.Object.saveAll([new TestObject({myString: "zax" + someAscii + "qub"}),
|
||||
new TestObject({myString: "start" + someAscii}),
|
||||
new TestObject({myString: someAscii + "end"}),
|
||||
@@ -1163,7 +1262,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("endsWith", function(done) {
|
||||
it_exclude_dbs(['postgres'])("endsWith", function(done) {
|
||||
Parse.Object.saveAll([new TestObject({myString: "zax" + someAscii + "qub"}),
|
||||
new TestObject({myString: "start" + someAscii}),
|
||||
new TestObject({myString: someAscii + "end"}),
|
||||
@@ -1179,7 +1278,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("exists", function(done) {
|
||||
it_exclude_dbs(['postgres'])("exists", function(done) {
|
||||
var objects = [];
|
||||
for (var i of [0, 1, 2, 3, 4, 5, 6, 7, 8]) {
|
||||
var item = new TestObject();
|
||||
@@ -1205,7 +1304,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("doesNotExist", function(done) {
|
||||
it_exclude_dbs(['postgres'])("doesNotExist", function(done) {
|
||||
var objects = [];
|
||||
for (var i of [0, 1, 2, 3, 4, 5, 6, 7, 8]) {
|
||||
var item = new TestObject();
|
||||
@@ -1231,7 +1330,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("exists relation", function(done) {
|
||||
it_exclude_dbs(['postgres'])("exists relation", function(done) {
|
||||
var objects = [];
|
||||
for (var i of [0, 1, 2, 3, 4, 5, 6, 7, 8]) {
|
||||
var container = new Container();
|
||||
@@ -1260,7 +1359,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("doesNotExist relation", function(done) {
|
||||
it_exclude_dbs(['postgres'])("doesNotExist relation", function(done) {
|
||||
var objects = [];
|
||||
for (var i of [0, 1, 2, 3, 4, 5, 6, 7]) {
|
||||
var container = new Container();
|
||||
@@ -1289,7 +1388,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("don't include by default", function(done) {
|
||||
it_exclude_dbs(['postgres'])("don't include by default", function(done) {
|
||||
var child = new TestObject();
|
||||
var parent = new Container();
|
||||
child.set("foo", "bar");
|
||||
@@ -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_exclude_dbs(['postgres'])('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.
|
||||
@@ -1457,7 +1703,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("matches query", function(done) {
|
||||
it_exclude_dbs(['postgres'])("matches query", function(done) {
|
||||
var ParentObject = Parse.Object.extend("ParentObject");
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var objects = [];
|
||||
@@ -1496,7 +1742,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("select query", function(done) {
|
||||
it_exclude_dbs(['postgres'])("select query", function(done) {
|
||||
var RestaurantObject = Parse.Object.extend("Restaurant");
|
||||
var PersonObject = Parse.Object.extend("Person");
|
||||
var objects = [
|
||||
@@ -1522,7 +1768,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('$select inside $or', (done) => {
|
||||
it_exclude_dbs(['postgres'])('$select inside $or', (done) => {
|
||||
var Restaurant = Parse.Object.extend('Restaurant');
|
||||
var Person = Parse.Object.extend('Person');
|
||||
var objects = [
|
||||
@@ -1551,7 +1797,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("dontSelect query", function(done) {
|
||||
it_exclude_dbs(['postgres'])("dontSelect query", function(done) {
|
||||
var RestaurantObject = Parse.Object.extend("Restaurant");
|
||||
var PersonObject = Parse.Object.extend("Person");
|
||||
var objects = [
|
||||
@@ -1577,7 +1823,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("dontSelect query without conditions", function(done) {
|
||||
it_exclude_dbs(['postgres'])("dontSelect query without conditions", function(done) {
|
||||
const RestaurantObject = Parse.Object.extend("Restaurant");
|
||||
const PersonObject = Parse.Object.extend("Person");
|
||||
const objects = [
|
||||
@@ -1664,7 +1910,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("or queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("or queries", function(done) {
|
||||
var objects = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function(x) {
|
||||
var object = new Parse.Object('BoxedNumber');
|
||||
object.set('x', x);
|
||||
@@ -1691,7 +1937,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
|
||||
// This relies on matchesQuery aka the $inQuery operator
|
||||
it("or complex queries", function(done) {
|
||||
it_exclude_dbs(['postgres'])("or complex queries", function(done) {
|
||||
var objects = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function(x) {
|
||||
var child = new Parse.Object('Child');
|
||||
child.set('x', x);
|
||||
@@ -1720,7 +1966,7 @@ describe('Parse.Query testing', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("async methods", function(done) {
|
||||
it_exclude_dbs(['postgres'])("async methods", function(done) {
|
||||
var saves = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function(x) {
|
||||
var obj = new Parse.Object("TestObject");
|
||||
obj.set("x", x + 1);
|
||||
@@ -1757,7 +2003,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("query.each", function(done) {
|
||||
it_exclude_dbs(['postgres'])("query.each", function(done) {
|
||||
var TOTAL = 50;
|
||||
var COUNT = 25;
|
||||
|
||||
@@ -1792,7 +2038,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("query.each async", function(done) {
|
||||
it_exclude_dbs(['postgres'])("query.each async", function(done) {
|
||||
var TOTAL = 50;
|
||||
var COUNT = 25;
|
||||
|
||||
@@ -1999,7 +2245,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('notEqual with array of pointers', (done) => {
|
||||
it_exclude_dbs(['postgres'])('notEqual with array of pointers', (done) => {
|
||||
var children = [];
|
||||
var parents = [];
|
||||
var promises = [];
|
||||
@@ -2029,7 +2275,7 @@ describe('Parse.Query testing', () => {
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
it('querying for null value', (done) => {
|
||||
it_exclude_dbs(['postgres'])('querying for null value', (done) => {
|
||||
var obj = new Parse.Object('TestObject');
|
||||
obj.set('aNull', null);
|
||||
obj.save().then(() => {
|
||||
@@ -2043,7 +2289,7 @@ describe('Parse.Query testing', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('query within dictionary', (done) => {
|
||||
it_exclude_dbs(['postgres'])('query within dictionary', (done) => {
|
||||
var objs = [];
|
||||
var promises = [];
|
||||
for (var i = 0; i < 2; i++) {
|
||||
@@ -2082,7 +2328,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('query match on array with single object', (done) => {
|
||||
it_exclude_dbs(['postgres'])('query match on array with single object', (done) => {
|
||||
var target = {__type: 'Pointer', className: 'TestObject', objectId: 'abc123'};
|
||||
var obj = new Parse.Object('TestObject');
|
||||
obj.set('someObjs', [target]);
|
||||
@@ -2098,7 +2344,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('query match on array with multiple objects', (done) => {
|
||||
it_exclude_dbs(['postgres'])('query match on array with multiple objects', (done) => {
|
||||
var target1 = {__type: 'Pointer', className: 'TestObject', objectId: 'abc'};
|
||||
var target2 = {__type: 'Pointer', className: 'TestObject', objectId: '123'};
|
||||
var obj= new Parse.Object('TestObject');
|
||||
@@ -2116,7 +2362,7 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
|
||||
// #371
|
||||
it('should properly interpret a query', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should properly interpret a query v1', (done) => {
|
||||
var query = new Parse.Query("C1");
|
||||
var auxQuery = new Parse.Query("C1");
|
||||
query.matchesKeyInQuery("A1", "A2", auxQuery);
|
||||
@@ -2131,7 +2377,7 @@ describe('Parse.Query testing', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('should properly interpret a query', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should properly interpret a query v2', (done) => {
|
||||
var user = new Parse.User();
|
||||
user.set("username", "foo");
|
||||
user.set("password", "bar");
|
||||
@@ -2166,11 +2412,9 @@ describe('Parse.Query testing', () => {
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
it('should find objects with array of pointers', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should find objects with array of pointers', (done) => {
|
||||
var objects = [];
|
||||
while(objects.length != 5) {
|
||||
var object = new Parse.Object('ContainedObject');
|
||||
@@ -2209,4 +2453,80 @@ describe('Parse.Query testing', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it_exclude_dbs(['postgres'])('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();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
+132
-46
@@ -6,7 +6,7 @@ var ChildObject = Parse.Object.extend({className: "ChildObject"});
|
||||
var ParentObject = Parse.Object.extend({className: "ParentObject"});
|
||||
|
||||
describe('Parse.Relation testing', () => {
|
||||
it("simple add and remove relation", (done) => {
|
||||
it_exclude_dbs(['postgres'])("simple add and remove relation", (done) => {
|
||||
var child = new ChildObject();
|
||||
child.set("x", 2);
|
||||
var parent = new ParentObject();
|
||||
@@ -41,7 +41,7 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("query relation without schema", (done) => {
|
||||
it_exclude_dbs(['postgres'])("query relation without schema", (done) => {
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
for (var i = 0; i < 10; i++) {
|
||||
@@ -75,7 +75,7 @@ describe('Parse.Relation testing', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("relations are constructed right from query", (done) => {
|
||||
it_exclude_dbs(['postgres'])("relations are constructed right from query", (done) => {
|
||||
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
@@ -121,7 +121,7 @@ describe('Parse.Relation testing', () => {
|
||||
|
||||
});
|
||||
|
||||
it("compound add and remove relation", (done) => {
|
||||
it_exclude_dbs(['postgres'])("compound add and remove relation", (done) => {
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
for (var i = 0; i < 10; i++) {
|
||||
@@ -164,7 +164,7 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
|
||||
|
||||
it("queries with relations", (done) => {
|
||||
it_exclude_dbs(['postgres'])("queries with relations", (done) => {
|
||||
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
@@ -202,7 +202,7 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("queries on relation fields", (done) => {
|
||||
it_exclude_dbs(['postgres'])("queries on relation fields", (done) => {
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
for (var i = 0; i < 10; i++) {
|
||||
@@ -248,51 +248,55 @@ 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_exclude_dbs(['postgres'])("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();
|
||||
});
|
||||
});
|
||||
|
||||
it("query on pointer and relation fields with equal", (done) => {
|
||||
it_exclude_dbs(['postgres'])("query on pointer and relation fields with equal", (done) => {
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
for (var i = 0; i < 10; i++) {
|
||||
@@ -330,7 +334,7 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("query on pointer and relation fields with equal bis", (done) => {
|
||||
it_exclude_dbs(['postgres'])("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++) {
|
||||
@@ -370,7 +374,7 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("or queries on pointer and relation fields", (done) => {
|
||||
it_exclude_dbs(['postgres'])("or queries on pointer and relation fields", (done) => {
|
||||
var ChildObject = Parse.Object.extend("ChildObject");
|
||||
var childObjects = [];
|
||||
for (var i = 0; i < 10; i++) {
|
||||
@@ -415,7 +419,7 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
|
||||
|
||||
it("Get query on relation using un-fetched parent object", (done) => {
|
||||
it_exclude_dbs(['postgres'])("Get query on relation using un-fetched parent object", (done) => {
|
||||
// Setup data model
|
||||
var Wheel = Parse.Object.extend('Wheel');
|
||||
var Car = Parse.Object.extend('Car');
|
||||
@@ -448,7 +452,7 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Find query on relation using un-fetched parent object", (done) => {
|
||||
it_exclude_dbs(['postgres'])("Find query on relation using un-fetched parent object", (done) => {
|
||||
// Setup data model
|
||||
var Wheel = Parse.Object.extend('Wheel');
|
||||
var Car = Parse.Object.extend('Car');
|
||||
@@ -482,7 +486,7 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Find objects with a related object using equalTo', (done) => {
|
||||
it_exclude_dbs(['postgres'])('Find objects with a related object using equalTo', (done) => {
|
||||
// Setup the objects
|
||||
var Card = Parse.Object.extend('Card');
|
||||
var House = Parse.Object.extend('House');
|
||||
@@ -502,7 +506,7 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly get related objects with unfetched queries', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should properly get related objects with unfetched queries', (done) => {
|
||||
let objects = [];
|
||||
let owners = [];
|
||||
let allObjects = [];
|
||||
@@ -571,7 +575,7 @@ describe('Parse.Relation testing', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it("select query", function(done) {
|
||||
it_exclude_dbs(['postgres'])("select query", function(done) {
|
||||
var RestaurantObject = Parse.Object.extend("Restaurant");
|
||||
var PersonObject = Parse.Object.extend("Person");
|
||||
var OwnerObject = Parse.Object.extend('Owner');
|
||||
@@ -611,7 +615,7 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("dontSelect query", function(done) {
|
||||
it_exclude_dbs(['postgres'])("dontSelect query", function(done) {
|
||||
var RestaurantObject = Parse.Object.extend("Restaurant");
|
||||
var PersonObject = Parse.Object.extend("Person");
|
||||
var OwnerObject = Parse.Object.extend('Owner');
|
||||
@@ -652,4 +656,86 @@ describe('Parse.Relation testing', () => {
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('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();
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('can query roles in Cloud Code (regession test #1489)', done => {
|
||||
Parse.Cloud.define('isAdmin', (request, response) => {
|
||||
let query = new Parse.Query(Parse.Role);
|
||||
query.equalTo('name', 'admin');
|
||||
query.first({ useMasterKey: true })
|
||||
.then(role => {
|
||||
let relation = new Parse.Relation(role, 'users');
|
||||
let admins = relation.query();
|
||||
admins.equalTo('username', request.user.get('username'));
|
||||
admins.first({ useMasterKey: true })
|
||||
.then(user => {
|
||||
if (user) {
|
||||
response.success(user);
|
||||
done();
|
||||
} else {
|
||||
fail('Should have found admin user, found nothing instead');
|
||||
done();
|
||||
}
|
||||
}, error => {
|
||||
fail('User not admin');
|
||||
done();
|
||||
})
|
||||
}, error => {
|
||||
fail('Should have found admin user, errored instead');
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
let adminUser = new Parse.User();
|
||||
adminUser.set('username', 'name');
|
||||
adminUser.set('password', 'pass');
|
||||
adminUser.signUp()
|
||||
.then(adminUser => {
|
||||
let adminACL = new Parse.ACL();
|
||||
adminACL.setPublicReadAccess(true);
|
||||
|
||||
// Create admin role
|
||||
let adminRole = new Parse.Role('admin', adminACL);
|
||||
adminRole.getUsers().add(adminUser);
|
||||
adminRole.save()
|
||||
.then(() => {
|
||||
Parse.Cloud.run('isAdmin');
|
||||
}, error => {
|
||||
fail('failed to save role');
|
||||
fail(error);
|
||||
done()
|
||||
});
|
||||
}, error => {
|
||||
fail('failed to sign up');
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+112
-42
@@ -2,20 +2,23 @@
|
||||
|
||||
// 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");
|
||||
|
||||
describe('Parse Role testing', () => {
|
||||
|
||||
it('Do a bunch of basic role testing', (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])('Do a bunch of basic role testing', done => {
|
||||
var user;
|
||||
var role;
|
||||
|
||||
createTestUser().then((x) => {
|
||||
user = x;
|
||||
let acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(false);
|
||||
role = new Parse.Object('_Role');
|
||||
role.set('name', 'Foos');
|
||||
role.setACL(acl);
|
||||
var users = role.relation('users');
|
||||
users.add(user);
|
||||
return role.save({}, { useMasterKey: true });
|
||||
@@ -55,26 +58,92 @@ describe('Parse Role testing', () => {
|
||||
}).then((x) => {
|
||||
fail('Should not have been able to save.');
|
||||
}, (e) => {
|
||||
expect(e.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
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_exclude_dbs(['postgres'])("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, "_getAllRolesNamesForRoleIds").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
|
||||
// 1 query for the parent roles
|
||||
expect(restExecute.calls.count()).toEqual(2);
|
||||
|
||||
// 1 call for the 1st layer of roles
|
||||
// 1 call for the 2nd layer
|
||||
expect(getAllRolesSpy.calls.count()).toEqual(2);
|
||||
done()
|
||||
}).catch( (err) => {
|
||||
fail("should succeed");
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])("should recursively load roles", (done) => {
|
||||
var rolesNames = ["FooRole", "BarRole", "BazRole"];
|
||||
var roleIds = {};
|
||||
createTestUser().then( (user) => {
|
||||
// Put the user on the 1st role
|
||||
@@ -97,7 +166,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")
|
||||
@@ -105,7 +174,7 @@ describe('Parse Role testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("_Role object should not save without name.", (done) => {
|
||||
it_exclude_dbs(['postgres'])("_Role object should not save without name.", (done) => {
|
||||
var role = new Parse.Role();
|
||||
role.save(null,{useMasterKey:true})
|
||||
.then((r) => {
|
||||
@@ -122,7 +191,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,50 +203,49 @@ 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
|
||||
let promises = [admin, moderator, contentManager, superModerator].map((role) => {
|
||||
return auth._getAllRoleNamesForId(role.id).then((result) => {
|
||||
return auth._getAllRolesNamesForRoleIds([role.id]).then((result) => {
|
||||
return Parse.Promise.as({
|
||||
id: role.id,
|
||||
name: role.get('name'),
|
||||
roleIds: result
|
||||
roleNames: result
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
return Parse.Promise.when(promises);
|
||||
}).then((results) => {
|
||||
results.forEach((result) => {
|
||||
let id = result.id;
|
||||
let roleIds = result.roleIds;
|
||||
let roleNames = result.roleNames;
|
||||
if (id == admin.id) {
|
||||
expect(roleIds.length).toBe(2);
|
||||
expect(roleIds.indexOf(moderator.id)).not.toBe(-1);
|
||||
expect(roleIds.indexOf(contentManager.id)).not.toBe(-1);
|
||||
expect(roleNames.length).toBe(2);
|
||||
expect(roleNames.indexOf("Moderator")).not.toBe(-1);
|
||||
expect(roleNames.indexOf("ContentManager")).not.toBe(-1);
|
||||
} else if (id == moderator.id) {
|
||||
expect(roleIds.length).toBe(1);
|
||||
expect(roleIds.indexOf(contentManager.id)).toBe(0);
|
||||
expect(roleNames.length).toBe(1);
|
||||
expect(roleNames.indexOf("ContentManager")).toBe(0);
|
||||
} else if (id == contentManager.id) {
|
||||
expect(roleIds.length).toBe(0);
|
||||
expect(roleNames.length).toBe(0);
|
||||
} else if (id == superModerator.id) {
|
||||
expect(roleIds.length).toBe(3);
|
||||
expect(roleIds.indexOf(moderator.id)).not.toBe(-1);
|
||||
expect(roleIds.indexOf(contentManager.id)).not.toBe(-1);
|
||||
expect(roleIds.indexOf(superContentManager.id)).not.toBe(-1);
|
||||
expect(roleNames.length).toBe(3);
|
||||
expect(roleNames.indexOf("Moderator")).not.toBe(-1);
|
||||
expect(roleNames.indexOf("ContentManager")).not.toBe(-1);
|
||||
expect(roleNames.indexOf("SuperContentManager")).not.toBe(-1);
|
||||
}
|
||||
});
|
||||
done();
|
||||
}).fail((err) => {
|
||||
console.error(err);
|
||||
done();
|
||||
})
|
||||
|
||||
|
||||
});
|
||||
|
||||
it('can create role and query empty users', (done)=> {
|
||||
it_exclude_dbs(['postgres'])('can create role and query empty users', (done)=> {
|
||||
var roleACL = new Parse.ACL();
|
||||
roleACL.setPublicReadAccess(true);
|
||||
var role = new Parse.Role('subscribers', roleACL);
|
||||
@@ -192,13 +260,12 @@ describe('Parse Role testing', () => {
|
||||
done();
|
||||
});
|
||||
}, (e) => {
|
||||
console.log(e);
|
||||
fail('should not have errored');
|
||||
});
|
||||
});
|
||||
|
||||
// Based on various scenarios described in issues #827 and #683,
|
||||
it('should properly handle role permissions on objects', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should properly handle role permissions on objects', (done) => {
|
||||
var user, user2, user3;
|
||||
var role, role2, role3;
|
||||
var obj, obj2;
|
||||
@@ -271,10 +338,13 @@ describe('Parse Role testing', () => {
|
||||
fail('Customer user should not have been able to save.');
|
||||
done();
|
||||
}, (e) => {
|
||||
expect(e.code).toEqual(101);
|
||||
if (e) {
|
||||
expect(e.code).toEqual(101);
|
||||
} else {
|
||||
fail('should return an error');
|
||||
}
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
+368
-59
@@ -10,6 +10,7 @@
|
||||
var request = require('request');
|
||||
var passwordCrypto = require('../src/password');
|
||||
var Config = require('../src/Config');
|
||||
const rp = require('request-promise');
|
||||
|
||||
function verifyACL(user) {
|
||||
const ACL = user.getACL();
|
||||
@@ -87,7 +88,56 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("user login with files", (done) => {
|
||||
it_exclude_dbs(['postgres'])('should respect ACL without locking user out', (done) => {
|
||||
let user = new Parse.User();
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setPublicReadAccess(false);
|
||||
ACL.setPublicWriteAccess(false);
|
||||
user.setUsername('asdf');
|
||||
user.setPassword('zxcv');
|
||||
user.setACL(ACL);
|
||||
user.signUp().then((user) => {
|
||||
return Parse.User.logIn("asdf", "zxcv");
|
||||
}).then((user) => {
|
||||
equal(user.get("username"), "asdf");
|
||||
const ACL = user.getACL();
|
||||
expect(ACL.getReadAccess(user)).toBe(true);
|
||||
expect(ACL.getWriteAccess(user)).toBe(true);
|
||||
expect(ACL.getPublicReadAccess()).toBe(false);
|
||||
expect(ACL.getPublicWriteAccess()).toBe(false);
|
||||
const perms = ACL.permissionsById;
|
||||
expect(Object.keys(perms).length).toBe(1);
|
||||
expect(perms[user.id].read).toBe(true);
|
||||
expect(perms[user.id].write).toBe(true);
|
||||
expect(perms['*']).toBeUndefined();
|
||||
// Try to lock out user
|
||||
let newACL = new Parse.ACL();
|
||||
newACL.setReadAccess(user.id, false);
|
||||
newACL.setWriteAccess(user.id, false);
|
||||
user.setACL(newACL);
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return Parse.User.logIn("asdf", "zxcv");
|
||||
}).then((user) => {
|
||||
equal(user.get("username"), "asdf");
|
||||
const ACL = user.getACL();
|
||||
expect(ACL.getReadAccess(user)).toBe(true);
|
||||
expect(ACL.getWriteAccess(user)).toBe(true);
|
||||
expect(ACL.getPublicReadAccess()).toBe(false);
|
||||
expect(ACL.getPublicWriteAccess()).toBe(false);
|
||||
const perms = ACL.permissionsById;
|
||||
expect(Object.keys(perms).length).toBe(1);
|
||||
expect(perms[user.id].read).toBe(true);
|
||||
expect(perms[user.id].write).toBe(true);
|
||||
expect(perms['*']).toBeUndefined();
|
||||
done();
|
||||
}).catch((err) => {
|
||||
fail("Should not fail");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])("user login with files", (done) => {
|
||||
let file = new Parse.File("yolo.txt", [1,2,3], "text/plain");
|
||||
file.save().then((file) => {
|
||||
return Parse.User.signUp("asdf", "zxcv", { "file" : file });
|
||||
@@ -101,30 +151,28 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('become', () => {
|
||||
it('sends token back', done => {
|
||||
let user = null;
|
||||
var sessionToken = null;
|
||||
it_exclude_dbs(['postgres'])('become sends token back', done => {
|
||||
let user = null;
|
||||
var sessionToken = null;
|
||||
|
||||
Parse.User.signUp('Jason', 'Parse', { 'code': 'red' }).then(newUser => {
|
||||
user = newUser;
|
||||
expect(user.get('code'), 'red');
|
||||
Parse.User.signUp('Jason', 'Parse', { 'code': 'red' }).then(newUser => {
|
||||
user = newUser;
|
||||
expect(user.get('code'), 'red');
|
||||
|
||||
sessionToken = newUser.getSessionToken();
|
||||
expect(sessionToken).toBeDefined();
|
||||
sessionToken = newUser.getSessionToken();
|
||||
expect(sessionToken).toBeDefined();
|
||||
|
||||
return Parse.User.become(sessionToken);
|
||||
}).then(newUser => {
|
||||
expect(newUser.id).toEqual(user.id);
|
||||
expect(newUser.get('username'), 'Jason');
|
||||
expect(newUser.get('code'), 'red');
|
||||
expect(newUser.getSessionToken()).toEqual(sessionToken);
|
||||
}).then(() => {
|
||||
done();
|
||||
}, error => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
return Parse.User.become(sessionToken);
|
||||
}).then(newUser => {
|
||||
expect(newUser.id).toEqual(user.id);
|
||||
expect(newUser.get('username'), 'Jason');
|
||||
expect(newUser.get('code'), 'red');
|
||||
expect(newUser.getSessionToken()).toEqual(sessionToken);
|
||||
}).then(() => {
|
||||
done();
|
||||
}, error => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -247,7 +295,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("cannot saveAll with non-authed user", (done) => {
|
||||
it_exclude_dbs(['postgres'])("cannot saveAll with non-authed user", (done) => {
|
||||
var user = new Parse.User();
|
||||
user.signUp({
|
||||
"password": "asdf",
|
||||
@@ -440,7 +488,7 @@ describe('Parse.User testing', () => {
|
||||
return promise._thenRunCallbacks(optionsOrCallback);
|
||||
}
|
||||
|
||||
it("contained in user array queries", (done) => {
|
||||
it_exclude_dbs(['postgres'])("contained in user array queries", (done) => {
|
||||
var USERS = 4;
|
||||
var MESSAGES = 5;
|
||||
|
||||
@@ -535,7 +583,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("count users", (done) => {
|
||||
it_exclude_dbs(['postgres'])("count users", (done) => {
|
||||
var james = new Parse.User();
|
||||
james.set("username", "james");
|
||||
james.set("password", "mypass");
|
||||
@@ -663,7 +711,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("saving user after browser refresh", (done) => {
|
||||
it_exclude_dbs(['postgres'])("saving user after browser refresh", (done) => {
|
||||
var _ = Parse._;
|
||||
var id;
|
||||
|
||||
@@ -811,8 +859,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("user on disk gets updated after save", (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])("user on disk gets updated after save", (done) => {
|
||||
var SuperUser = Parse.User.extend({
|
||||
isSuper: function() {
|
||||
return true;
|
||||
@@ -956,7 +1003,7 @@ describe('Parse.User testing', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("log in with provider", (done) => {
|
||||
it_exclude_dbs(['postgres'])("log in with provider", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
@@ -978,7 +1025,24 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('log in with provider with files', done => {
|
||||
it_exclude_dbs(['postgres'])('returns authData when authed and logged in with provider (regression test for #1498)', done => {
|
||||
Parse.Object.enableSingleInstance();
|
||||
let provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith('facebook', {
|
||||
success: user => {
|
||||
let userQuery = new Parse.Query(Parse.User);
|
||||
userQuery.get(user.id)
|
||||
.then(user => {
|
||||
expect(user.get('authData')).not.toBeUndefined();
|
||||
Parse.Object.disableSingleInstance();
|
||||
done();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('log in with provider with files', done => {
|
||||
let provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
let file = new Parse.File("yolo.txt", [1, 2, 3], "text/plain");
|
||||
@@ -1001,7 +1065,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("log in with provider twice", (done) => {
|
||||
it_exclude_dbs(['postgres'])("log in with provider twice", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
@@ -1076,7 +1140,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("login with provider should not call beforeSave trigger", (done) => {
|
||||
it_exclude_dbs(['postgres'])("login with provider should not call beforeSave trigger", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
@@ -1089,12 +1153,10 @@ describe('Parse.User testing', () => {
|
||||
|
||||
Parse.User._logInWith("facebook", {
|
||||
success: function(innerModel) {
|
||||
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
|
||||
done();
|
||||
},
|
||||
error: function(model, error) {
|
||||
ok(undefined, error);
|
||||
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
|
||||
done();
|
||||
}
|
||||
});
|
||||
@@ -1102,7 +1164,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("link with provider", (done) => {
|
||||
it_exclude_dbs(['postgres'])("link with provider", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
var user = new Parse.User();
|
||||
@@ -1135,7 +1197,7 @@ describe('Parse.User testing', () => {
|
||||
|
||||
// What this means is, only one Parse User can be linked to a
|
||||
// particular Facebook account.
|
||||
it("link with provider for already linked user", (done) => {
|
||||
it_exclude_dbs(['postgres'])("link with provider for already linked user", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
var user = new Parse.User();
|
||||
@@ -1242,7 +1304,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("unlink with provider", (done) => {
|
||||
it_exclude_dbs(['postgres'])("unlink with provider", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
@@ -1278,7 +1340,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("unlink and link", (done) => {
|
||||
it_exclude_dbs(['postgres'])("unlink and link", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
@@ -1330,7 +1392,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("link multiple providers", (done) => {
|
||||
it_exclude_dbs(['postgres'])("link multiple providers", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
var mockProvider = getMockMyOauthProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
@@ -1366,7 +1428,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("link multiple providers and update token", (done) => {
|
||||
it_exclude_dbs(['postgres'])("link multiple providers and update token", (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
var mockProvider = getMockMyOauthProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
@@ -1412,7 +1474,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail linking with existing', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should fail linking with existing', (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
@@ -1438,7 +1500,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail linking with existing', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should fail linking with existing', (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
@@ -1464,7 +1526,30 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have authData in beforeSave and afterSave', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should properly error when password is missing', (done) => {
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
success: function(user) {
|
||||
user.set('username', 'myUser');
|
||||
user.set('email', 'foo@example.com');
|
||||
user.save().then(() => {
|
||||
return Parse.User.logOut();
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('myUser', 'password');
|
||||
}).then(() => {
|
||||
fail('should not succeed');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
|
||||
expect(err.message).toEqual('Invalid username/password.');
|
||||
done();
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should have authData in beforeSave and afterSave', (done) => {
|
||||
|
||||
Parse.Cloud.beforeSave('_User', (request, response) => {
|
||||
let authData = request.object.get('authData');
|
||||
@@ -1494,8 +1579,6 @@ describe('Parse.User testing', () => {
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith("facebook", {
|
||||
success: function(model) {
|
||||
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
|
||||
Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className);
|
||||
done();
|
||||
}
|
||||
});
|
||||
@@ -1529,7 +1612,7 @@ describe('Parse.User testing', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("log in with explicit facebook auth data", (done) => {
|
||||
it_exclude_dbs(['postgres'])("log in with explicit facebook auth data", (done) => {
|
||||
Parse.FacebookUtils.logIn({
|
||||
id: "8675309",
|
||||
access_token: "jenny",
|
||||
@@ -1537,7 +1620,7 @@ describe('Parse.User testing', () => {
|
||||
}, expectSuccess({success: done}));
|
||||
});
|
||||
|
||||
it("log in async with explicit facebook auth data", (done) => {
|
||||
it_exclude_dbs(['postgres'])("log in async with explicit facebook auth data", (done) => {
|
||||
Parse.FacebookUtils.logIn({
|
||||
id: "8675309",
|
||||
access_token: "jenny",
|
||||
@@ -1550,7 +1633,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("link with explicit facebook auth data", (done) => {
|
||||
it_exclude_dbs(['postgres'])("link with explicit facebook auth data", (done) => {
|
||||
Parse.User.signUp("mask", "open sesame", null, expectSuccess({
|
||||
success: function(user) {
|
||||
Parse.FacebookUtils.link(user, {
|
||||
@@ -1565,7 +1648,7 @@ describe('Parse.User testing', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("link async with explicit facebook auth data", (done) => {
|
||||
it_exclude_dbs(['postgres'])("link async with explicit facebook auth data", (done) => {
|
||||
Parse.User.signUp("mask", "open sesame", null, expectSuccess({
|
||||
success: function(user) {
|
||||
Parse.FacebookUtils.link(user, {
|
||||
@@ -1620,7 +1703,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
notWorking("querying for users doesn't get session tokens", (done) => {
|
||||
xit("querying for users doesn't get session tokens", (done) => {
|
||||
Parse.Promise.as().then(function() {
|
||||
return Parse.User.signUp("finn", "human", { foo: "bar" });
|
||||
|
||||
@@ -1744,6 +1827,22 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('unset user email', (done) => {
|
||||
var user = new Parse.User();
|
||||
user.set('username', 'test');
|
||||
user.set('password', 'test');
|
||||
user.set('email', 'test@test.com');
|
||||
user.signUp().then(() => {
|
||||
user.unset('email');
|
||||
return user.save();
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('test', 'test');
|
||||
}).then((user) => {
|
||||
expect(user.getEmail()).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create session from user', (done) => {
|
||||
Parse.Promise.as().then(() => {
|
||||
return Parse.User.signUp("finn", "human", { foo: "bar" });
|
||||
@@ -1850,7 +1949,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('get session only for current user', (done) => {
|
||||
it_exclude_dbs(['postgres'])('get session only for current user', (done) => {
|
||||
Parse.Promise.as().then(() => {
|
||||
return Parse.User.signUp("test1", "test", { foo: "bar" });
|
||||
}).then(() => {
|
||||
@@ -1874,7 +1973,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('delete session by object', (done) => {
|
||||
it_exclude_dbs(['postgres'])('delete session by object', (done) => {
|
||||
Parse.Promise.as().then(() => {
|
||||
return Parse.User.signUp("test1", "test", { foo: "bar" });
|
||||
}).then(() => {
|
||||
@@ -1954,7 +2053,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('test parse user become', (done) => {
|
||||
it_exclude_dbs(['postgres'])('test parse user become', (done) => {
|
||||
var sessionToken = null;
|
||||
Parse.Promise.as().then(function() {
|
||||
return Parse.User.signUp("flessard", "folo",{'foo':1});
|
||||
@@ -2007,7 +2106,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("session expiresAt correct format", (done) => {
|
||||
it_exclude_dbs(['postgres'])("session expiresAt correct format", (done) => {
|
||||
Parse.User.signUp("asdf", "zxcv", null, {
|
||||
success: function(user) {
|
||||
request.get({
|
||||
@@ -2025,13 +2124,11 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Sometimes the authData still has null on that keys
|
||||
// https://github.com/ParsePlatform/parse-server/issues/935
|
||||
it('should cleanup null authData keys', (done) => {
|
||||
it_exclude_dbs(['postgres'])('should cleanup null authData keys (regression test for #935)', (done) => {
|
||||
let database = new Config(Parse.applicationId).database;
|
||||
database.create('_User', {
|
||||
username: 'user',
|
||||
password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie',
|
||||
_hashed_password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie',
|
||||
_auth_data_facebook: null
|
||||
}, {}).then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -2061,6 +2158,62 @@ describe('Parse.User testing', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('should cleanup null authData keys ParseUser update (regression test for #1198)', (done) => {
|
||||
Parse.Cloud.beforeSave('_User', (req, res) => {
|
||||
req.object.set('foo', 'bar');
|
||||
res.success();
|
||||
});
|
||||
|
||||
// Simulate anonymous user save
|
||||
new Promise((resolve, reject) => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/classes/_User',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
}).then((user) => {
|
||||
// Simulate registration
|
||||
return new Promise((resolve, reject) => {
|
||||
request.put({
|
||||
url: 'http://localhost:8378/1/classes/_User/' + user.objectId,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Session-Token': user.sessionToken,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: {
|
||||
authData: {anonymous: null},
|
||||
user: 'user',
|
||||
password: 'password',
|
||||
}
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).then((user) => {
|
||||
expect(typeof user).toEqual('object');
|
||||
expect(user.authData).toBeUndefined();
|
||||
done();
|
||||
}).catch((err) => {
|
||||
fail('no request should fail: ' + JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should aftersave with full object', (done) => {
|
||||
var hit = 0;
|
||||
Parse.Cloud.afterSave('_User', (req, res) => {
|
||||
@@ -2075,7 +2228,6 @@ describe('Parse.User testing', () => {
|
||||
user.set('hello', 'world');
|
||||
return user.save();
|
||||
}).then(() => {
|
||||
Parse.Cloud._removeHook('Triggers', 'afterSave', '_User');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -2101,4 +2253,161 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should fail to become user with expired token', (done) => {
|
||||
let token;
|
||||
Parse.User.signUp("auser", "somepass", null)
|
||||
.then(user => rp({
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8378/1/classes/_Session',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}))
|
||||
.then(body => {
|
||||
var id = body.results[0].objectId;
|
||||
var expiresAt = new Date((new Date()).setYear(2015));
|
||||
token = body.results[0].sessionToken;
|
||||
return rp({
|
||||
method: '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() },
|
||||
},
|
||||
})
|
||||
})
|
||||
.then(() => Parse.User.become(token))
|
||||
.then(() => {
|
||||
fail("Should not have succeded")
|
||||
done();
|
||||
}, error => {
|
||||
expect(error.code).toEqual(209);
|
||||
expect(error.message).toEqual("Session token is expired.");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should not create extraneous session tokens', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
config.database.loadSchema().then((s) => {
|
||||
// Lock down the _User class for creation
|
||||
return s.addClassIfNotExists('_User', {}, {create: {}})
|
||||
}).then((res) => {
|
||||
let user = new Parse.User();
|
||||
return user.save({'username': 'user', 'password': 'pass'});
|
||||
}).then(() => {
|
||||
fail('should not be able to save the user');
|
||||
}, (err) => {
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('_Session');
|
||||
return q.find({useMasterKey: true})
|
||||
}).then((res) => {
|
||||
// We should have no session created
|
||||
expect(res.length).toBe(0);
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('should not fail');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should not overwrite username when unlinking facebook user (regression test for #1532)', done => {
|
||||
Parse.Object.disableSingleInstance();
|
||||
var provider = getMockFacebookProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
var user = new Parse.User();
|
||||
user.set("username", "testLinkWithProvider");
|
||||
user.set("password", "mypass");
|
||||
user.signUp()
|
||||
.then(user => user._linkWith("facebook", {
|
||||
success: user => {
|
||||
expect(user.get('username')).toEqual('testLinkWithProvider');
|
||||
expect(Parse.FacebookUtils.isLinked(user)).toBeTruthy();
|
||||
return user._unlinkFrom('facebook')
|
||||
.then(() => user.fetch())
|
||||
.then(user => {
|
||||
expect(user.get('username')).toEqual('testLinkWithProvider');
|
||||
expect(Parse.FacebookUtils.isLinked(user)).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: error => {
|
||||
fail('Unexpected failure testing linking');
|
||||
fail(JSON.stringify(error));
|
||||
done();
|
||||
}
|
||||
}))
|
||||
.catch(error => {
|
||||
fail('Unexpected failure testing in unlink user test');
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should revoke sessions when converting anonymous user to "normal" user', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/classes/_User',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
|
||||
}, (err, res, body) => {
|
||||
Parse.User.become(body.sessionToken)
|
||||
.then(user => {
|
||||
let obj = new Parse.Object('TestObject');
|
||||
obj.setACL(new Parse.ACL(user));
|
||||
return obj.save()
|
||||
.then(() => {
|
||||
// Change password, revoking session
|
||||
user.set('username', 'no longer anonymous');
|
||||
user.set('password', 'password');
|
||||
return user.save()
|
||||
})
|
||||
.then(() => obj.fetch())
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should not revoke session tokens if the server is configures to not revoke session tokens', done => {
|
||||
reconfigureServer({ revokeSessionOnPasswordReset: false })
|
||||
.then(() => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/classes/_User',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
|
||||
}, (err, res, body) => {
|
||||
Parse.User.become(body.sessionToken)
|
||||
.then(user => {
|
||||
let obj = new Parse.Object('TestObject');
|
||||
obj.setACL(new Parse.ACL(user));
|
||||
return obj.save()
|
||||
.then(() => {
|
||||
// Change password, revoking session
|
||||
user.set('username', 'no longer anonymous');
|
||||
user.set('password', 'password');
|
||||
return user.save()
|
||||
})
|
||||
.then(() => obj.fetch())
|
||||
// fetch should succeed as we still have our session token
|
||||
.then(done, fail);
|
||||
})
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
@@ -0,0 +1,693 @@
|
||||
'use strict';
|
||||
var Schema = require('../src/Controllers/SchemaController');
|
||||
|
||||
var Config = require('../src/Config');
|
||||
|
||||
describe('Pointer Permissions', () => {
|
||||
it_exclude_dbs(['postgres'])('should work with find', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
let obj2 = new Parse.Object('AnObject');
|
||||
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
obj.set('owner', user);
|
||||
obj2.set('owner', user2);
|
||||
return Parse.Object.saveAll([obj, obj2]);
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
return schema.updateClass('AnObject', {}, {readUserFields: ['owner']})
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].id).toBe(obj.id);
|
||||
done();
|
||||
}).catch(error => {
|
||||
fail(JSON.stringify(error));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it_exclude_dbs(['postgres'])('should work with write', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
let obj2 = new Parse.Object('AnObject');
|
||||
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
obj.set('owner', user);
|
||||
obj.set('reader', user2);
|
||||
obj2.set('owner', user2);
|
||||
obj2.set('reader', user);
|
||||
return Parse.Object.saveAll([obj, obj2]);
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
return schema.updateClass('AnObject', {}, {writeUserFields: ['owner'], readUserFields: ['reader', 'owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
obj2.set('hello', 'world');
|
||||
return obj2.save();
|
||||
}).then((res) => {
|
||||
fail('User should not be able to update obj2');
|
||||
}, (err) => {
|
||||
// User 1 should not be able to update obj2
|
||||
expect(err.code).toBe(101);
|
||||
return Promise.resolve();
|
||||
}).then(()=> {
|
||||
obj.set('hello', 'world');
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}, (err) => {
|
||||
fail('User should be able to update');
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find();
|
||||
}, (err) => {
|
||||
fail('should login with user 2');
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(2);
|
||||
res.forEach((result) => {
|
||||
if (result.id == obj.id) {
|
||||
expect(result.get('hello')).toBe('world');
|
||||
} else {
|
||||
expect(result.id).toBe(obj2.id);
|
||||
}
|
||||
})
|
||||
done();
|
||||
}, (err) => {
|
||||
fail("failed");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should let a proper user find', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
let obj2 = new Parse.Object('AnObject');
|
||||
user.signUp().then(() => {
|
||||
return user2.signUp()
|
||||
}).then(() => {
|
||||
Parse.User.logOut();
|
||||
}).then(() => {
|
||||
obj.set('owner', user);
|
||||
return Parse.Object.saveAll([obj, obj2]);
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
return schema.updateClass('AnObject', {}, {find: {}, get:{}, readUserFields: ['owner']})
|
||||
});
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(0);
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(0);
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.get(obj.id);
|
||||
}).then(() => {
|
||||
fail('User 2 should not get the obj1 object');
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
expect(err.message).toBe('Object not found.');
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(1);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
fail('should not fail');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should not allow creating objects', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
user.save().then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
return schema.addClassIfNotExists('AnObject', {owner: {type:'Pointer', targetClass: '_User'}}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
obj.set('owner', user);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
fail('should not succeed');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(119);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should handle multiple writeUserFields', done => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2])
|
||||
.then(() => {
|
||||
obj.set('owner', user);
|
||||
obj.set('otherOwner', user2);
|
||||
return obj.save();
|
||||
})
|
||||
.then(() => config.database.loadSchema())
|
||||
.then(schema => schema.updateClass('AnObject', {}, {find: {"*": true},writeUserFields: ['owner', 'otherOwner']}))
|
||||
.then(() => Parse.User.logIn('user1', 'password'))
|
||||
.then(() => obj.save({hello: 'fromUser1'}))
|
||||
.then(() => Parse.User.logIn('user2', 'password'))
|
||||
.then(() => obj.save({hello: 'fromUser2'}))
|
||||
.then(() => Parse.User.logOut())
|
||||
.then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.first();
|
||||
})
|
||||
.then(result => {
|
||||
expect(result.get('hello')).toBe('fromUser2');
|
||||
done();
|
||||
}).catch(err => {
|
||||
fail('should not fail');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should prevent creating pointer permission on missing field', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
config.database.loadSchema().then((schema) => {
|
||||
return schema.addClassIfNotExists('AnObject', {}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
|
||||
}).then(() => {
|
||||
fail('should not succeed');
|
||||
}).catch((err) => {
|
||||
expect(err.code).toBe(107);
|
||||
expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should prevent creating pointer permission on bad field', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
config.database.loadSchema().then((schema) => {
|
||||
return schema.addClassIfNotExists('AnObject', {owner: {type: 'String'}}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
|
||||
}).then(() => {
|
||||
fail('should not succeed');
|
||||
}).catch((err) => {
|
||||
expect(err.code).toBe(107);
|
||||
expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should prevent creating pointer permission on bad field', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('owner', 'value');
|
||||
object.save().then(() => {
|
||||
return config.database.loadSchema();
|
||||
}).then((schema) => {
|
||||
return schema.updateClass('AnObject', {}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
|
||||
}).then(() => {
|
||||
fail('should not succeed');
|
||||
}).catch((err) => {
|
||||
expect(err.code).toBe(107);
|
||||
expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields");
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL write (PP Locked)', (done) => {
|
||||
/*
|
||||
tests:
|
||||
CLP: update closed ({})
|
||||
PointerPerm: "owner"
|
||||
ACL: logged in user has access
|
||||
|
||||
The owner is another user than the ACL
|
||||
*/
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setReadAccess(user, true);
|
||||
ACL.setWriteAccess(user, true);
|
||||
obj.setACL(ACL);
|
||||
obj.set('owner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {}, {update: {}, writeUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
// user1 has ACL read/write but should be blocked by PP
|
||||
return obj.save({key: 'value'});
|
||||
}).then(() => {
|
||||
fail('Should not succeed saving');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL write (ACL Locked)', (done) => {
|
||||
/*
|
||||
tests:
|
||||
CLP: update closed ({})
|
||||
PointerPerm: "owner"
|
||||
ACL: logged in user has access
|
||||
*/
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setReadAccess(user, true);
|
||||
ACL.setWriteAccess(user, true);
|
||||
obj.setACL(ACL);
|
||||
obj.set('owner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {}, {update: {}, writeUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}).then(() => {
|
||||
// user1 has ACL read/write but should be blocked by ACL
|
||||
return obj.save({key: 'value'});
|
||||
}).then(() => {
|
||||
fail('Should not succeed saving');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL write (ACL/PP OK)', (done) => {
|
||||
/*
|
||||
tests:
|
||||
CLP: update closed ({})
|
||||
PointerPerm: "owner"
|
||||
ACL: logged in user has access
|
||||
*/
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setWriteAccess(user, true);
|
||||
ACL.setWriteAccess(user2, true);
|
||||
obj.setACL(ACL);
|
||||
obj.set('owner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {}, {update: {}, writeUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}).then(() => {
|
||||
// user1 has ACL read/write but should be blocked by ACL
|
||||
return obj.save({key: 'value'});
|
||||
}).then((objAgain) => {
|
||||
expect(objAgain.get('key')).toBe('value');
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('Should not fail saving');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL read (PP locked)', (done) => {
|
||||
/*
|
||||
tests:
|
||||
CLP: find/get open ({})
|
||||
PointerPerm: "owner" : read
|
||||
ACL: logged in user has access
|
||||
|
||||
The owner is another user than the ACL
|
||||
*/
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setReadAccess(user, true);
|
||||
ACL.setWriteAccess(user, true);
|
||||
obj.setACL(ACL);
|
||||
obj.set('owner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {}, {find: {}, get: {}, readUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user1', 'password');
|
||||
}).then(() => {
|
||||
// user1 has ACL read/write but should be block
|
||||
return obj.fetch();
|
||||
}).then(() => {
|
||||
fail('Should not succeed saving');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL read (PP/ACL OK)', (done) => {
|
||||
/*
|
||||
tests:
|
||||
CLP: find/get open ({"*": true})
|
||||
PointerPerm: "owner" : read
|
||||
ACL: logged in user has access
|
||||
*/
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setReadAccess(user, true);
|
||||
ACL.setWriteAccess(user, true);
|
||||
ACL.setReadAccess(user2, true);
|
||||
ACL.setWriteAccess(user2, true);
|
||||
obj.setACL(ACL);
|
||||
obj.set('owner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {}, {find: {"*": true}, get: {"*": true}, readUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}).then(() => {
|
||||
// user1 has ACL read/write but should be block
|
||||
return obj.fetch();
|
||||
}).then((objAgain) => {
|
||||
expect(objAgain.id).toBe(obj.id);
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('Should not fail fetching');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL read (ACL locked)', (done) => {
|
||||
/*
|
||||
tests:
|
||||
CLP: find/get open ({"*": true})
|
||||
PointerPerm: "owner" : read // proper owner
|
||||
ACL: logged in user has not access
|
||||
*/
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let user2 = new Parse.User();
|
||||
user.set({
|
||||
username: 'user1',
|
||||
password: 'password'
|
||||
});
|
||||
user2.set({
|
||||
username: 'user2',
|
||||
password: 'password'
|
||||
});
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, user2]).then(() => {
|
||||
let ACL = new Parse.ACL();
|
||||
ACL.setReadAccess(user, true);
|
||||
ACL.setWriteAccess(user, true);
|
||||
obj.setACL(ACL);
|
||||
obj.set('owner', user2);
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {}, {find: {"*": true}, get: {"*": true}, readUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user2', 'password');
|
||||
}).then(() => {
|
||||
// user2 has ACL read/write but should be block by ACL
|
||||
return obj.fetch();
|
||||
}).then(() => {
|
||||
fail('Should not succeed saving');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should let master key find objects', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('hello', 'world');
|
||||
return object.save().then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {find: {}, get: {}, readUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find();
|
||||
}).then(() => {
|
||||
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.find({useMasterKey: true});
|
||||
}).then((objects) => {
|
||||
expect(objects.length).toBe(1);
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('master key should find the object');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should let master key get objects', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('hello', 'world');
|
||||
return object.save().then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {find: {}, get: {}, readUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.get(object.id);
|
||||
}).then(() => {
|
||||
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
let q = new Parse.Query('AnObject');
|
||||
return q.get(object.id, {useMasterKey: true});
|
||||
}).then((objectAgain) => {
|
||||
expect(objectAgain).not.toBeUndefined();
|
||||
expect(objectAgain.id).toBe(object.id);
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('master key should find the object');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it_exclude_dbs(['postgres'])('should let master key update objects', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('hello', 'world');
|
||||
return object.save().then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {update: {}, writeUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return object.save({'hello': 'bar'});
|
||||
}).then(() => {
|
||||
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
return object.save({'hello': 'baz'}, {useMasterKey: true});
|
||||
}).then((objectAgain) => {
|
||||
expect(objectAgain.get('hello')).toBe('baz');
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('master key should save the object');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should let master key delete objects', (done) => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
let user = new Parse.User();
|
||||
let object = new Parse.Object('AnObject');
|
||||
object.set('hello', 'world');
|
||||
return object.save().then(() => {
|
||||
return config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: ['owner']});
|
||||
});
|
||||
}).then(() => {
|
||||
return object.destroy();
|
||||
}).then(() => {
|
||||
fail();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(101);
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
return object.destroy({useMasterKey: true});
|
||||
}).then((objectAgain) => {
|
||||
done();
|
||||
}, (err) => {
|
||||
fail('master key should destroy the object');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should fail with invalid pointer perms', () => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.addClassIfNotExists('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: 'owner'});
|
||||
}).catch((err) => {
|
||||
expect(err.code).toBe(Parse.Error.INVALID_JSON);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail with invalid pointer perms', () => {
|
||||
let config = new Config(Parse.applicationId);
|
||||
config.database.loadSchema().then((schema) => {
|
||||
// Lock the update, and let only owner write
|
||||
return schema.addClassIfNotExists('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: ['owner', 'invalid']});
|
||||
}).catch((err) => {
|
||||
expect(err.code).toBe(Parse.Error.INVALID_JSON);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
var PromiseRouter = require("../src/PromiseRouter").default;
|
||||
|
||||
describe("PromiseRouter", () => {
|
||||
|
||||
it("should properly handle rejects", (done) => {
|
||||
var router = new PromiseRouter();
|
||||
router.route("GET", "/dummy", (req)=> {
|
||||
@@ -12,7 +11,7 @@ describe("PromiseRouter", () => {
|
||||
}, (req) => {
|
||||
fail("this should not be called");
|
||||
});
|
||||
|
||||
|
||||
router.routes[0].handler({}).then((result) => {
|
||||
console.error(result);
|
||||
fail("this should not be called");
|
||||
@@ -23,4 +22,4 @@ describe("PromiseRouter", () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
+18
-39
@@ -2,43 +2,33 @@
|
||||
var request = require('request');
|
||||
|
||||
describe("public API", () => {
|
||||
beforeEach(done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
});
|
||||
done();
|
||||
})
|
||||
it("should get invalid_link.html", (done) => {
|
||||
request('http://localhost:8378/1/apps/invalid_link.html', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should get choose_password", (done) => {
|
||||
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
})
|
||||
.then(() => {
|
||||
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it("should get verify_email_success.html", (done) => {
|
||||
request('http://localhost:8378/1/apps/verify_email_success.html', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should get password_reset_success.html", (done) => {
|
||||
request('http://localhost:8378/1/apps/password_reset_success.html', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(200);
|
||||
@@ -48,20 +38,9 @@ describe("public API", () => {
|
||||
});
|
||||
|
||||
describe("public API without publicServerURL", () => {
|
||||
beforeEach(done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
});
|
||||
done();
|
||||
beforeEach(done => {
|
||||
reconfigureServer({ appName: 'unused' })
|
||||
.then(done, fail);
|
||||
})
|
||||
it("should get 404 on verify_email", (done) => {
|
||||
request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse, body) => {
|
||||
@@ -69,14 +48,14 @@ describe("public API without publicServerURL", () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should get 404 choose_password", (done) => {
|
||||
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should get 404 on request_password_reset", (done) => {
|
||||
request('http://localhost:8378/1/apps/test/request_password_reset', (err, httpResponse, body) => {
|
||||
expect(httpResponse.statusCode).toBe(404);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
var request = require("request");
|
||||
|
||||
|
||||
|
||||
function createProduct() {
|
||||
const file = new Parse.File("name", {
|
||||
base64: new Buffer("download_file", "utf-8").toString("base64")
|
||||
@@ -9,7 +7,7 @@ function createProduct() {
|
||||
return file.save().then(function(){
|
||||
var product = new Parse.Object("_Product");
|
||||
product.set({
|
||||
download: file,
|
||||
download: file,
|
||||
icon: file,
|
||||
title: "a product",
|
||||
subtitle: "a product",
|
||||
@@ -18,21 +16,19 @@ function createProduct() {
|
||||
})
|
||||
return product.save();
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
describe("test validate_receipt endpoint", () => {
|
||||
|
||||
beforeEach( done => {
|
||||
createProduct().then(done).fail(function(err){
|
||||
console.error(err);
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
it("should bypass appstore validation", (done) => {
|
||||
|
||||
|
||||
it_exclude_dbs(['postgres'])("should bypass appstore validation", (done) => {
|
||||
|
||||
request.post({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -40,7 +36,7 @@ describe("test validate_receipt endpoint", () => {
|
||||
url: 'http://localhost:8378/1/validate_purchase',
|
||||
json: true,
|
||||
body: {
|
||||
productIdentifier: "a-product",
|
||||
productIdentifier: "a-product",
|
||||
receipt: {
|
||||
__type: "Bytes",
|
||||
base64: new Buffer("receipt", "utf-8").toString("base64")
|
||||
@@ -63,7 +59,7 @@ describe("test validate_receipt endpoint", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should fail for missing receipt", (done) => {
|
||||
request.post({
|
||||
headers: {
|
||||
@@ -72,7 +68,7 @@ describe("test validate_receipt endpoint", () => {
|
||||
url: 'http://localhost:8378/1/validate_purchase',
|
||||
json: true,
|
||||
body: {
|
||||
productIdentifier: "a-product",
|
||||
productIdentifier: "a-product",
|
||||
bypassAppStoreValidation: true
|
||||
}
|
||||
}, function(err, res, body){
|
||||
@@ -85,7 +81,7 @@ describe("test validate_receipt endpoint", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should fail for missing product identifier", (done) => {
|
||||
request.post({
|
||||
headers: {
|
||||
@@ -110,9 +106,9 @@ describe("test validate_receipt endpoint", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should bypass appstore validation and not find product", (done) => {
|
||||
|
||||
|
||||
request.post({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -120,7 +116,7 @@ describe("test validate_receipt endpoint", () => {
|
||||
url: 'http://localhost:8378/1/validate_purchase',
|
||||
json: true,
|
||||
body: {
|
||||
productIdentifier: "another-product",
|
||||
productIdentifier: "another-product",
|
||||
receipt: {
|
||||
__type: "Bytes",
|
||||
base64: new Buffer("receipt", "utf-8").toString("base64")
|
||||
@@ -138,9 +134,8 @@ describe("test validate_receipt endpoint", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail at appstore validation", (done) => {
|
||||
|
||||
|
||||
it("should fail at appstore validation", done => {
|
||||
request.post({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -148,7 +143,7 @@ describe("test validate_receipt endpoint", () => {
|
||||
url: 'http://localhost:8378/1/validate_purchase',
|
||||
json: true,
|
||||
body: {
|
||||
productIdentifier: "a-product",
|
||||
productIdentifier: "a-product",
|
||||
receipt: {
|
||||
__type: "Bytes",
|
||||
base64: new Buffer("receipt", "utf-8").toString("base64")
|
||||
@@ -164,8 +159,8 @@ describe("test validate_receipt endpoint", () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not create a _Product", (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])("should not create a _Product", (done) => {
|
||||
var product = new Parse.Object("_Product");
|
||||
product.save().then(function(){
|
||||
fail("Should not be able to save");
|
||||
@@ -175,8 +170,8 @@ describe("test validate_receipt endpoint", () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should be able to update a _Product", (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])("should be able to update a _Product", (done) => {
|
||||
var query = new Parse.Query("_Product");
|
||||
query.first().then(function(product){
|
||||
product.set("title", "a new title");
|
||||
@@ -190,8 +185,8 @@ describe("test validate_receipt endpoint", () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not be able to remove a require key in a _Product", (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])("should not be able to remove a require key in a _Product", (done) => {
|
||||
var query = new Parse.Query("_Product");
|
||||
query.first().then(function(product){
|
||||
product.unset("title");
|
||||
@@ -205,5 +200,4 @@ describe("test validate_receipt endpoint", () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
+51
-11
@@ -130,7 +130,7 @@ describe('PushController', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('properly increment badges', (done) => {
|
||||
it_exclude_dbs(['postgres'])('properly increment badges', (done) => {
|
||||
|
||||
var payload = {data:{
|
||||
alert: "Hello World!",
|
||||
@@ -178,20 +178,19 @@ describe('PushController', () => {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
|
||||
Parse.Object.saveAll(installations).then((installations) => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then((result) => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('properly set badges to 1', (done) => {
|
||||
it_exclude_dbs(['postgres'])('properly set badges to 1', (done) => {
|
||||
|
||||
var payload = {data: {
|
||||
alert: "Hello World!",
|
||||
@@ -227,20 +226,19 @@ describe('PushController', () => {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
|
||||
Parse.Object.saveAll(installations).then((installations) => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then((result) => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('properly creates _PushStatus', (done) => {
|
||||
it_exclude_dbs(['postgres'])('properly creates _PushStatus', (done) => {
|
||||
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
@@ -279,7 +277,7 @@ describe('PushController', () => {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
|
||||
Parse.Object.saveAll(installations).then(() => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then((result) => {
|
||||
@@ -295,9 +293,12 @@ describe('PushController', () => {
|
||||
expect(results.length).toBe(1);
|
||||
let result = results[0];
|
||||
expect(result.createdAt instanceof Date).toBe(true);
|
||||
expect(result.updatedAt instanceof Date).toBe(true);
|
||||
expect(result.id.length).toBe(10);
|
||||
expect(result.get('source')).toEqual('rest');
|
||||
expect(result.get('query')).toEqual(JSON.stringify({}));
|
||||
expect(result.get('payload')).toEqual(payload.data);
|
||||
expect(typeof result.get('payload')).toEqual("string");
|
||||
expect(JSON.parse(result.get('payload'))).toEqual(payload.data);
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
expect(result.get('numSent')).toEqual(10);
|
||||
expect(result.get('sentPerType')).toEqual({
|
||||
@@ -317,7 +318,46 @@ describe('PushController', () => {
|
||||
|
||||
});
|
||||
|
||||
it('should support full RESTQuery for increment', (done) => {
|
||||
it_exclude_dbs(['postgres'])('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, defaultConfiguration.push);
|
||||
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_exclude_dbs(['postgres'])('should support full RESTQuery for increment', (done) => {
|
||||
var payload = {data: {
|
||||
alert: "Hello World!",
|
||||
badge: 'Increment',
|
||||
@@ -348,7 +388,7 @@ describe('PushController', () => {
|
||||
}
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId);
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
|
||||
pushController.sendPush(payload, where, config, auth).then((result) => {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
|
||||
@@ -67,8 +67,8 @@ describe('PushRouter', () => {
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('sends a push through REST', (done) => {
|
||||
|
||||
it_exclude_dbs(['postgres'])('sends a push through REST', (done) => {
|
||||
request.post({
|
||||
url: Parse.serverURL+"/push",
|
||||
json: true,
|
||||
@@ -82,8 +82,11 @@ describe('PushRouter', () => {
|
||||
'X-Parse-Master-Key': Parse.masterKey
|
||||
}
|
||||
}, function(err, res, body){
|
||||
expect(res.headers['x-parse-push-status-id']).not.toBe(undefined);
|
||||
expect(res.headers['x-parse-push-status-id'].length).toBe(10);
|
||||
expect(res.headers[''])
|
||||
expect(body.result).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
+175
-55
@@ -1,79 +1,83 @@
|
||||
"use strict";
|
||||
// These tests check the "create" / "update" functionality of the REST API.
|
||||
var auth = require('../src/Auth');
|
||||
var cache = require('../src/cache');
|
||||
var Config = require('../src/Config');
|
||||
var DatabaseAdapter = require('../src/DatabaseAdapter');
|
||||
var Parse = require('parse/node').Parse;
|
||||
var rest = require('../src/rest');
|
||||
var request = require('request');
|
||||
|
||||
var config = new Config('test');
|
||||
var database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
|
||||
let database = config.database;
|
||||
|
||||
describe('rest create', () => {
|
||||
it('handles _id', (done) => {
|
||||
rest.create(config, auth.nobody(config), 'Foo', {}).then(() => {
|
||||
return database.mongoFind('Foo', {});
|
||||
}).then((results) => {
|
||||
it_exclude_dbs(['postgres'])('handles _id', done => {
|
||||
rest.create(config, auth.nobody(config), 'Foo', {})
|
||||
.then(() => database.adapter.find('Foo', { fields: {} }, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
var obj = results[0];
|
||||
expect(typeof obj._id).toEqual('string');
|
||||
expect(obj.objectId).toBeUndefined();
|
||||
expect(typeof obj.objectId).toEqual('string');
|
||||
expect(obj._id).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles array, object, date', (done) => {
|
||||
it_exclude_dbs(['postgres'])('handles array, object, date', (done) => {
|
||||
let now = new Date();
|
||||
var obj = {
|
||||
array: [1, 2, 3],
|
||||
object: {foo: 'bar'},
|
||||
date: Parse._encode(new Date()),
|
||||
date: Parse._encode(now),
|
||||
};
|
||||
rest.create(config, auth.nobody(config), 'MyClass', obj).then(() => {
|
||||
return database.mongoFind('MyClass', {}, {});
|
||||
}).then((results) => {
|
||||
rest.create(config, auth.nobody(config), 'MyClass', obj)
|
||||
.then(() => database.adapter.find('MyClass', { fields: {
|
||||
array: { type: 'Array' },
|
||||
object: { type: 'Object' },
|
||||
date: { type: 'Date' },
|
||||
} }, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
var mob = results[0];
|
||||
expect(mob.array instanceof Array).toBe(true);
|
||||
expect(typeof mob.object).toBe('object');
|
||||
expect(mob.date instanceof Date).toBe(true);
|
||||
expect(mob.date.__type).toBe('Date');
|
||||
expect(new Date(mob.date.iso).getTime()).toBe(now.getTime());
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles object and subdocument', (done) => {
|
||||
var obj = {
|
||||
subdoc: {foo: 'bar', wu: 'tan'},
|
||||
};
|
||||
rest.create(config, auth.nobody(config), 'MyClass', obj).then(() => {
|
||||
return database.mongoFind('MyClass', {}, {});
|
||||
}).then((results) => {
|
||||
it_exclude_dbs(['postgres'])('handles object and subdocument', done => {
|
||||
let obj = { subdoc: {foo: 'bar', wu: 'tan'} };
|
||||
rest.create(config, auth.nobody(config), 'MyClass', obj)
|
||||
.then(() => database.adapter.find('MyClass', { fields: {} }, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
var mob = results[0];
|
||||
let mob = results[0];
|
||||
expect(typeof mob.subdoc).toBe('object');
|
||||
expect(mob.subdoc.foo).toBe('bar');
|
||||
expect(mob.subdoc.wu).toBe('tan');
|
||||
expect(typeof mob._id).toEqual('string');
|
||||
|
||||
var obj = {
|
||||
'subdoc.wu': 'clan',
|
||||
};
|
||||
|
||||
rest.update(config, auth.nobody(config), 'MyClass', mob._id, obj).then(() => {
|
||||
return database.mongoFind('MyClass', {}, {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
var mob = results[0];
|
||||
expect(typeof mob.subdoc).toBe('object');
|
||||
expect(mob.subdoc.foo).toBe('bar');
|
||||
expect(mob.subdoc.wu).toBe('clan');
|
||||
done();
|
||||
});
|
||||
|
||||
expect(typeof mob.objectId).toEqual('string');
|
||||
let obj = { 'subdoc.wu': 'clan' };
|
||||
return rest.update(config, auth.nobody(config), 'MyClass', mob.objectId, obj)
|
||||
})
|
||||
.then(() => database.adapter.find('MyClass', { fields: {} }, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
let mob = results[0];
|
||||
expect(typeof mob.subdoc).toBe('object');
|
||||
expect(mob.subdoc.foo).toBe('bar');
|
||||
expect(mob.subdoc.wu).toBe('clan');
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
fail();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles create on non-existent class when disabled client class creation', (done) => {
|
||||
it_exclude_dbs(['postgres'])('handles create on non-existent class when disabled client class creation', (done) => {
|
||||
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
|
||||
rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {})
|
||||
.then(() => {
|
||||
@@ -87,6 +91,21 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('handles create on existent class when disabled client class creation', (done) => {
|
||||
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('ClientClassCreation', {}))
|
||||
.then(actualSchema => {
|
||||
expect(actualSchema.className).toEqual('ClientClassCreation');
|
||||
return rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {});
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
}, err => {
|
||||
fail('Should not throw error')
|
||||
});
|
||||
});
|
||||
|
||||
it('handles user signup', (done) => {
|
||||
var user = {
|
||||
username: 'asdf',
|
||||
@@ -103,7 +122,7 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles anonymous user signup', (done) => {
|
||||
it_exclude_dbs(['postgres'])('handles anonymous user signup', (done) => {
|
||||
var data1 = {
|
||||
authData: {
|
||||
anonymous: {
|
||||
@@ -147,7 +166,7 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles anonymous user signup and upgrade to new user', (done) => {
|
||||
it_exclude_dbs(['postgres'])('handles anonymous user signup and upgrade to new user', (done) => {
|
||||
var data1 = {
|
||||
authData: {
|
||||
anonymous: {
|
||||
@@ -207,7 +226,7 @@ describe('rest create', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('test facebook signup and login', (done) => {
|
||||
it_exclude_dbs(['postgres'])('test facebook signup and login', (done) => {
|
||||
var data = {
|
||||
authData: {
|
||||
facebook: {
|
||||
@@ -240,8 +259,8 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('stores pointers with a _p_ prefix', (done) => {
|
||||
var obj = {
|
||||
it_exclude_dbs(['postgres'])('stores pointers', done => {
|
||||
let obj = {
|
||||
foo: 'bar',
|
||||
aPointer: {
|
||||
__type: 'Pointer',
|
||||
@@ -250,17 +269,23 @@ describe('rest create', () => {
|
||||
}
|
||||
};
|
||||
rest.create(config, auth.nobody(config), 'APointerDarkly', obj)
|
||||
.then((r) => {
|
||||
return database.mongoFind('APointerDarkly', {});
|
||||
}).then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
var output = results[0];
|
||||
expect(typeof output._id).toEqual('string');
|
||||
expect(typeof output._p_aPointer).toEqual('string');
|
||||
expect(output._p_aPointer).toEqual('JustThePointer$qwerty');
|
||||
expect(output.aPointer).toBeUndefined();
|
||||
done();
|
||||
.then(() => database.adapter.find('APointerDarkly', { fields: {
|
||||
foo: { type: 'String' },
|
||||
aPointer: { type: 'Pointer', targetClass: 'JustThePointer' },
|
||||
}}, {}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
let output = results[0];
|
||||
expect(typeof output.foo).toEqual('string');
|
||||
expect(typeof output._p_aPointer).toEqual('undefined');
|
||||
expect(output._p_aPointer).toBeUndefined();
|
||||
expect(output.aPointer).toEqual({
|
||||
__type: 'Pointer',
|
||||
className: 'JustThePointer',
|
||||
objectId: 'qwerty'
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("cannot set objectId", (done) => {
|
||||
@@ -284,4 +309,99 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])("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_exclude_dbs(['postgres'])("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();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])("can create a session with no expiration", (done) => {
|
||||
var user = {
|
||||
username: 'asdf',
|
||||
password: 'zxcv',
|
||||
foo: 'bar'
|
||||
};
|
||||
config.expireInactiveSessions = false;
|
||||
|
||||
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];
|
||||
expect(session.expiresAt).toBeUndefined();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+78
-4
@@ -1,3 +1,4 @@
|
||||
'use strict'
|
||||
// These tests check the "find" functionality of the REST API.
|
||||
var auth = require('../src/Auth');
|
||||
var cache = require('../src/cache');
|
||||
@@ -8,6 +9,7 @@ var querystring = require('querystring');
|
||||
var request = require('request');
|
||||
|
||||
var config = new Config('test');
|
||||
let database = config.database;
|
||||
var nobody = auth.nobody(config);
|
||||
|
||||
describe('rest query', () => {
|
||||
@@ -35,8 +37,38 @@ describe('rest query', () => {
|
||||
});
|
||||
});
|
||||
|
||||
var data = {
|
||||
username: 'blah',
|
||||
password: 'pass',
|
||||
sessionToken: 'abc123',
|
||||
}
|
||||
|
||||
it_exclude_dbs(['postgres'])('query for user w/ legacy credentials without masterKey has them stripped from results', done => {
|
||||
database.create('_User', data).then(() => {
|
||||
return rest.find(config, nobody, '_User')
|
||||
}).then((result) => {
|
||||
var user = result.results[0];
|
||||
expect(user.username).toEqual('blah');
|
||||
expect(user.sessionToken).toBeUndefined();
|
||||
expect(user.password).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('query for user w/ legacy credentials with masterKey has them stripped from results', done => {
|
||||
database.create('_User', data).then(() => {
|
||||
return rest.find(config, {isMaster: true}, '_User')
|
||||
}).then((result) => {
|
||||
var user = result.results[0];
|
||||
expect(user.username).toEqual('blah');
|
||||
expect(user.sessionToken).toBeUndefined();
|
||||
expect(user.password).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// Created to test a scenario in AnyPic
|
||||
it('query with include', (done) => {
|
||||
it_exclude_dbs(['postgres'])('query with include', (done) => {
|
||||
var photo = {
|
||||
foo: 'bar'
|
||||
};
|
||||
@@ -95,7 +127,7 @@ describe('rest query', () => {
|
||||
}).catch((error) => { console.log(error); });
|
||||
});
|
||||
|
||||
it('query non-existent class when disabled client class creation', (done) => {
|
||||
it_exclude_dbs(['postgres'])('query non-existent class when disabled client class creation', (done) => {
|
||||
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
|
||||
rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {})
|
||||
.then(() => {
|
||||
@@ -109,6 +141,22 @@ describe('rest query', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('query existent class when disabled client class creation', (done) => {
|
||||
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('ClientClassCreation', {}))
|
||||
.then(actualSchema => {
|
||||
expect(actualSchema.className).toEqual('ClientClassCreation');
|
||||
return rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {});
|
||||
})
|
||||
.then((result) => {
|
||||
expect(result.results.length).toEqual(0);
|
||||
done();
|
||||
}, err => {
|
||||
fail('Should not throw error')
|
||||
});
|
||||
});
|
||||
|
||||
it('query with wrongly encoded parameter', (done) => {
|
||||
rest.create(config, nobody, 'TestParameterEncode', {foo: 'bar'}
|
||||
).then(() => {
|
||||
@@ -130,7 +178,6 @@ describe('rest query', () => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
|
||||
expect(b.error).toEqual('Improper encode of parameter');
|
||||
done();
|
||||
});
|
||||
}).then(() => {
|
||||
@@ -148,10 +195,37 @@ describe('rest query', () => {
|
||||
expect(error).toBe(null);
|
||||
var b = JSON.parse(body);
|
||||
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
|
||||
expect(b.error).toEqual('Improper encode of parameter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('query with limit = 0', (done) => {
|
||||
rest.create(config, nobody, 'TestObject', {foo: 'baz'}
|
||||
).then(() => {
|
||||
return rest.create(config, nobody,
|
||||
'TestObject', {foo: 'qux'});
|
||||
}).then(() => {
|
||||
return rest.find(config, nobody,
|
||||
'TestObject', {}, {limit: 0});
|
||||
}).then((response) => {
|
||||
expect(response.results.length).toEqual(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('query with limit = 0 and count = 1', (done) => {
|
||||
rest.create(config, nobody, 'TestObject', {foo: 'baz'}
|
||||
).then(() => {
|
||||
return rest.create(config, nobody,
|
||||
'TestObject', {foo: 'qux'});
|
||||
}).then(() => {
|
||||
return rest.find(config, nobody,
|
||||
'TestObject', {}, {limit: 0, count: 1});
|
||||
}).then((response) => {
|
||||
expect(response.results.length).toEqual(0);
|
||||
expect(response.count).toEqual(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+242
-176
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var Config = require('../src/Config');
|
||||
var Schema = require('../src/Schema');
|
||||
var SchemaController = require('../src/Controllers/SchemaController');
|
||||
var dd = require('deep-diff');
|
||||
|
||||
var config = new Config('test');
|
||||
@@ -19,7 +19,7 @@ var hasAllPODobject = () => {
|
||||
return obj;
|
||||
};
|
||||
|
||||
describe('Schema', () => {
|
||||
describe('SchemaController', () => {
|
||||
it('can validate one object', (done) => {
|
||||
config.database.loadSchema().then((schema) => {
|
||||
return schema.validateObject('TestObject', {a: 1, b: 'yo', c: false});
|
||||
@@ -95,7 +95,7 @@ describe('Schema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('class-level permissions test user', (done) => {
|
||||
it_exclude_dbs(['postgres'])('class-level permissions test user', (done) => {
|
||||
var user;
|
||||
createTestUser().then((u) => {
|
||||
user = u;
|
||||
@@ -120,57 +120,70 @@ describe('Schema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('class-level permissions test get', (done) => {
|
||||
var user;
|
||||
it_exclude_dbs(['postgres'])('class-level permissions test get', (done) => {
|
||||
var obj;
|
||||
createTestUser().then((u) => {
|
||||
user = u;
|
||||
return config.database.loadSchema();
|
||||
}).then((schema) => {
|
||||
// Just to create a valid class
|
||||
return schema.validateObject('Stuff', {foo: 'bar'});
|
||||
}).then((schema) => {
|
||||
var find = {};
|
||||
var get = {};
|
||||
get[user.id] = true;
|
||||
return schema.setPermissions('Stuff', {
|
||||
'find': find,
|
||||
'get': get
|
||||
});
|
||||
}).then((schema) => {
|
||||
obj = new Parse.Object('Stuff');
|
||||
obj.set('foo', 'bar');
|
||||
return obj.save();
|
||||
}).then((o) => {
|
||||
obj = o;
|
||||
var query = new Parse.Query('Stuff');
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
fail('Class permissions should have rejected this query.');
|
||||
done();
|
||||
}, (e) => {
|
||||
var query = new Parse.Query('Stuff');
|
||||
return query.get(obj.id).then((o) => {
|
||||
createTestUser()
|
||||
.then(user => {
|
||||
return config.database.loadSchema()
|
||||
// Create a valid class
|
||||
.then(schema => schema.validateObject('Stuff', {foo: 'bar'}))
|
||||
.then(schema => {
|
||||
var find = {};
|
||||
var get = {};
|
||||
get[user.id] = true;
|
||||
return schema.setPermissions('Stuff', {
|
||||
'create': {'*': true},
|
||||
'find': find,
|
||||
'get': get
|
||||
});
|
||||
}).then((schema) => {
|
||||
obj = new Parse.Object('Stuff');
|
||||
obj.set('foo', 'bar');
|
||||
return obj.save();
|
||||
}).then((o) => {
|
||||
obj = o;
|
||||
var query = new Parse.Query('Stuff');
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
fail('Class permissions should have rejected this query.');
|
||||
done();
|
||||
}, (e) => {
|
||||
fail('Class permissions should have allowed this get query');
|
||||
var query = new Parse.Query('Stuff');
|
||||
return query.get(obj.id).then((o) => {
|
||||
done();
|
||||
}, (e) => {
|
||||
fail('Class permissions should have allowed this get query');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can add classes without needing an object', done => {
|
||||
it_exclude_dbs(['postgres'])('can add classes without needing an object', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
foo: {type: 'String'}
|
||||
}))
|
||||
.then(result => {
|
||||
expect(result).toEqual({
|
||||
_id: 'NewClass',
|
||||
objectId: 'string',
|
||||
updatedAt: 'string',
|
||||
createdAt: 'string',
|
||||
foo: 'string',
|
||||
})
|
||||
.then(actualSchema => {
|
||||
const expectedSchema = {
|
||||
className: 'NewClass',
|
||||
fields: {
|
||||
objectId: { type: 'String' },
|
||||
updatedAt: { type: 'Date' },
|
||||
createdAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
foo: { type: 'String' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: { '*': true },
|
||||
},
|
||||
}
|
||||
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -194,22 +207,34 @@ describe('Schema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('will resolve class creation races appropriately', done => {
|
||||
it_exclude_dbs(['postgres'])('will resolve class creation races appropriately', done => {
|
||||
// If two callers race to create the same schema, the response to the
|
||||
// race loser should be the same as if they hadn't been racing.
|
||||
config.database.loadSchema()
|
||||
.then(schema => {
|
||||
var p1 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}});
|
||||
var p2 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}});
|
||||
Promise.race([p1, p2]) //Use race because we expect the first completed promise to be the successful one
|
||||
.then(response => {
|
||||
expect(response).toEqual({
|
||||
_id: 'NewClass',
|
||||
objectId: 'string',
|
||||
updatedAt: 'string',
|
||||
createdAt: 'string',
|
||||
foo: 'string',
|
||||
});
|
||||
Promise.race([p1, p2])
|
||||
.then(actualSchema => {
|
||||
const expectedSchema = {
|
||||
className: 'NewClass',
|
||||
fields: {
|
||||
objectId: { type: 'String' },
|
||||
updatedAt: { type: 'Date' },
|
||||
createdAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
foo: { type: 'String' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: { '*': true },
|
||||
},
|
||||
}
|
||||
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
|
||||
});
|
||||
Promise.all([p1,p2])
|
||||
.catch(error => {
|
||||
@@ -359,7 +384,7 @@ describe('Schema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('will create classes', done => {
|
||||
it_exclude_dbs(['postgres'])('will create classes', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
aNumber: {type: 'Number'},
|
||||
@@ -373,86 +398,139 @@ describe('Schema', () => {
|
||||
aPointer: {type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet'},
|
||||
aRelation: {type: 'Relation', targetClass: 'NewClass'},
|
||||
}))
|
||||
.then(mongoObj => {
|
||||
expect(mongoObj).toEqual({
|
||||
_id: 'NewClass',
|
||||
objectId: 'string',
|
||||
createdAt: 'string',
|
||||
updatedAt: 'string',
|
||||
aNumber: 'number',
|
||||
aString: 'string',
|
||||
aBool: 'boolean',
|
||||
aDate: 'date',
|
||||
aObject: 'object',
|
||||
aArray: 'array',
|
||||
aGeoPoint: 'geopoint',
|
||||
aFile: 'file',
|
||||
aPointer: '*ThisClassDoesNotExistYet',
|
||||
aRelation: 'relation<NewClass>',
|
||||
});
|
||||
.then(actualSchema => {
|
||||
const expectedSchema = {
|
||||
className: 'NewClass',
|
||||
fields: {
|
||||
objectId: { type: 'String' },
|
||||
updatedAt: { type: 'Date' },
|
||||
createdAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
aString: { type: 'String' },
|
||||
aNumber: { type: 'Number' },
|
||||
aString: { type: 'String' },
|
||||
aBool: { type: 'Boolean' },
|
||||
aDate: { type: 'Date' },
|
||||
aObject: { type: 'Object' },
|
||||
aArray: { type: 'Array' },
|
||||
aGeoPoint: { type: 'GeoPoint' },
|
||||
aFile: { type: 'File' },
|
||||
aPointer: { type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet' },
|
||||
aRelation: { type: 'Relation', targetClass: 'NewClass' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: { '*': true },
|
||||
},
|
||||
}
|
||||
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('creates the default fields for non-custom classes', done => {
|
||||
it_exclude_dbs(['postgres'])('creates the default fields for non-custom classes', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('_Installation', {
|
||||
foo: {type: 'Number'},
|
||||
}))
|
||||
.then(mongoObj => {
|
||||
expect(mongoObj).toEqual({
|
||||
_id: '_Installation',
|
||||
createdAt: 'string',
|
||||
updatedAt: 'string',
|
||||
objectId: 'string',
|
||||
foo: 'number',
|
||||
installationId: 'string',
|
||||
deviceToken: 'string',
|
||||
channels: 'array',
|
||||
deviceType: 'string',
|
||||
pushType: 'string',
|
||||
GCMSenderId: 'string',
|
||||
timeZone: 'string',
|
||||
localeIdentifier: 'string',
|
||||
badge: 'number',
|
||||
});
|
||||
.then(actualSchema => {
|
||||
const expectedSchema = {
|
||||
className: '_Installation',
|
||||
fields: {
|
||||
objectId: { type: 'String' },
|
||||
updatedAt: { type: 'Date' },
|
||||
createdAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
foo: { type: 'Number' },
|
||||
installationId: { type: 'String' },
|
||||
deviceToken: { type: 'String' },
|
||||
channels: { type: 'Array' },
|
||||
deviceType: { type: 'String' },
|
||||
pushType: { type: 'String' },
|
||||
GCMSenderId: { type: 'String' },
|
||||
timeZone: { type: 'String' },
|
||||
localeIdentifier: { type: 'String' },
|
||||
badge: { type: 'Number' },
|
||||
appVersion: { type: 'String' },
|
||||
appName: { type: 'String' },
|
||||
appIdentifier: { type: 'String' },
|
||||
parseVersion: { type: 'String' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: { '*': true },
|
||||
},
|
||||
}
|
||||
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('creates non-custom classes which include relation field', done => {
|
||||
it_exclude_dbs(['postgres'])('creates non-custom classes which include relation field', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('_Role', {}))
|
||||
.then(mongoObj => {
|
||||
expect(mongoObj).toEqual({
|
||||
_id: '_Role',
|
||||
createdAt: 'string',
|
||||
updatedAt: 'string',
|
||||
objectId: 'string',
|
||||
name: 'string',
|
||||
users: 'relation<_User>',
|
||||
roles: 'relation<_Role>',
|
||||
});
|
||||
.then(actualSchema => {
|
||||
const expectedSchema = {
|
||||
className: '_Role',
|
||||
fields: {
|
||||
objectId: { type: 'String' },
|
||||
updatedAt: { type: 'Date' },
|
||||
createdAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
name: { type: 'String' },
|
||||
users: { type: 'Relation', targetClass: '_User' },
|
||||
roles: { type: 'Relation', targetClass: '_Role' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: { '*': true },
|
||||
},
|
||||
};
|
||||
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('creates non-custom classes which include pointer field', done => {
|
||||
it_exclude_dbs(['postgres'])('creates non-custom classes which include pointer field', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('_Session', {}))
|
||||
.then(mongoObj => {
|
||||
expect(mongoObj).toEqual({
|
||||
_id: '_Session',
|
||||
createdAt: 'string',
|
||||
updatedAt: 'string',
|
||||
objectId: 'string',
|
||||
restricted: 'boolean',
|
||||
user: '*_User',
|
||||
installationId: 'string',
|
||||
sessionToken: 'string',
|
||||
expiresAt: 'date',
|
||||
createdWith: 'object'
|
||||
});
|
||||
.then(actualSchema => {
|
||||
const expectedSchema = {
|
||||
className: '_Session',
|
||||
fields: {
|
||||
objectId: { type: 'String' },
|
||||
updatedAt: { type: 'Date' },
|
||||
createdAt: { type: 'Date' },
|
||||
restricted: { type: 'Boolean' },
|
||||
user: { type: 'Pointer', targetClass: '_User' },
|
||||
installationId: { type: 'String' },
|
||||
sessionToken: { type: 'String' },
|
||||
expiresAt: { type: 'Date' },
|
||||
createdWith: { type: 'Object' },
|
||||
ACL: { type: 'ACL' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: { '*': true },
|
||||
},
|
||||
};
|
||||
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -470,7 +548,7 @@ describe('Schema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can check if a class exists', done => {
|
||||
it_exclude_dbs(['postgres'])('can check if a class exists', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => {
|
||||
return schema.addClassIfNotExists('NewClass', {})
|
||||
@@ -535,7 +613,7 @@ describe('Schema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to delete fields that dont exist', done => {
|
||||
it_exclude_dbs(['postgres'])('refuses to delete fields that dont exist', done => {
|
||||
hasAllPODobject().save()
|
||||
.then(() => config.database.loadSchema())
|
||||
.then(schema => schema.deleteField('missingField', 'HasAllPOD'))
|
||||
@@ -546,7 +624,7 @@ describe('Schema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('drops related collection when deleting relation field', done => {
|
||||
it_exclude_dbs(['postgres'])('drops related collection when deleting relation field', done => {
|
||||
var obj1 = hasAllPODobject();
|
||||
obj1.save()
|
||||
.then(savedObj1 => {
|
||||
@@ -577,20 +655,32 @@ describe('Schema', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can delete relation field when related _Join collection not exist', done => {
|
||||
it_exclude_dbs(['postgres'])('can delete relation field when related _Join collection not exist', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => {
|
||||
schema.addClassIfNotExists('NewClass', {
|
||||
relationField: {type: 'Relation', targetClass: '_User'}
|
||||
})
|
||||
.then(mongoObj => {
|
||||
expect(mongoObj).toEqual({
|
||||
_id: 'NewClass',
|
||||
objectId: 'string',
|
||||
updatedAt: 'string',
|
||||
createdAt: 'string',
|
||||
relationField: 'relation<_User>',
|
||||
});
|
||||
.then(actualSchema => {
|
||||
const expectedSchema = {
|
||||
className: 'NewClass',
|
||||
fields: {
|
||||
objectId: { type: 'String' },
|
||||
updatedAt: { type: 'Date' },
|
||||
createdAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
relationField: { type: 'Relation', targetClass: '_User' },
|
||||
},
|
||||
classLevelPermissions: {
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: { '*': true },
|
||||
},
|
||||
};
|
||||
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
|
||||
})
|
||||
.then(() => config.database.collectionExists('_Join:relationField:NewClass'))
|
||||
.then(exist => {
|
||||
@@ -599,17 +689,19 @@ describe('Schema', () => {
|
||||
.then(() => schema.deleteField('relationField', 'NewClass', config.database))
|
||||
.then(() => schema.reloadData())
|
||||
.then(() => {
|
||||
expect(schema['data']['NewClass']).toEqual({
|
||||
objectId: 'string',
|
||||
updatedAt: 'string',
|
||||
createdAt: 'string'
|
||||
});
|
||||
const expectedSchema = {
|
||||
objectId: { type: 'String' },
|
||||
updatedAt: { type: 'Date' },
|
||||
createdAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
};
|
||||
expect(dd(schema.data.NewClass, expectedSchema)).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can delete string fields and resave as number field', done => {
|
||||
it_exclude_dbs(['postgres'])('can delete string fields and resave as number field', done => {
|
||||
Parse.Object.disableSingleInstance();
|
||||
var obj1 = hasAllPODobject();
|
||||
var obj2 = hasAllPODobject();
|
||||
@@ -630,10 +722,14 @@ describe('Schema', () => {
|
||||
done();
|
||||
Parse.Object.enableSingleInstance();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
fail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can delete pointer fields and resave as string', done => {
|
||||
it_exclude_dbs(['postgres'])('can delete pointer fields and resave as string', done => {
|
||||
Parse.Object.disableSingleInstance();
|
||||
var obj1 = new Parse.Object('NewClass');
|
||||
obj1.save()
|
||||
@@ -660,9 +756,9 @@ describe('Schema', () => {
|
||||
});
|
||||
|
||||
it('can merge schemas', done => {
|
||||
expect(Schema.buildMergedSchemaObject({
|
||||
expect(SchemaController.buildMergedSchemaObject({
|
||||
_id: 'SomeClass',
|
||||
someType: 'number'
|
||||
someType: { type: 'Number' }
|
||||
}, {
|
||||
newType: {type: 'Number'}
|
||||
})).toEqual({
|
||||
@@ -673,10 +769,10 @@ describe('Schema', () => {
|
||||
});
|
||||
|
||||
it('can merge deletions', done => {
|
||||
expect(Schema.buildMergedSchemaObject({
|
||||
expect(SchemaController.buildMergedSchemaObject({
|
||||
_id: 'SomeClass',
|
||||
someType: 'number',
|
||||
outDatedType: 'string',
|
||||
someType: { type: 'Number' },
|
||||
outDatedType: { type: 'String' },
|
||||
},{
|
||||
newType: {type: 'GeoPoint'},
|
||||
outDatedType: {__op: 'Delete'},
|
||||
@@ -688,47 +784,17 @@ describe('Schema', () => {
|
||||
});
|
||||
|
||||
it('ignore default field when merge with system class', done => {
|
||||
expect(Schema.buildMergedSchemaObject({
|
||||
expect(SchemaController.buildMergedSchemaObject({
|
||||
_id: '_User',
|
||||
username: 'string',
|
||||
password: 'string',
|
||||
authData: 'object',
|
||||
email: 'string',
|
||||
emailVerified: 'boolean'
|
||||
username: { type: 'String' },
|
||||
password: { type: 'String' },
|
||||
email: { type: 'String' },
|
||||
emailVerified: { type: 'Boolean' },
|
||||
},{
|
||||
authData: {type: 'string'},
|
||||
customField: {type: 'string'},
|
||||
emailVerified: { type: 'String' },
|
||||
customField: { type: 'String' },
|
||||
})).toEqual({
|
||||
customField: {type: 'string'}
|
||||
});
|
||||
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",
|
||||
customField: { type: 'String' }
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
'use strict';
|
||||
|
||||
var request = require('request');
|
||||
const Parse = require("parse/node");
|
||||
let Config = require('../src/Config');
|
||||
|
||||
describe('Uniqueness', function() {
|
||||
it('fail when create duplicate value in unique field', done => {
|
||||
let obj = new Parse.Object('UniqueField');
|
||||
obj.set('unique', 'value');
|
||||
obj.save().then(() => {
|
||||
expect(obj.id).not.toBeUndefined();
|
||||
let config = new Config('test');
|
||||
return config.database.adapter.ensureUniqueness('UniqueField', { fields: { unique: { __type: 'String' } } }, ['unique'])
|
||||
})
|
||||
.then(() => {
|
||||
let obj = new Parse.Object('UniqueField');
|
||||
obj.set('unique', 'value');
|
||||
return obj.save()
|
||||
}).then(() => {
|
||||
fail('Saving duplicate field should have failed');
|
||||
done();
|
||||
}, error => {
|
||||
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('unique indexing works on pointer fields', done => {
|
||||
let obj = new Parse.Object('UniquePointer');
|
||||
obj.save({ string: 'who cares' })
|
||||
.then(() => obj.save({ ptr: obj }))
|
||||
.then(() => {
|
||||
let config = new Config('test');
|
||||
return config.database.adapter.ensureUniqueness('UniquePointer', { fields: {
|
||||
string: { __type: 'String' },
|
||||
ptr: { __type: 'Pointer', targetClass: 'UniquePointer' }
|
||||
} }, ['ptr']);
|
||||
})
|
||||
.then(() => {
|
||||
let newObj = new Parse.Object('UniquePointer')
|
||||
newObj.set('ptr', obj)
|
||||
return newObj.save()
|
||||
})
|
||||
.then(() => {
|
||||
fail('save should have failed due to duplicate value');
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('fails when attempting to ensure uniqueness of fields that are not currently unique', done => {
|
||||
let o1 = new Parse.Object('UniqueFail');
|
||||
o1.set('key', 'val');
|
||||
let o2 = new Parse.Object('UniqueFail');
|
||||
o2.set('key', 'val');
|
||||
Parse.Object.saveAll([o1, o2])
|
||||
.then(() => {
|
||||
let config = new Config('test');
|
||||
return config.database.adapter.ensureUniqueness('UniqueFail', { fields: { key: { __type: 'String' } } }, ['key']);
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('can do compound uniqueness', done => {
|
||||
let config = new Config('test');
|
||||
config.database.adapter.ensureUniqueness('CompoundUnique', { fields: { k1: { __type: 'String' }, k2: { __type: 'String' } } }, ['k1', 'k2'])
|
||||
.then(() => {
|
||||
let o1 = new Parse.Object('CompoundUnique');
|
||||
o1.set('k1', 'v1');
|
||||
o1.set('k2', 'v2');
|
||||
return o1.save();
|
||||
})
|
||||
.then(() => {
|
||||
let o2 = new Parse.Object('CompoundUnique');
|
||||
o2.set('k1', 'v1');
|
||||
o2.set('k2', 'not a dupe');
|
||||
return o2.save();
|
||||
})
|
||||
.then(() => {
|
||||
let o3 = new Parse.Object('CompoundUnique');
|
||||
o3.set('k1', 'not a dupe');
|
||||
o3.set('k2', 'v2');
|
||||
return o3.save();
|
||||
})
|
||||
.then(() => {
|
||||
let o4 = new Parse.Object('CompoundUnique');
|
||||
o4.set('k1', 'v1');
|
||||
o4.set('k2', 'v2');
|
||||
return o4.save();
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,13 @@
|
||||
"use strict";
|
||||
|
||||
var request = require('request');
|
||||
var Config = require("../src/Config");
|
||||
describe("Custom Pages Configuration", () => {
|
||||
let MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions');
|
||||
let request = require('request');
|
||||
let Config = require("../src/Config");
|
||||
|
||||
describe("Custom Pages, Email Verification, Password Reset", () => {
|
||||
it("should set the custom pages", (done) => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
customPages: {
|
||||
invalidLink: "myInvalidLink",
|
||||
verifyEmailSuccess: "myVerifyEmailSuccess",
|
||||
@@ -22,60 +15,51 @@ describe("Custom Pages Configuration", () => {
|
||||
passwordResetSuccess: "myPasswordResetSuccess"
|
||||
},
|
||||
publicServerURL: "https://my.public.server.com/1"
|
||||
})
|
||||
.then(() => {
|
||||
var config = new Config("test");
|
||||
expect(config.invalidLinkURL).toEqual("myInvalidLink");
|
||||
expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess");
|
||||
expect(config.choosePasswordURL).toEqual("myChoosePassword");
|
||||
expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess");
|
||||
expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email");
|
||||
expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset");
|
||||
done();
|
||||
});
|
||||
|
||||
var config = new Config("test");
|
||||
|
||||
expect(config.invalidLinkURL).toEqual("myInvalidLink");
|
||||
expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess");
|
||||
expect(config.choosePasswordURL).toEqual("myChoosePassword");
|
||||
expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess");
|
||||
expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email");
|
||||
expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Email Verification", () => {
|
||||
it('sends verification email if email verification is enabled', done => {
|
||||
it_exclude_dbs(['postgres'])('sends verification email if email verification is enabled', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.setEmail('cool_guy@parse.com');
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
})
|
||||
.then(() => {
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.setEmail('testIfEnabled@parse.com');
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,150 +69,129 @@ describe("Email Verification", () => {
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(undefined);
|
||||
})
|
||||
.then(() => {
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does send a validation email when updating the email', done => {
|
||||
it_exclude_dbs(['postgres'])('does send a validation email when updating the email', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then((user) => {
|
||||
user.set("email", "cool_guy@parse.com");
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return user.fetch();
|
||||
}).then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
// Wait as on update email, we need to fetch the username
|
||||
setTimeout(function(){
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then((user) => {
|
||||
user.set("email", "testWhenUpdating@parse.com");
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return user.fetch();
|
||||
}).then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
// Wait as on update email, we need to fetch the username
|
||||
setTimeout(function(){
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does send a validation email with valid verification link when updating the email', done => {
|
||||
it_exclude_dbs(['postgres'])('does send a validation email with valid verification link when updating the email', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail').and.callFake((options) => {
|
||||
expect(options.link).not.toBeNull();
|
||||
expect(options.link).not.toMatch(/token=undefined/);
|
||||
Promise.resolve();
|
||||
});
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then((user) => {
|
||||
user.set("email", "cool_guy@parse.com");
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return user.fetch();
|
||||
}).then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
// Wait as on update email, we need to fetch the username
|
||||
setTimeout(function(){
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
spyOn(emailAdapter, 'sendVerificationEmail').and.callFake((options) => {
|
||||
expect(options.link).not.toBeNull();
|
||||
expect(options.link).not.toMatch(/token=undefined/);
|
||||
Promise.resolve();
|
||||
});
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
|
||||
user.fetch()
|
||||
.then((user) => {
|
||||
user.set("email", "testValidLinkWhenUpdating@parse.com");
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return user.fetch();
|
||||
}).then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
// Wait as on update email, we need to fetch the username
|
||||
setTimeout(function(){
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does send with a simple adapter', done => {
|
||||
it_exclude_dbs(['postgres'])('does send with a simple adapter', done => {
|
||||
var calls = 0;
|
||||
var emailAdapter = {
|
||||
sendMail: function(options){
|
||||
expect(options.to).toBe('cool_guy@parse.com');
|
||||
expect(options.to).toBe('testSendSimpleAdapter@parse.com');
|
||||
if (calls == 0) {
|
||||
expect(options.subject).toEqual('Please verify your e-mail for My Cool App');
|
||||
expect(options.text.match(/verify_email/)).not.toBe(null);
|
||||
@@ -240,42 +203,70 @@ describe("Email Verification", () => {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'My Cool App',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set("email", "cool_guy@parse.com");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(calls).toBe(1);
|
||||
user.fetch()
|
||||
.then((user) => {
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return Parse.User.requestPasswordReset("cool_guy@parse.com");
|
||||
}).then(() => {
|
||||
expect(calls).toBe(2);
|
||||
})
|
||||
.then(() => {
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set("email", "testSendSimpleAdapter@parse.com");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
expect(calls).toBe(1);
|
||||
user.fetch()
|
||||
.then((user) => {
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return Parse.User.requestPasswordReset("testSendSimpleAdapter@parse.com").catch((err) => {
|
||||
fail('Should not fail requesting a password');
|
||||
done();
|
||||
})
|
||||
}).then(() => {
|
||||
expect(calls).toBe(2);
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('fails if you include an emailAdapter, set verifyUserEmails to false, dont set a publicServerURL, and try to send a password reset email (regression test for #1649)', done => {
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
verifyUserEmails: false,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
})
|
||||
.then(() => {
|
||||
let user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set("email", "testInvalidConfig@parse.com");
|
||||
user.signUp(null)
|
||||
.then(user => Parse.User.requestPasswordReset("testInvalidConfig@parse.com"))
|
||||
.then(result => {
|
||||
console.log(result);
|
||||
fail('sending password reset email should not have succeeded');
|
||||
done();
|
||||
}
|
||||
}, error => {
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
fail(JSON.stringify(error));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -285,41 +276,34 @@ describe("Email Verification", () => {
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: false,
|
||||
emailAdapter: emailAdapter,
|
||||
});
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(emailAdapter.sendVerificationEmail.calls.count()).toEqual(0);
|
||||
expect(user.get('emailVerified')).toEqual(undefined);
|
||||
})
|
||||
.then(() => {
|
||||
spyOn(emailAdapter, 'sendVerificationEmail');
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.signUp(null, {
|
||||
success: function(user) {
|
||||
user.fetch()
|
||||
.then(() => {
|
||||
expect(emailAdapter.sendVerificationEmail.calls.count()).toEqual(0);
|
||||
expect(user.get('emailVerified')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
});
|
||||
},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('receives the app name and user in the adapter', done => {
|
||||
it_exclude_dbs(['postgres'])('receives the app name and user in the adapter', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
expect(options.appName).toEqual('emailing app');
|
||||
@@ -329,35 +313,28 @@ describe("Email Verification", () => {
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp(null, {
|
||||
success: () => {},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
var user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp(null, {
|
||||
success: () => {},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
it('when you click the link in the email it sets emailVerified to true and redirects you', done => {
|
||||
it_exclude_dbs(['postgres'])('when you click the link in the email it sets emailVerified to true and redirects you', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
@@ -380,39 +357,23 @@ describe("Email Verification", () => {
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
})
|
||||
.then(() => {
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("user");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp();
|
||||
});
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("user");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp();
|
||||
});
|
||||
|
||||
it('redirects you to invalid link if you try to verify email incorrecly', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
@@ -420,28 +381,21 @@ describe("Email Verification", () => {
|
||||
sendMail: () => {}
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
request.get('http://localhost:8378/1/apps/test/verify_email', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
done()
|
||||
})
|
||||
.then(() => {
|
||||
request.get('http://localhost:8378/1/apps/test/verify_email', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
done()
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects you to invalid link if you try to validate a nonexistant users email', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
@@ -449,17 +403,19 @@ describe("Email Verification", () => {
|
||||
sendMail: () => {}
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
request.get('http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
done();
|
||||
})
|
||||
.then(() => {
|
||||
request.get('http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not update email verified if you use an invalid token', done => {
|
||||
it_exclude_dbs(['postgres'])('does not update email verified if you use an invalid token', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
@@ -478,37 +434,27 @@ describe("Email Verification", () => {
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp(null, {
|
||||
success: () => {},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp(null, {
|
||||
success: () => {},
|
||||
error: function(userAgain, error) {
|
||||
fail('Failed to save user');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Password Reset", () => {
|
||||
|
||||
it('should send a password reset link', done => {
|
||||
it_exclude_dbs(['postgres'])('should send a password reset link', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
@@ -529,47 +475,31 @@ describe("Password Reset", () => {
|
||||
},
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv+zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.requestPasswordReset('user@parse.com', {
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail");
|
||||
done();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv+zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.requestPasswordReset('user@parse.com', {
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail requesting a password");
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects you to invalid link if you try to request password for a nonexistant users email', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
@@ -577,17 +507,19 @@ describe("Password Reset", () => {
|
||||
sendMail: () => {}
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
request.get('http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
done();
|
||||
})
|
||||
.then(() => {
|
||||
request.get('http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', {
|
||||
followRedirect: false,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should programatically reset password', done => {
|
||||
it_exclude_dbs(['postgres'])('should programatically reset password', done => {
|
||||
var user = new Parse.User();
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
@@ -628,9 +560,8 @@ describe("Password Reset", () => {
|
||||
|
||||
Parse.User.logIn("zxcv", "hello").then(function(user){
|
||||
let config = new Config('test');
|
||||
config.database.adaptiveCollection('_User')
|
||||
.then(coll => coll.find({ 'username': 'zxcv' }, { limit: 1 }))
|
||||
.then((results) => {
|
||||
config.database.adapter.find('_User', { fields: {} }, { 'username': 'zxcv' }, { limit: 1 })
|
||||
.then(results => {
|
||||
// _perishable_token should be unset after reset password
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]['_perishable_token']).toEqual(undefined);
|
||||
@@ -647,33 +578,25 @@ describe("Password Reset", () => {
|
||||
},
|
||||
sendMail: () => {}
|
||||
}
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
reconfigureServer({
|
||||
appName: 'emailing app',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
});
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.requestPasswordReset('user@parse.com', {
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail");
|
||||
done();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("zxcv");
|
||||
user.set('email', 'user@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.requestPasswordReset('user@parse.com', {
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail");
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Parse.Cloud.define('cloudCodeInFile', (req, res) => {
|
||||
res.success('It is possible to define cloud code in a file.');
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
Parse.Cloud.define('cloudCodeInFile', (req, res) => {
|
||||
res.success('It is possible to define cloud code in a file.');
|
||||
});
|
||||
@@ -1,117 +0,0 @@
|
||||
Parse.Cloud.define('hello', function(req, res) {
|
||||
res.success('Hello world!');
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeSave('BeforeSaveFail', function(req, res) {
|
||||
res.error('You shall not pass!');
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', function (req, res) {
|
||||
var query = new Parse.Query('Yolo');
|
||||
query.find().then(() => {
|
||||
res.error('Nope');
|
||||
}, () => {
|
||||
res.success();
|
||||
});
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeSave('BeforeSaveUnchanged', function(req, res) {
|
||||
res.success();
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) {
|
||||
req.object.set('foo', 'baz');
|
||||
res.success();
|
||||
});
|
||||
|
||||
Parse.Cloud.afterSave('AfterSaveTest', function(req) {
|
||||
var obj = new Parse.Object('AfterSaveProof');
|
||||
obj.set('proof', req.object.id);
|
||||
obj.save();
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeDelete('BeforeDeleteFail', function(req, res) {
|
||||
res.error('Nope');
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', function (req, res) {
|
||||
var query = new Parse.Query('Yolo');
|
||||
query.find().then(() => {
|
||||
res.error('Nope');
|
||||
}, () => {
|
||||
res.success();
|
||||
});
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeDelete('BeforeDeleteTest', function(req, res) {
|
||||
res.success();
|
||||
});
|
||||
|
||||
Parse.Cloud.afterDelete('AfterDeleteTest', function(req) {
|
||||
var obj = new Parse.Object('AfterDeleteProof');
|
||||
obj.set('proof', req.object.id);
|
||||
obj.save();
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeSave('SaveTriggerUser', function(req, res) {
|
||||
if (req.user && req.user.id) {
|
||||
res.success();
|
||||
} else {
|
||||
res.error('No user present on request object for beforeSave.');
|
||||
}
|
||||
});
|
||||
|
||||
Parse.Cloud.afterSave('SaveTriggerUser', function(req) {
|
||||
if (!req.user || !req.user.id) {
|
||||
console.log('No user present on request object for afterSave.');
|
||||
}
|
||||
});
|
||||
|
||||
Parse.Cloud.define('foo', function(req, res) {
|
||||
res.success({
|
||||
object: {
|
||||
__type: 'Object',
|
||||
className: 'Foo',
|
||||
objectId: '123',
|
||||
x: 2,
|
||||
relation: {
|
||||
__type: 'Object',
|
||||
className: 'Bar',
|
||||
objectId: '234',
|
||||
x: 3
|
||||
}
|
||||
},
|
||||
array: [{
|
||||
__type: 'Object',
|
||||
className: 'Bar',
|
||||
objectId: '345',
|
||||
x: 2
|
||||
}],
|
||||
a: 2
|
||||
});
|
||||
});
|
||||
|
||||
Parse.Cloud.define('bar', function(req, res) {
|
||||
res.error('baz');
|
||||
});
|
||||
|
||||
Parse.Cloud.define('requiredParameterCheck', function(req, res) {
|
||||
res.success();
|
||||
}, function(params) {
|
||||
return params.name;
|
||||
});
|
||||
|
||||
Parse.Cloud.define('echoKeys', function(req, res){
|
||||
return res.success({
|
||||
applicationId: Parse.applicationId,
|
||||
masterKey: Parse.masterKey,
|
||||
javascriptKey: Parse.javascriptKey
|
||||
})
|
||||
});
|
||||
|
||||
Parse.Cloud.define('createBeforeSaveChangedObject', function(req, res){
|
||||
var obj = new Parse.Object('BeforeSaveChanged');
|
||||
obj.save().then(() => {
|
||||
res.success(obj);
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"arg1": "my_app",
|
||||
"arg2": "8888",
|
||||
"arg3": "hello",
|
||||
"arg4": "/1"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"arg1": "my_app",
|
||||
"arg2": 8888,
|
||||
"arg3": "hello",
|
||||
"arg4": "/1"
|
||||
}]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"arg1": "my_app",
|
||||
"arg2": "8888",
|
||||
"arg3": "hello",
|
||||
"myArg": "/1"
|
||||
}
|
||||
+124
-55
@@ -1,6 +1,7 @@
|
||||
"use strict"
|
||||
// Sets up a Parse API server for testing.
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000;
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 5000;
|
||||
|
||||
var cache = require('../src/cache').default;
|
||||
var DatabaseAdapter = require('../src/DatabaseAdapter');
|
||||
@@ -8,30 +9,49 @@ var express = require('express');
|
||||
var facebook = require('../src/authDataManager/facebook');
|
||||
var ParseServer = require('../src/index').ParseServer;
|
||||
var path = require('path');
|
||||
var TestUtils = require('../src/index').TestUtils;
|
||||
var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
|
||||
const GridStoreAdapter = require('../src/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
|
||||
const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter');
|
||||
|
||||
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||
let databaseAdapter;
|
||||
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
|
||||
var postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
|
||||
databaseAdapter = new PostgresStorageAdapter({
|
||||
uri: postgresURI,
|
||||
collectionPrefix: 'test_',
|
||||
});
|
||||
} else {
|
||||
databaseAdapter = new MongoStorageAdapter({
|
||||
uri: mongoURI,
|
||||
collectionPrefix: 'test_',
|
||||
})
|
||||
}
|
||||
|
||||
var databaseURI = process.env.DATABASE_URI;
|
||||
var cloudMain = process.env.CLOUD_CODE_MAIN || '../spec/cloud/main.js';
|
||||
var port = 8378;
|
||||
|
||||
let gridStoreAdapter = new GridStoreAdapter(mongoURI);
|
||||
|
||||
// Default server configuration for tests.
|
||||
var defaultConfiguration = {
|
||||
databaseURI: databaseURI,
|
||||
cloud: cloudMain,
|
||||
filesAdapter: gridStoreAdapter,
|
||||
serverURL: 'http://localhost:' + port + '/1',
|
||||
databaseAdapter,
|
||||
appId: 'test',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
webhookKey: 'hook',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
push: {
|
||||
'ios': {
|
||||
cert: 'prodCert.pem',
|
||||
key: 'prodKey.pem',
|
||||
production: true,
|
||||
bundleId: 'bundleId'
|
||||
bundleId: 'bundleId',
|
||||
}
|
||||
},
|
||||
oauth: { // Override the facebook provider
|
||||
@@ -39,36 +59,48 @@ var defaultConfiguration = {
|
||||
myoauth: {
|
||||
module: path.resolve(__dirname, "myoauth") // relative path as it's run from src
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let openConnections = {};
|
||||
|
||||
// Set up a default API server for testing with default configuration.
|
||||
var api = new ParseServer(defaultConfiguration);
|
||||
var app = express();
|
||||
app.use('/1', api);
|
||||
|
||||
var server = app.listen(port);
|
||||
server.on('connection', connection => {
|
||||
let key = `${connection.remoteAddress}:${connection.remotePort}`;
|
||||
openConnections[key] = connection;
|
||||
connection.on('close', () => { delete openConnections[key] });
|
||||
});
|
||||
|
||||
// Prevent reinitializing the server from clobbering Cloud Code
|
||||
delete defaultConfiguration.cloud;
|
||||
|
||||
var currentConfiguration;
|
||||
// Allows testing specific configurations of Parse Server
|
||||
var setServerConfiguration = configuration => {
|
||||
// the configuration hasn't changed
|
||||
if (configuration === currentConfiguration) {
|
||||
return;
|
||||
}
|
||||
DatabaseAdapter.clearDatabaseSettings();
|
||||
currentConfiguration = configuration;
|
||||
server.close();
|
||||
cache.clearCache();
|
||||
app = express();
|
||||
api = new ParseServer(configuration);
|
||||
app.use('/1', api);
|
||||
server = app.listen(port);
|
||||
};
|
||||
const reconfigureServer = changedConfiguration => {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.close(() => {
|
||||
try {
|
||||
let newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, {
|
||||
__indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(resolve, reject)
|
||||
});
|
||||
cache.clear();
|
||||
app = express();
|
||||
api = new ParseServer(newConfiguration);
|
||||
app.use('/1', api);
|
||||
|
||||
var restoreServerConfiguration = () => setServerConfiguration(defaultConfiguration);
|
||||
server = app.listen(port);
|
||||
server.on('connection', connection => {
|
||||
let key = `${connection.remoteAddress}:${connection.remotePort}`;
|
||||
openConnections[key] = connection;
|
||||
connection.on('close', () => { delete openConnections[key] });
|
||||
});
|
||||
} catch(error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Set up a Parse client to talk to our test API server
|
||||
var Parse = require('parse/node');
|
||||
@@ -78,23 +110,60 @@ Parse.serverURL = 'http://localhost:' + port + '/1';
|
||||
// TODO: update tests to work in an A+ way
|
||||
Parse.Promise.disableAPlusCompliant();
|
||||
|
||||
beforeEach(function(done) {
|
||||
restoreServerConfiguration();
|
||||
Parse.initialize('test', 'test', 'test');
|
||||
Parse.serverURL = 'http://localhost:' + port + '/1';
|
||||
Parse.User.enableUnsafeCurrentUser();
|
||||
done();
|
||||
beforeEach(done => {
|
||||
try {
|
||||
Parse.User.enableUnsafeCurrentUser();
|
||||
} catch (error) {
|
||||
if (error !== 'You need to call Parse.initialize before using Parse.') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
TestUtils.destroyAllDataPermanently()
|
||||
.catch(error => {
|
||||
// For tests that connect to their own mongo, there won't be any data to delete.
|
||||
if (error.message === 'ns not found' || error.message.startsWith('connect ECONNREFUSED')) {
|
||||
return;
|
||||
} else {
|
||||
fail(error);
|
||||
return;
|
||||
}
|
||||
})
|
||||
.then(reconfigureServer)
|
||||
.then(() => {
|
||||
Parse.initialize('test', 'test', 'test');
|
||||
Parse.serverURL = 'http://localhost:' + port + '/1';
|
||||
done();
|
||||
}, error => {
|
||||
fail(JSON.stringify(error));
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
Parse.User.logOut().then(() => {
|
||||
return clearData();
|
||||
}).then(() => {
|
||||
let afterLogOut = () => {
|
||||
if (Object.keys(openConnections).length > 0) {
|
||||
fail('There were open connections to the server left after the test finished');
|
||||
}
|
||||
done();
|
||||
}, (error) => {
|
||||
console.log('error in clearData', error);
|
||||
done();
|
||||
});
|
||||
};
|
||||
Parse.Cloud._removeAllHooks();
|
||||
databaseAdapter.getAllClasses()
|
||||
.then(allSchemas => {
|
||||
allSchemas.forEach((schema) => {
|
||||
var className = schema.className;
|
||||
expect(className).toEqual({ asymmetricMatch: className => {
|
||||
if (!className.startsWith('_')) {
|
||||
return true;
|
||||
} else {
|
||||
// Other system classes will break Parse.com, so make sure that we don't save anything to _SCHEMA that will
|
||||
// break it.
|
||||
return ['_User', '_Installation', '_Role', '_Session', '_Product'].includes(className);
|
||||
}
|
||||
}});
|
||||
});
|
||||
})
|
||||
.then(() => Parse.User.logOut())
|
||||
.then(afterLogOut, afterLogOut)
|
||||
});
|
||||
|
||||
var TestObject = Parse.Object.extend({
|
||||
@@ -133,9 +202,6 @@ function createTestUser(success, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the tests that are known to not work.
|
||||
function notWorking() {}
|
||||
|
||||
// Shims for compatibility with the old qunit tests.
|
||||
function ok(bool, message) {
|
||||
expect(bool).toBeTruthy(message);
|
||||
@@ -186,7 +252,7 @@ function arrayContains(arr, item) {
|
||||
|
||||
// Normalizes a JSON object.
|
||||
function normalize(obj) {
|
||||
if (typeof obj !== 'object') {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
if (obj instanceof Array) {
|
||||
@@ -220,25 +286,21 @@ function mockFacebook() {
|
||||
facebook.validateAuthData = function(authData) {
|
||||
if (authData.id === '8675309' && authData.access_token === 'jenny') {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
throw undefined;
|
||||
}
|
||||
return Promise.reject();
|
||||
};
|
||||
facebook.validateAppId = function(appId, authData) {
|
||||
if (authData.access_token === 'jenny') {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
throw undefined;
|
||||
}
|
||||
return Promise.reject();
|
||||
};
|
||||
return facebook;
|
||||
}
|
||||
|
||||
function clearData() {
|
||||
var promises = [];
|
||||
for (var conn in DatabaseAdapter.dbConnections) {
|
||||
promises.push(DatabaseAdapter.dbConnections[conn].deleteEverything());
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
|
||||
// This is polluting, but, it makes it way easier to directly port old tests.
|
||||
global.Parse = Parse;
|
||||
@@ -247,7 +309,6 @@ global.Item = Item;
|
||||
global.Container = Container;
|
||||
global.create = create;
|
||||
global.createTestUser = createTestUser;
|
||||
global.notWorking = notWorking;
|
||||
global.ok = ok;
|
||||
global.equal = equal;
|
||||
global.strictEqual = strictEqual;
|
||||
@@ -257,9 +318,17 @@ global.expectError = expectError;
|
||||
global.arrayContains = arrayContains;
|
||||
global.jequal = jequal;
|
||||
global.range = range;
|
||||
global.setServerConfiguration = setServerConfiguration;
|
||||
global.reconfigureServer = reconfigureServer;
|
||||
global.defaultConfiguration = defaultConfiguration;
|
||||
|
||||
global.it_exclude_dbs = excluded => {
|
||||
if (excluded.includes(process.env.PARSE_SERVER_TEST_DB)) {
|
||||
return xit;
|
||||
} else {
|
||||
return it;
|
||||
}
|
||||
}
|
||||
|
||||
// LiveQuery test setting
|
||||
require('../src/LiveQuery/PLog').logLevel = 'NONE';
|
||||
var libraryCache = {};
|
||||
|
||||
+211
-146
@@ -1,59 +1,78 @@
|
||||
"use strict"
|
||||
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', () => {
|
||||
it('requires a master key and app id', done => {
|
||||
expect(setServerConfiguration.bind(undefined, { })).toThrow('You must provide an appId!');
|
||||
expect(setServerConfiguration.bind(undefined, { appId: 'myId' })).toThrow('You must provide a masterKey!');
|
||||
expect(setServerConfiguration.bind(undefined, { appId: 'myId', masterKey: 'mk' })).toThrow('You must provide a serverURL!');
|
||||
done();
|
||||
});
|
||||
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
|
||||
|
||||
it('fails if database is unreachable', done => {
|
||||
setServerConfiguration({
|
||||
databaseURI: 'mongodb://fake:fake@ds043605.mongolab.com:43605/drew3',
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
});
|
||||
//Need to use rest api because saving via JS SDK results in fail() not getting called
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/classes/NewClass',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
body: {},
|
||||
json: true,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(500);
|
||||
expect(body.code).toEqual(1);
|
||||
expect(body.message).toEqual('Internal server error.');
|
||||
describe('server', () => {
|
||||
it_exclude_dbs(['postgres'])('requires a master key and app id', done => {
|
||||
reconfigureServer({ appId: undefined })
|
||||
.catch(error => {
|
||||
expect(error).toEqual('You must provide an appId!');
|
||||
return reconfigureServer({ masterKey: undefined });
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error).toEqual('You must provide a masterKey!');
|
||||
return reconfigureServer({ serverURL: undefined });
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error).toEqual('You must provide a serverURL!');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can load email adapter via object', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
it_exclude_dbs(['postgres'])('support http basic authentication with masterkey', done => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + new Buffer('test:' + 'test').toString('base64')
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('support http basic authentication with javascriptKey', done => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + new Buffer('test:javascript-key=' + 'test').toString('base64')
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('fails if database is unreachable', done => {
|
||||
reconfigureServer({ databaseAdapter: new MongoStorageAdapter({ uri: 'mongodb://fake:fake@localhost:43605/drew3' }) })
|
||||
.catch(() => {
|
||||
//Need to use rest api because saving via JS SDK results in fail() not getting called
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/classes/NewClass',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
body: {},
|
||||
json: true,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(500);
|
||||
expect(body.code).toEqual(1);
|
||||
expect(body.message).toEqual('Internal server error.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('can load email adapter via object', done => {
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
@@ -61,22 +80,12 @@ describe('server', () => {
|
||||
domain: 'd',
|
||||
}),
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
});
|
||||
done();
|
||||
}).then(done, fail);
|
||||
});
|
||||
|
||||
it('can load email adapter via class', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
it_exclude_dbs(['postgres'])('can load email adapter via class', done => {
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
class: MockEmailAdapterWithOptions,
|
||||
@@ -87,22 +96,12 @@ describe('server', () => {
|
||||
}
|
||||
},
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
});
|
||||
done();
|
||||
}).then(done, fail);
|
||||
});
|
||||
|
||||
it('can load email adapter via module name', done => {
|
||||
setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
it_exclude_dbs(['postgres'])('can load email adapter via module name', done => {
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
module: 'parse-server-simple-mailgun-adapter',
|
||||
@@ -113,41 +112,25 @@ describe('server', () => {
|
||||
}
|
||||
},
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
});
|
||||
done();
|
||||
}).then(done, fail);
|
||||
});
|
||||
|
||||
it('can load email adapter via only module name', done => {
|
||||
expect(() => setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
it_exclude_dbs(['postgres'])('can load email adapter via only module name', done => {
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: 'parse-server-simple-mailgun-adapter',
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error).toEqual('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if you initialize email adapter incorrecly', done => {
|
||||
expect(() => setServerConfiguration({
|
||||
serverURL: 'http://localhost:8378/1',
|
||||
appId: 'test',
|
||||
it_exclude_dbs(['postgres'])('throws if you initialize email adapter incorrecly', done => {
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
javascriptKey: 'test',
|
||||
dotNetKey: 'windows',
|
||||
clientKey: 'client',
|
||||
restAPIKey: 'rest',
|
||||
masterKey: 'test',
|
||||
collectionPrefix: 'test_',
|
||||
fileKey: 'test',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: {
|
||||
module: 'parse-server-simple-mailgun-adapter',
|
||||
@@ -156,11 +139,14 @@ describe('server', () => {
|
||||
}
|
||||
},
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error).toEqual('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can report the server version', done => {
|
||||
it_exclude_dbs(['postgres'])('can report the server version', done => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/serverInfo',
|
||||
headers: {
|
||||
@@ -174,69 +160,148 @@ describe('server', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('can create a parse-server', done => {
|
||||
var parseServer = new ParseServer.default({
|
||||
it_exclude_dbs(['postgres'])('can create a parse-server v1', done => {
|
||||
var parseServer = new ParseServer.default(Object.assign({},
|
||||
defaultConfiguration, {
|
||||
appId: "aTestApp",
|
||||
masterKey: "aTestMasterKey",
|
||||
serverURL: "http://localhost:12666/parse",
|
||||
databaseURI: 'mongodb://localhost:27017/aTestApp'
|
||||
});
|
||||
__indexBuildCompletionCallbackForTests: promise => {
|
||||
promise
|
||||
.then(() => {
|
||||
expect(Parse.applicationId).toEqual("aTestApp");
|
||||
var app = express();
|
||||
app.use('/parse', parseServer.app);
|
||||
|
||||
expect(Parse.applicationId).toEqual("aTestApp");
|
||||
var app = express();
|
||||
app.use('/parse', parseServer.app);
|
||||
|
||||
var server = app.listen(12666);
|
||||
var obj = new Parse.Object("AnObject");
|
||||
var objId;
|
||||
obj.save().then((obj) => {
|
||||
objId = obj.id;
|
||||
var q = new Parse.Query("AnObject");
|
||||
return q.first();
|
||||
}).then((obj) => {
|
||||
expect(obj.id).toEqual(objId);
|
||||
server.close();
|
||||
done();
|
||||
}).fail((err) => {
|
||||
server.close();
|
||||
done();
|
||||
})
|
||||
var server = app.listen(12666);
|
||||
var obj = new Parse.Object("AnObject");
|
||||
var objId;
|
||||
obj.save().then((obj) => {
|
||||
objId = obj.id;
|
||||
var q = new Parse.Query("AnObject");
|
||||
return q.first();
|
||||
}).then((obj) => {
|
||||
expect(obj.id).toEqual(objId);
|
||||
server.close(done);
|
||||
}).fail((err) => {
|
||||
server.close(done);
|
||||
})
|
||||
});
|
||||
}})
|
||||
);
|
||||
});
|
||||
|
||||
it('can create a parse-server', done => {
|
||||
var parseServer = ParseServer.ParseServer({
|
||||
it_exclude_dbs(['postgres'])('can create a parse-server v2', done => {
|
||||
let objId;
|
||||
let server
|
||||
let parseServer = ParseServer.ParseServer(Object.assign({},
|
||||
defaultConfiguration, {
|
||||
appId: "anOtherTestApp",
|
||||
masterKey: "anOtherTestMasterKey",
|
||||
serverURL: "http://localhost:12667/parse",
|
||||
databaseURI: 'mongodb://localhost:27017/anotherTstApp'
|
||||
});
|
||||
__indexBuildCompletionCallbackForTests: promise => {
|
||||
promise
|
||||
.then(() => {
|
||||
expect(Parse.applicationId).toEqual("anOtherTestApp");
|
||||
let app = express();
|
||||
app.use('/parse', parseServer);
|
||||
|
||||
expect(Parse.applicationId).toEqual("anOtherTestApp");
|
||||
var app = express();
|
||||
app.use('/parse', parseServer);
|
||||
|
||||
var server = app.listen(12667);
|
||||
var obj = new Parse.Object("AnObject");
|
||||
var objId;
|
||||
obj.save().then((obj) => {
|
||||
objId = obj.id;
|
||||
var q = new Parse.Query("AnObject");
|
||||
return q.first();
|
||||
}).then((obj) => {
|
||||
expect(obj.id).toEqual(objId);
|
||||
server.close();
|
||||
done();
|
||||
}).fail((err) => {
|
||||
server.close();
|
||||
done();
|
||||
})
|
||||
server = app.listen(12667);
|
||||
let obj = new Parse.Object("AnObject");
|
||||
return obj.save()
|
||||
})
|
||||
.then(obj => {
|
||||
objId = obj.id;
|
||||
let q = new Parse.Query("AnObject");
|
||||
return q.first();
|
||||
})
|
||||
.then(obj => {
|
||||
expect(obj.id).toEqual(objId);
|
||||
server.close(done);
|
||||
})
|
||||
.catch(error => {
|
||||
fail(JSON.stringify(error))
|
||||
done();
|
||||
});
|
||||
}}
|
||||
));
|
||||
});
|
||||
|
||||
it('has createLiveQueryServer', done => {
|
||||
it_exclude_dbs(['postgres'])('has createLiveQueryServer', done => {
|
||||
// original implementation through the factory
|
||||
expect(typeof ParseServer.ParseServer.createLiveQueryServer).toEqual('function');
|
||||
// For import calls
|
||||
expect(typeof ParseServer.default.createLiveQueryServer).toEqual('function');
|
||||
done();
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('core adapters are not exposed anymore', done => {
|
||||
expect(ParseServer.S3Adapter).toThrow();
|
||||
expect(ParseServer.GCSAdapter).toThrow('GCSAdapter is not provided by parse-server anymore; please install parse-server-gcs-adapter');
|
||||
expect(ParseServer.FileSystemAdapter).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('properly gives publicServerURL when set', done => {
|
||||
reconfigureServer({ publicServerURL: 'https://myserver.com/1' })
|
||||
.then(() => {
|
||||
var config = new Config('test', 'http://localhost:8378/1');
|
||||
expect(config.mount).toEqual('https://myserver.com/1');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('properly removes trailing slash in mount', done => {
|
||||
reconfigureServer({})
|
||||
.then(() => {
|
||||
var config = new Config('test', 'http://localhost:8378/1/');
|
||||
expect(config.mount).toEqual('http://localhost:8378/1');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('should throw when getting invalid mount', done => {
|
||||
reconfigureServer({ publicServerURL: 'blabla:/some' })
|
||||
.catch(error => {
|
||||
expect(error).toEqual('publicServerURL should be a valid HTTPS URL starting with https://')
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('fails if the session length is not a number', done => {
|
||||
reconfigureServer({ sessionLength: 'test' })
|
||||
.catch(error => {
|
||||
expect(error).toEqual('Session length must be a valid number.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('fails if the session length is less than or equal to 0', done => {
|
||||
reconfigureServer({ sessionLength: '-33' })
|
||||
.catch(error => {
|
||||
expect(error).toEqual('Session length must be a value greater than 0.');
|
||||
return reconfigureServer({ sessionLength: '0' })
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error).toEqual('Session length must be a value greater than 0.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('ignores the session length when expireInactiveSessions set to false', (done) => {
|
||||
reconfigureServer({
|
||||
sessionLength: '-33',
|
||||
expireInactiveSessions: false
|
||||
})
|
||||
.then(() => reconfigureServer({
|
||||
sessionLength: '0',
|
||||
expireInactiveSessions: false
|
||||
}))
|
||||
.then(done);
|
||||
})
|
||||
|
||||
it_exclude_dbs(['postgres'])('fails if you try to set revokeSessionOnPasswordReset to non-boolean', done => {
|
||||
reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' })
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
+219
-86
@@ -86,6 +86,21 @@ var pointersAndRelationsSchema = {
|
||||
classLevelPermissions: defaultClassLevelPermissions
|
||||
}
|
||||
|
||||
const userSchema = {
|
||||
"className": "_User",
|
||||
"fields": {
|
||||
"objectId": {"type": "String"},
|
||||
"createdAt": {"type": "Date"},
|
||||
"updatedAt": {"type": "Date"},
|
||||
"ACL": {"type": "ACL"},
|
||||
"username": {"type": "String"},
|
||||
"password": {"type": "String"},
|
||||
"email": {"type": "String"},
|
||||
"emailVerified": {"type": "Boolean"}
|
||||
},
|
||||
"classLevelPermissions": defaultClassLevelPermissions,
|
||||
}
|
||||
|
||||
var noAuthHeaders = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
};
|
||||
@@ -139,18 +154,18 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('responds with empty list when there are no schemas', done => {
|
||||
it_exclude_dbs(['postgres'])('creates _User schema when server starts', done => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/schemas',
|
||||
json: true,
|
||||
headers: masterKeyHeaders,
|
||||
}, (error, response, body) => {
|
||||
expect(body.results).toEqual([]);
|
||||
expect(dd(body.results, [userSchema])).toEqual();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('responds with a list of schemas after creating objects', done => {
|
||||
it_exclude_dbs(['postgres'])('responds with a list of schemas after creating objects', done => {
|
||||
var obj1 = hasAllPODobject();
|
||||
obj1.save().then(savedObj1 => {
|
||||
var obj2 = new Parse.Object('HasPointersAndRelations');
|
||||
@@ -165,15 +180,15 @@ describe('schemas', () => {
|
||||
headers: masterKeyHeaders,
|
||||
}, (error, response, body) => {
|
||||
var expected = {
|
||||
results: [plainOldDataSchema,pointersAndRelationsSchema]
|
||||
results: [userSchema,plainOldDataSchema,pointersAndRelationsSchema]
|
||||
};
|
||||
expect(body).toEqual(expected);
|
||||
expect(dd(body, expected)).toEqual(undefined);
|
||||
done();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('responds with a single schema', done => {
|
||||
it_exclude_dbs(['postgres'])('responds with a single schema', done => {
|
||||
var obj = hasAllPODobject();
|
||||
obj.save().then(() => {
|
||||
request.get({
|
||||
@@ -187,7 +202,7 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('treats class names case sensitively', done => {
|
||||
it_exclude_dbs(['postgres'])('treats class names case sensitively', done => {
|
||||
var obj = hasAllPODobject();
|
||||
obj.save().then(() => {
|
||||
request.get({
|
||||
@@ -297,7 +312,7 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('responds with all fields when you create a class', done => {
|
||||
it_exclude_dbs(['postgres'])('responds with all fields when you create a class', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas',
|
||||
headers: masterKeyHeaders,
|
||||
@@ -326,7 +341,48 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('lets you specify class name in both places', done => {
|
||||
it_exclude_dbs(['postgres'])('responds with all fields when getting incomplete schema', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schemaController => schemaController.addClassIfNotExists('_Installation', {}, defaultClassLevelPermissions))
|
||||
.then(() => {
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/schemas/_Installation',
|
||||
headers: masterKeyHeaders,
|
||||
json: true
|
||||
}, (error, response, body) => {
|
||||
expect(dd(body,{
|
||||
className: '_Installation',
|
||||
fields: {
|
||||
objectId: {type: 'String'},
|
||||
updatedAt: {type: 'Date'},
|
||||
createdAt: {type: 'Date'},
|
||||
installationId: {type: 'String'},
|
||||
deviceToken: {type: 'String'},
|
||||
channels: {type: 'Array'},
|
||||
deviceType: {type: 'String'},
|
||||
pushType: {type: 'String'},
|
||||
GCMSenderId: {type: 'String'},
|
||||
timeZone: {type: 'String'},
|
||||
badge: {type: 'Number'},
|
||||
appIdentifier: {type: 'String'},
|
||||
localeIdentifier: {type: 'String'},
|
||||
appVersion: {type: 'String'},
|
||||
appName: {type: 'String'},
|
||||
parseVersion: {type: 'String'},
|
||||
ACL: {type: 'ACL'}
|
||||
},
|
||||
classLevelPermissions: defaultClassLevelPermissions
|
||||
})).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
fail(JSON.stringify(error))
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('lets you specify class name in both places', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||
headers: masterKeyHeaders,
|
||||
@@ -401,7 +457,7 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to put to existing fields, even if it would not be a change', done => {
|
||||
it_exclude_dbs(['postgres'])('refuses to put to existing fields, even if it would not be a change', done => {
|
||||
var obj = hasAllPODobject();
|
||||
obj.save()
|
||||
.then(() => {
|
||||
@@ -423,7 +479,7 @@ describe('schemas', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('refuses to delete non-existent fields', done => {
|
||||
it_exclude_dbs(['postgres'])('refuses to delete non-existent fields', done => {
|
||||
var obj = hasAllPODobject();
|
||||
obj.save()
|
||||
.then(() => {
|
||||
@@ -445,7 +501,7 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to add a geopoint to a class that already has one', done => {
|
||||
it_exclude_dbs(['postgres'])('refuses to add a geopoint to a class that already has one', done => {
|
||||
var obj = hasAllPODobject();
|
||||
obj.save()
|
||||
.then(() => {
|
||||
@@ -491,7 +547,7 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows you to delete and add a geopoint in the same request', done => {
|
||||
it_exclude_dbs(['postgres'])('allows you to delete and add a geopoint in the same request', done => {
|
||||
var obj = new Parse.Object('NewClass');
|
||||
obj.set('geo1', new Parse.GeoPoint({latitude: 0, longitude: 0}));
|
||||
obj.save()
|
||||
@@ -523,7 +579,7 @@ describe('schemas', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('put with no modifications returns all fields', done => {
|
||||
it_exclude_dbs(['postgres'])('put with no modifications returns all fields', done => {
|
||||
var obj = hasAllPODobject();
|
||||
obj.save()
|
||||
.then(() => {
|
||||
@@ -539,7 +595,7 @@ describe('schemas', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('lets you add fields', done => {
|
||||
it_exclude_dbs(['postgres'])('lets you add fields', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||
headers: masterKeyHeaders,
|
||||
@@ -589,7 +645,7 @@ describe('schemas', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('lets you add fields to system schema', done => {
|
||||
it_exclude_dbs(['postgres'])('lets you add fields to system schema', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/_User',
|
||||
headers: masterKeyHeaders,
|
||||
@@ -605,7 +661,7 @@ describe('schemas', () => {
|
||||
}
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(body).toEqual({
|
||||
expect(dd(body,{
|
||||
className: '_User',
|
||||
fields: {
|
||||
objectId: {type: 'String'},
|
||||
@@ -613,20 +669,19 @@ describe('schemas', () => {
|
||||
createdAt: {type: 'Date'},
|
||||
username: {type: 'String'},
|
||||
password: {type: 'String'},
|
||||
authData: {type: 'Object'},
|
||||
email: {type: 'String'},
|
||||
emailVerified: {type: 'Boolean'},
|
||||
newField: {type: 'String'},
|
||||
ACL: {type: 'ACL'}
|
||||
},
|
||||
classLevelPermissions: defaultClassLevelPermissions
|
||||
});
|
||||
})).toBeUndefined();
|
||||
request.get({
|
||||
url: 'http://localhost:8378/1/schemas/_User',
|
||||
headers: masterKeyHeaders,
|
||||
json: true
|
||||
}, (error, response, body) => {
|
||||
expect(body).toEqual({
|
||||
expect(dd(body,{
|
||||
className: '_User',
|
||||
fields: {
|
||||
objectId: {type: 'String'},
|
||||
@@ -634,21 +689,20 @@ describe('schemas', () => {
|
||||
createdAt: {type: 'Date'},
|
||||
username: {type: 'String'},
|
||||
password: {type: 'String'},
|
||||
authData: {type: 'Object'},
|
||||
email: {type: 'String'},
|
||||
emailVerified: {type: 'Boolean'},
|
||||
newField: {type: 'String'},
|
||||
ACL: {type: 'ACL'}
|
||||
},
|
||||
classLevelPermissions: defaultClassLevelPermissions
|
||||
});
|
||||
})).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('lets you delete multiple fields and add fields', done => {
|
||||
it_exclude_dbs(['postgres'])('lets you delete multiple fields and add fields', done => {
|
||||
var obj1 = hasAllPODobject();
|
||||
obj1.save()
|
||||
.then(() => {
|
||||
@@ -698,7 +752,7 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('will not delete any fields if the additions are invalid', done => {
|
||||
it_exclude_dbs(['postgres'])('will not delete any fields if the additions are invalid', done => {
|
||||
var obj = hasAllPODobject();
|
||||
obj.save()
|
||||
.then(() => {
|
||||
@@ -739,7 +793,7 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to delete non-empty collection', done => {
|
||||
it_exclude_dbs(['postgres'])('refuses to delete non-empty collection', done => {
|
||||
var obj = hasAllPODobject();
|
||||
obj.save()
|
||||
.then(() => {
|
||||
@@ -770,7 +824,7 @@ describe('schemas', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('does not fail when deleting nonexistant collections', done => {
|
||||
it_exclude_dbs(['postgres'])('does not fail when deleting nonexistant collections', done => {
|
||||
request.del({
|
||||
url: 'http://localhost:8378/1/schemas/Missing',
|
||||
headers: masterKeyHeaders,
|
||||
@@ -782,7 +836,7 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes collections including join tables', done => {
|
||||
it_exclude_dbs(['postgres'])('deletes collections including join tables', done => {
|
||||
var obj = new Parse.Object('MyClass');
|
||||
obj.set('data', 'data');
|
||||
obj.save()
|
||||
@@ -833,7 +887,7 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes schema when actual collection does not exist', done => {
|
||||
it_exclude_dbs(['postgres'])('deletes schema when actual collection does not exist', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
|
||||
headers: masterKeyHeaders,
|
||||
@@ -861,7 +915,7 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes schema when actual collection exists', done => {
|
||||
it_exclude_dbs(['postgres'])('deletes schema when actual collection exists', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
|
||||
headers: masterKeyHeaders,
|
||||
@@ -904,7 +958,7 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should set/get schema permissions', done => {
|
||||
it_exclude_dbs(['postgres'])('should set/get schema permissions', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/AClass',
|
||||
headers: masterKeyHeaders,
|
||||
@@ -934,18 +988,10 @@ describe('schemas', () => {
|
||||
create: {
|
||||
'role:admin': true
|
||||
},
|
||||
get: {
|
||||
'*': true
|
||||
},
|
||||
update: {
|
||||
'*': true
|
||||
},
|
||||
addField: {
|
||||
'*': true
|
||||
},
|
||||
delete: {
|
||||
'*': true
|
||||
}
|
||||
get: {},
|
||||
update: {},
|
||||
delete: {},
|
||||
addField: {}
|
||||
});
|
||||
done();
|
||||
});
|
||||
@@ -981,7 +1027,7 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not be able to add a field', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/AClass',
|
||||
@@ -989,6 +1035,9 @@ describe('schemas', () => {
|
||||
json: true,
|
||||
body: {
|
||||
classLevelPermissions: {
|
||||
create: {
|
||||
'*': true
|
||||
},
|
||||
find: {
|
||||
'*': true
|
||||
},
|
||||
@@ -1010,15 +1059,15 @@ describe('schemas', () => {
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
it('should not be able to add a field', done => {
|
||||
|
||||
it('should be able to add a field', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/AClass',
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
body: {
|
||||
classLevelPermissions: {
|
||||
find: {
|
||||
create: {
|
||||
'*': true
|
||||
},
|
||||
addField: {
|
||||
@@ -1038,7 +1087,7 @@ describe('schemas', () => {
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it('should throw with invalid userId (>10 chars)', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/AClass',
|
||||
@@ -1056,7 +1105,7 @@ describe('schemas', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it('should throw with invalid userId (<10 chars)', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/AClass',
|
||||
@@ -1074,7 +1123,7 @@ describe('schemas', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it('should throw with invalid userId (invalid char)', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/AClass',
|
||||
@@ -1092,7 +1141,7 @@ describe('schemas', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it('should throw with invalid * (spaces)', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/AClass',
|
||||
@@ -1110,7 +1159,7 @@ describe('schemas', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it('should throw with invalid * (spaces)', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/AClass',
|
||||
@@ -1128,7 +1177,7 @@ describe('schemas', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it('should throw with invalid value', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/AClass',
|
||||
@@ -1146,7 +1195,7 @@ describe('schemas', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it('should throw with invalid value', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/AClass',
|
||||
@@ -1164,10 +1213,10 @@ describe('schemas', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
function setPermissionsOnClass(className, permissions, doPut) {
|
||||
let op = request.post;
|
||||
if (doPut)
|
||||
if (doPut)
|
||||
{
|
||||
op = request.put;
|
||||
}
|
||||
@@ -1190,18 +1239,18 @@ describe('schemas', () => {
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
it('validate CLP 1', done => {
|
||||
|
||||
it_exclude_dbs(['postgres'])('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
|
||||
@@ -1214,7 +1263,7 @@ describe('schemas', () => {
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user', 'user').then(() => {
|
||||
let obj = new Parse.Object('AClass');
|
||||
return obj.save();
|
||||
return obj.save(null, {useMasterKey: true});
|
||||
})
|
||||
}).then(() => {
|
||||
let query = new Parse.Query('AClass');
|
||||
@@ -1239,18 +1288,18 @@ describe('schemas', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('validate CLP 2', done => {
|
||||
|
||||
it_exclude_dbs(['postgres'])('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
|
||||
@@ -1263,7 +1312,7 @@ describe('schemas', () => {
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user', 'user').then(() => {
|
||||
let obj = new Parse.Object('AClass');
|
||||
return obj.save();
|
||||
return obj.save(null, {useMasterKey: true});
|
||||
})
|
||||
}).then(() => {
|
||||
let query = new Parse.Query('AClass');
|
||||
@@ -1304,18 +1353,18 @@ describe('schemas', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('validate CLP 3', done => {
|
||||
|
||||
it_exclude_dbs(['postgres'])('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
|
||||
@@ -1328,7 +1377,7 @@ describe('schemas', () => {
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user', 'user').then(() => {
|
||||
let obj = new Parse.Object('AClass');
|
||||
return obj.save();
|
||||
return obj.save(null, {useMasterKey: true});
|
||||
})
|
||||
}).then(() => {
|
||||
let query = new Parse.Query('AClass');
|
||||
@@ -1362,18 +1411,18 @@ describe('schemas', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('validate CLP 4', done => {
|
||||
|
||||
it_exclude_dbs(['postgres'])('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
|
||||
@@ -1386,7 +1435,7 @@ describe('schemas', () => {
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user', 'user').then(() => {
|
||||
let obj = new Parse.Object('AClass');
|
||||
return obj.save();
|
||||
return obj.save(null, {useMasterKey: true});
|
||||
})
|
||||
}).then(() => {
|
||||
let query = new Parse.Query('AClass');
|
||||
@@ -1400,7 +1449,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 +1479,21 @@ describe('schemas', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('validate CLP 5', done => {
|
||||
|
||||
it_exclude_dbs(['postgres'])('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 +1544,89 @@ 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_exclude_dbs(['postgres'])('can login when addFields is false (issue #1355)', (done) => {
|
||||
setPermissionsOnClass('_User', {
|
||||
'create': {'*': true},
|
||||
'addField': {}
|
||||
}, true).then(() => {
|
||||
return Parse.User.signUp('foo', 'bar');
|
||||
}).then((user) => {
|
||||
expect(user.getUsername()).toBe('foo');
|
||||
done()
|
||||
}, error => {
|
||||
fail(JSON.stringify(error));
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
it_exclude_dbs(['postgres'])('gives correct response when deleting a schema with CLPs (regression test #1919)', done => {
|
||||
new Parse.Object('MyClass').save({ data: 'foo'})
|
||||
.then(obj => obj.destroy())
|
||||
.then(() => setPermissionsOnClass('MyClass', { find: {}, get: {} }, true))
|
||||
.then(() => {
|
||||
request.del({
|
||||
url: 'http://localhost:8378/1/schemas/MyClass',
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.body).toEqual({});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])("regression test for #1991", done => {
|
||||
let user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('user');
|
||||
let role = new Parse.Role('admin', new Parse.ACL());
|
||||
let obj = new Parse.Object('AnObject');
|
||||
Parse.Object.saveAll([user, role]).then(() => {
|
||||
role.relation('users').add(user);
|
||||
return role.save(null, {useMasterKey: true});
|
||||
}).then(() => {
|
||||
return setPermissionsOnClass('AnObject', {
|
||||
'get': {"*": true},
|
||||
'find': {"*": true},
|
||||
'create': {'*': true},
|
||||
'update': {'role:admin': true},
|
||||
'delete': {'role:admin': true}
|
||||
})
|
||||
}).then(() => {
|
||||
return obj.save();
|
||||
}).then(() => {
|
||||
return Parse.User.logIn('user', 'user')
|
||||
}).then(() => {
|
||||
return obj.destroy();
|
||||
}).then((result) => {
|
||||
let query = new Parse.Query('AnObject');
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(0);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
fail('should not fail');
|
||||
console.error(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
// These tests are unit tests designed to only test transform.js.
|
||||
|
||||
var transform = require('../src/transform');
|
||||
|
||||
var dummySchema = {
|
||||
data: {},
|
||||
getExpectedType: function(className, key) {
|
||||
if (key == 'userPointer') {
|
||||
return '*_User';
|
||||
} else if (key == 'picture') {
|
||||
return 'file';
|
||||
} else if (key == 'location') {
|
||||
return 'geopoint';
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
describe('transformCreate', () => {
|
||||
|
||||
it('a basic number', (done) => {
|
||||
var input = {five: 5};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
jequal(input, output);
|
||||
done();
|
||||
});
|
||||
|
||||
it('built-in timestamps', (done) => {
|
||||
var input = {
|
||||
createdAt: "2015-10-06T21:24:50.332Z",
|
||||
updatedAt: "2015-10-06T21:24:50.332Z"
|
||||
};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
expect(output._created_at instanceof Date).toBe(true);
|
||||
expect(output._updated_at instanceof Date).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it('array of pointers', (done) => {
|
||||
var pointer = {
|
||||
__type: 'Pointer',
|
||||
objectId: 'myId',
|
||||
className: 'Blah',
|
||||
};
|
||||
var out = transform.transformCreate(dummySchema, null, {pointers: [pointer]});
|
||||
jequal([pointer], out.pointers);
|
||||
done();
|
||||
});
|
||||
|
||||
it('a delete op', (done) => {
|
||||
var input = {deleteMe: {__op: 'Delete'}};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
jequal(output, {});
|
||||
done();
|
||||
});
|
||||
|
||||
it('basic ACL', (done) => {
|
||||
var input = {ACL: {'0123': {'read': true, 'write': true}}};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
// This just checks that it doesn't crash, but it should check format.
|
||||
done();
|
||||
});
|
||||
|
||||
describe('GeoPoints', () => {
|
||||
it('plain', (done) => {
|
||||
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
|
||||
var out = transform.transformCreate(dummySchema, null, {location: geoPoint});
|
||||
expect(out.location).toEqual([180, -180]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('in array', (done) => {
|
||||
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
|
||||
var out = transform.transformCreate(dummySchema, null, {locations: [geoPoint, geoPoint]});
|
||||
expect(out.locations).toEqual([geoPoint, geoPoint]);
|
||||
done();
|
||||
});
|
||||
|
||||
it('in sub-object', (done) => {
|
||||
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
|
||||
var out = transform.transformCreate(dummySchema, null, { locations: { start: geoPoint }});
|
||||
expect(out).toEqual({ locations: { start: geoPoint } });
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformWhere', () => {
|
||||
it('objectId', (done) => {
|
||||
var out = transform.transformWhere(dummySchema, null, {objectId: 'foo'});
|
||||
expect(out._id).toEqual('foo');
|
||||
done();
|
||||
});
|
||||
|
||||
it('objectId in a list', (done) => {
|
||||
var input = {
|
||||
objectId: {'$in': ['one', 'two', 'three']},
|
||||
};
|
||||
var output = transform.transformWhere(dummySchema, null, input);
|
||||
jequal(input.objectId, output._id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('untransformObject', () => {
|
||||
it('built-in timestamps', (done) => {
|
||||
var input = {createdAt: new Date(), updatedAt: new Date()};
|
||||
var output = transform.untransformObject(dummySchema, null, input);
|
||||
expect(typeof output.createdAt).toEqual('string');
|
||||
expect(typeof output.updatedAt).toEqual('string');
|
||||
done();
|
||||
});
|
||||
|
||||
it('pointer', (done) => {
|
||||
var input = {_p_userPointer: '_User$123'};
|
||||
var output = transform.untransformObject(dummySchema, null, input);
|
||||
expect(typeof output.userPointer).toEqual('object');
|
||||
expect(output.userPointer).toEqual(
|
||||
{__type: 'Pointer', className: '_User', objectId: '123'}
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
it('null pointer', (done) => {
|
||||
var input = {_p_userPointer: null};
|
||||
var output = transform.untransformObject(dummySchema, null, input);
|
||||
expect(output.userPointer).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
|
||||
it('file', (done) => {
|
||||
var input = {picture: 'pic.jpg'};
|
||||
var output = transform.untransformObject(dummySchema, null, input);
|
||||
expect(typeof output.picture).toEqual('object');
|
||||
expect(output.picture).toEqual({__type: 'File', name: 'pic.jpg'});
|
||||
done();
|
||||
});
|
||||
|
||||
it('geopoint', (done) => {
|
||||
var input = {location: [180, -180]};
|
||||
var output = transform.untransformObject(dummySchema, null, input);
|
||||
expect(typeof output.location).toEqual('object');
|
||||
expect(output.location).toEqual(
|
||||
{__type: 'GeoPoint', longitude: 180, latitude: -180}
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('transformKey', () => {
|
||||
it('throws out _password', (done) => {
|
||||
try {
|
||||
transform.transformKey(dummySchema, '_User', '_password');
|
||||
fail('should have thrown');
|
||||
} catch (e) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('transform schema key changes', () => {
|
||||
|
||||
it('changes new pointer key', (done) => {
|
||||
var input = {
|
||||
somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'}
|
||||
};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
expect(typeof output._p_somePointer).toEqual('string');
|
||||
expect(output._p_somePointer).toEqual('Micro$oft');
|
||||
done();
|
||||
});
|
||||
|
||||
it('changes existing pointer keys', (done) => {
|
||||
var input = {
|
||||
userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'}
|
||||
};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
expect(typeof output._p_userPointer).toEqual('string');
|
||||
expect(output._p_userPointer).toEqual('_User$qwerty');
|
||||
done();
|
||||
});
|
||||
|
||||
it('changes ACL storage to _rperm and _wperm', (done) => {
|
||||
var input = {
|
||||
ACL: {
|
||||
"*": { "read": true },
|
||||
"Kevin": { "write": true }
|
||||
}
|
||||
};
|
||||
var output = transform.transformCreate(dummySchema, null, input);
|
||||
expect(typeof output._rperm).toEqual('object');
|
||||
expect(typeof output._wperm).toEqual('object');
|
||||
expect(output.ACL).toBeUndefined();
|
||||
expect(output._rperm[0]).toEqual('*');
|
||||
expect(output._wperm[0]).toEqual('Kevin');
|
||||
done();
|
||||
});
|
||||
|
||||
it('untransforms from _rperm and _wperm to ACL', (done) => {
|
||||
var input = {
|
||||
_rperm: ["*"],
|
||||
_wperm: ["Kevin"]
|
||||
};
|
||||
var output = transform.untransformObject(dummySchema, null, input);
|
||||
expect(typeof output.ACL).toEqual('object');
|
||||
expect(output._rperm).toBeUndefined();
|
||||
expect(output._wperm).toBeUndefined();
|
||||
expect(output.ACL['*']['read']).toEqual(true);
|
||||
expect(output.ACL['Kevin']['write']).toEqual(true);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
export function loadAdapter(adapter, defaultAdapter, options) {
|
||||
if (!adapter)
|
||||
{
|
||||
if (!adapter) {
|
||||
if (!defaultAdapter) {
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
export class CacheAdapter {
|
||||
/**
|
||||
* Get a value in the cache
|
||||
* @param key Cache key to get
|
||||
* @return Promise that will eventually resolve to the value in the cache.
|
||||
*/
|
||||
get(key) {}
|
||||
|
||||
/**
|
||||
* Set a value in the cache
|
||||
* @param key Cache key to set
|
||||
* @param value Value to set the key
|
||||
* @param ttl Optional TTL
|
||||
*/
|
||||
put(key, value, ttl) {}
|
||||
|
||||
/**
|
||||
* Remove a value from the cache.
|
||||
* @param key Cache key to remove
|
||||
*/
|
||||
del(key) {}
|
||||
|
||||
/**
|
||||
* Empty a cache
|
||||
*/
|
||||
clear() {}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
const DEFAULT_CACHE_TTL = 5 * 1000;
|
||||
|
||||
|
||||
export class InMemoryCache {
|
||||
constructor({
|
||||
ttl = DEFAULT_CACHE_TTL
|
||||
}) {
|
||||
this.ttl = ttl;
|
||||
this.cache = Object.create(null);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
let record = this.cache[key];
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Has Record and isnt expired
|
||||
if (isNaN(record.expire) || record.expire >= Date.now()) {
|
||||
return record.value;
|
||||
}
|
||||
|
||||
// Record has expired
|
||||
delete this.cache[key];
|
||||
return null;
|
||||
}
|
||||
|
||||
put(key, value, ttl = this.ttl) {
|
||||
if (ttl < 0 || isNaN(ttl)) {
|
||||
ttl = NaN;
|
||||
}
|
||||
|
||||
var record = {
|
||||
value: value,
|
||||
expire: ttl + Date.now()
|
||||
}
|
||||
|
||||
if (!isNaN(record.expire)) {
|
||||
record.timeout = setTimeout(() => {
|
||||
this.del(key);
|
||||
}, ttl);
|
||||
}
|
||||
|
||||
this.cache[key] = record;
|
||||
}
|
||||
|
||||
del(key) {
|
||||
var record = this.cache[key];
|
||||
if (record == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (record.timeout) {
|
||||
clearTimeout(record.timeout);
|
||||
}
|
||||
delete this.cache[key];
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache = Object.create(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default InMemoryCache;
|
||||
@@ -0,0 +1,36 @@
|
||||
import {InMemoryCache} from './InMemoryCache';
|
||||
|
||||
export class InMemoryCacheAdapter {
|
||||
|
||||
constructor(ctx) {
|
||||
this.cache = new InMemoryCache(ctx)
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let record = this.cache.get(key);
|
||||
if (record == null) {
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
return resolve(JSON.parse(record));
|
||||
})
|
||||
}
|
||||
|
||||
put(key, value, ttl) {
|
||||
this.cache.put(key, JSON.stringify(value), ttl);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
del(key) {
|
||||
this.cache.del(key);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache.clear();
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export default InMemoryCacheAdapter;
|
||||
@@ -7,13 +7,15 @@
|
||||
*/
|
||||
|
||||
import { MongoClient, GridStore, Db} from 'mongodb';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
|
||||
const DefaultMongoURI = 'mongodb://localhost:27017/parse';
|
||||
|
||||
export class GridStoreAdapter extends FilesAdapter {
|
||||
_databaseURI: string;
|
||||
_connectionPromise: Promise<Db>;
|
||||
|
||||
constructor(mongoDatabaseURI: string) {
|
||||
constructor(mongoDatabaseURI = DefaultMongoURI) {
|
||||
super();
|
||||
this._databaseURI = mongoDatabaseURI;
|
||||
this._connect();
|
||||
|
||||
@@ -17,7 +17,7 @@ export default class MongoCollection {
|
||||
return this._rawFind(query, { skip, limit, sort })
|
||||
.catch(error => {
|
||||
// Check for "no geoindex" error
|
||||
if (error.code != 17007 || !error.message.match(/unable to find index for .geoNear/)) {
|
||||
if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) {
|
||||
throw error;
|
||||
}
|
||||
// Figure out what key needs an index
|
||||
@@ -28,7 +28,6 @@ export default class MongoCollection {
|
||||
|
||||
var index = {};
|
||||
index[key] = '2d';
|
||||
//TODO: condiser moving index creation logic into Schema.js
|
||||
return this._mongoCollection.createIndex(index)
|
||||
// Retry, but just once.
|
||||
.then(() => this._rawFind(query, { skip, limit, sort }));
|
||||
@@ -45,18 +44,6 @@ export default class MongoCollection {
|
||||
return this._mongoCollection.count(query, { skip, limit, sort });
|
||||
}
|
||||
|
||||
// Atomically finds and updates an object based on query.
|
||||
// The result is the promise with an object that was in the database !AFTER! changes.
|
||||
// Postgres Note: Translates directly to `UPDATE * SET * ... RETURNING *`, which will return data after the change is done.
|
||||
findOneAndUpdate(query, update) {
|
||||
// arguments: query, sort, update, options(optional)
|
||||
// Setting `new` option to true makes it return the after document, not the before one.
|
||||
return this._mongoCollection.findAndModify(query, [], update, { new: true }).then(document => {
|
||||
// Value is the object where mongo returns multiple fields.
|
||||
return document.value;
|
||||
});
|
||||
}
|
||||
|
||||
insertOne(object) {
|
||||
return this._mongoCollection.insertOne(object);
|
||||
}
|
||||
@@ -65,7 +52,7 @@ export default class MongoCollection {
|
||||
// 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 });
|
||||
return this._mongoCollection.update(query, update, { upsert: true })
|
||||
}
|
||||
|
||||
updateOne(query, update) {
|
||||
@@ -84,6 +71,18 @@ export default class MongoCollection {
|
||||
return this._mongoCollection.deleteMany(query);
|
||||
}
|
||||
|
||||
_ensureSparseUniqueIndexInBackground(indexRequest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._mongoCollection.ensureIndex(indexRequest, { unique: true, background: true, sparse: true }, (error, indexName) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
drop() {
|
||||
return this._mongoCollection.drop();
|
||||
}
|
||||
|
||||
@@ -1,51 +1,130 @@
|
||||
|
||||
import MongoCollection from './MongoCollection';
|
||||
|
||||
function _mongoSchemaQueryFromNameQuery(name: string, query) {
|
||||
return _mongoSchemaObjectFromNameFields(name, query);
|
||||
function mongoFieldToParseSchemaField(type) {
|
||||
if (type[0] === '*') {
|
||||
return {
|
||||
type: 'Pointer',
|
||||
targetClass: type.slice(1),
|
||||
};
|
||||
}
|
||||
if (type.startsWith('relation<')) {
|
||||
return {
|
||||
type: 'Relation',
|
||||
targetClass: type.slice('relation<'.length, type.length - 1),
|
||||
};
|
||||
}
|
||||
switch (type) {
|
||||
case 'number': return {type: 'Number'};
|
||||
case 'string': return {type: 'String'};
|
||||
case 'boolean': return {type: 'Boolean'};
|
||||
case 'date': return {type: 'Date'};
|
||||
case 'map':
|
||||
case 'object': return {type: 'Object'};
|
||||
case 'array': return {type: 'Array'};
|
||||
case 'geopoint': return {type: 'GeoPoint'};
|
||||
case 'file': return {type: 'File'};
|
||||
case 'bytes': return {type: 'Bytes'};
|
||||
}
|
||||
}
|
||||
|
||||
function _mongoSchemaObjectFromNameFields(name: string, fields) {
|
||||
const nonFieldSchemaKeys = ['_id', '_metadata', '_client_permissions'];
|
||||
function mongoSchemaFieldsToParseSchemaFields(schema) {
|
||||
var fieldNames = Object.keys(schema).filter(key => nonFieldSchemaKeys.indexOf(key) === -1);
|
||||
var response = fieldNames.reduce((obj, fieldName) => {
|
||||
obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName])
|
||||
return obj;
|
||||
}, {});
|
||||
response.ACL = {type: 'ACL'};
|
||||
response.createdAt = {type: 'Date'};
|
||||
response.updatedAt = {type: 'Date'};
|
||||
response.objectId = {type: 'String'};
|
||||
return response;
|
||||
}
|
||||
|
||||
const emptyCLPS = Object.freeze({
|
||||
find: {},
|
||||
get: {},
|
||||
create: {},
|
||||
update: {},
|
||||
delete: {},
|
||||
addField: {},
|
||||
});
|
||||
|
||||
const defaultCLPS = Object.freeze({
|
||||
find: {'*': true},
|
||||
get: {'*': true},
|
||||
create: {'*': true},
|
||||
update: {'*': true},
|
||||
delete: {'*': true},
|
||||
addField: {'*': true},
|
||||
});
|
||||
|
||||
function mongoSchemaToParseSchema(mongoSchema) {
|
||||
let clps = defaultCLPS;
|
||||
if (mongoSchema._metadata && mongoSchema._metadata.class_permissions) {
|
||||
clps = {...emptyCLPS, ...mongoSchema._metadata.class_permissions};
|
||||
}
|
||||
return {
|
||||
className: mongoSchema._id,
|
||||
fields: mongoSchemaFieldsToParseSchemaFields(mongoSchema),
|
||||
classLevelPermissions: clps,
|
||||
};
|
||||
}
|
||||
|
||||
function _mongoSchemaQueryFromNameQuery(name: string, query) {
|
||||
let object = { _id: name };
|
||||
if (fields) {
|
||||
Object.keys(fields).forEach(key => {
|
||||
object[key] = fields[key];
|
||||
if (query) {
|
||||
Object.keys(query).forEach(key => {
|
||||
object[key] = query[key];
|
||||
});
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
export default class MongoSchemaCollection {
|
||||
|
||||
|
||||
// Returns a type suitable for inserting into mongo _SCHEMA collection.
|
||||
// Does no validation. That is expected to be done in Parse Server.
|
||||
function parseFieldTypeToMongoFieldType({ type, targetClass }) {
|
||||
switch (type) {
|
||||
case 'Pointer': return `*${targetClass}`;
|
||||
case 'Relation': return `relation<${targetClass}>`;
|
||||
case 'Number': return 'number';
|
||||
case 'String': return 'string';
|
||||
case 'Boolean': return 'boolean';
|
||||
case 'Date': return 'date';
|
||||
case 'Object': return 'object';
|
||||
case 'Array': return 'array';
|
||||
case 'GeoPoint': return 'geopoint';
|
||||
case 'File': return 'file';
|
||||
}
|
||||
}
|
||||
|
||||
class MongoSchemaCollection {
|
||||
_collection: MongoCollection;
|
||||
|
||||
constructor(collection: MongoCollection) {
|
||||
this._collection = collection;
|
||||
}
|
||||
|
||||
getAllSchemas() {
|
||||
return this._collection._rawFind({});
|
||||
_fetchAllSchemasFrom_SCHEMA() {
|
||||
return this._collection._rawFind({})
|
||||
.then(schemas => schemas.map(mongoSchemaToParseSchema));
|
||||
}
|
||||
|
||||
findSchema(name: string) {
|
||||
_fechOneSchemaFrom_SCHEMA(name: string) {
|
||||
return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(results => {
|
||||
return results[0];
|
||||
if (results.length === 1) {
|
||||
return mongoSchemaToParseSchema(results[0]);
|
||||
} else {
|
||||
throw undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Atomically find and delete an object based on query.
|
||||
// The result is the promise with an object that was in the database before deleting.
|
||||
// Postgres Note: Translates directly to `DELETE * FROM ... RETURNING *`, which will return data after delete is done.
|
||||
findAndDeleteSchema(name: string) {
|
||||
// arguments: query, sort
|
||||
return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []).then(document => {
|
||||
// Value is the object where mongo returns multiple fields.
|
||||
return document.value;
|
||||
});
|
||||
}
|
||||
|
||||
addSchema(name: string, fields) {
|
||||
let mongoObject = _mongoSchemaObjectFromNameFields(name, fields);
|
||||
return this._collection.insertOne(mongoObject);
|
||||
return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []);
|
||||
}
|
||||
|
||||
updateSchema(name: string, update) {
|
||||
@@ -55,4 +134,52 @@ export default class MongoSchemaCollection {
|
||||
upsertSchema(name: string, query: string, update) {
|
||||
return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update);
|
||||
}
|
||||
|
||||
// Add a field to the schema. If database does not support the field
|
||||
// type (e.g. mongo doesn't support more than one GeoPoint in a class) reject with an "Incorrect Type"
|
||||
// Parse error with a desciptive message. If the field already exists, this function must
|
||||
// not modify the schema, and must reject with DUPLICATE_VALUE error.
|
||||
// If this is called for a class that doesn't exist, this function must create that class.
|
||||
|
||||
// TODO: throw an error if an unsupported field type is passed. Deciding whether a type is supported
|
||||
// should be the job of the adapter. Some adapters may not support GeoPoint at all. Others may
|
||||
// Support additional types that Mongo doesn't, like Money, or something.
|
||||
|
||||
// TODO: don't spend an extra query on finding the schema if the type we are trying to add isn't a GeoPoint.
|
||||
addFieldIfNotExists(className: string, fieldName: string, type: string) {
|
||||
return this._fechOneSchemaFrom_SCHEMA(className)
|
||||
.then(schema => {
|
||||
// The schema exists. Check for existing GeoPoints.
|
||||
if (type.type === 'GeoPoint') {
|
||||
// Make sure there are not other geopoint fields
|
||||
if (Object.keys(schema.fields).some(existingField => schema.fields[existingField].type === 'GeoPoint')) {
|
||||
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, 'MongoDB only supports one GeoPoint field in a class.');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}, error => {
|
||||
// If error is undefined, the schema doesn't exist, and we can create the schema with the field.
|
||||
// If some other error, reject with it.
|
||||
if (error === undefined) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
.then(() => {
|
||||
// We use $exists and $set to avoid overwriting the field type if it
|
||||
// already exists. (it could have added inbetween the last query and the update)
|
||||
return this.upsertSchema(
|
||||
className,
|
||||
{ [fieldName]: { '$exists': false } },
|
||||
{ '$set' : { [fieldName]: parseFieldTypeToMongoFieldType(type) } }
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Exported for testing reasons and because we haven't moved all mongo schema format
|
||||
// related logic into the database adapter yet.
|
||||
MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema
|
||||
MongoSchemaCollection.parseFieldTypeToMongoFieldType = parseFieldTypeToMongoFieldType
|
||||
|
||||
export default MongoSchemaCollection
|
||||
|
||||
@@ -1,24 +1,98 @@
|
||||
|
||||
import MongoCollection from './MongoCollection';
|
||||
import MongoCollection from './MongoCollection';
|
||||
import MongoSchemaCollection from './MongoSchemaCollection';
|
||||
import {parse as parseUrl, format as formatUrl} from '../../../vendor/mongodbUrl';
|
||||
import {
|
||||
parse as parseUrl,
|
||||
format as formatUrl,
|
||||
} from '../../../vendor/mongodbUrl';
|
||||
import {
|
||||
parseObjectToMongoObjectForCreate,
|
||||
mongoObjectToParseObject,
|
||||
transformKey,
|
||||
transformWhere,
|
||||
transformUpdate,
|
||||
} from './MongoTransform';
|
||||
import _ from 'lodash';
|
||||
|
||||
let mongodb = require('mongodb');
|
||||
let MongoClient = mongodb.MongoClient;
|
||||
|
||||
const MongoSchemaCollectionName = '_SCHEMA';
|
||||
const DefaultMongoURI = 'mongodb://localhost:27017/parse';
|
||||
|
||||
const storageAdapterAllCollections = mongoAdapter => {
|
||||
return mongoAdapter.connect()
|
||||
.then(() => mongoAdapter.database.collections())
|
||||
.then(collections => {
|
||||
return collections.filter(collection => {
|
||||
if (collection.namespace.match(/\.system\./)) {
|
||||
return false;
|
||||
}
|
||||
// TODO: If you have one app with a collection prefix that happens to be a prefix of another
|
||||
// apps prefix, this will go very very badly. We should fix that somehow.
|
||||
return (collection.collectionName.indexOf(mongoAdapter._collectionPrefix) == 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const convertParseSchemaToMongoSchema = ({...schema}) => {
|
||||
delete schema.fields._rperm;
|
||||
delete schema.fields._wperm;
|
||||
|
||||
if (schema.className === '_User') {
|
||||
// Legacy mongo adapter knows about the difference between password and _hashed_password.
|
||||
// Future database adapters will only know about _hashed_password.
|
||||
// Note: Parse Server will bring back password with injectDefaultSchema, so we don't need
|
||||
// to add _hashed_password back ever.
|
||||
delete schema.fields._hashed_password;
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
// Returns { code, error } if invalid, or { result }, an object
|
||||
// suitable for inserting into _SCHEMA collection, otherwise.
|
||||
const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPermissions) => {
|
||||
let mongoObject = {
|
||||
_id: className,
|
||||
objectId: 'string',
|
||||
updatedAt: 'string',
|
||||
createdAt: 'string'
|
||||
};
|
||||
|
||||
for (let fieldName in fields) {
|
||||
mongoObject[fieldName] = MongoSchemaCollection.parseFieldTypeToMongoFieldType(fields[fieldName]);
|
||||
}
|
||||
|
||||
if (typeof classLevelPermissions !== 'undefined') {
|
||||
mongoObject._metadata = mongoObject._metadata || {};
|
||||
if (!classLevelPermissions) {
|
||||
delete mongoObject._metadata.class_permissions;
|
||||
} else {
|
||||
mongoObject._metadata.class_permissions = classLevelPermissions;
|
||||
}
|
||||
}
|
||||
|
||||
return mongoObject;
|
||||
}
|
||||
|
||||
|
||||
export class MongoStorageAdapter {
|
||||
// Private
|
||||
_uri: string;
|
||||
_options: Object;
|
||||
_collectionPrefix: string;
|
||||
_mongoOptions: Object;
|
||||
// Public
|
||||
connectionPromise;
|
||||
database;
|
||||
|
||||
constructor(uri: string, options: Object) {
|
||||
constructor({
|
||||
uri = DefaultMongoURI,
|
||||
collectionPrefix = '',
|
||||
mongoOptions = {},
|
||||
}) {
|
||||
this._uri = uri;
|
||||
this._options = options;
|
||||
this._collectionPrefix = collectionPrefix;
|
||||
this._mongoOptions = mongoOptions;
|
||||
}
|
||||
|
||||
connect() {
|
||||
@@ -30,53 +104,252 @@ export class MongoStorageAdapter {
|
||||
// encoded
|
||||
const encodedUri = formatUrl(parseUrl(this._uri));
|
||||
|
||||
this.connectionPromise = MongoClient.connect(encodedUri, this._options).then(database => {
|
||||
this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions).then(database => {
|
||||
this.database = database;
|
||||
});
|
||||
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
collection(name: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.collection(name);
|
||||
});
|
||||
}
|
||||
|
||||
adaptiveCollection(name: string) {
|
||||
_adaptiveCollection(name: string) {
|
||||
return this.connect()
|
||||
.then(() => this.database.collection(name))
|
||||
.then(() => this.database.collection(this._collectionPrefix + name))
|
||||
.then(rawCollection => new MongoCollection(rawCollection));
|
||||
}
|
||||
|
||||
schemaCollection(collectionPrefix: string) {
|
||||
_schemaCollection() {
|
||||
return this.connect()
|
||||
.then(() => this.adaptiveCollection(collectionPrefix + MongoSchemaCollectionName))
|
||||
.then(() => this._adaptiveCollection(MongoSchemaCollectionName))
|
||||
.then(collection => new MongoSchemaCollection(collection));
|
||||
}
|
||||
|
||||
collectionExists(name: string) {
|
||||
classExists(name) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.listCollections({ name: name }).toArray();
|
||||
return this.database.listCollections({ name: this._collectionPrefix + name }).toArray();
|
||||
}).then(collections => {
|
||||
return collections.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
dropCollection(name: string) {
|
||||
return this.collection(name).then(collection => collection.drop());
|
||||
setClassLevelPermissions(className, CLPs) {
|
||||
return this._schemaCollection()
|
||||
.then(schemaCollection => schemaCollection.updateSchema(className, {
|
||||
$set: { _metadata: { class_permissions: CLPs } }
|
||||
}));
|
||||
}
|
||||
// Used for testing only right now.
|
||||
collectionsContaining(match: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.collections();
|
||||
}).then(collections => {
|
||||
return collections.filter(collection => {
|
||||
if (collection.namespace.match(/\.system\./)) {
|
||||
return false;
|
||||
}
|
||||
return (collection.collectionName.indexOf(match) == 0);
|
||||
});
|
||||
|
||||
createClass(className, schema) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions);
|
||||
mongoObject._id = className;
|
||||
return this._schemaCollection()
|
||||
.then(schemaCollection => schemaCollection._collection.insertOne(mongoObject))
|
||||
.then(result => MongoSchemaCollection._TESTmongoSchemaToParseSchema(result.ops[0]))
|
||||
.catch(error => {
|
||||
if (error.code === 11000) { //Mongo's duplicate key error
|
||||
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'Class already exists.');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
addFieldIfNotExists(className, fieldName, type) {
|
||||
return this._schemaCollection()
|
||||
.then(schemaCollection => schemaCollection.addFieldIfNotExists(className, fieldName, type));
|
||||
}
|
||||
|
||||
// Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.)
|
||||
// and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible.
|
||||
deleteClass(className) {
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.drop())
|
||||
.catch(error => {
|
||||
// 'ns not found' means collection was already gone. Ignore deletion attempt.
|
||||
if (error.message == 'ns not found') {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
// We've dropped the collection, now remove the _SCHEMA document
|
||||
.then(() => this._schemaCollection())
|
||||
.then(schemaCollection => schemaCollection.findAndDeleteSchema(className))
|
||||
}
|
||||
|
||||
// Delete all data known to this adatper. Used for testing.
|
||||
deleteAllClasses() {
|
||||
return storageAdapterAllCollections(this)
|
||||
.then(collections => Promise.all(collections.map(collection => collection.drop())));
|
||||
}
|
||||
|
||||
// Remove the column and all the data. For Relations, the _Join collection is handled
|
||||
// specially, this function does not delete _Join columns. It should, however, indicate
|
||||
// that the relation fields does not exist anymore. In mongo, this means removing it from
|
||||
// the _SCHEMA collection. There should be no actual data in the collection under the same name
|
||||
// as the relation column, so it's fine to attempt to delete it. If the fields listed to be
|
||||
// deleted do not exist, this function should return successfully anyways. Checking for
|
||||
// attempts to delete non-existent fields is the responsibility of Parse Server.
|
||||
|
||||
// Pointer field names are passed for legacy reasons: the original mongo
|
||||
// format stored pointer field names differently in the database, and therefore
|
||||
// needed to know the type of the field before it could delete it. Future database
|
||||
// adatpers should ignore the pointerFieldNames argument. All the field names are in
|
||||
// fieldNames, they show up additionally in the pointerFieldNames database for use
|
||||
// by the mongo adapter, which deals with the legacy mongo format.
|
||||
|
||||
// This function is not obligated to delete fields atomically. It is given the field
|
||||
// names in a list so that databases that are capable of deleting fields atomically
|
||||
// may do so.
|
||||
|
||||
// Returns a Promise.
|
||||
deleteFields(className, schema, fieldNames) {
|
||||
const mongoFormatNames = fieldNames.map(fieldName => {
|
||||
if (schema.fields[fieldName].type === 'Pointer') {
|
||||
return `_p_${fieldName}`
|
||||
} else {
|
||||
return fieldName;
|
||||
}
|
||||
});
|
||||
const collectionUpdate = { '$unset' : {} };
|
||||
mongoFormatNames.forEach(name => {
|
||||
collectionUpdate['$unset'][name] = null;
|
||||
});
|
||||
|
||||
const schemaUpdate = { '$unset' : {} };
|
||||
fieldNames.forEach(name => {
|
||||
schemaUpdate['$unset'][name] = null;
|
||||
});
|
||||
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.updateMany({}, collectionUpdate))
|
||||
.then(() => this._schemaCollection())
|
||||
.then(schemaCollection => schemaCollection.updateSchema(className, schemaUpdate));
|
||||
}
|
||||
|
||||
// Return a promise for all schemas known to this adapter, in Parse format. In case the
|
||||
// schemas cannot be retrieved, returns a promise that rejects. Requirements for the
|
||||
// rejection reason are TBD.
|
||||
getAllClasses() {
|
||||
return this._schemaCollection().then(schemasCollection => schemasCollection._fetchAllSchemasFrom_SCHEMA());
|
||||
}
|
||||
|
||||
// Return a promise for the schema with the given name, in Parse format. If
|
||||
// this adapter doesn't know about the schema, return a promise that rejects with
|
||||
// undefined as the reason.
|
||||
getClass(className) {
|
||||
return this._schemaCollection()
|
||||
.then(schemasCollection => schemasCollection._fechOneSchemaFrom_SCHEMA(className))
|
||||
}
|
||||
|
||||
// TODO: As yet not particularly well specified. Creates an object. Maybe shouldn't even need the schema,
|
||||
// and should infer from the type. Or maybe does need the schema for validations. Or maybe needs
|
||||
// the schem only for the legacy mongo format. We'll figure that out later.
|
||||
createObject(className, schema, object) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.insertOne(mongoObject))
|
||||
.catch(error => {
|
||||
if (error.code === 11000) { // Duplicate value
|
||||
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE,
|
||||
'A duplicate value for a field with unique values was provided');
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
// Remove all objects that match the given Parse Query.
|
||||
// If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined.
|
||||
// If there is some other error, reject with INTERNAL_SERVER_ERROR.
|
||||
deleteObjectsByQuery(className, schema, query) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => {
|
||||
let mongoWhere = transformWhere(className, query, schema);
|
||||
return collection.deleteMany(mongoWhere)
|
||||
})
|
||||
.then(({ result }) => {
|
||||
if (result.n === 0) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}, error => {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database adapter error');
|
||||
});
|
||||
}
|
||||
|
||||
// Apply the update to all objects that match the given Parse Query.
|
||||
updateObjectsByQuery(className, schema, query, update) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoUpdate = transformUpdate(className, update, schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.updateMany(mongoWhere, mongoUpdate));
|
||||
}
|
||||
|
||||
// Atomically finds and updates an object based on query.
|
||||
// Return value not currently well specified.
|
||||
findOneAndUpdate(className, schema, query, update) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoUpdate = transformUpdate(className, update, schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection._mongoCollection.findAndModify(mongoWhere, [], mongoUpdate, { new: true }))
|
||||
.then(result => result.value);
|
||||
}
|
||||
|
||||
// Hopefully we can get rid of this. It's only used for config and hooks.
|
||||
upsertOneObject(className, schema, query, update) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoUpdate = transformUpdate(className, update, schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.upsertOne(mongoWhere, mongoUpdate));
|
||||
}
|
||||
|
||||
// Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }.
|
||||
find(className, schema, query, { skip, limit, sort }) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
let mongoWhere = transformWhere(className, query, schema);
|
||||
let mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema));
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.find(mongoWhere, { skip, limit, sort: mongoSort }))
|
||||
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)))
|
||||
}
|
||||
|
||||
// Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't
|
||||
// currently know which fields are nullable and which aren't, we ignore that criteria.
|
||||
// As such, we shouldn't expose this function to users of parse until we have an out-of-band
|
||||
// Way of determining if a field is nullable. Undefined doesn't count against uniqueness,
|
||||
// which is why we use sparse indexes.
|
||||
ensureUniqueness(className, schema, fieldNames) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
let indexCreationRequest = {};
|
||||
let mongoFieldNames = fieldNames.map(fieldName => transformKey(className, fieldName, schema));
|
||||
mongoFieldNames.forEach(fieldName => {
|
||||
indexCreationRequest[fieldName] = 1;
|
||||
});
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection._ensureSparseUniqueIndexInBackground(indexCreationRequest))
|
||||
.catch(error => {
|
||||
if (error.code === 11000) {
|
||||
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'Tried to ensure field uniqueness for a class that already has duplicates.');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Used in tests
|
||||
_rawFind(className, query) {
|
||||
return this._adaptiveCollection(className).then(collection => collection.find(query));
|
||||
}
|
||||
|
||||
// Executs a count.
|
||||
count(className, schema, query) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.count(transformWhere(className, query, schema)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,884 @@
|
||||
import log from '../../../logger';
|
||||
import _ from 'lodash';
|
||||
var mongodb = require('mongodb');
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
const transformKey = (className, fieldName, schema) => {
|
||||
// Check if the schema is known since it's a built-in field.
|
||||
switch(fieldName) {
|
||||
case 'objectId': return '_id';
|
||||
case 'createdAt': return '_created_at';
|
||||
case 'updatedAt': return '_updated_at';
|
||||
case 'sessionToken': return '_session_token';
|
||||
}
|
||||
|
||||
if (schema.fields[fieldName] && schema.fields[fieldName].__type == 'Pointer') {
|
||||
fieldName = '_p_' + fieldName;
|
||||
}
|
||||
|
||||
return fieldName;
|
||||
}
|
||||
|
||||
const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSchema) => {
|
||||
// Check if the schema is known since it's a built-in field.
|
||||
var key = restKey;
|
||||
var timeField = false;
|
||||
switch(key) {
|
||||
case 'objectId':
|
||||
case '_id':
|
||||
key = '_id';
|
||||
break;
|
||||
case 'createdAt':
|
||||
case '_created_at':
|
||||
key = '_created_at';
|
||||
timeField = true;
|
||||
break;
|
||||
case 'updatedAt':
|
||||
case '_updated_at':
|
||||
key = '_updated_at';
|
||||
timeField = true;
|
||||
break;
|
||||
case 'sessionToken':
|
||||
case '_session_token':
|
||||
key = '_session_token';
|
||||
break;
|
||||
case 'expiresAt':
|
||||
case '_expiresAt':
|
||||
key = 'expiresAt';
|
||||
timeField = true;
|
||||
break;
|
||||
case '_rperm':
|
||||
case '_wperm':
|
||||
return {key: key, value: restValue};
|
||||
break;
|
||||
}
|
||||
|
||||
if ((parseFormatSchema.fields[key] && parseFormatSchema.fields[key].type === 'Pointer') || (!parseFormatSchema.fields[key] && restValue && restValue.__type == 'Pointer')) {
|
||||
key = '_p_' + key;
|
||||
}
|
||||
|
||||
// Handle atomic values
|
||||
var value = transformTopLevelAtom(restValue);
|
||||
if (value !== CannotTransform) {
|
||||
if (timeField && (typeof value === 'string')) {
|
||||
value = new Date(value);
|
||||
}
|
||||
return {key, value};
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (restValue instanceof Array) {
|
||||
value = restValue.map(transformInteriorValue);
|
||||
return {key, value};
|
||||
}
|
||||
|
||||
// Handle update operators
|
||||
if (typeof restValue === 'object' && '__op' in restValue) {
|
||||
return {key, value: transformUpdateOperator(restValue, false)};
|
||||
}
|
||||
|
||||
// Handle normal objects by recursing
|
||||
value = _.mapValues(restValue, transformInteriorValue);
|
||||
return {key, value};
|
||||
}
|
||||
|
||||
const transformInteriorValue = restValue => {
|
||||
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
|
||||
}
|
||||
// Handle atomic values
|
||||
var value = transformInteriorAtom(restValue);
|
||||
if (value !== CannotTransform) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (restValue instanceof Array) {
|
||||
return restValue.map(transformInteriorValue);
|
||||
}
|
||||
|
||||
// Handle update operators
|
||||
if (typeof restValue === 'object' && '__op' in restValue) {
|
||||
return transformUpdateOperator(restValue, true);
|
||||
}
|
||||
|
||||
// Handle normal objects by recursing
|
||||
return _.mapValues(restValue, transformInteriorValue);
|
||||
}
|
||||
|
||||
const valueAsDate = value => {
|
||||
if (typeof value === 'string') {
|
||||
return new Date(value);
|
||||
} else if (value instanceof Date) {
|
||||
return value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function transformQueryKeyValue(className, key, value, schema) {
|
||||
switch(key) {
|
||||
case 'createdAt':
|
||||
if (valueAsDate(value)) {
|
||||
return {key: '_created_at', value: valueAsDate(value)}
|
||||
}
|
||||
key = '_created_at';
|
||||
break;
|
||||
case 'updatedAt':
|
||||
if (valueAsDate(value)) {
|
||||
return {key: '_updated_at', value: valueAsDate(value)}
|
||||
}
|
||||
key = '_updated_at';
|
||||
break;
|
||||
case 'expiresAt':
|
||||
if (valueAsDate(value)) {
|
||||
return {key: 'expiresAt', value: valueAsDate(value)}
|
||||
}
|
||||
break;
|
||||
case 'objectId': return {key: '_id', value}
|
||||
case 'sessionToken': return {key: '_session_token', value}
|
||||
case '_rperm':
|
||||
case '_wperm':
|
||||
case '_perishable_token':
|
||||
case '_email_verify_token': return {key, value}
|
||||
case '$or':
|
||||
return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, schema))};
|
||||
case '$and':
|
||||
return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, schema))};
|
||||
default:
|
||||
// Other auth data
|
||||
const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/);
|
||||
if (authDataMatch) {
|
||||
const provider = authDataMatch[1];
|
||||
// Special-case auth data.
|
||||
return {key: `_auth_data_${provider}.id`, value};
|
||||
}
|
||||
}
|
||||
|
||||
const expectedTypeIsArray =
|
||||
schema &&
|
||||
schema.fields[key] &&
|
||||
schema.fields[key].type === 'Array';
|
||||
|
||||
const expectedTypeIsPointer =
|
||||
schema &&
|
||||
schema.fields[key] &&
|
||||
schema.fields[key].type === 'Pointer';
|
||||
|
||||
if (expectedTypeIsPointer || !schema && value && value.__type === 'Pointer') {
|
||||
key = '_p_' + key;
|
||||
}
|
||||
|
||||
// Handle query constraints
|
||||
if (transformConstraint(value, expectedTypeIsArray) !== CannotTransform) {
|
||||
return {key, value: transformConstraint(value, expectedTypeIsArray)};
|
||||
}
|
||||
|
||||
if (expectedTypeIsArray && !(value instanceof Array)) {
|
||||
return {key, value: { '$all' : [value] }};
|
||||
}
|
||||
|
||||
// Handle atomic values
|
||||
if (transformTopLevelAtom(value) !== CannotTransform) {
|
||||
return {key, value: transformTopLevelAtom(value)};
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `You cannot use ${value} as a query parameter.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Main exposed method to help run queries.
|
||||
// restWhere is the "where" clause in REST API form.
|
||||
// Returns the mongo form of the query.
|
||||
function transformWhere(className, restWhere, schema) {
|
||||
let mongoWhere = {};
|
||||
for (let restKey in restWhere) {
|
||||
let out = transformQueryKeyValue(className, restKey, restWhere[restKey], schema);
|
||||
mongoWhere[out.key] = out.value;
|
||||
}
|
||||
return mongoWhere;
|
||||
}
|
||||
|
||||
const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) => {
|
||||
// Check if the schema is known since it's a built-in field.
|
||||
let transformedValue;
|
||||
let coercedToDate;
|
||||
switch(restKey) {
|
||||
case 'objectId': return {key: '_id', value: restValue};
|
||||
case 'expiresAt':
|
||||
transformedValue = transformTopLevelAtom(restValue);
|
||||
coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue
|
||||
return {key: 'expiresAt', value: coercedToDate};
|
||||
case '_rperm':
|
||||
case '_wperm':
|
||||
case '_email_verify_token':
|
||||
case '_hashed_password':
|
||||
case '_perishable_token': return {key: restKey, value: restValue};
|
||||
case 'sessionToken': return {key: '_session_token', value: restValue};
|
||||
default:
|
||||
// Auth data should have been transformed already
|
||||
if (restKey.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'can only query on ' + restKey);
|
||||
}
|
||||
// Trust that the auth data has been transformed and save it directly
|
||||
if (restKey.match(/^_auth_data_[a-zA-Z0-9_]+$/)) {
|
||||
return {key: restKey, value: restValue};
|
||||
}
|
||||
}
|
||||
//skip straight to transformTopLevelAtom for Bytes, they don't show up in the schema for some reason
|
||||
if (restValue && restValue.__type !== 'Bytes') {
|
||||
//Note: We may not know the type of a field here, as the user could be saving (null) to a field
|
||||
//That never existed before, meaning we can't infer the type.
|
||||
if (schema.fields[restKey] && schema.fields[restKey].type == 'Pointer' || restValue.__type == 'Pointer') {
|
||||
restKey = '_p_' + restKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle atomic values
|
||||
var value = transformTopLevelAtom(restValue);
|
||||
if (value !== CannotTransform) {
|
||||
return {key: restKey, value: value};
|
||||
}
|
||||
|
||||
// ACLs are handled before this method is called
|
||||
// If an ACL key still exists here, something is wrong.
|
||||
if (restKey === 'ACL') {
|
||||
throw 'There was a problem transforming an ACL.';
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (restValue instanceof Array) {
|
||||
value = restValue.map(transformInteriorValue);
|
||||
return {key: restKey, value: value};
|
||||
}
|
||||
|
||||
// Handle normal objects by recursing
|
||||
if (Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
|
||||
}
|
||||
value = _.mapValues(restValue, transformInteriorValue);
|
||||
return {key: restKey, value};
|
||||
}
|
||||
|
||||
const parseObjectToMongoObjectForCreate = (className, restCreate, schema) => {
|
||||
restCreate = addLegacyACL(restCreate);
|
||||
let mongoCreate = {}
|
||||
for (let restKey in restCreate) {
|
||||
let { key, value } = parseObjectKeyValueToMongoObjectKeyValue(
|
||||
restKey,
|
||||
restCreate[restKey],
|
||||
schema
|
||||
);
|
||||
if (value !== undefined) {
|
||||
mongoCreate[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the legacy mongo format for createdAt and updatedAt
|
||||
if (mongoCreate.createdAt) {
|
||||
mongoCreate._created_at = new Date(mongoCreate.createdAt.iso || mongoCreate.createdAt);
|
||||
delete mongoCreate.createdAt;
|
||||
}
|
||||
if (mongoCreate.updatedAt) {
|
||||
mongoCreate._updated_at = new Date(mongoCreate.updatedAt.iso || mongoCreate.updatedAt);
|
||||
delete mongoCreate.updatedAt;
|
||||
}
|
||||
|
||||
return mongoCreate;
|
||||
}
|
||||
|
||||
// Main exposed method to help update old objects.
|
||||
const transformUpdate = (className, restUpdate, parseFormatSchema) => {
|
||||
let mongoUpdate = {};
|
||||
let acl = addLegacyACL(restUpdate)._acl;
|
||||
if (acl) {
|
||||
mongoUpdate.$set = {};
|
||||
if (acl._rperm) {
|
||||
mongoUpdate.$set._rperm = acl._rperm;
|
||||
}
|
||||
if (acl._wperm) {
|
||||
mongoUpdate.$set._wperm = acl._wperm;
|
||||
}
|
||||
if (acl._acl) {
|
||||
mongoUpdate.$set._acl = acl._acl;
|
||||
}
|
||||
}
|
||||
for (var restKey in restUpdate) {
|
||||
var out = transformKeyValueForUpdate(className, restKey, restUpdate[restKey], parseFormatSchema);
|
||||
|
||||
// If the output value is an object with any $ keys, it's an
|
||||
// operator that needs to be lifted onto the top level update
|
||||
// object.
|
||||
if (typeof out.value === 'object' && out.value !== null && out.value.__op) {
|
||||
mongoUpdate[out.value.__op] = mongoUpdate[out.value.__op] || {};
|
||||
mongoUpdate[out.value.__op][out.key] = out.value.arg;
|
||||
} else {
|
||||
mongoUpdate['$set'] = mongoUpdate['$set'] || {};
|
||||
mongoUpdate['$set'][out.key] = out.value;
|
||||
}
|
||||
}
|
||||
|
||||
return mongoUpdate;
|
||||
}
|
||||
|
||||
// Add the legacy _acl format.
|
||||
const addLegacyACL = restObject => {
|
||||
let restObjectCopy = {...restObject};
|
||||
let _acl = {};
|
||||
|
||||
if (restObject._wperm) {
|
||||
restObject._wperm.forEach(entry => {
|
||||
_acl[entry] = { w: true };
|
||||
});
|
||||
}
|
||||
|
||||
if (restObject._rperm) {
|
||||
restObject._rperm.forEach(entry => {
|
||||
if (!(entry in _acl)) {
|
||||
_acl[entry] = { r: true };
|
||||
} else {
|
||||
_acl[entry].r = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(_acl).length > 0) {
|
||||
restObjectCopy._acl = _acl;
|
||||
}
|
||||
|
||||
return restObjectCopy;
|
||||
}
|
||||
|
||||
|
||||
// A sentinel value that helper transformations return when they
|
||||
// cannot perform a transformation
|
||||
function CannotTransform() {}
|
||||
|
||||
const transformInteriorAtom = atom => {
|
||||
// TODO: check validity harder for the __type-defined types
|
||||
if (typeof atom === 'object' && atom && !(atom instanceof Date) && atom.__type === 'Pointer') {
|
||||
return {
|
||||
__type: 'Pointer',
|
||||
className: atom.className,
|
||||
objectId: atom.objectId
|
||||
};
|
||||
} else if (typeof atom === 'function' || typeof atom === 'symbol') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `cannot transform value: ${atom}`);
|
||||
} else if (DateCoder.isValidJSON(atom)) {
|
||||
return DateCoder.JSONToDatabase(atom);
|
||||
} else if (BytesCoder.isValidJSON(atom)) {
|
||||
return BytesCoder.JSONToDatabase(atom);
|
||||
} else {
|
||||
return atom;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to transform an atom from REST format to Mongo format.
|
||||
// An atom is anything that can't contain other expressions. So it
|
||||
// includes things where objects are used to represent other
|
||||
// datatypes, like pointers and dates, but it does not include objects
|
||||
// or arrays with generic stuff inside.
|
||||
// Raises an error if this cannot possibly be valid REST format.
|
||||
// Returns CannotTransform if it's just not an atom
|
||||
function transformTopLevelAtom(atom) {
|
||||
switch(typeof atom) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
return atom;
|
||||
case 'undefined':
|
||||
return atom;
|
||||
case 'symbol':
|
||||
case 'function':
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `cannot transform value: ${atom}`);
|
||||
case 'object':
|
||||
if (atom instanceof Date) {
|
||||
// Technically dates are not rest format, but, it seems pretty
|
||||
// clear what they should be transformed to, so let's just do it.
|
||||
return atom;
|
||||
}
|
||||
|
||||
if (atom === null) {
|
||||
return atom;
|
||||
}
|
||||
|
||||
// TODO: check validity harder for the __type-defined types
|
||||
if (atom.__type == 'Pointer') {
|
||||
return `${atom.className}$${atom.objectId}`;
|
||||
}
|
||||
if (DateCoder.isValidJSON(atom)) {
|
||||
return DateCoder.JSONToDatabase(atom);
|
||||
}
|
||||
if (BytesCoder.isValidJSON(atom)) {
|
||||
return BytesCoder.JSONToDatabase(atom);
|
||||
}
|
||||
if (GeoPointCoder.isValidJSON(atom)) {
|
||||
return GeoPointCoder.JSONToDatabase(atom);
|
||||
}
|
||||
if (FileCoder.isValidJSON(atom)) {
|
||||
return FileCoder.JSONToDatabase(atom);
|
||||
}
|
||||
return CannotTransform;
|
||||
|
||||
default:
|
||||
// I don't think typeof can ever let us get here
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, `really did not expect value: ${atom}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Transforms a query constraint from REST API format to Mongo format.
|
||||
// A constraint is something with fields like $lt.
|
||||
// If it is not a valid constraint but it could be a valid something
|
||||
// else, return CannotTransform.
|
||||
// inArray is whether this is an array field.
|
||||
function transformConstraint(constraint, inArray) {
|
||||
if (typeof constraint !== 'object' || !constraint) {
|
||||
return CannotTransform;
|
||||
}
|
||||
|
||||
// keys is the constraints in reverse alphabetical order.
|
||||
// This is a hack so that:
|
||||
// $regex is handled before $options
|
||||
// $nearSphere is handled before $maxDistance
|
||||
var keys = Object.keys(constraint).sort().reverse();
|
||||
var answer = {};
|
||||
for (var key of keys) {
|
||||
switch(key) {
|
||||
case '$lt':
|
||||
case '$lte':
|
||||
case '$gt':
|
||||
case '$gte':
|
||||
case '$exists':
|
||||
case '$ne':
|
||||
case '$eq':
|
||||
answer[key] = inArray ? transformInteriorAtom(constraint[key]) : transformTopLevelAtom(constraint[key]);
|
||||
if (answer[key] === CannotTransform) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `bad atom: ${atom}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case '$in':
|
||||
case '$nin':
|
||||
var arr = constraint[key];
|
||||
if (!(arr instanceof Array)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value');
|
||||
}
|
||||
answer[key] = arr.map(value => {
|
||||
let result = inArray ? transformInteriorAtom(value) : transformTopLevelAtom(value);
|
||||
if (result === CannotTransform) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `bad atom: ${atom}`);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
break;
|
||||
|
||||
case '$all':
|
||||
var arr = constraint[key];
|
||||
if (!(arr instanceof Array)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||
'bad ' + key + ' value');
|
||||
}
|
||||
answer[key] = arr.map(transformInteriorAtom);
|
||||
break;
|
||||
|
||||
case '$regex':
|
||||
var s = constraint[key];
|
||||
if (typeof s !== 'string') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad regex: ' + s);
|
||||
}
|
||||
answer[key] = s;
|
||||
break;
|
||||
|
||||
case '$options':
|
||||
answer[key] = constraint[key];
|
||||
break;
|
||||
|
||||
case '$nearSphere':
|
||||
var point = constraint[key];
|
||||
answer[key] = [point.longitude, point.latitude];
|
||||
break;
|
||||
|
||||
case '$maxDistance':
|
||||
answer[key] = constraint[key];
|
||||
break;
|
||||
|
||||
// The SDKs don't seem to use these but they are documented in the
|
||||
// REST API docs.
|
||||
case '$maxDistanceInRadians':
|
||||
answer['$maxDistance'] = constraint[key];
|
||||
break;
|
||||
case '$maxDistanceInMiles':
|
||||
answer['$maxDistance'] = constraint[key] / 3959;
|
||||
break;
|
||||
case '$maxDistanceInKilometers':
|
||||
answer['$maxDistance'] = constraint[key] / 6371;
|
||||
break;
|
||||
|
||||
case '$select':
|
||||
case '$dontSelect':
|
||||
throw new Parse.Error(
|
||||
Parse.Error.COMMAND_UNAVAILABLE,
|
||||
'the ' + key + ' constraint is not supported yet');
|
||||
|
||||
case '$within':
|
||||
var box = constraint[key]['$box'];
|
||||
if (!box || box.length != 2) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'malformatted $within arg');
|
||||
}
|
||||
answer[key] = {
|
||||
'$box': [
|
||||
[box[0].longitude, box[0].latitude],
|
||||
[box[1].longitude, box[1].latitude]
|
||||
]
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
if (key.match(/^\$+/)) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'bad constraint: ' + key);
|
||||
}
|
||||
return CannotTransform;
|
||||
}
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
|
||||
// Transforms an update operator from REST format to mongo format.
|
||||
// To be transformed, the input should have an __op field.
|
||||
// If flatten is true, this will flatten operators to their static
|
||||
// data format. For example, an increment of 2 would simply become a
|
||||
// 2.
|
||||
// The output for a non-flattened operator is a hash with __op being
|
||||
// the mongo op, and arg being the argument.
|
||||
// The output for a flattened operator is just a value.
|
||||
// Returns undefined if this should be a no-op.
|
||||
|
||||
function transformUpdateOperator({
|
||||
__op,
|
||||
amount,
|
||||
objects,
|
||||
}, flatten) {
|
||||
switch(__op) {
|
||||
case 'Delete':
|
||||
if (flatten) {
|
||||
return undefined;
|
||||
} else {
|
||||
return {__op: '$unset', arg: ''};
|
||||
}
|
||||
|
||||
case 'Increment':
|
||||
if (typeof amount !== 'number') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'incrementing must provide a number');
|
||||
}
|
||||
if (flatten) {
|
||||
return amount;
|
||||
} else {
|
||||
return {__op: '$inc', arg: amount};
|
||||
}
|
||||
|
||||
case 'Add':
|
||||
case 'AddUnique':
|
||||
if (!(objects instanceof Array)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array');
|
||||
}
|
||||
var toAdd = objects.map(transformInteriorAtom);
|
||||
if (flatten) {
|
||||
return toAdd;
|
||||
} else {
|
||||
var mongoOp = {
|
||||
Add: '$push',
|
||||
AddUnique: '$addToSet'
|
||||
}[__op];
|
||||
return {__op: mongoOp, arg: {'$each': toAdd}};
|
||||
}
|
||||
|
||||
case 'Remove':
|
||||
if (!(objects instanceof Array)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to remove must be an array');
|
||||
}
|
||||
var toRemove = objects.map(transformInteriorAtom);
|
||||
if (flatten) {
|
||||
return [];
|
||||
} else {
|
||||
return {__op: '$pullAll', arg: toRemove};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, `The ${__op} operator is not supported yet.`);
|
||||
}
|
||||
}
|
||||
|
||||
const nestedMongoObjectToNestedParseObject = mongoObject => {
|
||||
switch(typeof mongoObject) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
return mongoObject;
|
||||
case 'undefined':
|
||||
case 'symbol':
|
||||
case 'function':
|
||||
throw 'bad value in mongoObjectToParseObject';
|
||||
case 'object':
|
||||
if (mongoObject === null) {
|
||||
return null;
|
||||
}
|
||||
if (mongoObject instanceof Array) {
|
||||
return mongoObject.map(nestedMongoObjectToNestedParseObject);
|
||||
}
|
||||
|
||||
if (mongoObject instanceof Date) {
|
||||
return Parse._encode(mongoObject);
|
||||
}
|
||||
|
||||
if (mongoObject instanceof mongodb.Long) {
|
||||
return mongoObject.toNumber();
|
||||
}
|
||||
|
||||
if (mongoObject instanceof mongodb.Double) {
|
||||
return mongoObject.value;
|
||||
}
|
||||
|
||||
if (BytesCoder.isValidDatabaseObject(mongoObject)) {
|
||||
return BytesCoder.databaseToJSON(mongoObject);
|
||||
}
|
||||
|
||||
return _.mapValues(mongoObject, nestedMongoObjectToNestedParseObject);
|
||||
default:
|
||||
throw 'unknown js type';
|
||||
}
|
||||
}
|
||||
|
||||
// Converts from a mongo-format object to a REST-format object.
|
||||
// Does not strip out anything based on a lack of authentication.
|
||||
const mongoObjectToParseObject = (className, mongoObject, schema) => {
|
||||
switch(typeof mongoObject) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
return mongoObject;
|
||||
case 'undefined':
|
||||
case 'symbol':
|
||||
case 'function':
|
||||
throw 'bad value in mongoObjectToParseObject';
|
||||
case 'object':
|
||||
if (mongoObject === null) {
|
||||
return null;
|
||||
}
|
||||
if (mongoObject instanceof Array) {
|
||||
return mongoObject.map(nestedMongoObjectToNestedParseObject);
|
||||
}
|
||||
|
||||
if (mongoObject instanceof Date) {
|
||||
return Parse._encode(mongoObject);
|
||||
}
|
||||
|
||||
if (mongoObject instanceof mongodb.Long) {
|
||||
return mongoObject.toNumber();
|
||||
}
|
||||
|
||||
if (mongoObject instanceof mongodb.Double) {
|
||||
return mongoObject.value;
|
||||
}
|
||||
|
||||
if (BytesCoder.isValidDatabaseObject(mongoObject)) {
|
||||
return BytesCoder.databaseToJSON(mongoObject);
|
||||
}
|
||||
|
||||
let restObject = {};
|
||||
if (mongoObject._rperm || mongoObject._wperm) {
|
||||
restObject._rperm = mongoObject._rperm || [];
|
||||
restObject._wperm = mongoObject._wperm || [];
|
||||
delete mongoObject._rperm;
|
||||
delete mongoObject._wperm;
|
||||
}
|
||||
|
||||
for (var key in mongoObject) {
|
||||
switch(key) {
|
||||
case '_id':
|
||||
restObject['objectId'] = '' + mongoObject[key];
|
||||
break;
|
||||
case '_hashed_password':
|
||||
restObject._hashed_password = mongoObject[key];
|
||||
break;
|
||||
case '_acl':
|
||||
case '_email_verify_token':
|
||||
case '_perishable_token':
|
||||
case '_tombstone':
|
||||
break;
|
||||
case '_session_token':
|
||||
restObject['sessionToken'] = mongoObject[key];
|
||||
break;
|
||||
case 'updatedAt':
|
||||
case '_updated_at':
|
||||
restObject['updatedAt'] = Parse._encode(new Date(mongoObject[key])).iso;
|
||||
break;
|
||||
case 'createdAt':
|
||||
case '_created_at':
|
||||
restObject['createdAt'] = Parse._encode(new Date(mongoObject[key])).iso;
|
||||
break;
|
||||
case 'expiresAt':
|
||||
case '_expiresAt':
|
||||
restObject['expiresAt'] = Parse._encode(new Date(mongoObject[key]));
|
||||
break;
|
||||
default:
|
||||
// Check other auth data keys
|
||||
var authDataMatch = key.match(/^_auth_data_([a-zA-Z0-9_]+)$/);
|
||||
if (authDataMatch) {
|
||||
var provider = authDataMatch[1];
|
||||
restObject['authData'] = restObject['authData'] || {};
|
||||
restObject['authData'][provider] = mongoObject[key];
|
||||
break;
|
||||
}
|
||||
|
||||
if (key.indexOf('_p_') == 0) {
|
||||
var newKey = key.substring(3);
|
||||
if (!schema.fields[newKey]) {
|
||||
log.info('transform.js', 'Found a pointer column not in the schema, dropping it.', className, newKey);
|
||||
break;
|
||||
}
|
||||
if (schema.fields[newKey].type !== 'Pointer') {
|
||||
log.info('transform.js', 'Found a pointer in a non-pointer column, dropping it.', className, key);
|
||||
break;
|
||||
}
|
||||
if (mongoObject[key] === null) {
|
||||
break;
|
||||
}
|
||||
var objData = mongoObject[key].split('$');
|
||||
if (objData[0] !== schema.fields[newKey].targetClass) {
|
||||
throw 'pointer to incorrect className';
|
||||
}
|
||||
restObject[newKey] = {
|
||||
__type: 'Pointer',
|
||||
className: objData[0],
|
||||
objectId: objData[1]
|
||||
};
|
||||
break;
|
||||
} else if (key[0] == '_' && key != '__type') {
|
||||
throw ('bad key in untransform: ' + key);
|
||||
} else {
|
||||
var value = mongoObject[key];
|
||||
if (schema.fields[key] && schema.fields[key].type === 'File' && FileCoder.isValidDatabaseObject(value)) {
|
||||
restObject[key] = FileCoder.databaseToJSON(value);
|
||||
break;
|
||||
}
|
||||
if (schema.fields[key] && schema.fields[key].type === 'GeoPoint' && GeoPointCoder.isValidDatabaseObject(value)) {
|
||||
restObject[key] = GeoPointCoder.databaseToJSON(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
restObject[key] = nestedMongoObjectToNestedParseObject(mongoObject[key]);
|
||||
}
|
||||
}
|
||||
|
||||
const relationFieldNames = Object.keys(schema.fields).filter(fieldName => schema.fields[fieldName].type === 'Relation');
|
||||
let relationFields = {};
|
||||
relationFieldNames.forEach(relationFieldName => {
|
||||
relationFields[relationFieldName] = {
|
||||
__type: 'Relation',
|
||||
className: schema.fields[relationFieldName].targetClass,
|
||||
}
|
||||
});
|
||||
|
||||
return { ...restObject, ...relationFields };
|
||||
default:
|
||||
throw 'unknown js type';
|
||||
}
|
||||
}
|
||||
|
||||
var DateCoder = {
|
||||
JSONToDatabase(json) {
|
||||
return new Date(json.iso);
|
||||
},
|
||||
|
||||
isValidJSON(value) {
|
||||
return (typeof value === 'object' &&
|
||||
value !== null &&
|
||||
value.__type === 'Date'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
var BytesCoder = {
|
||||
databaseToJSON(object) {
|
||||
return {
|
||||
__type: 'Bytes',
|
||||
base64: object.buffer.toString('base64')
|
||||
};
|
||||
},
|
||||
|
||||
isValidDatabaseObject(object) {
|
||||
return (object instanceof mongodb.Binary);
|
||||
},
|
||||
|
||||
JSONToDatabase(json) {
|
||||
return new mongodb.Binary(new Buffer(json.base64, 'base64'));
|
||||
},
|
||||
|
||||
isValidJSON(value) {
|
||||
return (typeof value === 'object' &&
|
||||
value !== null &&
|
||||
value.__type === 'Bytes'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
var GeoPointCoder = {
|
||||
databaseToJSON(object) {
|
||||
return {
|
||||
__type: 'GeoPoint',
|
||||
latitude: object[1],
|
||||
longitude: object[0]
|
||||
}
|
||||
},
|
||||
|
||||
isValidDatabaseObject(object) {
|
||||
return (object instanceof Array &&
|
||||
object.length == 2
|
||||
);
|
||||
},
|
||||
|
||||
JSONToDatabase(json) {
|
||||
return [ json.longitude, json.latitude ];
|
||||
},
|
||||
|
||||
isValidJSON(value) {
|
||||
return (typeof value === 'object' &&
|
||||
value !== null &&
|
||||
value.__type === 'GeoPoint'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
var FileCoder = {
|
||||
databaseToJSON(object) {
|
||||
return {
|
||||
__type: 'File',
|
||||
name: object
|
||||
}
|
||||
},
|
||||
|
||||
isValidDatabaseObject(object) {
|
||||
return (typeof object === 'string');
|
||||
},
|
||||
|
||||
JSONToDatabase(json) {
|
||||
return json.name;
|
||||
},
|
||||
|
||||
isValidJSON(value) {
|
||||
return (typeof value === 'object' &&
|
||||
value !== null &&
|
||||
value.__type === 'File'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
transformKey,
|
||||
parseObjectToMongoObjectForCreate,
|
||||
transformUpdate,
|
||||
transformWhere,
|
||||
mongoObjectToParseObject,
|
||||
};
|
||||
@@ -0,0 +1,431 @@
|
||||
const pgp = require('pg-promise')();
|
||||
|
||||
const PostgresRelationDoesNotExistError = '42P01';
|
||||
const PostgresDuplicateRelationError = '42P07';
|
||||
const PostgresDuplicateColumnError = '42701';
|
||||
const PostgresUniqueIndexViolationError = '23505';
|
||||
|
||||
const parseTypeToPostgresType = type => {
|
||||
switch (type.type) {
|
||||
case 'String': return 'text';
|
||||
case 'Date': return 'timestamp';
|
||||
case 'Object': return 'jsonb';
|
||||
case 'Boolean': return 'boolean';
|
||||
case 'Pointer': return 'char(10)';
|
||||
case 'Number': return 'double precision';
|
||||
case 'Array':
|
||||
if (type.contents && type.contents.type === 'String') {
|
||||
return 'text[]';
|
||||
} else {
|
||||
return 'jsonb';
|
||||
}
|
||||
default: throw `no type for ${JSON.stringify(type)} yet`;
|
||||
}
|
||||
};
|
||||
|
||||
const buildWhereClause = ({ schema, query, index }) => {
|
||||
let patterns = [];
|
||||
let values = [];
|
||||
for (let fieldName in query) {
|
||||
let fieldValue = query[fieldName];
|
||||
if (typeof fieldValue === 'string') {
|
||||
patterns.push(`$${index}:name = $${index + 1}`);
|
||||
values.push(fieldName, fieldValue);
|
||||
index += 2;
|
||||
} else if (fieldValue.$ne) {
|
||||
patterns.push(`$${index}:name <> $${index + 1}`);
|
||||
values.push(fieldName, fieldValue.$ne);
|
||||
index += 2;
|
||||
} else if (fieldName === '$or') {
|
||||
fieldValue.map(subQuery => buildWhereClause({ schema, query: subQuery, index })).forEach(result => {
|
||||
patterns.push(result.pattern);
|
||||
values.push(...result.values);
|
||||
});
|
||||
} else if (Array.isArray(fieldValue.$in) && schema.fields[fieldName].type === 'Array') {
|
||||
let inPatterns = [];
|
||||
let allowNull = false;
|
||||
values.push(fieldName);
|
||||
fieldValue.$in.forEach((listElem, listIndex) => {
|
||||
if (listElem === null ) {
|
||||
allowNull = true;
|
||||
} else {
|
||||
values.push(listElem);
|
||||
inPatterns.push(`$${index + 1 + listIndex - (allowNull ? 1 : 0)}`);
|
||||
}
|
||||
});
|
||||
if (allowNull) {
|
||||
patterns.push(`($${index}:name IS NULL OR $${index}:name && ARRAY[${inPatterns.join(',')}])`);
|
||||
} else {
|
||||
patterns.push(`$${index}:name && ARRAY[${inPatterns.join(',')}]`);
|
||||
}
|
||||
index = index + 1 + inPatterns.length;
|
||||
} else if (Array.isArray(fieldValue.$in) && schema.fields[fieldName].type === 'String') {
|
||||
let inPatterns = [];
|
||||
values.push(fieldName);
|
||||
fieldValue.$in.forEach((listElem, listIndex) => {
|
||||
values.push(listElem);
|
||||
inPatterns.push(`$${index + 1 + listIndex}`);
|
||||
});
|
||||
patterns.push(`$${index}:name IN (${inPatterns.join(',')})`);
|
||||
index = index + 1 + inPatterns.length;
|
||||
} else if (fieldValue.__type === 'Pointer') {
|
||||
patterns.push(`$${index}:name = $${index + 1}`);
|
||||
values.push(fieldName, fieldValue.objectId);
|
||||
index += 2;
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Postgres doesn't support this query type yet`);
|
||||
}
|
||||
}
|
||||
return { pattern: patterns.join(' AND '), values };
|
||||
}
|
||||
|
||||
export class PostgresStorageAdapter {
|
||||
// Private
|
||||
_collectionPrefix: string;
|
||||
_client;
|
||||
|
||||
constructor({
|
||||
uri,
|
||||
collectionPrefix = '',
|
||||
}) {
|
||||
this._collectionPrefix = collectionPrefix;
|
||||
this._client = pgp(uri);
|
||||
}
|
||||
|
||||
_ensureSchemaCollectionExists() {
|
||||
return this._client.query('CREATE TABLE "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )')
|
||||
.catch(error => {
|
||||
if (error.code === PostgresDuplicateRelationError) {
|
||||
// Table already exists, must have been created by a different request. Ignore error.
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
classExists(name) {
|
||||
return Promise.reject('Not implemented yet.')
|
||||
}
|
||||
|
||||
setClassLevelPermissions(className, CLPs) {
|
||||
return Promise.reject('Not implemented yet.')
|
||||
}
|
||||
|
||||
createClass(className, schema) {
|
||||
let valuesArray = [];
|
||||
let patternsArray = [];
|
||||
Object.keys(schema.fields).forEach((fieldName, index) => {
|
||||
valuesArray.push(fieldName);
|
||||
let parseType = schema.fields[fieldName];
|
||||
if (['_rperm', '_wperm'].includes(fieldName)) {
|
||||
parseType.contents = { type: 'String' };
|
||||
}
|
||||
valuesArray.push(parseTypeToPostgresType(parseType));
|
||||
patternsArray.push(`$${index * 2 + 2}:name $${index * 2 + 3}:raw`);
|
||||
});
|
||||
return this._ensureSchemaCollectionExists()
|
||||
.then(() => this._client.query(`CREATE TABLE $1:name (${patternsArray.join(',')})`, [className, ...valuesArray]))
|
||||
.catch(error => {
|
||||
if (error.code === PostgresDuplicateRelationError) {
|
||||
// Table already exists, must have been created by a different request. Ignore error.
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
.then(() => this._client.query('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($<className>, $<schema>, true)', { className, schema }))
|
||||
.then(() => schema);
|
||||
}
|
||||
|
||||
addFieldIfNotExists(className, fieldName, type) {
|
||||
// TODO: Must be revised for invalid logic...
|
||||
return this._client.tx("addFieldIfNotExists", t=> {
|
||||
return t.query('ALTER TABLE $<className:name> ADD COLUMN $<fieldName:name> $<postgresType:raw>', {
|
||||
className,
|
||||
fieldName,
|
||||
postgresType: parseTypeToPostgresType(type)
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.code === PostgresRelationDoesNotExistError) {
|
||||
return this.createClass(className, {fields: {[fieldName]: type}})
|
||||
} else if (error.code === PostgresDuplicateColumnError) {
|
||||
// Column already exists, created by other request. Carry on to
|
||||
// See if it's the right type.
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
.then(() => t.query('SELECT "schema" FROM "_SCHEMA" WHERE "className" = $<className>', {className}))
|
||||
.then(result => {
|
||||
if (fieldName in result[0].schema) {
|
||||
throw "Attempted to add a field that already exists";
|
||||
} else {
|
||||
result[0].schema.fields[fieldName] = type;
|
||||
return t.query(
|
||||
'UPDATE "_SCHEMA" SET "schema"=$<schema> WHERE "className"=$<className>',
|
||||
{schema: result[0].schema, className}
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.)
|
||||
// and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible.
|
||||
deleteClass(className) {
|
||||
return Promise.reject('Not implemented yet.')
|
||||
}
|
||||
|
||||
// Delete all data known to this adapter. Used for testing.
|
||||
deleteAllClasses() {
|
||||
return this._client.query('SELECT "className" FROM "_SCHEMA"')
|
||||
.then(results => {
|
||||
const classes = ['_SCHEMA', ...results.map(result => result.className)];
|
||||
return this._client.tx(t=>t.batch(classes.map(className=>t.none('DROP TABLE $<className:name>', { className }))));
|
||||
}, error => {
|
||||
if (error.code === PostgresRelationDoesNotExistError) {
|
||||
// No _SCHEMA collection. Don't delete anything.
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Remove the column and all the data. For Relations, the _Join collection is handled
|
||||
// specially, this function does not delete _Join columns. It should, however, indicate
|
||||
// that the relation fields does not exist anymore. In mongo, this means removing it from
|
||||
// the _SCHEMA collection. There should be no actual data in the collection under the same name
|
||||
// as the relation column, so it's fine to attempt to delete it. If the fields listed to be
|
||||
// deleted do not exist, this function should return successfully anyways. Checking for
|
||||
// attempts to delete non-existent fields is the responsibility of Parse Server.
|
||||
|
||||
// This function is not obligated to delete fields atomically. It is given the field
|
||||
// names in a list so that databases that are capable of deleting fields atomically
|
||||
// may do so.
|
||||
|
||||
// Returns a Promise.
|
||||
deleteFields(className, schema, fieldNames) {
|
||||
return Promise.reject('Not implemented yet.')
|
||||
}
|
||||
|
||||
// Return a promise for all schemas known to this adapter, in Parse format. In case the
|
||||
// schemas cannot be retrieved, returns a promise that rejects. Requirements for the
|
||||
// rejection reason are TBD.
|
||||
getAllClasses() {
|
||||
return this._ensureSchemaCollectionExists()
|
||||
.then(() => this._client.map('SELECT * FROM "_SCHEMA"', null, row => ({ className: row.className, ...row.schema })));
|
||||
}
|
||||
|
||||
// Return a promise for the schema with the given name, in Parse format. If
|
||||
// this adapter doesn't know about the schema, return a promise that rejects with
|
||||
// undefined as the reason.
|
||||
getClass(className) {
|
||||
return this._client.query('SELECT * FROM "_SCHEMA" WHERE "className"=$<className>', { className })
|
||||
.then(result => {
|
||||
if (result.length === 1) {
|
||||
return result[0].schema;
|
||||
} else {
|
||||
throw undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: remove the mongo format dependency in the return value
|
||||
createObject(className, schema, object) {
|
||||
let columnsArray = [];
|
||||
let valuesArray = [];
|
||||
Object.keys(object).forEach(fieldName => {
|
||||
columnsArray.push(fieldName);
|
||||
switch (schema.fields[fieldName].type) {
|
||||
case 'Date':
|
||||
valuesArray.push(object[fieldName].iso);
|
||||
break;
|
||||
case 'Pointer':
|
||||
valuesArray.push(object[fieldName].objectId);
|
||||
break;
|
||||
case 'Array':
|
||||
if (['_rperm', '_wperm'].includes(fieldName)) {
|
||||
valuesArray.push(object[fieldName]);
|
||||
} else {
|
||||
valuesArray.push(JSON.stringify(object[fieldName]));
|
||||
}
|
||||
break;
|
||||
case 'Object':
|
||||
valuesArray.push(object[fieldName]);
|
||||
break;
|
||||
case 'String':
|
||||
valuesArray.push(object[fieldName]);
|
||||
break;
|
||||
case 'Number':
|
||||
valuesArray.push(object[fieldName]);
|
||||
break;
|
||||
case 'Boolean':
|
||||
valuesArray.push(object[fieldName]);
|
||||
break;
|
||||
default:
|
||||
throw `Type ${schema.fields[fieldName].type} not supported yet`;
|
||||
break;
|
||||
}
|
||||
});
|
||||
let columnsPattern = columnsArray.map((col, index) => `$${index + 2}:name`).join(',');
|
||||
let valuesPattern = valuesArray.map((val, index) => `$${index + 2 + columnsArray.length}${(['_rperm','_wperm'].includes(columnsArray[index])) ? '::text[]' : ''}`).join(',');
|
||||
let qs = `INSERT INTO $1:name (${columnsPattern}) VALUES (${valuesPattern})`
|
||||
let values = [className, ...columnsArray, ...valuesArray]
|
||||
return this._client.query(qs, values)
|
||||
.then(() => ({ ops: [object] }))
|
||||
.catch(error => {
|
||||
if (error.code === PostgresUniqueIndexViolationError) {
|
||||
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Remove all objects that match the given Parse Query.
|
||||
// If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined.
|
||||
// If there is some other error, reject with INTERNAL_SERVER_ERROR.
|
||||
deleteObjectsByQuery(className, schema, query) {
|
||||
return this._client.query(`WITH deleted AS (DELETE FROM $<className:name> RETURNING *) SELECT count(*) FROM deleted`, { className })
|
||||
.then(result => {
|
||||
if (result[0].count === 0) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
} else {
|
||||
return result[0].count;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply the update to all objects that match the given Parse Query.
|
||||
updateObjectsByQuery(className, schema, query, update) {
|
||||
return Promise.reject('Not implemented yet.')
|
||||
}
|
||||
|
||||
// Return value not currently well specified.
|
||||
findOneAndUpdate(className, schema, query, update) {
|
||||
let conditionPatterns = [];
|
||||
let updatePatterns = [];
|
||||
let values = [className]
|
||||
let index = 2;
|
||||
|
||||
for (let fieldName in update) {
|
||||
let fieldValue = update[fieldName];
|
||||
if (fieldValue.__op === 'Increment') {
|
||||
updatePatterns.push(`$${index}:name = COALESCE($${index}:name, 0) + $${index + 1}`);
|
||||
values.push(fieldName, fieldValue.amount);
|
||||
index += 2;
|
||||
} else if (fieldValue.__op === 'Add') {
|
||||
updatePatterns.push(`$${index}:name = COALESCE($${index}:name, '[]'::jsonb) || $${index + 1}`);
|
||||
values.push(fieldName, fieldValue.objects);
|
||||
index += 2;
|
||||
} else if (fieldValue.__op === 'Remove') {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Postgres does not support Remove operator.'));
|
||||
} else if (fieldValue.__op === 'AddUnique') {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Postgres does not support AddUnique operator'));
|
||||
} else if (fieldName === 'updatedAt') { //TODO: stop special casing this. It should check for __type === 'Date' and use .iso
|
||||
updatePatterns.push(`$${index}:name = $${index + 1}`)
|
||||
values.push(fieldName, new Date(fieldValue));
|
||||
index += 2;
|
||||
} else if (typeof fieldValue === 'string') {
|
||||
updatePatterns.push(`$${index}:name = $${index + 1}`);
|
||||
values.push(fieldName, fieldValue);
|
||||
index += 2;
|
||||
} else if (fieldValue.__type === 'Pointer') {
|
||||
updatePatterns.push(`$${index}:name = $${index + 1}`);
|
||||
values.push(fieldName, fieldValue.objectId);
|
||||
index += 2;
|
||||
} else {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Postgres doesn't support update ${JSON.stringify(fieldValue)} yet`));
|
||||
}
|
||||
}
|
||||
|
||||
let where = buildWhereClause({ schema, index, query })
|
||||
values.push(...where.values);
|
||||
|
||||
let qs = `UPDATE $1:name SET ${updatePatterns.join(',')} WHERE ${where.pattern} RETURNING *`;
|
||||
return this._client.query(qs, values)
|
||||
.then(val => val[0]); // TODO: This is unsafe, verification is needed, or a different query method;
|
||||
}
|
||||
|
||||
// Hopefully, we can get rid of this. It's only used for config and hooks.
|
||||
upsertOneObject(className, schema, query, update) {
|
||||
return Promise.reject('Not implemented yet.')
|
||||
}
|
||||
|
||||
find(className, schema, query, { skip, limit, sort }) {
|
||||
let values = [className];
|
||||
let where = buildWhereClause({ schema, query, index: 2 })
|
||||
values.push(...where.values);
|
||||
|
||||
const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : '';
|
||||
const limitPattern = limit !== undefined ? `LIMIT $${values.length + 1}` : '';
|
||||
|
||||
const qs = `SELECT * FROM $1:name ${wherePattern} ${limitPattern}`;
|
||||
if (limit !== undefined) {
|
||||
values.push(limit);
|
||||
}
|
||||
return this._client.query(qs, values)
|
||||
.then(results => results.map(object => {
|
||||
Object.keys(schema.fields).filter(field => schema.fields[field].type === 'Pointer').forEach(fieldName => {
|
||||
object[fieldName] = { objectId: object[fieldName], __type: 'Pointer', className: schema.fields[fieldName].targetClass };
|
||||
});
|
||||
//TODO: remove this reliance on the mongo format. DB adapter shouldn't know there is a difference between created at and any other date field.
|
||||
if (object.createdAt) {
|
||||
object.createdAt = object.createdAt.toISOString();
|
||||
}
|
||||
if (object.updatedAt) {
|
||||
object.updatedAt = object.updatedAt.toISOString();
|
||||
}
|
||||
if (object.expiresAt) {
|
||||
object.expiresAt = { __type: 'Date', iso: object.expiresAt.toISOString() };
|
||||
}
|
||||
|
||||
for (let fieldName in object) {
|
||||
if (object[fieldName] === null) {
|
||||
delete object[fieldName];
|
||||
}
|
||||
if (object[fieldName] instanceof Date) {
|
||||
object[fieldName] = { __type: 'Date', iso: object[fieldName].toISOString() };
|
||||
}
|
||||
}
|
||||
|
||||
return object;
|
||||
}))
|
||||
}
|
||||
|
||||
// Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't
|
||||
// currently know which fields are nullable and which aren't, we ignore that criteria.
|
||||
// As such, we shouldn't expose this function to users of parse until we have an out-of-band
|
||||
// Way of determining if a field is nullable. Undefined doesn't count against uniqueness,
|
||||
// which is why we use sparse indexes.
|
||||
ensureUniqueness(className, schema, fieldNames) {
|
||||
// Use the same name for every ensureUniqueness attempt, because postgres
|
||||
// Will happily create the same index with multiple names.
|
||||
const constraintName = `unique_${fieldNames.sort().join('_')}`;
|
||||
const constraintPatterns = fieldNames.map((fieldName, index) => `$${index + 3}:name`);
|
||||
const qs = `ALTER TABLE $1:name ADD CONSTRAINT $2:name UNIQUE (${constraintPatterns.join(',')})`;
|
||||
return this._client.query(qs,[className, constraintName, ...fieldNames])
|
||||
.catch(error => {
|
||||
if (error.code === PostgresDuplicateRelationError && error.message.includes(constraintName)) {
|
||||
// Index already exists. Ignore error.
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Executes a count.
|
||||
count(className, schema, query) {
|
||||
let values = [className];
|
||||
let where = buildWhereClause({ schema, query, index: 2 });
|
||||
values.push(...where.values);
|
||||
|
||||
const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : '';
|
||||
const qs = `SELECT COUNT(*) FROM $1:name ${wherePattern}`;
|
||||
return this._client.query(qs, values)
|
||||
.then(result => parseInt(result[0].count))
|
||||
}
|
||||
}
|
||||
|
||||
export default PostgresStorageAdapter;
|
||||
module.exports = PostgresStorageAdapter; // Required for tests
|
||||
+114
-102
@@ -2,8 +2,6 @@ var deepcopy = require('deepcopy');
|
||||
var Parse = require('parse/node').Parse;
|
||||
var RestQuery = require('./RestQuery');
|
||||
|
||||
import cache from './cache';
|
||||
|
||||
// An Auth object tells you who is requesting something and whether
|
||||
// the master key was used.
|
||||
// userObject is a Parse.User and can be null if there's no user.
|
||||
@@ -42,33 +40,41 @@ function nobody(config) {
|
||||
return new Auth({ config, isMaster: false });
|
||||
}
|
||||
|
||||
|
||||
// Returns a promise that resolves to an Auth object
|
||||
var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) {
|
||||
var cachedUser = cache.users.get(sessionToken);
|
||||
if (cachedUser) {
|
||||
return Promise.resolve(new Auth({ config, isMaster: false, installationId, user: cachedUser }));
|
||||
}
|
||||
var restOptions = {
|
||||
limit: 1,
|
||||
include: 'user'
|
||||
};
|
||||
var restWhere = {
|
||||
_session_token: sessionToken
|
||||
};
|
||||
var query = new RestQuery(config, master(config), '_Session',
|
||||
restWhere, restOptions);
|
||||
return query.execute().then((response) => {
|
||||
var results = response.results;
|
||||
if (results.length !== 1 || !results[0]['user']) {
|
||||
return nobody(config);
|
||||
return config.cacheController.user.get(sessionToken).then((userJSON) => {
|
||||
if (userJSON) {
|
||||
let cachedUser = Parse.Object.fromJSON(userJSON);
|
||||
return Promise.resolve(new Auth({config, isMaster: false, installationId, user: cachedUser}));
|
||||
}
|
||||
var obj = results[0]['user'];
|
||||
delete obj.password;
|
||||
obj['className'] = '_User';
|
||||
obj['sessionToken'] = sessionToken;
|
||||
let userObject = Parse.Object.fromJSON(obj);
|
||||
cache.users.set(sessionToken, userObject);
|
||||
return new Auth({ config, isMaster: false, installationId, user: userObject });
|
||||
|
||||
var restOptions = {
|
||||
limit: 1,
|
||||
include: 'user'
|
||||
};
|
||||
|
||||
var query = new RestQuery(config, master(config), '_Session', {sessionToken}, restOptions);
|
||||
return query.execute().then((response) => {
|
||||
var results = response.results;
|
||||
if (results.length !== 1 || !results[0]['user']) {
|
||||
return nobody(config);
|
||||
}
|
||||
|
||||
var now = new Date(),
|
||||
expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined;
|
||||
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';
|
||||
obj['sessionToken'] = sessionToken;
|
||||
config.cacheController.user.put(sessionToken, obj);
|
||||
let userObject = Parse.Object.fromJSON(obj);
|
||||
return new Auth({config, isMaster: false, installationId, user: userObject});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -89,95 +95,101 @@ Auth.prototype.getUserRoles = function() {
|
||||
|
||||
// Iterates through the role tree and compiles a users roles
|
||||
Auth.prototype._loadRoles = function() {
|
||||
var restWhere = {
|
||||
'users': {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.user.id
|
||||
}
|
||||
};
|
||||
// First get the role ids this user is directly a member of
|
||||
var query = new RestQuery(this.config, master(this.config), '_Role',
|
||||
restWhere, {});
|
||||
return query.execute().then((response) => {
|
||||
var results = response.results;
|
||||
if (!results.length) {
|
||||
this.userRoles = [];
|
||||
var cacheAdapter = this.config.cacheController;
|
||||
return cacheAdapter.role.get(this.user.id).then((cachedRoles) => {
|
||||
if (cachedRoles != null) {
|
||||
this.fetchedRoles = true;
|
||||
this.rolePromise = null;
|
||||
return Promise.resolve(this.userRoles);
|
||||
this.userRoles = cachedRoles;
|
||||
return Promise.resolve(cachedRoles);
|
||||
}
|
||||
|
||||
var roleIDs = results.map(r => r.objectId);
|
||||
var promises = [Promise.resolve(roleIDs)];
|
||||
for (var role of roleIDs) {
|
||||
promises.push(this._getAllRoleNamesForId(role));
|
||||
}
|
||||
return Promise.all(promises).then((results) => {
|
||||
var allIDs = [];
|
||||
for (var x of results) {
|
||||
Array.prototype.push.apply(allIDs, x);
|
||||
var restWhere = {
|
||||
'users': {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.user.id
|
||||
}
|
||||
var restWhere = {
|
||||
objectId: {
|
||||
'$in': allIDs
|
||||
}
|
||||
};
|
||||
var query = new RestQuery(this.config, master(this.config),
|
||||
'_Role', restWhere, {});
|
||||
return query.execute();
|
||||
}).then((response) => {
|
||||
};
|
||||
// First get the role ids this user is directly a member of
|
||||
var query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {});
|
||||
return query.execute().then((response) => {
|
||||
var results = response.results;
|
||||
this.userRoles = results.map((r) => {
|
||||
return 'role:' + r.name;
|
||||
});
|
||||
this.fetchedRoles = true;
|
||||
this.rolePromise = null;
|
||||
return Promise.resolve(this.userRoles);
|
||||
if (!results.length) {
|
||||
this.userRoles = [];
|
||||
this.fetchedRoles = true;
|
||||
this.rolePromise = null;
|
||||
|
||||
cacheAdapter.role.put(this.user.id, this.userRoles);
|
||||
return Promise.resolve(this.userRoles);
|
||||
}
|
||||
var rolesMap = results.reduce((m, r) => {
|
||||
m.names.push(r.name);
|
||||
m.ids.push(r.objectId);
|
||||
return m;
|
||||
}, {ids: [], names: []});
|
||||
|
||||
// run the recursive finding
|
||||
return this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names)
|
||||
.then((roleNames) => {
|
||||
this.userRoles = roleNames.map((r) => {
|
||||
return 'role:' + r;
|
||||
});
|
||||
this.fetchedRoles = true;
|
||||
this.rolePromise = null;
|
||||
|
||||
cacheAdapter.role.put(this.user.id, this.userRoles);
|
||||
return Promise.resolve(this.userRoles);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Given a role object id, get any other roles it is part of
|
||||
Auth.prototype._getAllRoleNamesForId = function(roleID) {
|
||||
|
||||
// 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
|
||||
var rolePointer = {
|
||||
__type: 'Pointer',
|
||||
className: '_Role',
|
||||
objectId: roleID
|
||||
};
|
||||
var restWhere = {
|
||||
'roles': rolePointer
|
||||
};
|
||||
var query = new RestQuery(this.config, master(this.config), '_Role',
|
||||
restWhere, {});
|
||||
// Given a list of roleIds, find all the parent roles, returns a promise with all names
|
||||
Auth.prototype._getAllRolesNamesForRoleIds = function(roleIDs, names = [], queriedRoles = {}) {
|
||||
let ins = roleIDs.filter((roleID) => {
|
||||
return queriedRoles[roleID] !== true;
|
||||
}).map((roleID) => {
|
||||
// mark as queried
|
||||
queriedRoles[roleID] = true;
|
||||
return {
|
||||
__type: 'Pointer',
|
||||
className: '_Role',
|
||||
objectId: roleID
|
||||
}
|
||||
});
|
||||
|
||||
// all roles are accounted for, return the names
|
||||
if (ins.length == 0) {
|
||||
return Promise.resolve([...new Set(names)]);
|
||||
}
|
||||
// Build an OR query across all parentRoles
|
||||
let restWhere;
|
||||
if (ins.length == 1) {
|
||||
restWhere = { 'roles': ins[0] };
|
||||
} else {
|
||||
restWhere = { 'roles': { '$in': ins }}
|
||||
}
|
||||
let query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {});
|
||||
return query.execute().then((response) => {
|
||||
var results = response.results;
|
||||
// Nothing found
|
||||
if (!results.length) {
|
||||
return Promise.resolve([]);
|
||||
return Promise.resolve(names);
|
||||
}
|
||||
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);
|
||||
});
|
||||
parentRolesPromises.push(Promise.resolve(roleIDs));
|
||||
return Promise.all(parentRolesPromises);
|
||||
}).then(function(results){
|
||||
// Flatten
|
||||
let roleIDs = results.reduce( (memo, result) => {
|
||||
return memo.concat(result);
|
||||
}, []);
|
||||
return Promise.resolve([...new Set(roleIDs)]);
|
||||
});
|
||||
};
|
||||
// Map the results with all Ids and names
|
||||
let resultMap = results.reduce((memo, role) => {
|
||||
memo.names.push(role.name);
|
||||
memo.ids.push(role.objectId);
|
||||
return memo;
|
||||
}, {ids: [], names: []});
|
||||
// store the new found names
|
||||
names = names.concat(resultMap.names);
|
||||
// find the next ones, circular roles will be cut
|
||||
return this._getAllRolesNamesForRoleIds(resultMap.ids, names, queriedRoles)
|
||||
}).then((names) => {
|
||||
return Promise.resolve([...new Set(names)])
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Auth: Auth,
|
||||
|
||||
+78
-11
@@ -2,12 +2,21 @@
|
||||
// configured.
|
||||
// mount is the URL for the root of the API; includes http, domain, etc.
|
||||
|
||||
import cache from './cache';
|
||||
import AppCache 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');
|
||||
let cacheInfo = cache.apps.get(applicationId);
|
||||
let cacheInfo = AppCache.get(applicationId);
|
||||
if (!cacheInfo) {
|
||||
return;
|
||||
}
|
||||
@@ -18,16 +27,18 @@ export class Config {
|
||||
this.javascriptKey = cacheInfo.javascriptKey;
|
||||
this.dotNetKey = cacheInfo.dotNetKey;
|
||||
this.restAPIKey = cacheInfo.restAPIKey;
|
||||
this.webhookKey = cacheInfo.webhookKey;
|
||||
this.fileKey = cacheInfo.fileKey;
|
||||
this.facebookAppIds = cacheInfo.facebookAppIds;
|
||||
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
|
||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
|
||||
this.database = cacheInfo.databaseController;
|
||||
|
||||
this.serverURL = cacheInfo.serverURL;
|
||||
this.publicServerURL = cacheInfo.publicServerURL;
|
||||
this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL);
|
||||
this.verifyUserEmails = cacheInfo.verifyUserEmails;
|
||||
this.appName = cacheInfo.appName;
|
||||
|
||||
this.cacheController = cacheInfo.cacheController;
|
||||
this.hooksController = cacheInfo.hooksController;
|
||||
this.filesController = cacheInfo.filesController;
|
||||
this.pushController = cacheInfo.pushController;
|
||||
@@ -35,14 +46,39 @@ 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.expireInactiveSessions = cacheInfo.expireInactiveSessions;
|
||||
this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this);
|
||||
this.revokeSessionOnPasswordReset = cacheInfo.revokeSessionOnPasswordReset;
|
||||
}
|
||||
|
||||
static validate(options) {
|
||||
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
|
||||
appName: options.appName,
|
||||
publicServerURL: options.publicServerURL})
|
||||
static validate({
|
||||
verifyUserEmails,
|
||||
appName,
|
||||
publicServerURL,
|
||||
revokeSessionOnPasswordReset,
|
||||
expireInactiveSessions,
|
||||
sessionLength,
|
||||
}) {
|
||||
this.validateEmailConfiguration({
|
||||
verifyUserEmails: verifyUserEmails,
|
||||
appName: appName,
|
||||
publicServerURL: publicServerURL
|
||||
})
|
||||
|
||||
if (typeof revokeSessionOnPasswordReset !== 'boolean') {
|
||||
throw 'revokeSessionOnPasswordReset must be a boolean value';
|
||||
}
|
||||
|
||||
if (publicServerURL) {
|
||||
if (!publicServerURL.startsWith("http://") && !publicServerURL.startsWith("https://")) {
|
||||
throw "publicServerURL should be a valid HTTPS URL starting with https://"
|
||||
}
|
||||
}
|
||||
|
||||
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
|
||||
}
|
||||
|
||||
static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {
|
||||
@@ -56,6 +92,37 @@ export class Config {
|
||||
}
|
||||
}
|
||||
|
||||
get mount() {
|
||||
var mount = this._mount;
|
||||
if (this.publicServerURL) {
|
||||
mount = this.publicServerURL;
|
||||
}
|
||||
return mount;
|
||||
}
|
||||
|
||||
set mount(newValue) {
|
||||
this._mount = newValue;
|
||||
}
|
||||
|
||||
static validateSessionConfiguration(sessionLength, expireInactiveSessions) {
|
||||
if (expireInactiveSessions) {
|
||||
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() {
|
||||
if (!this.expireInactiveSessions) {
|
||||
return undefined;
|
||||
}
|
||||
var now = new Date();
|
||||
return new Date(now.getTime() + (this.sessionLength*1000));
|
||||
}
|
||||
|
||||
get invalidLinkURL() {
|
||||
return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
|
||||
}
|
||||
@@ -79,7 +146,7 @@ export class Config {
|
||||
get verifyEmailURL() {
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Config;
|
||||
module.exports = Config;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import AdaptableController from './AdaptableController';
|
||||
import CacheAdapter from '../Adapters/Cache/CacheAdapter';
|
||||
|
||||
const KEY_SEPARATOR_CHAR = ':';
|
||||
|
||||
function joinKeys(...keys) {
|
||||
return keys.join(KEY_SEPARATOR_CHAR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefix all calls to the cache via a prefix string, useful when grouping Cache by object type.
|
||||
*
|
||||
* eg "Role" or "Session"
|
||||
*/
|
||||
export class SubCache {
|
||||
constructor(prefix, cacheController) {
|
||||
this.prefix = prefix;
|
||||
this.cache = cacheController;
|
||||
}
|
||||
|
||||
get(key) {
|
||||
let cacheKey = joinKeys(this.prefix, key);
|
||||
return this.cache.get(cacheKey);
|
||||
}
|
||||
|
||||
put(key, value, ttl) {
|
||||
let cacheKey = joinKeys(this.prefix, key);
|
||||
return this.cache.put(cacheKey, value, ttl);
|
||||
}
|
||||
|
||||
del(key) {
|
||||
let cacheKey = joinKeys(this.prefix, key);
|
||||
return this.cache.del(cacheKey);
|
||||
}
|
||||
|
||||
clear() {
|
||||
return this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class CacheController extends AdaptableController {
|
||||
|
||||
constructor(adapter, appId, options = {}) {
|
||||
super(adapter, appId, options);
|
||||
|
||||
this.role = new SubCache('role', this);
|
||||
this.user = new SubCache('user', this);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
let cacheKey = joinKeys(this.appId, key);
|
||||
return this.adapter.get(cacheKey).then(null, () => Promise.resolve(null));
|
||||
}
|
||||
|
||||
put(key, value, ttl) {
|
||||
let cacheKey = joinKeys(this.appId, key);
|
||||
return this.adapter.put(cacheKey, value, ttl);
|
||||
}
|
||||
|
||||
del(key) {
|
||||
let cacheKey = joinKeys(this.appId, key);
|
||||
return this.adapter.del(cacheKey);
|
||||
}
|
||||
|
||||
clear() {
|
||||
return this.adapter.clear();
|
||||
}
|
||||
|
||||
expectedAdapterType() {
|
||||
return CacheAdapter;
|
||||
}
|
||||
}
|
||||
|
||||
export default CacheController;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,8 @@ import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
|
||||
import path from 'path';
|
||||
import mime from 'mime';
|
||||
|
||||
const legacyFilesRegex = new RegExp("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}-.*");
|
||||
|
||||
export class FilesController extends AdaptableController {
|
||||
|
||||
getFileData(config, filename) {
|
||||
@@ -59,8 +61,13 @@ export class FilesController extends AdaptableController {
|
||||
continue;
|
||||
}
|
||||
let filename = fileObject['name'];
|
||||
// all filenames starting with "tfss-" should be from files.parsetfss.com
|
||||
// all filenames starting with a "-" seperated UUID should be from files.parse.com
|
||||
// all other filenames have been migrated or created from Parse Server
|
||||
if (filename.indexOf('tfss-') === 0) {
|
||||
fileObject['url'] = 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename);
|
||||
} else if (legacyFilesRegex.test(filename)) {
|
||||
fileObject['url'] = 'http://files.parse.com/' + config.fileKey + '/' + encodeURIComponent(filename);
|
||||
} else {
|
||||
fileObject['url'] = this.adapter.getFileLocation(config, filename);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/** @flow weak */
|
||||
|
||||
import * as DatabaseAdapter from "../DatabaseAdapter";
|
||||
import * as triggers from "../triggers";
|
||||
import * as Parse from "parse/node";
|
||||
import * as request from "request";
|
||||
import * as triggers from "../triggers";
|
||||
import * as Parse from "parse/node";
|
||||
import * as request from "request";
|
||||
import { logger } from '../logger';
|
||||
|
||||
const DefaultHooksCollectionName = "_Hooks";
|
||||
|
||||
export class HooksController {
|
||||
_applicationId:string;
|
||||
_collectionPrefix:string;
|
||||
_collection;
|
||||
|
||||
constructor(applicationId:string, collectionPrefix:string = '') {
|
||||
constructor(applicationId:string, databaseController, webhookKey) {
|
||||
this._applicationId = applicationId;
|
||||
this._collectionPrefix = collectionPrefix;
|
||||
this._webhookKey = webhookKey;
|
||||
this.database = databaseController;
|
||||
}
|
||||
|
||||
load() {
|
||||
@@ -26,18 +26,6 @@ export class HooksController {
|
||||
});
|
||||
}
|
||||
|
||||
getCollection() {
|
||||
if (this._collection) {
|
||||
return Promise.resolve(this._collection)
|
||||
}
|
||||
|
||||
let database = DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix);
|
||||
return database.adaptiveCollection(DefaultHooksCollectionName).then(collection => {
|
||||
this._collection = collection;
|
||||
return collection;
|
||||
});
|
||||
}
|
||||
|
||||
getFunction(functionName) {
|
||||
return this._getHooks({ functionName: functionName }, 1).then(results => results[0]);
|
||||
}
|
||||
@@ -64,16 +52,19 @@ export class HooksController {
|
||||
return this._removeHooks({ className: className, triggerName: triggerName });
|
||||
}
|
||||
|
||||
_getHooks(query, limit) {
|
||||
_getHooks(query = {}, limit) {
|
||||
let options = limit ? { limit: limit } : undefined;
|
||||
return this.getCollection().then(collection => collection.find(query, options));
|
||||
return this.database.find(DefaultHooksCollectionName, query).then((results) => {
|
||||
return results.map((result) => {
|
||||
delete result.objectId;
|
||||
return result;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_removeHooks(query) {
|
||||
return this.getCollection().then(collection => {
|
||||
return collection.deleteMany(query);
|
||||
}).then(() => {
|
||||
return {};
|
||||
return this.database.destroy(DefaultHooksCollectionName, query).then(() => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,15 +77,13 @@ export class HooksController {
|
||||
} else {
|
||||
throw new Parse.Error(143, "invalid hook declaration");
|
||||
}
|
||||
return this.getCollection()
|
||||
.then(collection => collection.upsertOne(query, hook))
|
||||
.then(() => {
|
||||
return hook;
|
||||
});
|
||||
return this.database.update(DefaultHooksCollectionName, query, hook, {upsert: true}).then(() => {
|
||||
return Promise.resolve(hook);
|
||||
})
|
||||
}
|
||||
|
||||
addHookToTriggers(hook) {
|
||||
var wrappedFunction = wrapToHTTPRequest(hook);
|
||||
var wrappedFunction = wrapToHTTPRequest(hook, this._webhookKey);
|
||||
wrappedFunction.url = hook.url;
|
||||
if (hook.className) {
|
||||
triggers.addTrigger(hook.triggerName, hook.className, wrappedFunction, this._applicationId)
|
||||
@@ -168,7 +157,7 @@ export class HooksController {
|
||||
};
|
||||
}
|
||||
|
||||
function wrapToHTTPRequest(hook) {
|
||||
function wrapToHTTPRequest(hook, key) {
|
||||
return (req, res) => {
|
||||
let jsonBody = {};
|
||||
for (var i in req) {
|
||||
@@ -189,10 +178,16 @@ function wrapToHTTPRequest(hook) {
|
||||
body: JSON.stringify(jsonBody)
|
||||
};
|
||||
|
||||
if (key) {
|
||||
jsonRequest.headers['X-Parse-Webhook-Key'] = key;
|
||||
} else {
|
||||
logger.warn('Making outgoing webhook request without webhookKey being set!');
|
||||
}
|
||||
|
||||
request.post(hook.url, jsonRequest, function (err, httpResponse, body) {
|
||||
var result;
|
||||
if (body) {
|
||||
if (typeof body == "string") {
|
||||
if (typeof body === "string") {
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
} catch (e) {
|
||||
@@ -204,8 +199,15 @@ function wrapToHTTPRequest(hook) {
|
||||
err = body.error;
|
||||
}
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return res.error(err);
|
||||
} else if (hook.triggerName === 'beforeSave') {
|
||||
if (typeof result === 'object') {
|
||||
delete result.createdAt;
|
||||
delete result.updatedAt;
|
||||
}
|
||||
return res.success({object: result});
|
||||
} else {
|
||||
return res.success(result);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import AdaptableController from './AdaptableController';
|
||||
import { PushAdapter } from '../Adapters/Push/PushAdapter';
|
||||
import deepcopy from 'deepcopy';
|
||||
import RestQuery from '../RestQuery';
|
||||
import RestWrite from '../RestWrite';
|
||||
import { master } from '../Auth';
|
||||
import pushStatusHandler from '../pushStatusHandler';
|
||||
|
||||
const FEATURE_NAME = 'push';
|
||||
@@ -38,12 +40,16 @@ export class PushController extends AdaptableController {
|
||||
return !!this.adapter;
|
||||
}
|
||||
|
||||
sendPush(body = {}, where = {}, config, auth, wait) {
|
||||
sendPush(body = {}, where = {}, config, auth, onPushStatusSaved = () => {}) {
|
||||
var pushAdapter = this.adapter;
|
||||
if (!this.pushIsAvailable) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Push adapter is not available');
|
||||
}
|
||||
if (!this.options) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Missing push configuration');
|
||||
}
|
||||
PushController.validatePushType(where, pushAdapter.getValidPushTypes());
|
||||
// Replace the expiration_time with a valid Unix epoch milliseconds time
|
||||
body['expiration_time'] = PushController.getExpirationTime(body);
|
||||
@@ -52,47 +58,48 @@ export class PushController extends AdaptableController {
|
||||
let badgeUpdate = () => {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (body.data && body.data.badge) {
|
||||
let badge = body.data.badge;
|
||||
let op = {};
|
||||
let restUpdate = {};
|
||||
if (typeof badge == 'string' && badge.toLowerCase() === 'increment') {
|
||||
op = { $inc: { badge: 1 } }
|
||||
restUpdate = { badge: { __op: 'Increment', amount: 1 } }
|
||||
} else if (Number(badge)) {
|
||||
op = { $set: { badge: badge } }
|
||||
restUpdate = { badge: badge }
|
||||
} else {
|
||||
throw "Invalid value for badge, expected number or 'Increment'";
|
||||
}
|
||||
let updateWhere = deepcopy(where);
|
||||
|
||||
badgeUpdate = () => {
|
||||
let badgeQuery = new RestQuery(config, auth, '_Installation', updateWhere);
|
||||
return badgeQuery.buildRestWhere().then(() => {
|
||||
let restWhere = deepcopy(badgeQuery.restWhere);
|
||||
// Force iOS only devices
|
||||
if (!restWhere['$and']) {
|
||||
restWhere['$and'] = [badgeQuery.restWhere];
|
||||
}
|
||||
restWhere['$and'].push({
|
||||
'deviceType': 'ios'
|
||||
});
|
||||
return config.database.adaptiveCollection("_Installation")
|
||||
.then(coll => coll.updateMany(restWhere, op));
|
||||
})
|
||||
updateWhere.deviceType = 'ios';
|
||||
// Build a real RestQuery so we can use it in RestWrite
|
||||
let restQuery = new RestQuery(config, master(config), '_Installation', updateWhere);
|
||||
return restQuery.buildRestWhere().then(() => {
|
||||
let write = new RestWrite(config, master(config), '_Installation', restQuery.restWhere, restUpdate);
|
||||
write.runOptions.many = true;
|
||||
return write.execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
let pushStatus = pushStatusHandler(config);
|
||||
return Promise.resolve().then(() => {
|
||||
return pushStatus.setInitial(body, where);
|
||||
}).then(() => {
|
||||
onPushStatusSaved(pushStatus.objectId);
|
||||
return badgeUpdate();
|
||||
}).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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,858 @@
|
||||
// This class handles schema validation, persistence, and modification.
|
||||
//
|
||||
// Each individual Schema object should be immutable. The helpers to
|
||||
// do things with the Schema just return a new schema when the schema
|
||||
// is changed.
|
||||
//
|
||||
// The canonical place to store this Schema is in the database itself,
|
||||
// in a _SCHEMA collection. This is not the right way to do it for an
|
||||
// open source framework, but it's backward compatible, so we're
|
||||
// keeping it this way for now.
|
||||
//
|
||||
// In API-handling code, you should only use the Schema class via the
|
||||
// DatabaseController. This will let us replace the schema logic for
|
||||
// different databases.
|
||||
// TODO: hide all schema logic inside the database adapter.
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
import _ from 'lodash';
|
||||
|
||||
const defaultColumns = Object.freeze({
|
||||
// Contain the default columns for every parse object type (except _Join collection)
|
||||
_Default: {
|
||||
"objectId": {type:'String'},
|
||||
"createdAt": {type:'Date'},
|
||||
"updatedAt": {type:'Date'},
|
||||
"ACL": {type:'ACL'},
|
||||
},
|
||||
// The additional default columns for the _User collection (in addition to DefaultCols)
|
||||
_User: {
|
||||
"username": {type:'String'},
|
||||
"password": {type:'String'},
|
||||
"email": {type:'String'},
|
||||
"emailVerified": {type:'Boolean'},
|
||||
},
|
||||
// The additional default columns for the _Installation collection (in addition to DefaultCols)
|
||||
_Installation: {
|
||||
"installationId": {type:'String'},
|
||||
"deviceToken": {type:'String'},
|
||||
"channels": {type:'Array'},
|
||||
"deviceType": {type:'String'},
|
||||
"pushType": {type:'String'},
|
||||
"GCMSenderId": {type:'String'},
|
||||
"timeZone": {type:'String'},
|
||||
"localeIdentifier": {type:'String'},
|
||||
"badge": {type:'Number'},
|
||||
"appVersion": {type:'String'},
|
||||
"appName": {type:'String'},
|
||||
"appIdentifier": {type:'String'},
|
||||
"parseVersion": {type:'String'},
|
||||
},
|
||||
// The additional default columns for the _Role collection (in addition to DefaultCols)
|
||||
_Role: {
|
||||
"name": {type:'String'},
|
||||
"users": {type:'Relation', targetClass:'_User'},
|
||||
"roles": {type:'Relation', targetClass:'_Role'}
|
||||
},
|
||||
// The additional default columns for the _Session collection (in addition to DefaultCols)
|
||||
_Session: {
|
||||
"restricted": {type:'Boolean'},
|
||||
"user": {type:'Pointer', targetClass:'_User'},
|
||||
"installationId": {type:'String'},
|
||||
"sessionToken": {type:'String'},
|
||||
"expiresAt": {type:'Date'},
|
||||
"createdWith": {type:'Object'}
|
||||
},
|
||||
_Product: {
|
||||
"productIdentifier": {type:'String'},
|
||||
"download": {type:'File'},
|
||||
"downloadName": {type:'String'},
|
||||
"icon": {type:'File'},
|
||||
"order": {type:'Number'},
|
||||
"title": {type:'String'},
|
||||
"subtitle": {type:'String'},
|
||||
},
|
||||
_PushStatus: {
|
||||
"pushTime": {type:'String'},
|
||||
"source": {type:'String'}, // rest or webui
|
||||
"query": {type:'String'}, // the stringified JSON query
|
||||
"payload": {type:'Object'}, // the JSON payload,
|
||||
"title": {type:'String'},
|
||||
"expiry": {type:'Number'},
|
||||
"status": {type:'String'},
|
||||
"numSent": {type:'Number'},
|
||||
"numFailed": {type:'Number'},
|
||||
"pushHash": {type:'String'},
|
||||
"errorMessage": {type:'Object'},
|
||||
"sentPerType": {type:'Object'},
|
||||
"failedPerType":{type:'Object'},
|
||||
}
|
||||
});
|
||||
|
||||
const requiredColumns = Object.freeze({
|
||||
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"],
|
||||
_Role: ["name", "ACL"]
|
||||
});
|
||||
|
||||
const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product', '_PushStatus']);
|
||||
|
||||
const volatileClasses = Object.freeze(['_PushStatus', '_Hooks', '_GlobalConfig']);
|
||||
|
||||
// 10 alpha numberic chars + uppercase
|
||||
const userIdRegex = /^[a-zA-Z0-9]{10}$/;
|
||||
// Anything that start with role
|
||||
const roleRegex = /^role:.*/;
|
||||
// * permission
|
||||
const publicRegex = /^\*$/
|
||||
|
||||
const permissionKeyRegex = Object.freeze([userIdRegex, roleRegex, publicRegex]);
|
||||
|
||||
function verifyPermissionKey(key) {
|
||||
let result = permissionKeyRegex.reduce((isGood, regEx) => {
|
||||
isGood = isGood || key.match(regEx) != null;
|
||||
return isGood;
|
||||
}, false);
|
||||
if (!result) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${key}' is not a valid key for class level permissions`);
|
||||
}
|
||||
}
|
||||
|
||||
const CLPValidKeys = Object.freeze(['find', 'get', 'create', 'update', 'delete', 'addField', 'readUserFields', 'writeUserFields']);
|
||||
function validateCLP(perms, fields) {
|
||||
if (!perms) {
|
||||
return;
|
||||
}
|
||||
Object.keys(perms).forEach((operation) => {
|
||||
if (CLPValidKeys.indexOf(operation) == -1) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `${operation} is not a valid operation for class level permissions`);
|
||||
}
|
||||
|
||||
if (operation === 'readUserFields' || operation === 'writeUserFields') {
|
||||
if (!Array.isArray(perms[operation])) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perms[operation]}' is not a valid value for class level permissions ${operation}`);
|
||||
} else {
|
||||
perms[operation].forEach((key) => {
|
||||
if (!fields[key] || fields[key].type != 'Pointer' || fields[key].targetClass != '_User') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${key}' is not a valid column for class level pointer permissions ${operation}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(perms[operation]).forEach((key) => {
|
||||
verifyPermissionKey(key);
|
||||
let perm = perms[operation][key];
|
||||
if (perm !== true) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perm}' is not a valid value for class level permissions ${operation}:${key}:${perm}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
const joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
|
||||
const classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
|
||||
function classNameIsValid(className) {
|
||||
// Valid classes must:
|
||||
return (
|
||||
// Be one of _User, _Installation, _Role, _Session OR
|
||||
systemClasses.indexOf(className) > -1 ||
|
||||
// Be a join table OR
|
||||
joinClassRegex.test(className) ||
|
||||
// Include only alpha-numeric and underscores, and not start with an underscore or number
|
||||
fieldNameIsValid(className)
|
||||
);
|
||||
}
|
||||
|
||||
// Valid fields must be alpha-numeric, and not start with an underscore or number
|
||||
function fieldNameIsValid(fieldName) {
|
||||
return classAndFieldRegex.test(fieldName);
|
||||
}
|
||||
|
||||
// Checks that it's not trying to clobber one of the default fields of the class.
|
||||
function fieldNameIsValidForClass(fieldName, className) {
|
||||
if (!fieldNameIsValid(fieldName)) {
|
||||
return false;
|
||||
}
|
||||
if (defaultColumns._Default[fieldName]) {
|
||||
return false;
|
||||
}
|
||||
if (defaultColumns[className] && defaultColumns[className][fieldName]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function invalidClassNameMessage(className) {
|
||||
return 'Invalid classname: ' + className + ', classnames can only have alphanumeric characters and _, and must start with an alpha character ';
|
||||
}
|
||||
|
||||
const invalidJsonError = new Parse.Error(Parse.Error.INVALID_JSON, "invalid JSON");
|
||||
const validNonRelationOrPointerTypes = [
|
||||
'Number',
|
||||
'String',
|
||||
'Boolean',
|
||||
'Date',
|
||||
'Object',
|
||||
'Array',
|
||||
'GeoPoint',
|
||||
'File',
|
||||
];
|
||||
// Returns an error suitable for throwing if the type is invalid
|
||||
const fieldTypeIsInvalid = ({ type, targetClass }) => {
|
||||
if (['Pointer', 'Relation'].includes(type)) {
|
||||
if (!targetClass) {
|
||||
return new Parse.Error(135, `type ${type} needs a class name`);
|
||||
} else if (typeof targetClass !== 'string') {
|
||||
return invalidJsonError;
|
||||
} else if (!classNameIsValid(targetClass)) {
|
||||
return new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(targetClass));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (typeof type !== 'string') {
|
||||
return invalidJsonError;
|
||||
}
|
||||
if (!validNonRelationOrPointerTypes.includes(type)) {
|
||||
return new Parse.Error(Parse.Error.INCORRECT_TYPE, `invalid field type: ${type}`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const convertSchemaToAdapterSchema = schema => {
|
||||
schema = injectDefaultSchema(schema);
|
||||
delete schema.fields.ACL;
|
||||
schema.fields._rperm = { type: 'Array' };
|
||||
schema.fields._wperm = { type: 'Array' };
|
||||
|
||||
if (schema.className === '_User') {
|
||||
delete schema.fields.password;
|
||||
schema.fields._hashed_password = { type: 'String' };
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
const convertAdapterSchemaToParseSchema = ({...schema}) => {
|
||||
delete schema.fields._rperm;
|
||||
delete schema.fields._wperm;
|
||||
|
||||
schema.fields.ACL = { type: 'ACL' };
|
||||
|
||||
if (schema.className === '_User') {
|
||||
delete schema.fields.authData; //Auth data is implicit
|
||||
delete schema.fields._hashed_password;
|
||||
schema.fields.password = { type: 'String' };
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
const injectDefaultSchema = ({className, fields, classLevelPermissions}) => ({
|
||||
className,
|
||||
fields: {
|
||||
...defaultColumns._Default,
|
||||
...(defaultColumns[className] || {}),
|
||||
...fields,
|
||||
},
|
||||
classLevelPermissions,
|
||||
})
|
||||
|
||||
const dbTypeMatchesObjectType = (dbType, objectType) => {
|
||||
if (dbType.type !== objectType.type) return false;
|
||||
if (dbType.targetClass !== objectType.targetClass) return false;
|
||||
if (dbType === objectType.type) return true;
|
||||
if (dbType.type === objectType.type) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stores the entire schema of the app in a weird hybrid format somewhere between
|
||||
// the mongo format and the Parse format. Soon, this will all be Parse format.
|
||||
class SchemaController {
|
||||
_dbAdapter;
|
||||
data;
|
||||
perms;
|
||||
|
||||
constructor(databaseAdapter) {
|
||||
this._dbAdapter = databaseAdapter;
|
||||
|
||||
// this.data[className][fieldName] tells you the type of that field, in mongo format
|
||||
this.data = {};
|
||||
// this.perms[className][operation] tells you the acl-style permissions
|
||||
this.perms = {};
|
||||
}
|
||||
|
||||
reloadData() {
|
||||
this.data = {};
|
||||
this.perms = {};
|
||||
return this.getAllClasses()
|
||||
.then(allSchemas => {
|
||||
allSchemas.forEach(schema => {
|
||||
this.data[schema.className] = injectDefaultSchema(schema).fields;
|
||||
this.perms[schema.className] = schema.classLevelPermissions;
|
||||
});
|
||||
|
||||
// Inject the in-memory classes
|
||||
volatileClasses.forEach(className => {
|
||||
this.data[className] = injectDefaultSchema({
|
||||
className,
|
||||
fields: {},
|
||||
classLevelPermissions: {}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAllClasses() {
|
||||
return this._dbAdapter.getAllClasses()
|
||||
.then(allSchemas => allSchemas.map(injectDefaultSchema));
|
||||
}
|
||||
|
||||
getOneSchema(className, allowVolatileClasses = false) {
|
||||
if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) {
|
||||
return Promise.resolve(this.data[className]);
|
||||
}
|
||||
return this._dbAdapter.getClass(className)
|
||||
.then(injectDefaultSchema)
|
||||
}
|
||||
|
||||
// Create a new class that includes the three default fields.
|
||||
// ACL is an implicit column that does not get an entry in the
|
||||
// _SCHEMAS database. Returns a promise that resolves with the
|
||||
// created schema, in mongo format.
|
||||
// on success, and rejects with an error on fail. Ensure you
|
||||
// have authorization (master key, or client class creation
|
||||
// enabled) before calling this function.
|
||||
addClassIfNotExists(className, fields = {}, classLevelPermissions) {
|
||||
var validationError = this.validateNewClass(className, fields, classLevelPermissions);
|
||||
if (validationError) {
|
||||
return Promise.reject(validationError);
|
||||
}
|
||||
|
||||
return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields, classLevelPermissions, className }))
|
||||
.then(convertAdapterSchemaToParseSchema)
|
||||
.catch(error => {
|
||||
if (error && error.code === Parse.Error.DUPLICATE_VALUE) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateClass(className, submittedFields, classLevelPermissions, database) {
|
||||
return this.getOneSchema(className)
|
||||
.then(schema => {
|
||||
let existingFields = schema.fields;
|
||||
Object.keys(submittedFields).forEach(name => {
|
||||
let field = submittedFields[name];
|
||||
if (existingFields[name] && field.__op !== 'Delete') {
|
||||
throw new Parse.Error(255, `Field ${name} exists, cannot update.`);
|
||||
}
|
||||
if (!existingFields[name] && field.__op === 'Delete') {
|
||||
throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
|
||||
}
|
||||
});
|
||||
|
||||
delete existingFields._rperm;
|
||||
delete existingFields._wperm;
|
||||
let newSchema = buildMergedSchemaObject(existingFields, submittedFields);
|
||||
let validationError = this.validateSchemaData(className, newSchema, classLevelPermissions, Object.keys(existingFields));
|
||||
if (validationError) {
|
||||
throw new Parse.Error(validationError.code, validationError.error);
|
||||
}
|
||||
|
||||
// Finally we have checked to make sure the request is valid and we can start deleting fields.
|
||||
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
|
||||
let deletePromises = [];
|
||||
let insertedFields = [];
|
||||
Object.keys(submittedFields).forEach(fieldName => {
|
||||
if (submittedFields[fieldName].__op === 'Delete') {
|
||||
const promise = this.deleteField(fieldName, className, database);
|
||||
deletePromises.push(promise);
|
||||
} else {
|
||||
insertedFields.push(fieldName);
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(deletePromises) // Delete Everything
|
||||
.then(() => this.reloadData()) // Reload our Schema, so we have all the new values
|
||||
.then(() => {
|
||||
let promises = insertedFields.map(fieldName => {
|
||||
const type = submittedFields[fieldName];
|
||||
return this.enforceFieldExists(className, fieldName, type);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then(() => this.setPermissions(className, classLevelPermissions, newSchema))
|
||||
//TODO: Move this logic into the database adapter
|
||||
.then(() => ({
|
||||
className: className,
|
||||
fields: this.data[className],
|
||||
classLevelPermissions: this.perms[className]
|
||||
}));
|
||||
})
|
||||
.catch(error => {
|
||||
if (error === undefined) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Returns a promise that resolves successfully to the new schema
|
||||
// object or fails with a reason.
|
||||
enforceClassExists(className) {
|
||||
if (this.data[className]) {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
// We don't have this class. Update the schema
|
||||
return this.addClassIfNotExists(className)
|
||||
// The schema update succeeded. Reload the schema
|
||||
.then(() => this.reloadData())
|
||||
.catch(error => {
|
||||
// The schema update failed. This can be okay - it might
|
||||
// have failed because there's a race condition and a different
|
||||
// client is making the exact same schema update that we want.
|
||||
// So just reload the schema.
|
||||
return this.reloadData();
|
||||
})
|
||||
.then(() => {
|
||||
// Ensure that the schema now validates
|
||||
if (this.data[className]) {
|
||||
return this;
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `Failed to add ${className}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// The schema still doesn't validate. Give up
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate');
|
||||
});
|
||||
}
|
||||
|
||||
validateNewClass(className, fields = {}, classLevelPermissions) {
|
||||
if (this.data[className]) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
||||
}
|
||||
if (!classNameIsValid(className)) {
|
||||
return {
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: invalidClassNameMessage(className),
|
||||
};
|
||||
}
|
||||
return this.validateSchemaData(className, fields, classLevelPermissions, []);
|
||||
}
|
||||
|
||||
validateSchemaData(className, fields, classLevelPermissions, existingFieldNames) {
|
||||
for (let fieldName in fields) {
|
||||
if (!existingFieldNames.includes(fieldName)) {
|
||||
if (!fieldNameIsValid(fieldName)) {
|
||||
return {
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
error: 'invalid field name: ' + fieldName,
|
||||
};
|
||||
}
|
||||
if (!fieldNameIsValidForClass(fieldName, className)) {
|
||||
return {
|
||||
code: 136,
|
||||
error: 'field ' + fieldName + ' cannot be added',
|
||||
};
|
||||
}
|
||||
const error = fieldTypeIsInvalid(fields[fieldName]);
|
||||
if (error) return { code: error.code, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
for (let fieldName in defaultColumns[className]) {
|
||||
fields[fieldName] = defaultColumns[className][fieldName];
|
||||
}
|
||||
|
||||
let geoPoints = Object.keys(fields).filter(key => fields[key] && fields[key].type === 'GeoPoint');
|
||||
if (geoPoints.length > 1) {
|
||||
return {
|
||||
code: Parse.Error.INCORRECT_TYPE,
|
||||
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
|
||||
};
|
||||
}
|
||||
validateCLP(classLevelPermissions, fields);
|
||||
}
|
||||
|
||||
// Sets the Class-level permissions for a given className, which must exist.
|
||||
setPermissions(className, perms, newSchema) {
|
||||
if (typeof perms === 'undefined') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
validateCLP(perms, newSchema);
|
||||
return this._dbAdapter.setClassLevelPermissions(className, perms)
|
||||
.then(() => this.reloadData());
|
||||
}
|
||||
|
||||
// Returns a promise that resolves successfully to the new schema
|
||||
// object if the provided className-fieldName-type tuple is valid.
|
||||
// The className must already be validated.
|
||||
// If 'freeze' is true, refuse to update the schema for this field.
|
||||
enforceFieldExists(className, fieldName, type, freeze) {
|
||||
if (fieldName.indexOf(".") > 0) {
|
||||
// subdocument key (x.y) => ok if x is of type 'object'
|
||||
fieldName = fieldName.split(".")[ 0 ];
|
||||
type = 'Object';
|
||||
}
|
||||
if (!fieldNameIsValid(fieldName)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`);
|
||||
}
|
||||
|
||||
// If someone tries to create a new field with null/undefined as the value, return;
|
||||
if (!type) {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
return this.reloadData().then(() => {
|
||||
let expectedType = this.getExpectedType(className, fieldName);
|
||||
if (typeof type === 'string') {
|
||||
type = { type };
|
||||
}
|
||||
|
||||
if (expectedType) {
|
||||
if (!dbTypeMatchesObjectType(expectedType, type)) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INCORRECT_TYPE,
|
||||
`schema mismatch for ${className}.${fieldName}; expected ${expectedType.type || expectedType} but got ${type}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this._dbAdapter.addFieldIfNotExists(className, fieldName, type).then(() => {
|
||||
// The update succeeded. Reload the schema
|
||||
return this.reloadData();
|
||||
}, error => {
|
||||
//TODO: introspect the error and only reload if the error is one for which is makes sense to reload
|
||||
|
||||
// The update failed. This can be okay - it might have been a race
|
||||
// condition where another client updated the schema in the same
|
||||
// way that we wanted to. So, just reload the schema
|
||||
return this.reloadData();
|
||||
}).then(error => {
|
||||
// Ensure that the schema now validates
|
||||
if (!dbTypeMatchesObjectType(this.getExpectedType(className, fieldName), type)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `Could not add field ${fieldName}`);
|
||||
}
|
||||
return this;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Delete a field, and remove that data from all objects. This is intended
|
||||
// to remove unused fields, if other writers are writing objects that include
|
||||
// this field, the field may reappear. Returns a Promise that resolves with
|
||||
// no object on success, or rejects with { code, error } on failure.
|
||||
// Passing the database and prefix is necessary in order to drop relation collections
|
||||
// and remove fields from objects. Ideally the database would belong to
|
||||
// a database adapter and this function would close over it or access it via member.
|
||||
deleteField(fieldName, className, database) {
|
||||
if (!classNameIsValid(className)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className));
|
||||
}
|
||||
if (!fieldNameIsValid(fieldName)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${fieldName}`);
|
||||
}
|
||||
//Don't allow deleting the default fields.
|
||||
if (!fieldNameIsValidForClass(fieldName, className)) {
|
||||
throw new Parse.Error(136, `field ${fieldName} cannot be changed`);
|
||||
}
|
||||
|
||||
return this.getOneSchema(className)
|
||||
.catch(error => {
|
||||
if (error === undefined) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
.then(schema => {
|
||||
if (!schema.fields[fieldName]) {
|
||||
throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`);
|
||||
}
|
||||
if (schema.fields[fieldName].type == 'Relation') {
|
||||
//For relations, drop the _Join table
|
||||
return database.adapter.deleteFields(className, schema, [fieldName])
|
||||
.then(() => database.adapter.deleteClass(`_Join:${fieldName}:${className}`));
|
||||
}
|
||||
|
||||
return database.adapter.deleteFields(className, schema, [fieldName]);
|
||||
});
|
||||
}
|
||||
|
||||
// Validates an object provided in REST format.
|
||||
// Returns a promise that resolves to the new schema if this object is
|
||||
// valid.
|
||||
validateObject(className, object, query) {
|
||||
let geocount = 0;
|
||||
let promise = this.enforceClassExists(className);
|
||||
for (let fieldName in object) {
|
||||
if (object[fieldName] === undefined) {
|
||||
continue;
|
||||
}
|
||||
let expected = getType(object[fieldName]);
|
||||
if (expected === 'GeoPoint') {
|
||||
geocount++;
|
||||
}
|
||||
if (geocount > 1) {
|
||||
// Make sure all field validation operations run before we return.
|
||||
// If not - we are continuing to run logic, but already provided response from the server.
|
||||
return promise.then(() => {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.INCORRECT_TYPE,
|
||||
'there can only be one geopoint field in a class'));
|
||||
});
|
||||
}
|
||||
if (!expected) {
|
||||
continue;
|
||||
}
|
||||
if (fieldName === 'ACL') {
|
||||
// Every object has ACL implicitly.
|
||||
continue;
|
||||
}
|
||||
|
||||
promise = promise.then(schema => schema.enforceFieldExists(className, fieldName, expected));
|
||||
}
|
||||
promise = thenValidateRequiredColumns(promise, className, object, query);
|
||||
return promise;
|
||||
}
|
||||
|
||||
// Validates that all the properties are set for the object
|
||||
validateRequiredColumns(className, object, query) {
|
||||
let columns = requiredColumns[className];
|
||||
if (!columns || columns.length == 0) {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
let missingColumns = columns.filter(function(column){
|
||||
if (query && query.objectId) {
|
||||
if (object[column] && typeof object[column] === "object") {
|
||||
// Trying to delete a required column
|
||||
return object[column].__op == 'Delete';
|
||||
}
|
||||
// Not trying to do anything there
|
||||
return false;
|
||||
}
|
||||
return !object[column]
|
||||
});
|
||||
|
||||
if (missingColumns.length > 0) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INCORRECT_TYPE,
|
||||
missingColumns[0]+' is required.');
|
||||
}
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
// Validates the base CLP for an operation
|
||||
testBaseCLP(className, aclGroup, operation) {
|
||||
if (!this.perms[className] || !this.perms[className][operation]) {
|
||||
return true;
|
||||
}
|
||||
let classPerms = this.perms[className];
|
||||
let perms = classPerms[operation];
|
||||
// Handle the public scenario quickly
|
||||
if (perms['*']) {
|
||||
return true;
|
||||
}
|
||||
// Check permissions against the aclGroup provided (array of userId/roles)
|
||||
if (aclGroup.some(acl => { return perms[acl] === true })) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validates an operation passes class-level-permissions set in the schema
|
||||
validatePermission(className, aclGroup, operation) {
|
||||
if (this.testBaseCLP(className, aclGroup, operation)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (!this.perms[className] || !this.perms[className][operation]) {
|
||||
return true;
|
||||
}
|
||||
let classPerms = this.perms[className];
|
||||
let perms = classPerms[operation];
|
||||
// No matching CLP, let's check the Pointer permissions
|
||||
// And handle those later
|
||||
let permissionField = ['get', 'find'].indexOf(operation) > -1 ? 'readUserFields' : 'writeUserFields';
|
||||
|
||||
// Reject create when write lockdown
|
||||
if (permissionField == 'writeUserFields' && operation == 'create') {
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||
'Permission denied for this action.');
|
||||
}
|
||||
|
||||
// Process the readUserFields later
|
||||
if (Array.isArray(classPerms[permissionField]) && classPerms[permissionField].length > 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||
'Permission denied for this action.');
|
||||
};
|
||||
|
||||
// Returns the expected type for a className+key combination
|
||||
// or undefined if the schema is not set
|
||||
getExpectedType(className, fieldName) {
|
||||
if (this.data && this.data[className]) {
|
||||
const expectedType = this.data[className][fieldName]
|
||||
return expectedType === 'map' ? 'Object' : expectedType;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Checks if a given class is in the schema.
|
||||
hasClass(className) {
|
||||
return this.reloadData().then(() => !!(this.data[className]));
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise for a new Schema.
|
||||
const load = dbAdapter => {
|
||||
let schema = new SchemaController(dbAdapter);
|
||||
return schema.reloadData().then(() => schema);
|
||||
}
|
||||
|
||||
// Builds a new schema (in schema API response format) out of an
|
||||
// existing mongo schema + a schemas API put request. This response
|
||||
// does not include the default fields, as it is intended to be passed
|
||||
// to mongoSchemaFromFieldsAndClassName. No validation is done here, it
|
||||
// is done in mongoSchemaFromFieldsAndClassName.
|
||||
function buildMergedSchemaObject(existingFields, putRequest) {
|
||||
let newSchema = {};
|
||||
let sysSchemaField = Object.keys(defaultColumns).indexOf(existingFields._id) === -1 ? [] : Object.keys(defaultColumns[existingFields._id]);
|
||||
for (let oldField in existingFields) {
|
||||
if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') {
|
||||
if (sysSchemaField.length > 0 && sysSchemaField.indexOf(oldField) !== -1) {
|
||||
continue;
|
||||
}
|
||||
let fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete'
|
||||
if (!fieldIsDeleted) {
|
||||
newSchema[oldField] = existingFields[oldField];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let newField in putRequest) {
|
||||
if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') {
|
||||
if (sysSchemaField.length > 0 && sysSchemaField.indexOf(newField) !== -1) {
|
||||
continue;
|
||||
}
|
||||
newSchema[newField] = putRequest[newField];
|
||||
}
|
||||
}
|
||||
return newSchema;
|
||||
}
|
||||
|
||||
// Given a schema promise, construct another schema promise that
|
||||
// validates this field once the schema loads.
|
||||
function thenValidateRequiredColumns(schemaPromise, className, object, query) {
|
||||
return schemaPromise.then((schema) => {
|
||||
return schema.validateRequiredColumns(className, object, query);
|
||||
});
|
||||
}
|
||||
|
||||
// Gets the type from a REST API formatted object, where 'type' is
|
||||
// extended past javascript types to include the rest of the Parse
|
||||
// type system.
|
||||
// The output should be a valid schema value.
|
||||
// TODO: ensure that this is compatible with the format used in Open DB
|
||||
function getType(obj) {
|
||||
let type = typeof obj;
|
||||
switch(type) {
|
||||
case 'boolean':
|
||||
return 'Boolean';
|
||||
case 'string':
|
||||
return 'String';
|
||||
case 'number':
|
||||
return 'Number';
|
||||
case 'map':
|
||||
case 'object':
|
||||
if (!obj) {
|
||||
return undefined;
|
||||
}
|
||||
return getObjectType(obj);
|
||||
case 'function':
|
||||
case 'symbol':
|
||||
case 'undefined':
|
||||
default:
|
||||
throw 'bad obj: ' + obj;
|
||||
}
|
||||
}
|
||||
|
||||
// This gets the type for non-JSON types like pointers and files, but
|
||||
// also gets the appropriate type for $ operators.
|
||||
// Returns null if the type is unknown.
|
||||
function getObjectType(obj) {
|
||||
if (obj instanceof Array) {
|
||||
return 'Array';
|
||||
}
|
||||
if (obj.__type){
|
||||
switch(obj.__type) {
|
||||
case 'Pointer' :
|
||||
if(obj.className) {
|
||||
return {
|
||||
type: 'Pointer',
|
||||
targetClass: obj.className
|
||||
}
|
||||
}
|
||||
case 'File' :
|
||||
if(obj.name) {
|
||||
return 'File';
|
||||
}
|
||||
case 'Date' :
|
||||
if(obj.iso) {
|
||||
return 'Date';
|
||||
}
|
||||
case 'GeoPoint' :
|
||||
if(obj.latitude != null && obj.longitude != null) {
|
||||
return 'GeoPoint';
|
||||
}
|
||||
case 'Bytes' :
|
||||
if(obj.base64) {
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid "+obj.__type);
|
||||
}
|
||||
}
|
||||
if (obj['$ne']) {
|
||||
return getObjectType(obj['$ne']);
|
||||
}
|
||||
if (obj.__op) {
|
||||
switch(obj.__op) {
|
||||
case 'Increment':
|
||||
return 'Number';
|
||||
case 'Delete':
|
||||
return null;
|
||||
case 'Add':
|
||||
case 'AddUnique':
|
||||
case 'Remove':
|
||||
return 'Array';
|
||||
case 'AddRelation':
|
||||
case 'RemoveRelation':
|
||||
return {
|
||||
type: 'Relation',
|
||||
targetClass: obj.objects[0].className
|
||||
}
|
||||
case 'Batch':
|
||||
return getObjectType(obj.ops[0]);
|
||||
default:
|
||||
throw 'unexpected op: ' + obj.__op;
|
||||
}
|
||||
}
|
||||
return 'Object';
|
||||
}
|
||||
|
||||
export {
|
||||
load,
|
||||
classNameIsValid,
|
||||
fieldNameIsValid,
|
||||
invalidClassNameMessage,
|
||||
buildMergedSchemaObject,
|
||||
systemClasses,
|
||||
defaultColumns,
|
||||
convertSchemaToAdapterSchema,
|
||||
};
|
||||
@@ -43,40 +43,31 @@ export class UserController extends AdaptableController {
|
||||
if (!this.shouldVerifyEmails) {
|
||||
// Trying to verify email when not enabled
|
||||
// TODO: Better error here.
|
||||
return Promise.reject();
|
||||
throw undefined;
|
||||
}
|
||||
|
||||
return this.config.database
|
||||
.adaptiveCollection('_User')
|
||||
.then(collection => {
|
||||
// Need direct database access because verification token is not a parse field
|
||||
return collection.findOneAndUpdate({
|
||||
username: username,
|
||||
_email_verify_token: token
|
||||
}, {$set: {emailVerified: true}});
|
||||
})
|
||||
.then(document => {
|
||||
if (!document) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return document;
|
||||
});
|
||||
let database = this.config.database.WithoutValidation();
|
||||
return database.update('_User', {
|
||||
username: username,
|
||||
_email_verify_token: token
|
||||
}, {emailVerified: true}).then(document => {
|
||||
if (!document) {
|
||||
throw undefined;
|
||||
}
|
||||
return Promise.resolve(document);
|
||||
});
|
||||
}
|
||||
|
||||
checkResetTokenValidity(username, token) {
|
||||
return this.config.database.adaptiveCollection('_User')
|
||||
.then(collection => {
|
||||
return collection.find({
|
||||
username: username,
|
||||
_perishable_token: token
|
||||
}, { limit: 1 });
|
||||
})
|
||||
.then(results => {
|
||||
if (results.length != 1) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return results[0];
|
||||
});
|
||||
let database = this.config.database.WithoutValidation();
|
||||
return database.find('_User', {
|
||||
username: username,
|
||||
_perishable_token: token
|
||||
}, {limit: 1}).then(results => {
|
||||
if (results.length != 1) {
|
||||
throw undefined;
|
||||
}
|
||||
return results[0];
|
||||
});
|
||||
}
|
||||
|
||||
getUserIfNeeded(user) {
|
||||
@@ -94,7 +85,7 @@ export class UserController extends AdaptableController {
|
||||
var query = new RestQuery(this.config, Auth.master(this.config), '_User', where);
|
||||
return query.execute().then(function(result){
|
||||
if (result.results.length != 1) {
|
||||
return Promise.reject();
|
||||
throw undefined;
|
||||
}
|
||||
return result.results[0];
|
||||
})
|
||||
@@ -124,15 +115,8 @@ export class UserController extends AdaptableController {
|
||||
|
||||
setPasswordResetToken(email) {
|
||||
let token = randomString(25);
|
||||
return this.config.database
|
||||
.adaptiveCollection('_User')
|
||||
.then(collection => {
|
||||
// Need direct database access because verification token is not a parse field
|
||||
return collection.findOneAndUpdate(
|
||||
{ email: email}, // query
|
||||
{ $set: { _perishable_token: token } } // update
|
||||
);
|
||||
});
|
||||
let database = this.config.database.WithoutValidation();
|
||||
return database.update('_User', {email: email}, {_perishable_token: token});
|
||||
}
|
||||
|
||||
sendPasswordResetEmail(email) {
|
||||
@@ -166,14 +150,11 @@ export class UserController extends AdaptableController {
|
||||
|
||||
updatePassword(username, token, password, config) {
|
||||
return this.checkResetTokenValidity(username, token).then((user) => {
|
||||
return updateUserPassword(user._id, password, this.config);
|
||||
return updateUserPassword(user.objectId, password, this.config);
|
||||
}).then(() => {
|
||||
// clear reset password token
|
||||
return this.config.database.adaptiveCollection('_User').then(function (collection) {
|
||||
// Need direct database access because verification token is not a parse field
|
||||
return collection.findOneAndUpdate({ username: username },// query
|
||||
{ $unset: { _perishable_token: null } } // update
|
||||
);
|
||||
return this.config.database.WithoutValidation().update('_User', { username }, {
|
||||
_perishable_token: {__op: 'Delete'}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+16
-70
@@ -1,75 +1,21 @@
|
||||
/** @flow weak */
|
||||
// Database Adapter
|
||||
//
|
||||
// Allows you to change the underlying database.
|
||||
//
|
||||
// Adapter classes must implement the following methods:
|
||||
// * a constructor with signature (connectionString, optionsObject)
|
||||
// * connect()
|
||||
// * loadSchema()
|
||||
// * create(className, object)
|
||||
// * find(className, query, options)
|
||||
// * update(className, query, update, options)
|
||||
// * destroy(className, query, options)
|
||||
// * This list is incomplete and the database process is not fully modularized.
|
||||
//
|
||||
// Default is MongoStorageAdapter.
|
||||
|
||||
import DatabaseController from './Controllers/DatabaseController';
|
||||
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||
|
||||
const DefaultDatabaseURI = 'mongodb://localhost:27017/parse';
|
||||
|
||||
let adapter = MongoStorageAdapter;
|
||||
let dbConnections = {};
|
||||
let databaseURI = DefaultDatabaseURI;
|
||||
let appDatabaseURIs = {};
|
||||
let appDatabaseOptions = {};
|
||||
|
||||
function setAdapter(databaseAdapter) {
|
||||
adapter = databaseAdapter;
|
||||
}
|
||||
|
||||
function setDatabaseURI(uri) {
|
||||
databaseURI = uri;
|
||||
}
|
||||
|
||||
function setAppDatabaseURI(appId, uri) {
|
||||
appDatabaseURIs[appId] = uri;
|
||||
}
|
||||
|
||||
function setAppDatabaseOptions(appId: string, options: Object) {
|
||||
appDatabaseOptions[appId] = options;
|
||||
}
|
||||
import AppCache from './cache';
|
||||
|
||||
//Used by tests
|
||||
function clearDatabaseSettings() {
|
||||
appDatabaseURIs = {};
|
||||
dbConnections = {};
|
||||
appDatabaseOptions = {};
|
||||
}
|
||||
|
||||
function getDatabaseConnection(appId: string, collectionPrefix: string) {
|
||||
if (dbConnections[appId]) {
|
||||
return dbConnections[appId];
|
||||
function destroyAllDataPermanently() {
|
||||
if (process.env.TESTING) {
|
||||
// This is super janky, but destroyAllDataPermanently is
|
||||
// a janky interface, so we need to have some jankyness
|
||||
// to support it
|
||||
return Promise.all(Object.keys(AppCache.cache).map(appId => {
|
||||
const app = AppCache.get(appId);
|
||||
if (app.databaseController) {
|
||||
return app.databaseController.deleteEverything();
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
var dbURI = (appDatabaseURIs[appId] ? appDatabaseURIs[appId] : databaseURI);
|
||||
|
||||
let storageAdapter = new adapter(dbURI, appDatabaseOptions[appId]);
|
||||
dbConnections[appId] = new DatabaseController(storageAdapter, {
|
||||
collectionPrefix: collectionPrefix
|
||||
});
|
||||
return dbConnections[appId];
|
||||
throw 'Only supported in test environment';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dbConnections: dbConnections,
|
||||
getDatabaseConnection: getDatabaseConnection,
|
||||
setAdapter: setAdapter,
|
||||
setDatabaseURI: setDatabaseURI,
|
||||
setAppDatabaseOptions: setAppDatabaseOptions,
|
||||
setAppDatabaseURI: setAppDatabaseURI,
|
||||
clearDatabaseSettings: clearDatabaseSettings,
|
||||
defaultDatabaseURI: databaseURI
|
||||
};
|
||||
module.exports = { destroyAllDataPermanently };
|
||||
|
||||
@@ -141,6 +141,7 @@ function matchesKeyConstraints(object, key, constraints) {
|
||||
if (constraints.__type) {
|
||||
if (constraints.__type === 'Pointer') {
|
||||
return (
|
||||
typeof object[key] !== 'undefined' &&
|
||||
constraints.className === object[key].className &&
|
||||
constraints.objectId === object[key].objectId
|
||||
);
|
||||
@@ -206,7 +207,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;
|
||||
|
||||
+92
-30
@@ -1,7 +1,5 @@
|
||||
// ParseServer - open-source compatible API Server for Parse apps
|
||||
|
||||
import 'babel-polyfill';
|
||||
|
||||
var batch = require('./batch'),
|
||||
bodyParser = require('body-parser'),
|
||||
DatabaseAdapter = require('./DatabaseAdapter'),
|
||||
@@ -9,11 +7,16 @@ var batch = require('./batch'),
|
||||
middlewares = require('./middlewares'),
|
||||
multer = require('multer'),
|
||||
Parse = require('parse/node').Parse,
|
||||
path = require('path'),
|
||||
authDataManager = require('./authDataManager');
|
||||
|
||||
if (!global._babelPolyfill) {
|
||||
require('babel-polyfill');
|
||||
}
|
||||
|
||||
import { logger,
|
||||
configureLogger } from './logger';
|
||||
import cache from './cache';
|
||||
configureLogger } from './logger';
|
||||
import AppCache from './cache';
|
||||
import Config from './Config';
|
||||
import parseServerPackage from '../package.json';
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
@@ -21,6 +24,8 @@ import requiredParameter from './requiredParameter';
|
||||
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
|
||||
import { ClassesRouter } from './Routers/ClassesRouter';
|
||||
import { FeaturesRouter } from './Routers/FeaturesRouter';
|
||||
import { InMemoryCacheAdapter } from './Adapters/Cache/InMemoryCacheAdapter';
|
||||
import { CacheController } from './Controllers/CacheController';
|
||||
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
|
||||
import { FilesController } from './Controllers/FilesController';
|
||||
import { FilesRouter } from './Routers/FilesRouter';
|
||||
@@ -45,15 +50,21 @@ import { SchemasRouter } from './Routers/SchemasRouter';
|
||||
import { SessionsRouter } from './Routers/SessionsRouter';
|
||||
import { UserController } from './Controllers/UserController';
|
||||
import { UsersRouter } from './Routers/UsersRouter';
|
||||
import { PurgeRouter } from './Routers/PurgeRouter';
|
||||
|
||||
import DatabaseController from './Controllers/DatabaseController';
|
||||
const SchemaController = require('./Controllers/SchemaController');
|
||||
import ParsePushAdapter from 'parse-server-push-adapter';
|
||||
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||
// Mutate the Parse object to add the Cloud Code handlers
|
||||
addParseCloud();
|
||||
|
||||
|
||||
const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } };
|
||||
|
||||
|
||||
// ParseServer works like a constructor of an express app.
|
||||
// The args that we understand are:
|
||||
// "databaseAdapter": a class like DatabaseController providing create, find,
|
||||
// update, and delete
|
||||
// "filesAdapter": a class like GridStoreAdapter providing create, get,
|
||||
// and delete
|
||||
// "loggerAdapter": a class like FileLoggerAdapter providing info, error,
|
||||
@@ -73,8 +84,10 @@ addParseCloud();
|
||||
// "clientKey": optional key from Parse dashboard
|
||||
// "dotNetKey": optional key from Parse dashboard
|
||||
// "restAPIKey": optional key from Parse dashboard
|
||||
// "webhookKey": 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 {
|
||||
|
||||
@@ -82,19 +95,20 @@ class ParseServer {
|
||||
appId = requiredParameter('You must provide an appId!'),
|
||||
masterKey = requiredParameter('You must provide a masterKey!'),
|
||||
appName,
|
||||
databaseAdapter,
|
||||
filesAdapter,
|
||||
push,
|
||||
loggerAdapter,
|
||||
logsFolder,
|
||||
databaseURI = DatabaseAdapter.defaultDatabaseURI,
|
||||
databaseURI,
|
||||
databaseOptions,
|
||||
databaseAdapter,
|
||||
cloud,
|
||||
collectionPrefix = '',
|
||||
clientKey,
|
||||
javascriptKey,
|
||||
dotNetKey,
|
||||
restAPIKey,
|
||||
webhookKey,
|
||||
fileKey = 'invalid-file-key',
|
||||
facebookAppIds = [],
|
||||
enableAnonymousUsers = true,
|
||||
@@ -103,6 +117,7 @@ class ParseServer {
|
||||
serverURL = requiredParameter('You must provide a serverURL!'),
|
||||
maxUploadSize = '20mb',
|
||||
verifyUserEmails = false,
|
||||
cacheAdapter,
|
||||
emailAdapter,
|
||||
publicServerURL,
|
||||
customPages = {
|
||||
@@ -111,59 +126,94 @@ class ParseServer {
|
||||
choosePassword: undefined,
|
||||
passwordResetSuccess: undefined
|
||||
},
|
||||
liveQuery = {}
|
||||
liveQuery = {},
|
||||
sessionLength = 31536000, // 1 Year in seconds
|
||||
expireInactiveSessions = true,
|
||||
verbose = false,
|
||||
revokeSessionOnPasswordReset = true,
|
||||
__indexBuildCompletionCallbackForTests = () => {},
|
||||
}) {
|
||||
// Initialize the node client SDK automatically
|
||||
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
|
||||
Parse.serverURL = serverURL;
|
||||
|
||||
if ((databaseOptions || databaseURI || collectionPrefix !== '') && databaseAdapter) {
|
||||
throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/connectionPrefix.';
|
||||
} else if (!databaseAdapter) {
|
||||
databaseAdapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
collectionPrefix,
|
||||
mongoOptions: databaseOptions,
|
||||
});
|
||||
} else {
|
||||
databaseAdapter = loadAdapter(databaseAdapter)
|
||||
}
|
||||
|
||||
if (!filesAdapter && !databaseURI) {
|
||||
throw 'When using an explicit database adapter, you must also use and explicit filesAdapter.';
|
||||
}
|
||||
|
||||
if (logsFolder) {
|
||||
configureLogger({
|
||||
logsFolder
|
||||
})
|
||||
}
|
||||
|
||||
if (databaseAdapter) {
|
||||
DatabaseAdapter.setAdapter(databaseAdapter);
|
||||
}
|
||||
|
||||
if (databaseOptions) {
|
||||
DatabaseAdapter.setAppDatabaseOptions(appId, databaseOptions);
|
||||
}
|
||||
|
||||
if (databaseURI) {
|
||||
DatabaseAdapter.setAppDatabaseURI(appId, databaseURI);
|
||||
}
|
||||
|
||||
if (cloud) {
|
||||
addParseCloud();
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
if (verbose || process.env.VERBOSE || process.env.VERBOSE_PARSE_SERVER) {
|
||||
configureLogger({level: 'silly'});
|
||||
}
|
||||
|
||||
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
|
||||
return new GridStoreAdapter(databaseURI);
|
||||
});
|
||||
// Pass the push options too as it works with the default
|
||||
const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push);
|
||||
const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push || {});
|
||||
const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter);
|
||||
const emailControllerAdapter = loadAdapter(emailAdapter);
|
||||
const cacheControllerAdapter = loadAdapter(cacheAdapter, InMemoryCacheAdapter, {appId: appId});
|
||||
|
||||
// We pass the options and the base class for the adatper,
|
||||
// Note that passing an instance would work too
|
||||
const filesController = new FilesController(filesControllerAdapter, appId);
|
||||
const pushController = new PushController(pushControllerAdapter, appId);
|
||||
const pushController = new PushController(pushControllerAdapter, appId, push);
|
||||
const loggerController = new LoggerController(loggerControllerAdapter, appId);
|
||||
const hooksController = new HooksController(appId, collectionPrefix);
|
||||
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
|
||||
const liveQueryController = new LiveQueryController(liveQuery);
|
||||
const cacheController = new CacheController(cacheControllerAdapter, appId);
|
||||
const databaseController = new DatabaseController(databaseAdapter);
|
||||
const hooksController = new HooksController(appId, databaseController, webhookKey);
|
||||
|
||||
cache.apps.set(appId, {
|
||||
// TODO: create indexes on first creation of a _User object. Otherwise it's impossible to
|
||||
// have a Parse app without it having a _User collection.
|
||||
let userClassPromise = databaseController.loadSchema()
|
||||
.then(schema => schema.enforceClassExists('_User'))
|
||||
|
||||
let usernameUniqueness = userClassPromise
|
||||
.then(() => databaseController.adapter.ensureUniqueness('_User', requiredUserFields, ['username']))
|
||||
.catch(error => {
|
||||
logger.warn('Unable to ensure uniqueness for usernames: ', error);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
let emailUniqueness = userClassPromise
|
||||
.then(() => databaseController.adapter.ensureUniqueness('_User', requiredUserFields, ['email']))
|
||||
.catch(error => {
|
||||
logger.warn('Unabled to ensure uniqueness for user email addresses: ', error);
|
||||
return Promise.reject(error);
|
||||
})
|
||||
|
||||
AppCache.put(appId, {
|
||||
masterKey: masterKey,
|
||||
serverURL: serverURL,
|
||||
collectionPrefix: collectionPrefix,
|
||||
@@ -171,8 +221,10 @@ class ParseServer {
|
||||
javascriptKey: javascriptKey,
|
||||
dotNetKey: dotNetKey,
|
||||
restAPIKey: restAPIKey,
|
||||
webhookKey: webhookKey,
|
||||
fileKey: fileKey,
|
||||
facebookAppIds: facebookAppIds,
|
||||
cacheController: cacheController,
|
||||
filesController: filesController,
|
||||
pushController: pushController,
|
||||
loggerController: loggerController,
|
||||
@@ -185,17 +237,26 @@ class ParseServer {
|
||||
publicServerURL: publicServerURL,
|
||||
customPages: customPages,
|
||||
maxUploadSize: maxUploadSize,
|
||||
liveQueryController: liveQueryController
|
||||
liveQueryController: liveQueryController,
|
||||
sessionLength: Number(sessionLength),
|
||||
expireInactiveSessions: expireInactiveSessions,
|
||||
revokeSessionOnPasswordReset,
|
||||
databaseController,
|
||||
});
|
||||
|
||||
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
|
||||
if (process.env.FACEBOOK_APP_ID) {
|
||||
cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
|
||||
AppCache.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
|
||||
}
|
||||
|
||||
Config.validate(cache.apps.get(appId));
|
||||
this.config = cache.apps.get(appId);
|
||||
Config.validate(AppCache.get(appId));
|
||||
this.config = AppCache.get(appId);
|
||||
hooksController.load();
|
||||
|
||||
// Note: Tests will start to fail if any validation happens after this is called.
|
||||
if (process.env.TESTING) {
|
||||
__indexBuildCompletionCallbackForTests(Promise.all([usernameUniqueness, emailUniqueness]));
|
||||
}
|
||||
}
|
||||
|
||||
get app() {
|
||||
@@ -238,6 +299,7 @@ class ParseServer {
|
||||
new IAPValidationRouter(),
|
||||
new FeaturesRouter(),
|
||||
new GlobalConfigRouter(),
|
||||
new PurgeRouter(),
|
||||
];
|
||||
|
||||
if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) {
|
||||
|
||||
+40
-3
@@ -6,7 +6,8 @@
|
||||
// components that external developers may be modifying.
|
||||
|
||||
import express from 'express';
|
||||
import log from './logger';
|
||||
import url from 'url';
|
||||
import log from './logger';
|
||||
|
||||
export default class PromiseRouter {
|
||||
// Each entry should be an object with:
|
||||
@@ -154,8 +155,8 @@ export default class PromiseRouter {
|
||||
function makeExpressHandler(promiseHandler) {
|
||||
return function(req, res, next) {
|
||||
try {
|
||||
log.verbose(req.method, req.originalUrl, req.headers,
|
||||
JSON.stringify(req.body, null, 2));
|
||||
log.verbose(req.method, maskSensitiveUrl(req), req.headers,
|
||||
JSON.stringify(maskSensitiveBody(req), null, 2));
|
||||
promiseHandler(req).then((result) => {
|
||||
if (!result.response && !result.location && !result.text) {
|
||||
log.error('the handler did not include a "response" or a "location" field');
|
||||
@@ -178,6 +179,11 @@ function makeExpressHandler(promiseHandler) {
|
||||
return res.send('Found. Redirecting to '+result.location);
|
||||
}
|
||||
}
|
||||
if (result.headers) {
|
||||
Object.keys(result.headers).forEach((header) => {
|
||||
res.set(header, result.headers[header]);
|
||||
})
|
||||
}
|
||||
res.json(result.response);
|
||||
}, (e) => {
|
||||
log.verbose('error:', e);
|
||||
@@ -189,3 +195,34 @@ function makeExpressHandler(promiseHandler) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maskSensitiveBody(req) {
|
||||
let maskBody = Object.assign({}, req.body);
|
||||
let shouldMaskBody = (req.method === 'POST' && req.originalUrl.endsWith('/users')
|
||||
&& !req.originalUrl.includes('classes')) ||
|
||||
(req.method === 'PUT' && /users\/\w+$/.test(req.originalUrl)
|
||||
&& !req.originalUrl.includes('classes')) ||
|
||||
(req.originalUrl.includes('classes/_User'));
|
||||
if (shouldMaskBody) {
|
||||
for (let key of Object.keys(maskBody)) {
|
||||
if (key == 'password') {
|
||||
maskBody[key] = '********';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return maskBody;
|
||||
}
|
||||
|
||||
function maskSensitiveUrl(req) {
|
||||
let maskUrl = req.originalUrl.toString();
|
||||
let shouldMaskUrl = req.method === 'GET' && req.originalUrl.includes('/login')
|
||||
&& !req.originalUrl.includes('classes');
|
||||
if (shouldMaskUrl) {
|
||||
let password = url.parse(req.originalUrl, true).query.password;
|
||||
if (password) {
|
||||
maskUrl = maskUrl.replace('password=' + password, 'password=********')
|
||||
}
|
||||
}
|
||||
return maskUrl;
|
||||
}
|
||||
|
||||
+112
-94
@@ -1,7 +1,7 @@
|
||||
// An object that encapsulates everything we need to run a 'find'
|
||||
// operation, encoded in the REST API format.
|
||||
|
||||
var Schema = require('./Schema');
|
||||
var SchemaController = require('./Controllers/SchemaController');
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
import { default as FilesController } from './Controllers/FilesController';
|
||||
@@ -171,23 +171,39 @@ RestQuery.prototype.redirectClassNameForKey = function() {
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestQuery.prototype.validateClientClassCreation = function() {
|
||||
let sysClass = Schema.systemClasses;
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||
&& sysClass.indexOf(this.className) === -1) {
|
||||
return this.config.database.collectionExists(this.className).then((hasClass) => {
|
||||
if (hasClass === true) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||
'This user is not allowed to access ' +
|
||||
'non-existent class: ' + this.className);
|
||||
&& SchemaController.systemClasses.indexOf(this.className) === -1) {
|
||||
return this.config.database.loadSchema()
|
||||
.then(schemaController => schemaController.hasClass(this.className))
|
||||
.then(hasClass => {
|
||||
if (hasClass !== true) {
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||
'This user is not allowed to access ' +
|
||||
'non-existent class: ' + this.className);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
function transformInQuery(inQueryObject, className, results) {
|
||||
var values = [];
|
||||
for (var result of results) {
|
||||
values.push({
|
||||
__type: 'Pointer',
|
||||
className: className,
|
||||
objectId: result.objectId
|
||||
});
|
||||
}
|
||||
delete inQueryObject['$inQuery'];
|
||||
if (Array.isArray(inQueryObject['$in'])) {
|
||||
inQueryObject['$in'] = inQueryObject['$in'].concat(values);
|
||||
} else {
|
||||
inQueryObject['$in'] = values;
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces a $inQuery clause by running the subquery, if there is an
|
||||
// $inQuery clause.
|
||||
// The $inQuery clause turns into an $in with values that are just
|
||||
@@ -213,25 +229,29 @@ RestQuery.prototype.replaceInQuery = function() {
|
||||
this.config, this.auth, inQueryValue.className,
|
||||
inQueryValue.where, additionalOptions);
|
||||
return subquery.execute().then((response) => {
|
||||
var values = [];
|
||||
for (var result of response.results) {
|
||||
values.push({
|
||||
__type: 'Pointer',
|
||||
className: subquery.className,
|
||||
objectId: result.objectId
|
||||
});
|
||||
}
|
||||
delete inQueryObject['$inQuery'];
|
||||
if (Array.isArray(inQueryObject['$in'])) {
|
||||
inQueryObject['$in'] = inQueryObject['$in'].concat(values);
|
||||
} else {
|
||||
inQueryObject['$in'] = values;
|
||||
}
|
||||
transformInQuery(inQueryObject, subquery.className, response.results);
|
||||
// Recurse to repeat
|
||||
return this.replaceInQuery();
|
||||
});
|
||||
};
|
||||
|
||||
function transformNotInQuery(notInQueryObject, className, results) {
|
||||
var values = [];
|
||||
for (var result of results) {
|
||||
values.push({
|
||||
__type: 'Pointer',
|
||||
className: className,
|
||||
objectId: result.objectId
|
||||
});
|
||||
}
|
||||
delete notInQueryObject['$notInQuery'];
|
||||
if (Array.isArray(notInQueryObject['$nin'])) {
|
||||
notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values);
|
||||
} else {
|
||||
notInQueryObject['$nin'] = values;
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces a $notInQuery clause by running the subquery, if there is an
|
||||
// $notInQuery clause.
|
||||
// The $notInQuery clause turns into a $nin with values that are just
|
||||
@@ -257,26 +277,25 @@ RestQuery.prototype.replaceNotInQuery = function() {
|
||||
this.config, this.auth, notInQueryValue.className,
|
||||
notInQueryValue.where, additionalOptions);
|
||||
return subquery.execute().then((response) => {
|
||||
var values = [];
|
||||
for (var result of response.results) {
|
||||
values.push({
|
||||
__type: 'Pointer',
|
||||
className: subquery.className,
|
||||
objectId: result.objectId
|
||||
});
|
||||
}
|
||||
delete notInQueryObject['$notInQuery'];
|
||||
if (Array.isArray(notInQueryObject['$nin'])) {
|
||||
notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values);
|
||||
} else {
|
||||
notInQueryObject['$nin'] = values;
|
||||
}
|
||||
|
||||
transformNotInQuery(notInQueryObject, subquery.className, response.results);
|
||||
// Recurse to repeat
|
||||
return this.replaceNotInQuery();
|
||||
});
|
||||
};
|
||||
|
||||
const transformSelect = (selectObject, key ,objects) => {
|
||||
var values = [];
|
||||
for (var result of objects) {
|
||||
values.push(result[key]);
|
||||
}
|
||||
delete selectObject['$select'];
|
||||
if (Array.isArray(selectObject['$in'])) {
|
||||
selectObject['$in'] = selectObject['$in'].concat(values);
|
||||
} else {
|
||||
selectObject['$in'] = values;
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces a $select clause by running the subquery, if there is a
|
||||
// $select clause.
|
||||
// The $select clause turns into an $in with values selected out of
|
||||
@@ -308,22 +327,25 @@ RestQuery.prototype.replaceSelect = function() {
|
||||
this.config, this.auth, selectValue.query.className,
|
||||
selectValue.query.where, additionalOptions);
|
||||
return subquery.execute().then((response) => {
|
||||
var values = [];
|
||||
for (var result of response.results) {
|
||||
values.push(result[selectValue.key]);
|
||||
}
|
||||
delete selectObject['$select'];
|
||||
if (Array.isArray(selectObject['$in'])) {
|
||||
selectObject['$in'] = selectObject['$in'].concat(values);
|
||||
} else {
|
||||
selectObject['$in'] = values;
|
||||
}
|
||||
|
||||
transformSelect(selectObject, selectValue.key, response.results);
|
||||
// Keep replacing $select clauses
|
||||
return this.replaceSelect();
|
||||
})
|
||||
};
|
||||
|
||||
const transformDontSelect = (dontSelectObject, key, objects) => {
|
||||
var values = [];
|
||||
for (var result of objects) {
|
||||
values.push(result[key]);
|
||||
}
|
||||
delete dontSelectObject['$dontSelect'];
|
||||
if (Array.isArray(dontSelectObject['$nin'])) {
|
||||
dontSelectObject['$nin'] = dontSelectObject['$nin'].concat(values);
|
||||
} else {
|
||||
dontSelectObject['$nin'] = values;
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces a $dontSelect clause by running the subquery, if there is a
|
||||
// $dontSelect clause.
|
||||
// The $dontSelect clause turns into an $nin with values selected out of
|
||||
@@ -353,17 +375,7 @@ RestQuery.prototype.replaceDontSelect = function() {
|
||||
this.config, this.auth, dontSelectValue.query.className,
|
||||
dontSelectValue.query.where, additionalOptions);
|
||||
return subquery.execute().then((response) => {
|
||||
var values = [];
|
||||
for (var result of response.results) {
|
||||
values.push(result[dontSelectValue.key]);
|
||||
}
|
||||
delete dontSelectObject['$dontSelect'];
|
||||
if (Array.isArray(dontSelectObject['$nin'])) {
|
||||
dontSelectObject['$nin'] = dontSelectObject['$nin'].concat(values);
|
||||
} else {
|
||||
dontSelectObject['$nin'] = values;
|
||||
}
|
||||
|
||||
transformDontSelect(dontSelectObject, dontSelectValue.key, response.results);
|
||||
// Keep replacing $dontSelect clauses
|
||||
return this.replaceDontSelect();
|
||||
})
|
||||
@@ -372,9 +384,13 @@ RestQuery.prototype.replaceDontSelect = function() {
|
||||
// Returns a promise for whether it was successful.
|
||||
// Populates this.response with an object that only has 'results'.
|
||||
RestQuery.prototype.runFind = function() {
|
||||
if (this.findOptions.limit === 0) {
|
||||
this.response = {results: []};
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.config.database.find(
|
||||
this.className, this.restWhere, this.findOptions).then((results) => {
|
||||
if (this.className == '_User') {
|
||||
if (this.className === '_User') {
|
||||
for (var result of results) {
|
||||
delete result.password;
|
||||
}
|
||||
@@ -449,39 +465,42 @@ function includePath(config, auth, response, path) {
|
||||
if (pointers.length == 0) {
|
||||
return response;
|
||||
}
|
||||
var className = null;
|
||||
let pointersHash = {};
|
||||
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 (obj.className == "_User") {
|
||||
delete obj.sessionToken;
|
||||
delete obj.authData;
|
||||
}
|
||||
replace[obj.objectId] = obj;
|
||||
}
|
||||
return replace;
|
||||
}, {})
|
||||
|
||||
replace[obj.objectId] = obj;
|
||||
}
|
||||
var resp = {
|
||||
results: replacePointers(response.results, path, replace)
|
||||
};
|
||||
@@ -507,16 +526,14 @@ function findPointers(object, path) {
|
||||
}
|
||||
|
||||
if (typeof object !== 'object') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||
'can only include pointer fields');
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'can only include pointer fields');
|
||||
}
|
||||
|
||||
if (path.length == 0) {
|
||||
if (object.__type == 'Pointer') {
|
||||
return [object];
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||
'can only include pointer fields');
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'can only include pointer fields');
|
||||
}
|
||||
|
||||
var subobject = object[path[0]];
|
||||
@@ -534,15 +551,16 @@ 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') {
|
||||
return object;
|
||||
}
|
||||
|
||||
if (path.length == 0) {
|
||||
if (object.__type == 'Pointer') {
|
||||
if (path.length === 0) {
|
||||
if (object.__type === 'Pointer') {
|
||||
return replace[object.objectId];
|
||||
}
|
||||
return object;
|
||||
|
||||
+206
-148
@@ -2,8 +2,7 @@
|
||||
// that writes to the database.
|
||||
// This could be either a "create" or an "update".
|
||||
|
||||
import cache from './cache';
|
||||
var Schema = require('./Schema');
|
||||
var SchemaController = require('./Controllers/SchemaController');
|
||||
var deepcopy = require('deepcopy');
|
||||
|
||||
var Auth = require('./Auth');
|
||||
@@ -12,6 +11,8 @@ var cryptoUtils = require('./cryptoUtils');
|
||||
var passwordCrypto = require('./password');
|
||||
var Parse = require('parse/node');
|
||||
var triggers = require('./triggers');
|
||||
import RestQuery from './RestQuery';
|
||||
import _ from 'lodash';
|
||||
|
||||
// query and data are both provided in REST API format. So data
|
||||
// types are encoded by plain old objects.
|
||||
@@ -30,8 +31,7 @@ function RestWrite(config, auth, className, query, data, originalData) {
|
||||
this.runOptions = {};
|
||||
|
||||
if (!query && data.objectId) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'objectId ' +
|
||||
'is an invalid field name.');
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'objectId is an invalid field name.');
|
||||
}
|
||||
|
||||
// When the operation is complete, this.response may have several
|
||||
@@ -79,13 +79,17 @@ RestWrite.prototype.execute = function() {
|
||||
return this.expandFilesForExistingObjects();
|
||||
}).then(() => {
|
||||
return this.runDatabaseOperation();
|
||||
}).then(() => {
|
||||
return this.createSessionTokenIfNeeded();
|
||||
}).then(() => {
|
||||
return this.handleFollowup();
|
||||
}).then(() => {
|
||||
return this.runAfterTrigger();
|
||||
}).then(() => {
|
||||
return this.cleanUserAuthData();
|
||||
}).then(() => {
|
||||
return this.response;
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
// Uses the Auth object to get the list of roles, adds the user id
|
||||
@@ -100,26 +104,25 @@ RestWrite.prototype.getUserAndRoleACL = function() {
|
||||
return this.auth.getUserRoles().then((roles) => {
|
||||
roles.push(this.auth.user.id);
|
||||
this.runOptions.acl = this.runOptions.acl.concat(roles);
|
||||
return Promise.resolve();
|
||||
return;
|
||||
});
|
||||
}else{
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestWrite.prototype.validateClientClassCreation = function() {
|
||||
let sysClass = Schema.systemClasses;
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||
&& sysClass.indexOf(this.className) === -1) {
|
||||
return this.config.database.collectionExists(this.className).then((hasClass) => {
|
||||
if (hasClass === true) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||
'This user is not allowed to access ' +
|
||||
'non-existent class: ' + this.className);
|
||||
&& SchemaController.systemClasses.indexOf(this.className) === -1) {
|
||||
return this.config.database.loadSchema()
|
||||
.then(schemaController => schemaController.hasClass(this.className))
|
||||
.then(hasClass => {
|
||||
if (hasClass !== true) {
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||
'This user is not allowed to access ' +
|
||||
'non-existent class: ' + this.className);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
@@ -158,15 +161,18 @@ RestWrite.prototype.runBeforeTrigger = function() {
|
||||
updatedObject.set(this.sanitizedData());
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config.applicationId);
|
||||
return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config);
|
||||
}).then((response) => {
|
||||
if (response && response.object) {
|
||||
if (!_.isEqual(this.data, response.object)) {
|
||||
this.storage.changedByTrigger = true;
|
||||
}
|
||||
this.data = response.object;
|
||||
this.storage['changedByTrigger'] = true;
|
||||
// We should delete the objectId for an update write
|
||||
if (this.query && this.query.objectId) {
|
||||
delete this.data.objectId
|
||||
}
|
||||
return this.validateSchema();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -300,11 +306,12 @@ RestWrite.prototype.handleAuthData = function(authData) {
|
||||
'this auth is already used');
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// The non-third-party parts of User transformation
|
||||
RestWrite.prototype.transformUser = function() {
|
||||
if (this.className !== '_User') {
|
||||
@@ -313,41 +320,19 @@ RestWrite.prototype.transformUser = function() {
|
||||
|
||||
var promise = Promise.resolve();
|
||||
|
||||
if (!this.query) {
|
||||
var token = 'r:' + cryptoUtils.newToken();
|
||||
this.storage['token'] = token;
|
||||
promise = promise.then(() => {
|
||||
var expiresAt = new Date();
|
||||
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
|
||||
var sessionData = {
|
||||
sessionToken: token,
|
||||
user: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.objectId()
|
||||
},
|
||||
createdWith: {
|
||||
'action': 'signup',
|
||||
'authProvider': this.storage['authProvider'] || 'password'
|
||||
},
|
||||
restricted: false,
|
||||
installationId: this.auth.installationId,
|
||||
expiresAt: Parse._encode(expiresAt)
|
||||
};
|
||||
if (this.response && this.response.response) {
|
||||
this.response.response.sessionToken = token;
|
||||
}
|
||||
var create = new RestWrite(this.config, Auth.master(this.config),
|
||||
'_Session', null, sessionData);
|
||||
return create.execute();
|
||||
if (this.query) {
|
||||
// If we're updating a _User object, we need to clear out the cache for that user. Find all their
|
||||
// session tokens, and remove them from the cache.
|
||||
promise = new RestQuery(this.config, Auth.master(this.config), '_Session', { user: {
|
||||
__type: "Pointer",
|
||||
className: "_User",
|
||||
objectId: this.objectId(),
|
||||
}}).execute()
|
||||
.then(results => {
|
||||
results.results.forEach(session => this.config.cacheController.user.del(session.sessionToken));
|
||||
});
|
||||
}
|
||||
|
||||
// If we're updating a _User object, clear the user cache for the session
|
||||
if (this.query && this.auth.user && this.auth.user.getSessionToken()) {
|
||||
cache.users.remove(this.auth.user.getSessionToken());
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
// Transform the password
|
||||
if (!this.data.password) {
|
||||
@@ -369,51 +354,80 @@ RestWrite.prototype.transformUser = function() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// We need to a find to check for duplicate username in case they are missing the unique index on usernames
|
||||
// TODO: Check if there is a unique index, and if so, skip this query.
|
||||
return this.config.database.find(
|
||||
this.className, {
|
||||
username: this.data.username,
|
||||
objectId: {'$ne': this.objectId()}
|
||||
}, {limit: 1}).then((results) => {
|
||||
if (results.length > 0) {
|
||||
throw new Parse.Error(Parse.Error.USERNAME_TAKEN,
|
||||
'Account already exists for this username');
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}).then(() => {
|
||||
if (!this.data.email) {
|
||||
this.className,
|
||||
{ username: this.data.username, objectId: {'$ne': this.objectId()} },
|
||||
{ limit: 1 }
|
||||
)
|
||||
.then(results => {
|
||||
if (results.length > 0) {
|
||||
throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.');
|
||||
}
|
||||
return;
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
if (!this.data.email || this.data.email.__op === 'Delete') {
|
||||
return;
|
||||
}
|
||||
// Validate basic email address format
|
||||
if (!this.data.email.match(/^.+@.+$/)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS,
|
||||
'Email address format is invalid.');
|
||||
throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'Email address format is invalid.');
|
||||
}
|
||||
// Check for email uniqueness
|
||||
// Same problem for email as above for username
|
||||
return this.config.database.find(
|
||||
this.className, {
|
||||
email: this.data.email,
|
||||
objectId: {'$ne': this.objectId()}
|
||||
}, {limit: 1}).then((results) => {
|
||||
if (results.length > 0) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_TAKEN,
|
||||
'Account already exists for this email ' +
|
||||
'address');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
// We updated the email, send a new validation
|
||||
this.storage['sendVerificationEmail'] = true;
|
||||
this.config.userController.setEmailVerifyToken(this.data);
|
||||
return Promise.resolve();
|
||||
})
|
||||
});
|
||||
this.className,
|
||||
{ email: this.data.email, objectId: {'$ne': this.objectId()} },
|
||||
{ limit: 1 }
|
||||
)
|
||||
.then(results => {
|
||||
if (results.length > 0) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.');
|
||||
}
|
||||
// We updated the email, send a new validation
|
||||
this.storage['sendVerificationEmail'] = true;
|
||||
this.config.userController.setEmailVerifyToken(this.data);
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
RestWrite.prototype.createSessionTokenIfNeeded = function() {
|
||||
if (this.className !== '_User') {
|
||||
return;
|
||||
}
|
||||
if (this.query) {
|
||||
return;
|
||||
}
|
||||
var token = 'r:' + cryptoUtils.newToken();
|
||||
|
||||
var expiresAt = this.config.generateSessionExpiresAt();
|
||||
var sessionData = {
|
||||
sessionToken: token,
|
||||
user: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.objectId()
|
||||
},
|
||||
createdWith: {
|
||||
'action': 'signup',
|
||||
'authProvider': this.storage['authProvider'] || 'password'
|
||||
},
|
||||
restricted: false,
|
||||
installationId: this.auth.installationId,
|
||||
expiresAt: Parse._encode(expiresAt)
|
||||
};
|
||||
if (this.response && this.response.response) {
|
||||
this.response.response.sessionToken = token;
|
||||
}
|
||||
var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData);
|
||||
return create.execute();
|
||||
}
|
||||
|
||||
// Handles any followup logic
|
||||
RestWrite.prototype.handleFollowup = function() {
|
||||
|
||||
if (this.storage && this.storage['clearSessions']) {
|
||||
if (this.storage && this.storage['clearSessions'] && this.config.revokeSessionOnPasswordReset) {
|
||||
var sessionQuery = {
|
||||
user: {
|
||||
__type: 'Pointer',
|
||||
@@ -434,24 +448,6 @@ RestWrite.prototype.handleFollowup = function() {
|
||||
}
|
||||
};
|
||||
|
||||
// Handles the _Role class specialness.
|
||||
// Does nothing if this isn't a role object.
|
||||
RestWrite.prototype.handleRole = function() {
|
||||
if (this.response || this.className !== '_Role') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.auth.user && !this.auth.isMaster) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'Session token required.');
|
||||
}
|
||||
|
||||
if (!this.data.name) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_ROLE_NAME,
|
||||
'Invalid role name.');
|
||||
}
|
||||
};
|
||||
|
||||
// Handles the _Session class specialness.
|
||||
// Does nothing if this isn't an installation object.
|
||||
RestWrite.prototype.handleSession = function() {
|
||||
@@ -472,8 +468,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: {
|
||||
@@ -493,8 +488,7 @@ RestWrite.prototype.handleSession = function() {
|
||||
}
|
||||
sessionData[key] = this.data[key];
|
||||
}
|
||||
var create = new RestWrite(this.config, Auth.master(this.config),
|
||||
'_Session', null, sessionData);
|
||||
var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData);
|
||||
return create.execute().then((results) => {
|
||||
if (!results.response) {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR,
|
||||
@@ -579,7 +573,7 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
'deviceType may not be changed in this ' +
|
||||
'operation');
|
||||
}
|
||||
return Promise.resolve();
|
||||
return;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -658,10 +652,23 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
// device token.
|
||||
var delQuery = {
|
||||
'deviceToken': this.data.deviceToken,
|
||||
'installationId': {
|
||||
};
|
||||
// We have a unique install Id, use that to preserve
|
||||
// the interesting installation
|
||||
if (this.data.installationId) {
|
||||
delQuery['installationId'] = {
|
||||
'$ne': this.data.installationId
|
||||
}
|
||||
};
|
||||
} else if (idMatch.objectId && this.data.objectId
|
||||
&& idMatch.objectId == this.data.objectId) {
|
||||
// we passed an objectId, preserve that instalation
|
||||
delQuery['objectId'] = {
|
||||
'$ne': idMatch.objectId
|
||||
}
|
||||
} else {
|
||||
// What to do here? can't really clean up everything...
|
||||
return idMatch.objectId;
|
||||
}
|
||||
if (this.data.appIdentifier) {
|
||||
delQuery['appIdentifier'] = this.data.appIdentifier;
|
||||
}
|
||||
@@ -697,11 +704,14 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.className === '_Role') {
|
||||
this.config.cacheController.role.clear();
|
||||
}
|
||||
|
||||
if (this.className === '_User' &&
|
||||
this.query &&
|
||||
!this.auth.couldUpdateUserId(this.query.objectId)) {
|
||||
throw new Parse.Error(Parse.Error.SESSION_MISSING,
|
||||
'cannot modify user ' + this.query.objectId);
|
||||
throw new Parse.Error(Parse.Error.SESSION_MISSING, `Cannot modify user ${this.query.objectId}.`);
|
||||
}
|
||||
|
||||
if (this.className === '_Product' && this.data.download) {
|
||||
@@ -715,50 +725,82 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
||||
}
|
||||
|
||||
if (this.query) {
|
||||
// Force the user to not lockout
|
||||
// Matched with parse.com
|
||||
if (this.className === '_User' && this.data.ACL) {
|
||||
this.data.ACL[this.query.objectId] = { read: true, write: true };
|
||||
}
|
||||
// Run an update
|
||||
return this.config.database.update(
|
||||
this.className, this.query, this.data, this.runOptions).then((resp) => {
|
||||
resp.updatedAt = this.updatedAt;
|
||||
if (this.storage['changedByTrigger']) {
|
||||
resp = Object.keys(this.data).reduce((memo, key) => {
|
||||
memo[key] = resp[key] || this.data[key];
|
||||
return memo;
|
||||
}, resp);
|
||||
}
|
||||
this.response = {
|
||||
response: resp
|
||||
};
|
||||
});
|
||||
return this.config.database.update(this.className, this.query, this.data, this.runOptions)
|
||||
.then(response => {
|
||||
response.updatedAt = this.updatedAt;
|
||||
if (this.storage.changedByTrigger) {
|
||||
Object.keys(this.data).forEach(fieldName => {
|
||||
response[fieldName] = response[fieldName] || this.data[fieldName];
|
||||
});
|
||||
}
|
||||
this.response = { response };
|
||||
});
|
||||
} else {
|
||||
// Set the default ACL for the new _User
|
||||
if (!this.data.ACL && this.className === '_User') {
|
||||
var ACL = {};
|
||||
if (this.className === '_User') {
|
||||
var ACL = this.data.ACL;
|
||||
// default public r/w ACL
|
||||
if (!ACL) {
|
||||
ACL = {};
|
||||
ACL['*'] = { read: true, write: false };
|
||||
}
|
||||
// make sure the user is not locked down
|
||||
ACL[this.data.objectId] = { read: true, write: true };
|
||||
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) => {
|
||||
Object.assign(resp, {
|
||||
objectId: this.data.objectId,
|
||||
createdAt: this.data.createdAt
|
||||
});
|
||||
if (this.storage['changedByTrigger']) {
|
||||
resp = Object.keys(this.data).reduce((memo, key) => {
|
||||
memo[key] = resp[key] || this.data[key];
|
||||
return memo;
|
||||
}, resp);
|
||||
.catch(error => {
|
||||
if (this.className !== '_User' || error.code !== Parse.Error.DUPLICATE_VALUE) {
|
||||
throw error;
|
||||
}
|
||||
// If this was a failed user creation due to username or email already taken, we need to
|
||||
// check whether it was username or email and return the appropriate error.
|
||||
|
||||
// TODO: See if we can later do this without additional queries by using named indexes.
|
||||
return this.config.database.find(
|
||||
this.className,
|
||||
{ username: this.data.username, objectId: {'$ne': this.objectId()} },
|
||||
{ limit: 1 }
|
||||
)
|
||||
.then(results => {
|
||||
if (results.length > 0) {
|
||||
throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.');
|
||||
}
|
||||
if (this.storage['token']) {
|
||||
resp.sessionToken = this.storage['token'];
|
||||
return this.config.database.find(
|
||||
this.className,
|
||||
{ email: this.data.email, objectId: {'$ne': this.objectId()} },
|
||||
{ limit: 1 }
|
||||
);
|
||||
})
|
||||
.then(results => {
|
||||
if (results.length > 0) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.');
|
||||
}
|
||||
this.response = {
|
||||
status: 201,
|
||||
response: resp,
|
||||
location: this.location()
|
||||
};
|
||||
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
|
||||
});
|
||||
})
|
||||
.then(response => {
|
||||
response.objectId = this.data.objectId;
|
||||
response.createdAt = this.data.createdAt;
|
||||
if (this.storage.changedByTrigger) {
|
||||
Object.keys(this.data).forEach(fieldName => {
|
||||
response[fieldName] = response[fieldName] || this.data[fieldName];
|
||||
});
|
||||
}
|
||||
this.response = {
|
||||
status: 201,
|
||||
response,
|
||||
location: this.location()
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -796,7 +838,7 @@ RestWrite.prototype.runAfterTrigger = function() {
|
||||
this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject);
|
||||
|
||||
// Run afterSave trigger
|
||||
triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config.applicationId);
|
||||
triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config);
|
||||
};
|
||||
|
||||
// A helper to figure out what location this operation happens at.
|
||||
@@ -824,5 +866,21 @@ RestWrite.prototype.sanitizedData = function() {
|
||||
return Parse._decode(undefined, data);
|
||||
}
|
||||
|
||||
RestWrite.prototype.cleanUserAuthData = function() {
|
||||
if (this.response && this.response.response && this.className === '_User') {
|
||||
let user = this.response.response;
|
||||
if (user.authData) {
|
||||
Object.keys(user.authData).forEach((provider) => {
|
||||
if (user.authData[provider] === null) {
|
||||
delete user.authData[provider];
|
||||
}
|
||||
});
|
||||
if (Object.keys(user.authData).length == 0) {
|
||||
delete user.authData;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default RestWrite;
|
||||
module.exports = RestWrite;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import rest from '../rest';
|
||||
import rest from '../rest';
|
||||
|
||||
import url from 'url';
|
||||
import url from 'url';
|
||||
|
||||
const ALLOWED_GET_QUERY_KEYS = ['keys', 'include'];
|
||||
|
||||
export class ClassesRouter extends PromiseRouter {
|
||||
|
||||
|
||||
handleFind(req) {
|
||||
let body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
|
||||
let options = {};
|
||||
@@ -14,14 +16,14 @@ export class ClassesRouter extends PromiseRouter {
|
||||
|
||||
for (let key of Object.keys(body)) {
|
||||
if (allowConstraints.indexOf(key) === -1) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Improper encode of parameter');
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid paramater for query: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (body.skip) {
|
||||
options.skip = Number(body.skip);
|
||||
}
|
||||
if (body.limit) {
|
||||
if (body.limit || body.limit === 0) {
|
||||
options.limit = Number(body.limit);
|
||||
} else {
|
||||
options.limit = Number(100);
|
||||
@@ -59,23 +61,39 @@ 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.get(req.config, req.auth, req.params.className, 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.');
|
||||
}
|
||||
|
||||
|
||||
if (req.params.className === "_User") {
|
||||
|
||||
|
||||
delete response.results[0].sessionToken;
|
||||
|
||||
|
||||
const user = response.results[0];
|
||||
|
||||
|
||||
if (req.auth.user && user.objectId == req.auth.user.id) {
|
||||
// Force the session token
|
||||
response.results[0].sessionToken = req.info.sessionToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { response: response.results[0] };
|
||||
});
|
||||
}
|
||||
@@ -106,7 +124,7 @@ export class ClassesRouter extends PromiseRouter {
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/classes/:className', (req) => { return this.handleFind(req); });
|
||||
this.route('GET', '/classes/:className/:objectId', (req) => { return this.handleGet(req); });
|
||||
|
||||
@@ -19,16 +19,16 @@ export class FeaturesRouter extends PromiseRouter {
|
||||
delete: false,
|
||||
},
|
||||
logs: {
|
||||
level: false,
|
||||
size: false,
|
||||
order: false,
|
||||
until: false,
|
||||
from: false,
|
||||
level: true,
|
||||
size: true,
|
||||
order: true,
|
||||
until: true,
|
||||
from: true,
|
||||
},
|
||||
push: {
|
||||
immediatePush: req.config.pushController.pushIsAvailable,
|
||||
scheduledPush: false,
|
||||
storedPushData: false,
|
||||
storedPushData: req.config.pushController.pushIsAvailable,
|
||||
pushAudiences: false,
|
||||
},
|
||||
schemas: {
|
||||
@@ -36,9 +36,10 @@ export class FeaturesRouter extends PromiseRouter {
|
||||
removeField: true,
|
||||
addClass: true,
|
||||
removeClass: true,
|
||||
clearAllDataFromClass: false,
|
||||
clearAllDataFromClass: true,
|
||||
exportClass: false,
|
||||
editClassLevelPermissions: true,
|
||||
editPointerPermissions: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import express from 'express';
|
||||
import BodyParser from 'body-parser';
|
||||
import * as Middlewares from '../middlewares';
|
||||
import express from 'express';
|
||||
import BodyParser from 'body-parser';
|
||||
import * as Middlewares from '../middlewares';
|
||||
import { randomHexString } from '../cryptoUtils';
|
||||
import Config from '../Config';
|
||||
import mime from 'mime';
|
||||
import Config from '../Config';
|
||||
import mime from 'mime';
|
||||
|
||||
export class FilesRouter {
|
||||
|
||||
@@ -77,8 +77,7 @@ export class FilesRouter {
|
||||
res.set('Location', result.url);
|
||||
res.json(result);
|
||||
}).catch((err) => {
|
||||
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
|
||||
'Could not store file.'));
|
||||
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Could not store file.'));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -93,4 +92,4 @@ export class FilesRouter {
|
||||
'Could not delete file.'));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,36 @@ var express = require('express'),
|
||||
triggers = require('../triggers');
|
||||
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import _ from 'lodash';
|
||||
|
||||
function parseDate(params) {
|
||||
return _.mapValues(params, (obj) => {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => {
|
||||
if (item && item.__type == 'Date') {
|
||||
return new Date(item.iso);
|
||||
} else if (item && typeof item === 'object') {
|
||||
return parseDate(item);
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
} else if (obj && obj.__type == 'Date') {
|
||||
return new Date(obj.iso);
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
return parseDate(obj);
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export class FunctionsRouter extends PromiseRouter {
|
||||
|
||||
|
||||
mountRoutes() {
|
||||
this.route('POST', '/functions/:functionName', FunctionsRouter.handleCloudFunction);
|
||||
}
|
||||
|
||||
|
||||
static createResponseObject(resolve, reject) {
|
||||
return {
|
||||
success: function(result) {
|
||||
@@ -21,24 +44,29 @@ export class FunctionsRouter extends PromiseRouter {
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function(error) {
|
||||
reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error));
|
||||
error: function(code, message) {
|
||||
if (!message) {
|
||||
message = code;
|
||||
code = Parse.Error.SCRIPT_FAILED;
|
||||
}
|
||||
reject(new Parse.Error(code, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static handleCloudFunction(req) {
|
||||
var applicationId = req.config.applicationId;
|
||||
var theFunction = triggers.getFunction(req.params.functionName, applicationId);
|
||||
var theValidator = triggers.getValidator(req.params.functionName, applicationId);
|
||||
if (theFunction) {
|
||||
|
||||
const params = Object.assign({}, req.body, req.query);
|
||||
let params = Object.assign({}, req.body, req.query);
|
||||
params = parseDate(params);
|
||||
var request = {
|
||||
params: params,
|
||||
master: req.auth && req.auth.isMaster,
|
||||
user: req.auth && req.auth.user,
|
||||
installationId: req.info.installationId
|
||||
installationId: req.info.installationId,
|
||||
log: req.config.loggerController && req.config.loggerController.adapter
|
||||
};
|
||||
|
||||
if (theValidator && typeof theValidator === "function") {
|
||||
@@ -61,4 +89,3 @@ export class FunctionsRouter extends PromiseRouter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,30 @@
|
||||
// global_config.js
|
||||
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from "../middlewares";
|
||||
|
||||
export class GlobalConfigRouter extends PromiseRouter {
|
||||
getGlobalConfig(req) {
|
||||
return req.config.database.adaptiveCollection('_GlobalConfig')
|
||||
.then(coll => coll.find({ '_id': 1 }, { limit: 1 }))
|
||||
.then(results => {
|
||||
if (results.length != 1) {
|
||||
// If there is no config in the database - return empty config.
|
||||
return { response: { params: {} } };
|
||||
}
|
||||
let globalConfig = results[0];
|
||||
return { response: { params: globalConfig.params } };
|
||||
});
|
||||
let database = req.config.database.WithoutValidation();
|
||||
return database.find('_GlobalConfig', { objectId: 1 }, { limit: 1 }).then((results) => {
|
||||
if (results.length != 1) {
|
||||
// If there is no config in the database - return empty config.
|
||||
return { response: { params: {} } };
|
||||
}
|
||||
let globalConfig = results[0];
|
||||
return { response: { params: globalConfig.params } };
|
||||
});
|
||||
}
|
||||
|
||||
updateGlobalConfig(req) {
|
||||
const params = req.body.params;
|
||||
let params = req.body.params;
|
||||
// Transform in dot notation to make sure it works
|
||||
const update = Object.keys(params).reduce((acc, key) => {
|
||||
if(params[key] && params[key].__op && params[key].__op === "Delete") {
|
||||
if (!acc.$unset) acc.$unset = {};
|
||||
acc.$unset[`params.${key}`] = "";
|
||||
} else {
|
||||
if (!acc.$set) acc.$set = {};
|
||||
acc.$set[`params.${key}`] = params[key];
|
||||
}
|
||||
acc[`params.${key}`] = params[key];
|
||||
return acc;
|
||||
}, {});
|
||||
return req.config.database.adaptiveCollection('_GlobalConfig')
|
||||
.then(coll => coll.upsertOne({ _id: 1 }, update))
|
||||
.then(() => ({ response: { result: true } }));
|
||||
let database = req.config.database.WithoutValidation();
|
||||
return database.update('_GlobalConfig', {objectId: 1}, update, {upsert: true}).then(() => ({ response: { result: true } }));
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Parse } from 'parse/node';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import { HooksController } from '../Controllers/HooksController';
|
||||
import { Parse } from 'parse/node';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from "../middlewares";
|
||||
|
||||
export class HooksRouter extends PromiseRouter {
|
||||
@@ -26,7 +25,7 @@ export class HooksRouter extends PromiseRouter {
|
||||
return Promise.resolve({response: foundFunction});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return hooksController.getFunctions().then((functions) => {
|
||||
return { response: functions || [] };
|
||||
}, (err) => {
|
||||
@@ -37,7 +36,7 @@ export class HooksRouter extends PromiseRouter {
|
||||
handleGetTriggers(req) {
|
||||
var hooksController = req.config.hooksController;
|
||||
if (req.params.className && req.params.triggerName) {
|
||||
|
||||
|
||||
return hooksController.getTrigger(req.params.className, req.params.triggerName).then((foundTrigger) => {
|
||||
if (!foundTrigger) {
|
||||
throw new Parse.Error(143,`class ${req.params.className} does not exist`);
|
||||
@@ -45,7 +44,7 @@ export class HooksRouter extends PromiseRouter {
|
||||
return Promise.resolve({response: foundTrigger});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return hooksController.getTriggers().then((triggers) => ({ response: triggers || [] }));
|
||||
}
|
||||
|
||||
@@ -73,10 +72,10 @@ export class HooksRouter extends PromiseRouter {
|
||||
hook.url = req.body.url
|
||||
} else {
|
||||
throw new Parse.Error(143, "invalid hook declaration");
|
||||
}
|
||||
}
|
||||
return this.updateHook(hook, req.config);
|
||||
}
|
||||
|
||||
|
||||
handlePut(req) {
|
||||
var body = req.body;
|
||||
if (body.__op == "Delete") {
|
||||
@@ -85,7 +84,7 @@ export class HooksRouter extends PromiseRouter {
|
||||
return this.handleUpdate(req);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/hooks/functions', middleware.promiseEnforceMasterKeyAccess, this.handleGetFunctions.bind(this));
|
||||
this.route('GET', '/hooks/triggers', middleware.promiseEnforceMasterKeyAccess, this.handleGetTriggers.bind(this));
|
||||
|
||||
@@ -6,25 +6,27 @@ import rest from '../rest';
|
||||
|
||||
export class InstallationsRouter extends ClassesRouter {
|
||||
handleFind(req) {
|
||||
let body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
|
||||
var options = {};
|
||||
if (req.body.skip) {
|
||||
options.skip = Number(req.body.skip);
|
||||
|
||||
if (body.skip) {
|
||||
options.skip = Number(body.skip);
|
||||
}
|
||||
if (req.body.limit) {
|
||||
options.limit = Number(req.body.limit);
|
||||
if (body.limit || body.limit === 0) {
|
||||
options.limit = Number(body.limit);
|
||||
}
|
||||
if (req.body.order) {
|
||||
options.order = String(req.body.order);
|
||||
if (body.order) {
|
||||
options.order = String(body.order);
|
||||
}
|
||||
if (req.body.count) {
|
||||
if (body.count) {
|
||||
options.count = true;
|
||||
}
|
||||
if (req.body.include) {
|
||||
options.include = String(req.body.include);
|
||||
if (body.include) {
|
||||
options.include = String(body.include);
|
||||
}
|
||||
|
||||
return rest.find(req.config, req.auth,
|
||||
'_Installation', req.body.where, options)
|
||||
'_Installation', body.where, options)
|
||||
.then((response) => {
|
||||
return {response: response};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from '../middlewares';
|
||||
|
||||
export class PurgeRouter extends PromiseRouter {
|
||||
|
||||
handlePurge(req) {
|
||||
return req.config.database.purgeCollection(req.params.className)
|
||||
.then(() => {
|
||||
var cacheAdapter = req.config.cacheController;
|
||||
if (req.params.className == '_Session') {
|
||||
cacheAdapter.user.clear();
|
||||
} else if (req.params.className == '_Role') {
|
||||
cacheAdapter.role.clear();
|
||||
}
|
||||
return {response: {}};
|
||||
});
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('DELETE', '/purge/:className', middleware.promiseEnforceMasterKeyAccess, (req) => { return this.handlePurge(req); });
|
||||
}
|
||||
}
|
||||
|
||||
export default PurgeRouter;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user