mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b905137278 | ||
|
|
2c19c2e4d4 | ||
|
|
30576f1091 | ||
|
|
e016d813e0 | ||
|
|
c8bc200729 | ||
|
|
09d04b0aad | ||
|
|
38f64be596 | ||
|
|
9b34b0263b | ||
|
|
e373f09040 | ||
|
|
a9a9772b05 | ||
|
|
735669a86a | ||
|
|
fd8a11bc27 | ||
|
|
60c5a73d25 | ||
|
|
3e983c41ff | ||
|
|
d9c3c02e7d | ||
|
|
6728da1e35 | ||
|
|
46dbecdec1 | ||
|
|
2458a8c58d | ||
|
|
50eed3cffe | ||
|
|
0e30c76e39 | ||
|
|
1d277db467 | ||
|
|
5e9d494979 | ||
|
|
12e174bcb6 | ||
|
|
254954064f | ||
|
|
50409aa866 | ||
|
|
9053e79151 | ||
|
|
9f111158ed | ||
|
|
eab9cdd749 | ||
|
|
4918665dd2 | ||
|
|
c41e5fca95 | ||
|
|
b54af6b701 | ||
|
|
28f0d26677 | ||
|
|
fb503320c3 | ||
|
|
a0499521cc | ||
|
|
b2491234e8 | ||
|
|
8863ad2309 | ||
|
|
3cde50f82a | ||
|
|
aba0081ce1 | ||
|
|
4af13af991 | ||
|
|
2b00c5b762 | ||
|
|
c1a70fe820 | ||
|
|
2feb6c4608 | ||
|
|
6d0bc1eff0 | ||
|
|
c03908f74e | ||
|
|
8011b2fdac | ||
|
|
4c1befabf2 | ||
|
|
066f29673a | ||
|
|
89fad24bae | ||
|
|
8c30c81da6 | ||
|
|
0f763da17d | ||
|
|
2a82d19dbd | ||
|
|
63d51fa6c8 | ||
|
|
a0956b5496 | ||
|
|
777b4a9961 | ||
|
|
566592e443 | ||
|
|
65330e022f | ||
|
|
eb649f226f | ||
|
|
5376c3b145 | ||
|
|
0388956808 | ||
|
|
b96a4cbdc5 | ||
|
|
5908a40d2f | ||
|
|
b2dd1d9c0f | ||
|
|
c9bd87e2f3 | ||
|
|
c1dbd007c5 | ||
|
|
9872051e3b | ||
|
|
643d49199a | ||
|
|
ae8d08294b | ||
|
|
377ce67239 | ||
|
|
eb20e92f8f | ||
|
|
e8242423e4 | ||
|
|
d9afeb86a5 | ||
|
|
8c8ec71573 | ||
|
|
1a2b1b9bc1 | ||
|
|
e6dc487963 | ||
|
|
ecf0814499 | ||
|
|
83cdc89be9 | ||
|
|
7aac70cca6 | ||
|
|
37fed3062c | ||
|
|
6d0b2f5346 | ||
|
|
004faf41e4 | ||
|
|
bf7d5ba605 | ||
|
|
6fb4d68f4c | ||
|
|
9fe16738b6 | ||
|
|
df12ba3ba2 | ||
|
|
a5ba5da36d | ||
|
|
b2fe087a02 | ||
|
|
3b775a1fb8 | ||
|
|
73e1763a63 | ||
|
|
37af1d78fc | ||
|
|
41e44302b4 | ||
|
|
e424137406 | ||
|
|
cec3071170 | ||
|
|
1d9605bc93 | ||
|
|
4a45cc467c | ||
|
|
3c75c2ba48 | ||
|
|
5250c07a1c | ||
|
|
1109d0ca23 | ||
|
|
9cd4a35120 | ||
|
|
c85bc016e2 | ||
|
|
7c32bfe95f | ||
|
|
07acecdc77 | ||
|
|
dfe9168512 | ||
|
|
780ee0203a | ||
|
|
6a2651c325 | ||
|
|
a2d0de7656 | ||
|
|
149884fe3e | ||
|
|
f821dfd02a | ||
|
|
4c0c7c77b7 | ||
|
|
f0db4ca4a4 | ||
|
|
83fd16c1b9 | ||
|
|
e39d51bd32 | ||
|
|
5432082d82 | ||
|
|
3de466ba9b | ||
|
|
82eb4613d9 | ||
|
|
276c32a0dd | ||
|
|
4de1c9bdd5 | ||
|
|
0287098ef5 | ||
|
|
ef5d59d784 | ||
|
|
1db432db51 | ||
|
|
54649eca67 | ||
|
|
f693b55b1c | ||
|
|
5c2d2c5193 | ||
|
|
eef750aa3e | ||
|
|
c16f529f74 | ||
|
|
3351ca7cec | ||
|
|
2ea4e37a37 | ||
|
|
1246551be3 | ||
|
|
145008c7d1 | ||
|
|
4aa016b732 | ||
|
|
a96e15b3f7 | ||
|
|
861fb211c7 | ||
|
|
266011c5a5 | ||
|
|
e93a0aab57 | ||
|
|
24fe6dc939 | ||
|
|
38ba9b4f47 | ||
|
|
e3f634e740 | ||
|
|
7f5a15d5df | ||
|
|
6e68656629 | ||
|
|
0d16a64eea | ||
|
|
abd8536f48 | ||
|
|
65ce27440a | ||
|
|
9fd4516cde | ||
|
|
636d16e0f9 | ||
|
|
e42be5c526 | ||
|
|
309f64ced8 | ||
|
|
e8eb546c90 | ||
|
|
0fd600cf24 | ||
|
|
35cd6910de | ||
|
|
7844442840 | ||
|
|
42c9543189 | ||
|
|
86832b9b95 | ||
|
|
e26beb1f5c | ||
|
|
4c9e95674a | ||
|
|
1a04a347cf | ||
|
|
eb2952fff7 | ||
|
|
5be375dec2 | ||
|
|
75af9a26cc | ||
|
|
4c2aa63fd2 | ||
|
|
4a1039679c | ||
|
|
ed0baa87af | ||
|
|
ba2b0a9cb9 | ||
|
|
ed7a25d5be | ||
|
|
b9a69381d2 | ||
|
|
2235678de3 | ||
|
|
cf925e75e8 | ||
|
|
16c7656188 | ||
|
|
0d818879c2 | ||
|
|
03caae1e61 | ||
|
|
72fac8a5fc | ||
|
|
5f7d392a06 | ||
|
|
528690b7b6 | ||
|
|
f67cc99948 | ||
|
|
61ba1b2913 | ||
|
|
c484094759 | ||
|
|
5de4c4723f | ||
|
|
199dfc1722 | ||
|
|
0cd902b8c2 | ||
|
|
0dc2843503 | ||
|
|
2d5221e480 | ||
|
|
468e98785f | ||
|
|
9e506969c3 | ||
|
|
a2491ff036 | ||
|
|
5e15864dc9 | ||
|
|
c6dcad8d16 | ||
|
|
ac283d3cc0 | ||
|
|
c1e808f9e8 | ||
|
|
88b4d9dda1 | ||
|
|
34a4cca240 | ||
|
|
28901aa35f | ||
|
|
91bb235bc7 | ||
|
|
51fb14be3f | ||
|
|
8fb9a4a8bc | ||
|
|
47d796ea58 | ||
|
|
2e750b78cc | ||
|
|
a176de94d2 | ||
|
|
0081e1c81c | ||
|
|
1aa2204aeb | ||
|
|
b2ae2e1db4 | ||
|
|
b10182f35e | ||
|
|
2b1dc5c46a | ||
|
|
e692b5dd82 | ||
|
|
364e346548 | ||
|
|
4462b39d48 | ||
|
|
c35d74d176 | ||
|
|
330286d22b | ||
|
|
ad5e82f2b7 | ||
|
|
38ed96ace5 | ||
|
|
d691591630 | ||
|
|
2eb56039e2 | ||
|
|
3e9f292d84 | ||
|
|
68b15c298e | ||
|
|
39fbcde612 | ||
|
|
8f7393f847 | ||
|
|
0294d45078 | ||
|
|
b84acd578a | ||
|
|
261ce010dd | ||
|
|
e6d7d8f0a2 | ||
|
|
3fb6b2b4ab | ||
|
|
e849cf52d7 | ||
|
|
3c60d6f299 | ||
|
|
19717b8f84 | ||
|
|
3a70fda679 | ||
|
|
e0b588351a | ||
|
|
a31a1ff777 | ||
|
|
a68d0502bd | ||
|
|
89b412caff | ||
|
|
da99abeaec | ||
|
|
dd40dfa4d9 | ||
|
|
0a6faa81fa | ||
|
|
f47d58471e | ||
|
|
731c550714 | ||
|
|
f51d638246 | ||
|
|
519b6bac68 | ||
|
|
b1e5565b22 | ||
|
|
a169663304 | ||
|
|
e47b61de46 | ||
|
|
341f9f71e3 | ||
|
|
91eca2e45f | ||
|
|
5bfa7160d9 | ||
|
|
38eb5ace37 | ||
|
|
93667b4e84 | ||
|
|
58cd870a1e | ||
|
|
022a85619d | ||
|
|
94e27ef850 | ||
|
|
041197fb4c | ||
|
|
884f4c0ecd | ||
|
|
19900fcdf8 | ||
|
|
d609c72783 | ||
|
|
a8aef820af | ||
|
|
af4a0417a9 | ||
|
|
852bb4782a | ||
|
|
9c414804ac | ||
|
|
ff5b39123b | ||
|
|
2aeae76d80 | ||
|
|
065141f00e | ||
|
|
0575952509 | ||
|
|
bc07a3ff3a | ||
|
|
c5cf282d11 | ||
|
|
e2333ba262 | ||
|
|
892c6f94d5 | ||
|
|
0d6f9e951d | ||
|
|
765cfd02dc | ||
|
|
ccd3d86137 | ||
|
|
693d72060d | ||
|
|
2b7f772a1b | ||
|
|
443a509905 | ||
|
|
2830021990 | ||
|
|
d2d12d3d69 | ||
|
|
1099ebbcd8 | ||
|
|
4c50c4d11c | ||
|
|
e9d23830a3 | ||
|
|
01096b360a | ||
|
|
effed92cab | ||
|
|
9cdc6ca5b3 | ||
|
|
158a974c89 | ||
|
|
1593575a87 | ||
|
|
ef9ee66640 | ||
|
|
d5db318db8 | ||
|
|
9fa80eb4ed | ||
|
|
ff16839450 | ||
|
|
0c1b75fcbe | ||
|
|
69781ce0cc | ||
|
|
2eebc68f21 | ||
|
|
79e00e1dee | ||
|
|
1f6e19fcce | ||
|
|
2dceec7359 | ||
|
|
3e68491df0 | ||
|
|
5ace99d542 | ||
|
|
9b344da9d0 | ||
|
|
e569f402b1 | ||
|
|
972b800ae4 | ||
|
|
6bc021ee14 | ||
|
|
f235412c1b | ||
|
|
8a126fc1a2 | ||
|
|
ad5d17953b | ||
|
|
5ca2288823 | ||
|
|
9806912045 | ||
|
|
4af253d1f8 | ||
|
|
2a6cec173e | ||
|
|
c7012012aa | ||
|
|
8cece4a2a8 | ||
|
|
7a30d7c885 | ||
|
|
b6a20d9e9f | ||
|
|
82c3015dc0 | ||
|
|
d678a43696 | ||
|
|
70088a95a7 | ||
|
|
fc9d77f1d4 | ||
|
|
c6b2032039 | ||
|
|
43cc97d99f | ||
|
|
08089d6fcb | ||
|
|
1e05309e79 | ||
|
|
f88aa2a62a | ||
|
|
1299f0697c | ||
|
|
315290d161 | ||
|
|
433e82f1df | ||
|
|
350b59ac5f | ||
|
|
9082351411 | ||
|
|
f105d7aef1 | ||
|
|
f5f63bfc64 | ||
|
|
826aa79b9f | ||
|
|
c720fbba5a | ||
|
|
8f5a8618cf | ||
|
|
98abd7112c | ||
|
|
5af6e5dfaa | ||
|
|
a5ffb95022 | ||
|
|
a43638f300 | ||
|
|
d05fb9f2fa | ||
|
|
410a1c74fb | ||
|
|
0c3feaaa17 | ||
|
|
5e363eae44 | ||
|
|
7af5de4b98 | ||
|
|
75b9a56e0a | ||
|
|
caf4a2341f | ||
|
|
912edacb53 | ||
|
|
b106ffc994 | ||
|
|
acfdc54a06 | ||
|
|
16b1b2a197 | ||
|
|
7448521cb9 | ||
|
|
9e477591fd | ||
|
|
191d80b667 | ||
|
|
ea7c01400f | ||
|
|
ac789c8537 | ||
|
|
6a6248b6cb | ||
|
|
e7c7f44265 | ||
|
|
f5ef98bde3 | ||
|
|
37ac29d798 | ||
|
|
d4c1f47307 | ||
|
|
7dad413a17 | ||
|
|
35ac4c3e68 | ||
|
|
1b4d9324de | ||
|
|
ed86c80772 | ||
|
|
c789f6c979 | ||
|
|
260290409e | ||
|
|
899da8d765 | ||
|
|
fea308a01a | ||
|
|
45cc58c7e5 | ||
|
|
ae99b92809 | ||
|
|
251ff0ef71 | ||
|
|
426ccdc8f3 | ||
|
|
a2fefde4ba | ||
|
|
3d7ed8dfac | ||
|
|
e4aa1cb0a0 | ||
|
|
d72717d23a | ||
|
|
573558d3ad | ||
|
|
d35eeb825e | ||
|
|
b64640c570 | ||
|
|
611bd9baaa | ||
|
|
31960b59b0 | ||
|
|
69fd494ab2 | ||
|
|
b6a66761f0 |
+101
-52
@@ -1,18 +1,38 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [ release, alpha, beta, next-major ]
|
||||
branches: [ release*, alpha, beta ]
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
branches: [ release*, alpha, beta ]
|
||||
env:
|
||||
NODE_VERSION: 14.18.1
|
||||
NODE_VERSION: 18.1.0
|
||||
PARSE_SERVER_TEST_TIMEOUT: 20000
|
||||
jobs:
|
||||
check-code-analysis:
|
||||
name: Code Analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
source-root: src
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
check-ci:
|
||||
name: Node Engine Check
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
@@ -33,7 +53,7 @@ jobs:
|
||||
check-lint:
|
||||
name: Lint
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
@@ -53,7 +73,7 @@ jobs:
|
||||
check-circular:
|
||||
name: Circular Dependencies
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
@@ -73,7 +93,7 @@ jobs:
|
||||
check-docker:
|
||||
name: Docker Build
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
@@ -90,7 +110,7 @@ jobs:
|
||||
check-lock-file-version:
|
||||
name: NPM Lock File Version
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Check NPM lock file version
|
||||
@@ -101,63 +121,80 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: MongoDB 5.0, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 5.0.3
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 14.18.1
|
||||
- name: MongoDB 4.4, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.4.10
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 14.18.1
|
||||
- name: MongoDB 4.2, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.2.17
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 14.18.1
|
||||
- name: MongoDB 4.0, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.0.27
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 14.18.1
|
||||
- name: MongoDB 4.0, Standalone, MMAPv1
|
||||
MONGODB_VERSION: 4.0.27
|
||||
MONGODB_VERSION: 4.0.28
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: mmapv1
|
||||
NODE_VERSION: 14.18.1
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: MongoDB 4.0, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.0.28
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: MongoDB 4.2, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.2.19
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: MongoDB 4.4, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: MongoDB 5, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 5.3.2
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: MongoDB 6, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 6.0.2
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: Redis Cache
|
||||
PARSE_SERVER_TEST_CACHE: redis
|
||||
MONGODB_VERSION: 4.4.10
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 14.18.1
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: Node 12
|
||||
MONGODB_VERSION: 4.4.10
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 12.22.7
|
||||
- name: Node 15
|
||||
MONGODB_VERSION: 4.4.10
|
||||
NODE_VERSION: 12.22.11
|
||||
- name: Node 14
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 15.14.0
|
||||
NODE_VERSION: 14.19.1
|
||||
- name: Node 16
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 16.14.2
|
||||
- name: Node 17
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 17.9.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
env:
|
||||
MONGODB_VERSION: ${{ matrix.MONGODB_VERSION }}
|
||||
MONGODB_TOPOLOGY: ${{ matrix.MONGODB_TOPOLOGY }}
|
||||
MONGODB_STORAGE_ENGINE: ${{ matrix.MONGODB_STORAGE_ENGINE }}
|
||||
PARSE_SERVER_TEST_CACHE: ${{ matrix.PARSE_SERVER_TEST_CACHE }}
|
||||
NODE_VERSION: ${{ matrix.NODE_VERSION }}
|
||||
steps:
|
||||
- name: Fix usage of insecure GitHub protocol
|
||||
run: sudo git config --system url."https://github".insteadOf "git://github"
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
@@ -183,23 +220,32 @@ jobs:
|
||||
include:
|
||||
- name: PostgreSQL 11, PostGIS 3.0
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.0
|
||||
NODE_VERSION: 14.18.1
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: PostgreSQL 11, PostGIS 3.1
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.1
|
||||
NODE_VERSION: 14.18.1
|
||||
- name: PostgreSQL 12, PostGIS 3.1
|
||||
POSTGRES_IMAGE: postgis/postgis:12-3.1
|
||||
NODE_VERSION: 14.18.1
|
||||
- name: PostgreSQL 13, PostGIS 3.1
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.1
|
||||
NODE_VERSION: 14.18.1
|
||||
- name: PostgreSQL 14, PostGIS 3.1
|
||||
POSTGRES_IMAGE: postgis/postgis:14-3.1
|
||||
NODE_VERSION: 14.18.1
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: PostgreSQL 11, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.2
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: PostgreSQL 11, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.3
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: PostgreSQL 12, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:12-3.3
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: PostgreSQL 13, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.3
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: PostgreSQL 14, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:14-3.3
|
||||
NODE_VERSION: 18.1.0
|
||||
- name: PostgreSQL 15, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.3
|
||||
NODE_VERSION: 18.1.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
@@ -242,3 +288,6 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: release-automated
|
||||
on:
|
||||
push:
|
||||
branches: [ release, alpha, beta, next-major ]
|
||||
branches: [ release, alpha, beta, next-major, release-4.x.x, release-5.x.x ]
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- run: npx semantic-release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Determine tag on current commit
|
||||
id: tag
|
||||
|
||||
+1
-2
@@ -42,8 +42,7 @@ Details:
|
||||
- Suitable environment: experimental
|
||||
|
||||
|
||||
[log_release]: https://github.com/parse-community/parse-server/blob/release/CHANGELOG.md
|
||||
<!--[log_release]: https://github.com/parse-community/parse-server/blob/release/changelogs/CHANGELOG_release.md -->
|
||||
[log_release]: https://github.com/parse-community/parse-server/blob/release/changelogs/CHANGELOG_release.md
|
||||
[log_beta]: https://github.com/parse-community/parse-server/blob/beta/changelogs/CHANGELOG_beta.md
|
||||
[log_alpha]: https://github.com/parse-community/parse-server/blob/alpha/changelogs/CHANGELOG_alpha.md
|
||||
[branch_release]: https://github.com/parse-community/parse-server/tree/release
|
||||
|
||||
+180
-7
@@ -2,7 +2,14 @@
|
||||
|
||||
## Table of Contents <!-- omit in toc -->
|
||||
- [Contributing](#contributing)
|
||||
- [Issue vs. Pull Request](#issue-vs-pull-request)
|
||||
- [Templates](#templates)
|
||||
- [Why Contributing?](#why-contributing)
|
||||
- [Contribution FAQs](#contribution-faqs)
|
||||
- [Reviewer Role](#reviewer-role)
|
||||
- [Review Feedback](#review-feedback)
|
||||
- [Merge Readiness](#merge-readiness)
|
||||
- [Review Validity](#review-validity)
|
||||
- [Environment Setup](#environment-setup)
|
||||
- [Recommended Tools](#recommended-tools)
|
||||
- [Setting up your local machine](#setting-up-your-local-machine)
|
||||
@@ -20,8 +27,18 @@
|
||||
- [Parse Error](#parse-error)
|
||||
- [Parse Server Configuration](#parse-server-configuration)
|
||||
- [Pull Request](#pull-request)
|
||||
- [Commit Message](#commit-message)
|
||||
- [Breaking Change](#breaking-change)
|
||||
- [Merging](#merging)
|
||||
- [Breaking Change](#breaking-change-1)
|
||||
- [Reverting](#reverting)
|
||||
- [Releasing](#releasing)
|
||||
- [General Considerations](#general-considerations)
|
||||
- [Major Release / Long-Term-Support](#major-release--long-term-support)
|
||||
- [Preparing Release](#preparing-release)
|
||||
- [Publishing Release (forward-merge):](#publishing-release-forward-merge)
|
||||
- [Publishing Hotfix (back-merge):](#publishing-hotfix-back-merge)
|
||||
- [Publishing Major Release (Yearly Release)](#publishing-major-release-yearly-release)
|
||||
- [Versioning](#versioning)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
|
||||
@@ -39,6 +56,22 @@ When you are ready to code, you can find more information about opening a pull r
|
||||
|
||||
Whether this is your first contribution or you are already an experienced contributor, the Parse Community has your back – don't hesitate to ask for help!
|
||||
|
||||
### Issue vs. Pull Request
|
||||
|
||||
An issue is required to be linked in every pull request. We understand that no-one likes to create an issue for something that appears to be a simple pull request, but here is why this is beneficial for everyone:
|
||||
|
||||
- An issue get more visibility than a pull request as issues can be pinned, receive bounties and it is primarily the issue list that people browse through rather than the more technical pull request list. Visibility is a key aspect so others can weigh in on issues and contribute their opinion.
|
||||
- The discussion in the issue is different from the discussion in the pull request. The issue discussion is focused on the issue and how to address it, whereas the discussion in the pull request is focused on a specific implemention. An issue may even have multiple pull requests because either the issue requires multiple implementations or multiple pull requests are opened to compare and test different approaches to later decide for one.
|
||||
- High-level conceptual discussions about the issue should be still available, even if a pull request is closed because its appraoch was discarded. If these discussions are in the pull request instead, they can easily become fragmented over multiple pull requests and issues, which can make it very hard to make sense of all aspects of an issue.
|
||||
|
||||
### Templates
|
||||
|
||||
You are required to use and completely fill out the templates for new issues and pull requests. We understand that no-one enjoys filling out forms, but here is why this is beneficial for everyone:
|
||||
|
||||
- It may take you 30 seconds longer, but will save even more time for everyone else trying to understand your issue.
|
||||
- It helps to fix issues and merge pull requests faster as reviewers spend less time trying to understand your issue.
|
||||
- It makes investigations easier when others try to understand your issue and code changes made even years later.
|
||||
|
||||
## Why Contributing?
|
||||
|
||||
Buy cheap, buy twice. What? No, this is not the Economics 101 class, but the same is true for contributing.
|
||||
@@ -60,6 +93,46 @@ Consider the benefits you get:
|
||||
|
||||
Most importantly, with every contribution you improve your skills so that future contributions take even less time and you get all the benefits above for free — easy choice, right?
|
||||
|
||||
## Contribution FAQs
|
||||
|
||||
### Reviewer Role
|
||||
|
||||
> *Instead of writing review comments back-and-forth, why doesn't the reviewer just write the code themselves?*
|
||||
|
||||
A reviewer is already helping you to make a code contribution through their review. A reviewer *may* even help you to write code by actually writing it for you, but is not obliged to do so.
|
||||
|
||||
GitHub allows reviewers to suggest and write code changes as part of the review feedback. These code suggestions are likely to contain mistakes due to the lack of code syntax checks when writing code directly on GitHub. You should therefore always review these suggestions before accepting them, ideally in an IDE. If you merge a code suggestion and the CI then fails, take another look at the code change before asking the reviewer for help.
|
||||
|
||||
### Review Feedback
|
||||
|
||||
> *It takes too much effort to incorporate the review feedback, why why can't you just merge my pull request?*
|
||||
|
||||
If you are a new contributor, it's naturally a learning experience for you and therefore takes longer. We welcome contributors of any experience levels and gladly support you in getting familiar with the code base and our quality standards and contribution requirements. In return we expect you to be open to and appreciative of the reviewers' feedback.
|
||||
|
||||
In a large pull request, it can be a significant effort to bring it over the finish line. Luckily this is a collaborative environment and others are free to jump in to contribute to the pull request to share the effort. You can either give others access to your fork or they can open their own pull request based on your previous work.
|
||||
|
||||
If you are out of resources stay calm, explain your personal constraints (expertise or time) and ask for help. Wasting time by complaining about the amount of review comments will neither use your own time in a meaningful way, nor the time of others who read your complaint.
|
||||
|
||||
This is a collaborative enviroment in which everyone works on a common goal - to get a pull request ready for merging. Reviewers are working *with* you to get your pull request ready, *not against you*.
|
||||
|
||||
**❗️ Always be mindful that the reviewers' efforts are an integral part of code contribution. Their review is as important as your written code and their review time is a valuable as your coding time.**
|
||||
|
||||
### Merge Readiness
|
||||
|
||||
> *The feature already works, why do you request more changes instead of just merging my pull request?*
|
||||
|
||||
A feature may work for your own use case or in your own environment, but that doesn't necessarily mean that it's ready for merging. Aside from code quality and code style requirements, reviewers also review based on strategic and architectural considerations. It's often easy to just get a feature to work, but it needs to be also maintained in the future, robust therefore well tested and validated, intuitive for other developers to use, well documented, and not cause a forseeable breaking change in the near future.
|
||||
|
||||
### Review Validity
|
||||
|
||||
> *The reviewer has never worked on the issue and was never part of any previous discussion, why would I care about their opinion?*
|
||||
|
||||
It's contrary to an open, collaborative environment to expect others to be involved in an issue or discussion since its beginning. Such a mindset would close out any new views, which are important for a differentiated discussion.
|
||||
|
||||
> *The reviewer doesn't have any expertise in that matter, why would I care about their opinion?*
|
||||
|
||||
Your arguments must focus on the issue, not on your assumption of someone else's personal experience. We will take immediate and appropriate action in case of personal attacks, regardless of your previous contributions. Personal attacks are not permissible. If you became a victim of personal attacks, you can privately [report](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam) the GitHub comment to the Parse Platform PMC.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
### Recommended Tools
|
||||
@@ -154,7 +227,7 @@ If your pull request introduces a change that may affect the storage or retrieva
|
||||
[PostGIS images (select one with v2.2 or higher) on docker dashboard](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://registry.hub.docker.com/_/postgres/) image and will work out-of-the-box (as long as you create a user with the necessary extensions for each of your Parse databases; see below). To launch the compatible Postgres instance, copy and paste the following line into your shell:
|
||||
|
||||
```
|
||||
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:13-3.1-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:13-3.2-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
```
|
||||
To stop the Postgres instance:
|
||||
|
||||
@@ -162,7 +235,7 @@ To stop the Postgres instance:
|
||||
docker stop parse-postgres
|
||||
```
|
||||
|
||||
You can also use the [postgis/postgis:13-3.1-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
You can also use the [postgis/postgis:13-3.2-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
|
||||
```
|
||||
#Install additional scripts. These are run in abc order during initial start
|
||||
@@ -296,6 +369,8 @@ Introducing new [Parse Server configuration][config] parameters requires the fol
|
||||
|
||||
## Pull Request
|
||||
|
||||
### Commit Message
|
||||
|
||||
For release automation, the title of pull requests needs to be written in a defined syntax. We loosely follow the [Conventional Commits](https://www.conventionalcommits.org) specification, which defines this syntax:
|
||||
|
||||
```
|
||||
@@ -303,7 +378,7 @@ For release automation, the title of pull requests needs to be written in a defi
|
||||
```
|
||||
|
||||
The _type_ is the category of change that is made, possible types are:
|
||||
- `feat` - add a new feature
|
||||
- `feat` - add a new feature or improve an existing feature
|
||||
- `fix` - fix a bug
|
||||
- `refactor` - refactor code without impact on features or performance
|
||||
- `docs` - add or edit code comments, documentation, GitHub pages
|
||||
@@ -335,8 +410,13 @@ If a pull request contains a braking change, the description of the pull request
|
||||
|
||||
The following guide is for anyone who merges a contributor pull request into the working branch, the working branch into a release branch, a release branch into another release branch, or any other direct commits such as hotfixes into release branches or the working branch.
|
||||
|
||||
- For changelog generation, only the commit message set when merging the pull request is relevant. The title and description of the GitHub pull request as authored by the contributor have no influence on the changelog generation. However, the title of the GitHub pull request should be used as the commit message.
|
||||
- If the pull request contains a breaking change, the commit message must contain the phrase `BREAKING CHANGE`, capitalized and without any formatting, followed by a short description of the breaking change and ideally how the developer should address it, all in a single line. This line should contain more details focusing on the "breaking” aspect of the change and is intended to assist the developer in adapting. Keep it concise, as it will become part of the changelog entry, for example:
|
||||
- A contributor pull request must be merged into the working branch using `Squash and Merge`, to create a single commit message that describes the change.
|
||||
- A release branch or the default branch must be merged into another release branch using `Merge Commit`, to preserve each individual commit message that describes its respective change.
|
||||
- For changelog generation, only the commit message set when merging the pull request is relevant. The title and description of the GitHub pull request as authored by the contributor have no influence on the changelog generation. However, the title of the GitHub pull request should be used as the commit message. See the following chapters for considerations in special scenarios, e.g. merging a breaking change or reverting a commit.
|
||||
|
||||
### Breaking Change
|
||||
|
||||
If the pull request contains a breaking change, the commit message must contain the phrase `BREAKING CHANGE`, capitalized and without any formatting, followed by a short description of the breaking change and ideally how the developer should address it, all in a single line. This line should contain more details focusing on the "breaking” aspect of the change and is intended to assist the developer in adapting. Keep it concise, as it will become part of the changelog entry, for example:
|
||||
|
||||
```
|
||||
fix: remove handle from door
|
||||
@@ -344,8 +424,101 @@ The following guide is for anyone who merges a contributor pull request into the
|
||||
BREAKING CHANGE: You cannot open the door anymore by using a handle. See the [#migration guide](http://example.com) for more details.
|
||||
```
|
||||
Keep in mind that in a repository with release automation, merging such a commit message will trigger a release with a major version increment.
|
||||
- A contributor pull request must be merged into the working branch using `Squash and Merge`, to create a single commit message that describes the change.
|
||||
- A release branch or the default branch must be merged into another release branch using `Merge Commit`, to preserve each individual commit message that describes its respective change.
|
||||
|
||||
### Reverting
|
||||
|
||||
If the commit reverts a previous commit, use the prefix `revert:`, followed by the header of the reverted commit. In the body of the commit message add `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted. For example:
|
||||
|
||||
```
|
||||
revert: fix: remove handle from door
|
||||
|
||||
This reverts commit 1234567890abcdef.
|
||||
```
|
||||
|
||||
⚠️ A `revert` prefix will *always* trigger a release. Generally, a commit that did not trigger a release when it was initially merged should also not trigger a release when it is reverted. For example, do not use the `revert` prefix when reverting a commit that has a `ci` prefix:
|
||||
|
||||
```
|
||||
ci: add something
|
||||
```
|
||||
is reverted with:
|
||||
```
|
||||
ci: remove something
|
||||
```
|
||||
instead of:
|
||||
```
|
||||
revert: ci: add something
|
||||
|
||||
This reverts commit 1234567890abcdef.
|
||||
```
|
||||
|
||||
## Releasing
|
||||
|
||||
### General Considerations
|
||||
|
||||
- The `package-lock.json` file has to be deleted and recreated by npm from scratch in regular intervals using the `npm i` command. It is not enough to only update the file via automated security pull requests (e.g. dependabot, snyk), that can create inconsistencies between sub-dependencies of a dependency and increase the chances of vulnerabilities. The file should be recreated once every release cycle which is usually monthly.
|
||||
|
||||
### Major Release / Long-Term-Support
|
||||
|
||||
While the current major version is published on branch `release`, a Long-Term-Support (LTS) version is published on branch `release-#.x.x`, for example `release-4.x.x` for the Parse Server 4.x LTS branch.
|
||||
|
||||
### Preparing Release
|
||||
|
||||
The following changes are done in the `alpha` branch, before publishing the last `beta` version that will eventually become the major release. This way the changes trickle naturally through all branches and code consistency is ensured among branches.
|
||||
|
||||
- Make sure all [deprecations](https://github.com/parse-community/parse-server/blob/alpha/DEPRECATIONS.md) are reflected in code, old code is removed and the deprecations table is updated.
|
||||
- Add the future LTS branch `release-#.x.x` to the branch list in [release.config.js](https://github.com/parse-community/parse-server/blob/alpha/release.config.js) so that the branch will later be recognized for release automation.
|
||||
|
||||
|
||||
### Publishing Release (forward-merge):
|
||||
|
||||
1. Create new temporary branch `build` on branch `beta`.
|
||||
2. Create PR to merge `build` into `release`:
|
||||
- PR title: `build: release`
|
||||
- PR description: (leave empty)
|
||||
3. Resolve any conflicts:
|
||||
- For conflicts regarding the package version in `package.json` and `package-lock.json` it doesn't matter which version is chosen, as the version will be set by auto-release in a commit after merging. However, for both files the same version should be chosen when resolving the conflict.
|
||||
4. Merge PR with a "merge commit", do not "squash and merge":
|
||||
- Commit message: (use PR title)
|
||||
- Description: (leave empty)
|
||||
5. Wait for GitHub Action `release-automated` to finish:
|
||||
- If GitHub Action fails, investigate why; manual correction may be needed.
|
||||
6. Pull all remote branches into local branches.
|
||||
7. Delete temporary branch `build`.
|
||||
8. Create new temporary branch `build` on branch `alpha`.
|
||||
9. Create PR to merge `build` into `beta`:
|
||||
- PR title: `build: release`
|
||||
- PR description: (leave empty)
|
||||
8. Repeat steps 3-7 for PR from step 9.
|
||||
|
||||
### Publishing Hotfix (back-merge):
|
||||
|
||||
1. Create PR to merge hotfix PR into `release`:
|
||||
- Merge PR following the same rules as any PR would be merged into the working branch `alpha`.
|
||||
2. Wait for GitHub Action `release-automated` to finish:
|
||||
- GitHub Action will fail with error `! [rejected] HEAD -> beta (non-fast-forward)`; this is expected as auto-release currently cannot fully handle back-merging; docker will not publish the new release, so this has to be done manually using the GitHub workflow `release-manual-docker` and entering the version tag that has been created by auto-release.
|
||||
3. Pull all remote branches into local branches.
|
||||
4. Create a new temporary branch `backmerge` on branch `release`.
|
||||
5. Create PR to merge `backmerge` into `beta`:
|
||||
- PR title: `refactor: <commit-summary>` where `<commit-summary>` is the commit summary of step 1. The commit type needs to be `refactor`, otherwise the commit will show in the changelog of the `release` branch, once the `beta` branch is merged into release; this would a duplicate entry because the same changelog entry has already been generated when the PR was merged into the `release` branch in step 1.
|
||||
- PR description: (leave empty)
|
||||
6. Resolve any conflicts:
|
||||
- During back-merging, usually all changes are preserved; current changes come from the hotfix in the `release` branch, the incoming changes come from the `beta` branch usually being ahead of the `release` branch. This makes back-merging so complex and bug-prone and is the main reason why it should be avoided if possible.
|
||||
7. Merge PR with "squash and merge", do not do a "merge commit":
|
||||
- Commit message: (use PR title)
|
||||
- Description: (leave empty)
|
||||
|
||||
ℹ️ Merging this PR will not trigger a release; the back-merge will not appear in changelogs of the `beta`, `alpha` branches; the back-merged fix will be an undocumented change of these branches' next releases; if necessary, the change needs to be added manually to the pre-release changelogs *after* the next pre-releases.
|
||||
8. Delete temporary branch `backmerge`.
|
||||
10. Create a new temporary branch `backmerge` on branch `beta`.
|
||||
11. Repeat steps 4-8 to merge PR into `alpha`.
|
||||
|
||||
⚠️ Long-term-support branches are excluded from the processes above and handled individually as they do not have pre-releases branches and are not considered part of the current codebase anymore. It may be necessary to significantly adapt a PR for a LTS branch due to the differences in codebase and CI tests. This adaption should be done in advance before merging any related PR, especially for security fixes, as to not publish a vulnerability while it may still take significant time to adapt the fix for the older codebase of a LTS branch.
|
||||
|
||||
### Publishing Major Release (Yearly Release)
|
||||
|
||||
1. Create LTS branch `release-#.x.x` off the latest version tag on `release` branch.
|
||||
2. Create temporary branch `build-release` off branch `beta` and create a pull request with `release` as the base branch.
|
||||
3. Merge branch `build-release` into `release`. Given that there will be breaking changes, a new major release will be created. In the unlikely case that there have been no breaking changes between the previous major release and the upcoming release, a major version increment has to be triggered manually. See the docs of the release automation framework for how to do that.
|
||||
|
||||
## Versioning
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
|
||||
| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
|
||||
| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
|
||||
| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
|
||||
| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
|
||||
[i_deprecation]: ## "The version and date of the deprecation."
|
||||
[i_removal]: ## "The version and date of the planned removal."
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Aalpha)
|
||||
[](https://snyk.io/test/github/parse-community/parse-server)
|
||||
[](https://codecov.io/github/parse-community/parse-server?branch=alpha)
|
||||
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
[](https://github.com/parse-community/parse-dashboard/releases)
|
||||
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
|
||||
|
||||
[][open-collective-link]
|
||||
[][open-collective-link]
|
||||
[![License][license-svg]][license-link]
|
||||
[](https://community.parseplatform.org/c/parse-server)
|
||||
[](https://twitter.com/intent/follow?screen_name=ParsePlatform)
|
||||
[](https://chat.parseplatform.org)
|
||||
|
||||
---
|
||||
|
||||
@@ -30,14 +30,16 @@ The full documentation for Parse Server is available in the [wiki](https://githu
|
||||
|
||||
---
|
||||
|
||||
A big *thank you* to all our backers and sponsors who support the development of Parse Platform!
|
||||
A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who support the development of Parse Platform!
|
||||
|
||||
### 💎 Diamond Sponsors
|
||||
|
||||
[](https://opencollective.com/parse-server/sponsor/0/website)
|
||||
#### Bronze Sponsors
|
||||
|
||||
[](https://opencollective.com/parse-server/contribute/bronze-sponsor-10559)
|
||||
|
||||
---
|
||||
|
||||
- [Flavors & Branches](#flavors--branches)
|
||||
- [Long Term Support](#long-term-support)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Running Parse Server](#running-parse-server)
|
||||
- [Compatibility](#compatibility)
|
||||
@@ -85,13 +87,25 @@ A big *thank you* to all our backers and sponsors who support the development of
|
||||
- [Using automatically generated operations](#using-automatically-generated-operations)
|
||||
- [Customizing your GraphQL Schema](#customizing-your-graphql-schema)
|
||||
- [Learning more](#learning-more)
|
||||
- [Upgrading to 3.0.0](#upgrading-to-300)
|
||||
- [Want to ride the bleeding edge?](#want-to-ride-the-bleeding-edge)
|
||||
- [Upgrading to Parse Server 3.0](#upgrading-to-parse-server-30)
|
||||
- [Contributing](#contributing)
|
||||
- [Contributors](#contributors)
|
||||
- [Sponsors](#sponsors)
|
||||
- [Backers](#backers)
|
||||
|
||||
# Flavors & Branches
|
||||
|
||||
Parse Server is available in different flavors on different branches:
|
||||
|
||||
- The main branches are [release][log_release], [beta][log_beta] and [alpha][log_alpha]. See the [changelog overview](CHANGELOG.md) for details.
|
||||
- The long-term-support (LTS) branches are named `release-<version>.x.x`, for example `release-4.x.x`. LTS branches do not have pre-release branches.
|
||||
|
||||
## Long Term Support
|
||||
|
||||
Long-Term-Support (LTS) is provided for the previous Parse Server major version. For example, Parse Server 4.x will receive security updates until Parse Server 5.x is superseded by Parse Server 6.x and becomes the new LTS version. While the current major version is published on branch `release`, a LTS version is published on branch `release-#.x.x`, for example `release-4.x.x` for the Parse Server 4.x LTS branch.
|
||||
|
||||
⚠️ LTS versions are provided to help you transition as soon as possible to the current major version. While we aim to fix security vulnerabilities in the LTS version, our main focus is on developing the current major version and preparing the next major release. Therefore we may leave certain vulnerabilities up to the community to fix. Search for [pull requests with the specific LTS base branch](https://github.com/parse-community/parse-server/pulls?q=is%3Aopen+is%3Apr+base%3Arelease-4.x.x) to see the current open vulnerabilities for that LTS branch.
|
||||
|
||||
# Getting Started
|
||||
|
||||
The fastest and easiest way to get started is to run MongoDB and Parse Server locally.
|
||||
@@ -107,36 +121,43 @@ Before you start make sure you have installed:
|
||||
### Compatibility
|
||||
|
||||
#### Node.js
|
||||
|
||||
Parse Server is continuously tested with the most recent releases of Node.js to ensure compatibility. We follow the [Node.js Long Term Support plan](https://github.com/nodejs/Release) and only test against versions that are officially supported and have not reached their end-of-life date.
|
||||
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|------------|----------------|-------------|---------------|
|
||||
| Node.js 12 | 12.22.7 | April 2022 | ✅ Yes |
|
||||
| Node.js 14 | 14.18.1 | April 2023 | ✅ Yes |
|
||||
| Node.js 15 | 15.14.0 | June 2021 | ✅ Yes |
|
||||
| Node.js 16 | 16.x.x | April 2024 | ❌ Not tested |
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|------------|----------------|-------------|------------|
|
||||
| Node.js 12 | 12.22.11 | April 2022 | ✅ Yes |
|
||||
| Node.js 14 | 14.19.1 | April 2023 | ✅ Yes |
|
||||
| Node.js 16 | 16.14.2 | April 2024 | ✅ Yes |
|
||||
| Node.js 17 | 17.9.0 | June 2022 | ✅ Yes |
|
||||
| Node.js 18 | 18.1.0 | April 2025 | ✅ Yes |
|
||||
|
||||
#### MongoDB
|
||||
Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and only test against versions that are officially supported and have not reached their end-of-life date.
|
||||
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|-------------|----------------|--------------|------------|
|
||||
| MongoDB 4.0 | 4.0.27 | April 2022 | ✅ Yes |
|
||||
| MongoDB 4.2 | 4.2.17 | TBD | ✅ Yes |
|
||||
| MongoDB 4.4 | 4.4.10 | TBD | ✅ Yes |
|
||||
| MongoDB 5.0 | 5.0.3 | January 2024 | ✅ Yes |
|
||||
|
||||
Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and [MongoDB lifecycle schedule](https://www.mongodb.com/support-policy/lifecycles) and only test against versions that are officially supported and have not reached their end-of-life date. We consider the end-of-life date of a MongoDB "rapid release" to be the same as its major version release.
|
||||
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|-------------|----------------|---------------|--------------|
|
||||
| MongoDB 4.0 | 4.0.28 | April 2022 | ✅ Yes |
|
||||
| MongoDB 4.2 | 4.2.19 | April 2023 | ✅ Yes |
|
||||
| MongoDB 4.4 | 4.4.13 | February 2024 | ✅ Yes |
|
||||
| MongoDB 5 | 5.3.2 | October 2024 | ✅ Yes |
|
||||
| MongoDB 6 | 6.0.2 | July 2025 | ✅ Yes |
|
||||
|
||||
#### PostgreSQL
|
||||
Parse Server is continuously tested with the most recent releases of PostgreSQL and PostGIS to ensure compatibility, using [PostGIS docker images](https://registry.hub.docker.com/r/postgis/postgis/tags?page=1&ordering=last_updated). We follow the [PostgreSQL support schedule](https://www.postgresql.org/support/versioning) and [PostGIS support schedule](https://www.postgis.net/eol_policy/) and only test against versions that are officially supported and have not reached their end-of-life date. Due to the extensive PostgreSQL support duration of 5 years, Parse Server drops support if a version is older than 3.5 years and a newer version has been available for at least 2.5 years.
|
||||
|
||||
| Version | PostGIS Version | End-of-Life | Parse Server Support End | Compatible |
|
||||
|-------------|-----------------|---------------|--------------------------|------------|
|
||||
| Postgres 11 | 3.0, 3.1 | November 2023 | April 2022 | ✅ Yes |
|
||||
| Postgres 12 | 3.1 | November 2024 | April 2023 | ✅ Yes |
|
||||
| Postgres 13 | 3.1 | November 2025 | April 2024 | ✅ Yes |
|
||||
| Postgres 14 | 3.1 | November 2026 | April 2025 | ✅ Yes |
|
||||
Parse Server is continuously tested with the most recent releases of PostgreSQL and PostGIS to ensure compatibility, using [PostGIS docker images](https://registry.hub.docker.com/r/postgis/postgis/tags?page=1&ordering=last_updated). We follow the [PostgreSQL support schedule](https://www.postgresql.org/support/versioning) and [PostGIS support schedule](https://www.postgis.net/eol_policy/) and only test against versions that are officially supported and have not reached their end-of-life date. Due to the extensive PostgreSQL support duration of 5 years, Parse Server drops support about 2 years before the official end-of-life date.
|
||||
|
||||
| Version | PostGIS Version | End-of-Life | Parse Server Support | Compatible |
|
||||
|-------------|--------------------|---------------|----------------------|------------|
|
||||
| Postgres 11 | 3.0, 3.1, 3.2, 3.3 | November 2023 | <= 5.x (2022) | ✅ Yes |
|
||||
| Postgres 12 | 3.3 | November 2024 | <= 5.x (2022) | ✅ Yes |
|
||||
| Postgres 13 | 3.3 | November 2025 | <= 6.x (2023) | ✅ Yes |
|
||||
| Postgres 14 | 3.3 | November 2026 | <= 7.x (2024) | ✅ Yes |
|
||||
| Postgres 15 | 3.3 | November 2027 | <= 8.x (2025) | ✅ Yes |
|
||||
|
||||
### Locally
|
||||
|
||||
```bash
|
||||
$ npm install -g parse-server mongodb-runner
|
||||
$ mongodb-runner start
|
||||
@@ -229,7 +250,6 @@ $ curl -X GET \
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](http://docs.parseplatform.org).
|
||||
@@ -335,8 +355,10 @@ const server = ParseServer({
|
||||
});
|
||||
```
|
||||
|
||||
Offical email adapters maintained by Parse Platform:
|
||||
- [parse-server-api-mail-adapter](https://github.com/parse-community/parse-server-api-mail-adapter) (localization, templates, universally supports any email provider)
|
||||
|
||||
Email adapters contributed by the community:
|
||||
- [parse-server-api-mail-adapter](https://www.npmjs.com/package/parse-server-api-mail-adapter) (localization, templates, universally supports any email provider)
|
||||
- [parse-smtp-template](https://www.npmjs.com/package/parse-smtp-template) (localization, templates)
|
||||
- [parse-server-postmark-adapter](https://www.npmjs.com/package/parse-server-postmark-adapter)
|
||||
- [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter)
|
||||
@@ -379,6 +401,7 @@ const server = ParseServer({
|
||||
```
|
||||
|
||||
## Custom Routes
|
||||
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
Custom routes allow to build user flows with webpages, similar to the existing password reset and email verification features. Custom routes are defined with the `pages` option in the Parse Server configuration:
|
||||
@@ -411,6 +434,7 @@ The above route can be invoked by sending a `GET` request to:
|
||||
The `handler` receives the `request` and returns a `custom_page.html` webpage from the `pages.pagesPath` directory as response. The advantage of building a custom route this way is that it automatically makes use of Parse Server's built-in capabilities, such as [page localization](#pages) and [dynamic placeholders](#dynamic-placeholders).
|
||||
|
||||
### Reserved Paths
|
||||
|
||||
The following paths are already used by Parse Server's built-in features and are therefore not available for custom routes. Custom routes with an identical combination of `path` and `method` are ignored.
|
||||
|
||||
| Path | HTTP Method | Feature |
|
||||
@@ -486,11 +510,12 @@ You can also find more adapters maintained by the community by searching on [npm
|
||||
|
||||
Parse Server allows developers to choose from several options when hosting files:
|
||||
|
||||
* `GridFSBucketAdapter`, which is backed by MongoDB;
|
||||
* `S3Adapter`, which is backed by [Amazon S3](https://aws.amazon.com/s3/); or
|
||||
* `GCSAdapter`, which is backed by [Google Cloud Storage](https://cloud.google.com/storage/)
|
||||
* `GridFSBucketAdapter` - which is backed by MongoDB
|
||||
* `S3Adapter` - which is backed by [Amazon S3](https://aws.amazon.com/s3/)
|
||||
* `GCSAdapter` - which is backed by [Google Cloud Storage](https://cloud.google.com/storage/)
|
||||
* `FSAdapter` - local file storage
|
||||
|
||||
`GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters).
|
||||
`GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using Amazon S3, Google Cloud Storage, or local file storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters).
|
||||
|
||||
## Idempotency Enforcement
|
||||
|
||||
@@ -505,6 +530,7 @@ Identical requests are identified by their request header `X-Parse-Request-Id`.
|
||||
Deduplication is only done for object creation and update (`POST` and `PUT` requests). Deduplication is not done for object finding and deletion (`GET` and `DELETE` requests), as these operations are already idempotent by definition.
|
||||
|
||||
### Configuration example <!-- omit in toc -->
|
||||
|
||||
```
|
||||
let api = new ParseServer({
|
||||
idempotencyOptions: {
|
||||
@@ -513,6 +539,7 @@ let api = new ParseServer({
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters <!-- omit in toc -->
|
||||
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
@@ -521,13 +548,31 @@ let api = new ParseServer({
|
||||
| `idempotencyOptions.paths` | yes | `Array<String>` | `[]` | `.*` (all paths, includes the examples below), <br>`functions/.*` (all functions), <br>`jobs/.*` (all jobs), <br>`classes/.*` (all classes), <br>`functions/.*` (all functions), <br>`users` (user creation / update), <br>`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specify the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. |
|
||||
| `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. |
|
||||
|
||||
### Notes <!-- omit in toc -->
|
||||
### Postgres <!-- omit in toc -->
|
||||
|
||||
- This feature is currently only available for MongoDB and not for Postgres.
|
||||
To use this feature in Postgres, you will need to create a cron job using [pgAdmin](https://www.pgadmin.org/docs/pgadmin4/development/pgagent_jobs.html) or similar to call the Postgres function `idempotency_delete_expired_records()` that deletes expired idempotency records. You can find an example script below. Make sure the script has the same privileges to log into Postgres as Parse Server.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
SELECT idempotency_delete_expired_records();
|
||||
EOSQL
|
||||
|
||||
exec "$@"
|
||||
```
|
||||
|
||||
Assuming the script above is named, `parse_idempotency_delete_expired_records.sh`, a cron job that runs the script every 2 minutes may look like:
|
||||
|
||||
```bash
|
||||
2 * * * * /root/parse_idempotency_delete_expired_records.sh >/dev/null 2>&1
|
||||
```
|
||||
|
||||
## Localization
|
||||
|
||||
### Pages
|
||||
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
Custom pages as well as feature pages (e.g. password reset, email verification) can be localized with the `pages` option in the Parse Server configuration:
|
||||
@@ -1071,37 +1116,15 @@ You also have a very powerful tool inside your GraphQL Playground. Please look a
|
||||
|
||||
Additionally, the [GraphQL Learn Section](https://graphql.org/learn/) is a very good source to learn more about the power of the GraphQL language.
|
||||
|
||||
# Upgrading to 3.0.0
|
||||
# Upgrading to Parse Server 3.0
|
||||
|
||||
Starting 3.0.0, parse-server uses the JS SDK version 2.0.
|
||||
In short, parse SDK v2.0 removes the backbone style callbacks as well as the Parse.Promise object in favor of native promises.
|
||||
All the Cloud Code interfaces also have been updated to reflect those changes, and all backbone style response objects are removed and replaced by Promise style resolution.
|
||||
Starting Parse Server 3.0, Parse Server uses the Parse JavaScript SDK 2.0. In short, the Parse JavaScript SDK 2.0 removes the backbone style callbacks as well as the `Parse.Promise` object in favor of native promises. All the Cloud Code interfaces also have been updated to reflect those changes, and all backbone style response objects are removed and replaced by promise style resolution.
|
||||
|
||||
We have written up a [migration guide](3.0.0.md), hoping this will help you transition to the next major release.
|
||||
|
||||
# Want to ride the bleeding edge?
|
||||
|
||||
It is recommend to use builds deployed npm for many reasons, but if you want to use
|
||||
the latest not-yet-released version of parse-server, you can do so by depending
|
||||
directly on this branch:
|
||||
|
||||
```
|
||||
npm install parse-community/parse-server.git#master
|
||||
```
|
||||
|
||||
## Experimenting <!-- omit in toc -->
|
||||
|
||||
You can also use your own forks, and work in progress branches by specifying them:
|
||||
|
||||
```
|
||||
npm install github:myUsername/parse-server#my-awesome-feature
|
||||
```
|
||||
|
||||
And don't forget, if you plan to deploy it remotely, you should run `npm install` with the `--save` option.
|
||||
We have written up a [migration guide](3.0.0.md) to help you transition to the next major release.
|
||||
|
||||
# Contributing
|
||||
|
||||
We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md).
|
||||
Please see the [Contributing Guide](CONTRIBUTING.md).
|
||||
|
||||
# Contributors
|
||||
|
||||
@@ -1157,3 +1180,6 @@ As of April 5, 2017, Parse, LLC has transferred this code to the parse-community
|
||||
[license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg
|
||||
[license-link]: LICENSE
|
||||
[open-collective-link]: https://opencollective.com/parse-server
|
||||
[log_release]: https://github.com/parse-community/parse-server/blob/release/changelogs/CHANGELOG_release.md
|
||||
[log_beta]: https://github.com/parse-community/parse-server/blob/beta/changelogs/CHANGELOG_beta.md
|
||||
[log_alpha]: https://github.com/parse-community/parse-server/blob/alpha/changelogs/CHANGELOG_alpha.md
|
||||
|
||||
@@ -1,3 +1,459 @@
|
||||
# [5.3.0-alpha.32](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.31...5.3.0-alpha.32) (2022-10-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add convenience access to Parse Server configuration in Cloud Code via `Parse.Server` ([#8244](https://github.com/parse-community/parse-server/issues/8244)) ([9f11115](https://github.com/parse-community/parse-server/commit/9f111158edf7fd57a65db0c4f9244b37e58cf293))
|
||||
|
||||
# [5.3.0-alpha.31](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.30...5.3.0-alpha.31) (2022-10-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* relation constraints in compound queries `Parse.Query.or`, `Parse.Query.and` not working ([#8203](https://github.com/parse-community/parse-server/issues/8203)) ([28f0d26](https://github.com/parse-community/parse-server/commit/28f0d2667787d2ac68726607b811d6f0ef62b9f1))
|
||||
|
||||
# [5.3.0-alpha.30](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.29...5.3.0-alpha.30) (2022-10-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for MongoDB 6 ([#8242](https://github.com/parse-community/parse-server/issues/8242)) ([aba0081](https://github.com/parse-community/parse-server/commit/aba0081ce1a166a93de57f3928c19a05562b5cc1))
|
||||
|
||||
# [5.3.0-alpha.29](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.28...5.3.0-alpha.29) (2022-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) [skip release] ([#8238](https://github.com/parse-community/parse-server/issues/8238)) ([c03908f](https://github.com/parse-community/parse-server/commit/c03908f74e5c9eed834874a89df6c89c1a1e849f))
|
||||
|
||||
### Features
|
||||
|
||||
* add support for Postgres 15 ([#8215](https://github.com/parse-community/parse-server/issues/8215)) ([2feb6c4](https://github.com/parse-community/parse-server/commit/2feb6c46080946c984daa351187fa07cd582355d))
|
||||
|
||||
# [5.3.0-alpha.28](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.27...5.3.0-alpha.28) (2022-10-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* liveQuery support for unsorted distance queries ([#8221](https://github.com/parse-community/parse-server/issues/8221)) ([0f763da](https://github.com/parse-community/parse-server/commit/0f763da17d646b2fec2cd980d3857e46072a8a07))
|
||||
|
||||
# [5.3.0-alpha.27](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.26...5.3.0-alpha.27) (2022-09-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) [skip release] ([#8187](https://github.com/parse-community/parse-server/issues/8187)) ([8c8ec71](https://github.com/parse-community/parse-server/commit/8c8ec715739e0f851338cfed794409ebac66c51b))
|
||||
* session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) [skip release] ([#8180](https://github.com/parse-community/parse-server/issues/8180)) ([37fed30](https://github.com/parse-community/parse-server/commit/37fed3062ccc3ef1dfd49a9fc53318e72b3e4aff))
|
||||
|
||||
### Features
|
||||
|
||||
* add option to change the default value of the `Parse.Query.limit()` constraint ([#8152](https://github.com/parse-community/parse-server/issues/8152)) ([0388956](https://github.com/parse-community/parse-server/commit/038895680894984e569dff54bf5c7b31094f3891))
|
||||
|
||||
# [5.3.0-alpha.26](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.25...5.3.0-alpha.26) (2022-09-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* sorting by non-existing value throws `INVALID_SERVER_ERROR` on Postgres ([#8157](https://github.com/parse-community/parse-server/issues/8157)) ([3b775a1](https://github.com/parse-community/parse-server/commit/3b775a1fb8a1878714e3451191438963d688f1b0))
|
||||
|
||||
# [5.3.0-alpha.25](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.24...5.3.0-alpha.25) (2022-09-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* updating object includes unchanged keys in client response for certain key types ([#8159](https://github.com/parse-community/parse-server/issues/8159)) ([37af1d7](https://github.com/parse-community/parse-server/commit/37af1d78fce5a15039ffe3af7b323c1f1e8582fc))
|
||||
|
||||
# [5.3.0-alpha.24](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.23...5.3.0-alpha.24) (2022-09-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* query aggregation pipeline cannot handle value of type `Date` when `directAccess: true` ([#8167](https://github.com/parse-community/parse-server/issues/8167)) ([e424137](https://github.com/parse-community/parse-server/commit/e4241374061caef66538de15112fb6bbafb1f5bb))
|
||||
|
||||
# [5.3.0-alpha.23](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.22...5.3.0-alpha.23) (2022-09-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* liveQuery with `containedIn` not working when object field is an array ([#8128](https://github.com/parse-community/parse-server/issues/8128)) ([1d9605b](https://github.com/parse-community/parse-server/commit/1d9605bc93009263d3811df4d4249034ba6eb8c4))
|
||||
|
||||
# [5.3.0-alpha.22](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.21...5.3.0-alpha.22) (2022-09-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* brute force guessing of user sensitive data via search patterns (GHSA-2m6g-crv8-p3c6) ([#8146](https://github.com/parse-community/parse-server/issues/8146)) [skip release] ([4c0c7c7](https://github.com/parse-community/parse-server/commit/4c0c7c77b76257878b9bcb05ff9de01c9d790262))
|
||||
* push notifications `badge` doesn't update with Installation beforeSave trigger ([#8162](https://github.com/parse-community/parse-server/issues/8162)) ([3c75c2b](https://github.com/parse-community/parse-server/commit/3c75c2ba4851fae96a8c19b11a3efde03816c9a1))
|
||||
|
||||
# [5.3.0-alpha.21](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.20...5.3.0-alpha.21) (2022-08-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* internal indices for classes `_Idempotency` and `_Role` are not protected in defined schema ([#8121](https://github.com/parse-community/parse-server/issues/8121)) ([c16f529](https://github.com/parse-community/parse-server/commit/c16f529f74f92154401bf662f634b3c5fa45e18e))
|
||||
|
||||
# [5.3.0-alpha.20](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.19...5.3.0-alpha.20) (2022-07-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade undici from 5.6.0 to 5.8.0 ([#8108](https://github.com/parse-community/parse-server/issues/8108)) ([4aa016b](https://github.com/parse-community/parse-server/commit/4aa016b7322467422b9fdf05d8e29b9ecf910da7))
|
||||
|
||||
# [5.3.0-alpha.19](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.18...5.3.0-alpha.19) (2022-07-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* certificate in Apple Game Center auth adapter not validated [skip release] ([#8058](https://github.com/parse-community/parse-server/issues/8058)) ([75af9a2](https://github.com/parse-community/parse-server/commit/75af9a26cc8e9e88a33d1e452c93a0ee6e509f17))
|
||||
* graphQL query ignores condition `equalTo` with value `false` ([#8032](https://github.com/parse-community/parse-server/issues/8032)) ([7f5a15d](https://github.com/parse-community/parse-server/commit/7f5a15d5df0dfa3515e9f73709d6a49663545f9b))
|
||||
* invalid file request not properly handled [skip release] ([#8062](https://github.com/parse-community/parse-server/issues/8062)) ([4c9e956](https://github.com/parse-community/parse-server/commit/4c9e95674ad081f13062e8cd30b77b1962d5df57))
|
||||
* protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [skip release] ([#8076](https://github.com/parse-community/parse-server/issues/8076)) ([9fd4516](https://github.com/parse-community/parse-server/commit/9fd4516cde5c742f9f29dd05468b4a43a85639a6))
|
||||
|
||||
# [5.3.0-alpha.18](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.17...5.3.0-alpha.18) (2022-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* auto-release process may fail if optional back-merging task fails ([#8051](https://github.com/parse-community/parse-server/issues/8051)) ([cf925e7](https://github.com/parse-community/parse-server/commit/cf925e75e87a6989f41e2e2abb2aba4332b1e79f))
|
||||
|
||||
# [5.3.0-alpha.17](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.16...5.3.0-alpha.17) (2022-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* errors in GraphQL do not show the original error but a general `Unexpected Error` ([#8045](https://github.com/parse-community/parse-server/issues/8045)) ([0d81887](https://github.com/parse-community/parse-server/commit/0d818879c217f9c56100a5f59868fa37e6d24b71))
|
||||
* websocket connection of LiveQuery interrupts frequently ([#8048](https://github.com/parse-community/parse-server/issues/8048)) ([03caae1](https://github.com/parse-community/parse-server/commit/03caae1e611f28079cdddbbe433daaf69e3f595c))
|
||||
|
||||
# [5.3.0-alpha.16](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.15...5.3.0-alpha.16) (2022-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* live query role cache does not clear when a user is added to a role ([#8026](https://github.com/parse-community/parse-server/issues/8026)) ([199dfc1](https://github.com/parse-community/parse-server/commit/199dfc17226d85a78ab85f24362cce740f4ada39))
|
||||
|
||||
# [5.3.0-alpha.15](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.14...5.3.0-alpha.15) (2022-06-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* interrupted WebSocket connection not closed by LiveQuery server ([#8012](https://github.com/parse-community/parse-server/issues/8012)) ([2d5221e](https://github.com/parse-community/parse-server/commit/2d5221e48012fb7781c0406d543a922d313075ea))
|
||||
|
||||
# [5.3.0-alpha.14](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.13...5.3.0-alpha.14) (2022-05-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* align file trigger syntax with class trigger; use the new syntax `Parse.Cloud.beforeSave(Parse.File, (request) => {})`, the old syntax `Parse.Cloud.beforeSaveFile((request) => {})` has been deprecated ([#7966](https://github.com/parse-community/parse-server/issues/7966)) ([c6dcad8](https://github.com/parse-community/parse-server/commit/c6dcad8d167d44912dbd416d328519314c0809bd))
|
||||
|
||||
# [5.3.0-alpha.13](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.12...5.3.0-alpha.13) (2022-05-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* selectively enable / disable default authentication adapters ([#7953](https://github.com/parse-community/parse-server/issues/7953)) ([c1e808f](https://github.com/parse-community/parse-server/commit/c1e808f9e807fc49508acbde0d8b3f2b901a1638))
|
||||
|
||||
# [5.3.0-alpha.12](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.11...5.3.0-alpha.12) (2022-05-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* afterSave trigger removes pointer in Parse object ([#7913](https://github.com/parse-community/parse-server/issues/7913)) ([47d796e](https://github.com/parse-community/parse-server/commit/47d796ea58f65e71612ce37149be692abc9ea97f))
|
||||
|
||||
# [5.3.0-alpha.11](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.10...5.3.0-alpha.11) (2022-05-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* replace GraphQL Apollo with GraphQL Yoga ([#7967](https://github.com/parse-community/parse-server/issues/7967)) ([1aa2204](https://github.com/parse-community/parse-server/commit/1aa2204aebfdbe273d54d6d56c6029f7c34aab14))
|
||||
|
||||
# [5.3.0-alpha.10](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.9...5.3.0-alpha.10) (2022-05-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* upgrade mongodb from 4.4.1 to 4.5.0 ([#7991](https://github.com/parse-community/parse-server/issues/7991)) ([e692b5d](https://github.com/parse-community/parse-server/commit/e692b5dd8214cdb0ce79bedd30d9aa3cf4de76a5))
|
||||
|
||||
# [5.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.8...5.3.0-alpha.9) (2022-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* depreciate allowClientClassCreation defaulting to true ([#7925](https://github.com/parse-community/parse-server/issues/7925)) ([38ed96a](https://github.com/parse-community/parse-server/commit/38ed96ace534d639db007aa7dd5387b2da8f03ae))
|
||||
|
||||
# [5.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.7...5.3.0-alpha.8) (2022-05-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for Node 17 and 18 ([#7896](https://github.com/parse-community/parse-server/issues/7896)) ([3e9f292](https://github.com/parse-community/parse-server/commit/3e9f292d840334244934cee9a34545ac86313549))
|
||||
|
||||
# [5.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.6...5.3.0-alpha.7) (2022-04-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade @parse/fs-files-adapter from 1.2.1 to 1.2.2 ([#7948](https://github.com/parse-community/parse-server/issues/7948)) ([20fc4e2](https://github.com/parse-community/parse-server/commit/20fc4e23b53c91aac657f894bd70d049b7525c37))
|
||||
|
||||
# [5.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.5...5.3.0-alpha.6) (2022-04-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* peer dependency mismatch for GraphQL dependencies ([#7934](https://github.com/parse-community/parse-server/issues/7934)) ([b7a1d76](https://github.com/parse-community/parse-server/commit/b7a1d7617b4bcac677cecedfeb6ac4a27447083b))
|
||||
|
||||
# [5.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.4...5.3.0-alpha.5) (2022-04-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade moment from 2.29.1 to 2.29.2 ([#7931](https://github.com/parse-community/parse-server/issues/7931)) ([6b68593](https://github.com/parse-community/parse-server/commit/6b68593eaec17e8b183899d2b92699c9ede7625b))
|
||||
|
||||
# [5.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.3...5.3.0-alpha.4) (2022-04-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* custom database options are not passed to MongoDB GridFS ([#7911](https://github.com/parse-community/parse-server/issues/7911)) ([a72b384](https://github.com/parse-community/parse-server/commit/a72b384f76137a3d83ffb69f65cb25aff1bbab4f))
|
||||
|
||||
# [5.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.2...5.3.0-alpha.3) (2022-03-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add MongoDB 5.2 support ([#7894](https://github.com/parse-community/parse-server/issues/7894)) ([6b4b358](https://github.com/parse-community/parse-server/commit/6b4b358f0842ae920e45652f5e8b2afebc6caf3a))
|
||||
|
||||
# [5.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.1...5.3.0-alpha.2) (2022-03-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade parse push adapter from 4.1.0 to 4.1.2 ([#7893](https://github.com/parse-community/parse-server/issues/7893)) ([ef56e98](https://github.com/parse-community/parse-server/commit/ef56e98ef65041b4d3b7b82cce3473269c27f6fd))
|
||||
|
||||
# [5.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/5.2.1-alpha.2...5.3.0-alpha.1) (2022-03-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add MongoDB 5.1 compatibility ([#7682](https://github.com/parse-community/parse-server/issues/7682)) ([90155cf](https://github.com/parse-community/parse-server/commit/90155cf1680e5e0499b0000e071c6cb0ce3aef96))
|
||||
|
||||
## [5.2.1-alpha.2](https://github.com/parse-community/parse-server/compare/5.2.1-alpha.1...5.2.1-alpha.2) (2022-03-26)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* reduce database operations when using the constant parameter in Cloud Function validation ([#7892](https://github.com/parse-community/parse-server/issues/7892)) ([48bd512](https://github.com/parse-community/parse-server/commit/48bd512eeb47666967dff8c5e723ddc5b7801daa))
|
||||
|
||||
## [5.2.1-alpha.1](https://github.com/parse-community/parse-server/compare/5.2.0...5.2.1-alpha.1) (2022-03-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* return correct response when revert is used in beforeSave ([#7839](https://github.com/parse-community/parse-server/issues/7839)) ([f63fb2b](https://github.com/parse-community/parse-server/commit/f63fb2b338c908f0e7a648d338c26b9daa50c8f2))
|
||||
|
||||
# [5.2.0-alpha.3](https://github.com/parse-community/parse-server/compare/5.2.0-alpha.2...5.2.0-alpha.3) (2022-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security bump minimist from 1.2.5 to 1.2.6 ([#7884](https://github.com/parse-community/parse-server/issues/7884)) ([c5cf282](https://github.com/parse-community/parse-server/commit/c5cf282d11ffdc023764f8e7539a2bd6bc246fe1))
|
||||
|
||||
# [5.2.0-alpha.2](https://github.com/parse-community/parse-server/compare/5.2.0-alpha.1...5.2.0-alpha.2) (2022-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* sensitive keyword detection may produce false positives ([#7881](https://github.com/parse-community/parse-server/issues/7881)) ([0d6f9e9](https://github.com/parse-community/parse-server/commit/0d6f9e951d9e186e95e96d8869066ce7022bad02))
|
||||
|
||||
# [5.2.0-alpha.1](https://github.com/parse-community/parse-server/compare/5.1.1...5.2.0-alpha.1) (2022-03-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improved LiveQuery error logging with additional information ([#7837](https://github.com/parse-community/parse-server/issues/7837)) ([443a509](https://github.com/parse-community/parse-server/commit/443a5099059538d379fe491793a5871fcbb4f377))
|
||||
|
||||
# [5.0.0-alpha.29](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.28...5.0.0-alpha.29) (2022-03-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* bump required node engine to >=12.22.10 ([#7846](https://github.com/parse-community/parse-server/issues/7846)) ([5ace99d](https://github.com/parse-community/parse-server/commit/5ace99d542a11e422af46d9fd6b1d3d2513b34cf))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This requires Node.js version >=12.22.10. ([5ace99d](5ace99d))
|
||||
|
||||
# [5.0.0-alpha.28](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.27...5.0.0-alpha.28) (2022-03-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security vulnerability that allows remote code execution (GHSA-p6h4-93qp-jhcm) ([#7844](https://github.com/parse-community/parse-server/issues/7844)) ([e569f40](https://github.com/parse-community/parse-server/commit/e569f402b1fd8648fb0d1523b71b2a03273902a5))
|
||||
|
||||
# [5.0.0-alpha.27](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.26...5.0.0-alpha.27) (2022-03-12)
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* update node engine to 2.22.0 ([#7827](https://github.com/parse-community/parse-server/issues/7827)) ([f235412](https://github.com/parse-community/parse-server/commit/f235412c1b6c2b173b7531f285429ea7214b56a2))
|
||||
|
||||
# [5.0.0-alpha.26](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.25...5.0.0-alpha.26) (2022-02-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* package.json & package-lock.json to reduce vulnerabilities ([#7823](https://github.com/parse-community/parse-server/issues/7823)) ([5ca2288](https://github.com/parse-community/parse-server/commit/5ca228882332b65f3ac05407e6e4da1ee3ef3749))
|
||||
|
||||
# [5.0.0-alpha.25](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.24...5.0.0-alpha.25) (2022-02-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* upgrade winston from 3.5.0 to 3.5.1 ([#7820](https://github.com/parse-community/parse-server/issues/7820)) ([4af253d](https://github.com/parse-community/parse-server/commit/4af253d1f8654a6f57b5137ad310cdacadc922cc))
|
||||
|
||||
# [5.0.0-alpha.24](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.23...5.0.0-alpha.24) (2022-02-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade follow-redirects from 1.14.7 to 1.14.8 ([#7801](https://github.com/parse-community/parse-server/issues/7801)) ([70088a9](https://github.com/parse-community/parse-server/commit/70088a95a78393da2a4ac68be81e63107747626a))
|
||||
|
||||
# [5.0.0-alpha.23](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.22...5.0.0-alpha.23) (2022-02-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* server crash using GraphQL due to missing @apollo/client peer dependency ([#7787](https://github.com/parse-community/parse-server/issues/7787)) ([08089d6](https://github.com/parse-community/parse-server/commit/08089d6fcbb215412448ce7d92b21b9fe6c929f2))
|
||||
|
||||
# [5.0.0-alpha.22](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.21...5.0.0-alpha.22) (2022-02-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* upgrade to MongoDB Node.js driver 4.x for MongoDB 5.0 support ([#7794](https://github.com/parse-community/parse-server/issues/7794)) ([f88aa2a](https://github.com/parse-community/parse-server/commit/f88aa2a62a533e5344d1c13dd38c5a0b283a480a))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The MongoDB GridStore adapter has been removed. By default, Parse Server already uses GridFS, so if you do not manually use the GridStore adapter, you can ignore this change. ([f88aa2a](f88aa2a))
|
||||
|
||||
# [5.0.0-alpha.21](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.20...5.0.0-alpha.21) (2022-01-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Cloud Code context to `ParseObject.fetch` ([#7779](https://github.com/parse-community/parse-server/issues/7779)) ([315290d](https://github.com/parse-community/parse-server/commit/315290d16110110938f80a6b779cc2d1db58c552))
|
||||
|
||||
# [5.0.0-alpha.20](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.19...5.0.0-alpha.20) (2022-01-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump node-fetch from 2.6.1 to 3.1.1 ([#7782](https://github.com/parse-community/parse-server/issues/7782)) ([9082351](https://github.com/parse-community/parse-server/commit/90823514113a1a085ebc818f7109b3fd7591346f))
|
||||
|
||||
# [5.0.0-alpha.19](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.18...5.0.0-alpha.19) (2022-01-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump nanoid from 3.1.25 to 3.2.0 ([#7781](https://github.com/parse-community/parse-server/issues/7781)) ([f5f63bf](https://github.com/parse-community/parse-server/commit/f5f63bfc64d3481ed944ceb5e9f50b33dccd1ce9))
|
||||
|
||||
# [5.0.0-alpha.18](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.17...5.0.0-alpha.18) (2022-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade follow-redirects from 1.14.6 to 1.14.7 ([#7769](https://github.com/parse-community/parse-server/issues/7769)) ([8f5a861](https://github.com/parse-community/parse-server/commit/8f5a8618cfa7ed9a2a239a095abffa8f3fd8d31a))
|
||||
|
||||
# [5.0.0-alpha.17](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.16...5.0.0-alpha.17) (2022-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* schema cache not cleared in some cases ([#7678](https://github.com/parse-community/parse-server/issues/7678)) ([5af6e5d](https://github.com/parse-community/parse-server/commit/5af6e5dfaa129b1a350afcba4fb381b21c4cc35d))
|
||||
|
||||
# [5.0.0-alpha.16](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.15...5.0.0-alpha.16) (2022-01-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Idempotency to Postgres ([#7750](https://github.com/parse-community/parse-server/issues/7750)) ([0c3feaa](https://github.com/parse-community/parse-server/commit/0c3feaaa1751964c0db89f25674935c3354b1538))
|
||||
|
||||
# [5.0.0-alpha.15](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.14...5.0.0-alpha.15) (2022-01-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support `postgresql` protocol in database URI ([#7757](https://github.com/parse-community/parse-server/issues/7757)) ([caf4a23](https://github.com/parse-community/parse-server/commit/caf4a2341f554b28e3918c53e7e897a3ca47bf8b))
|
||||
|
||||
# [5.0.0-alpha.14](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.13...5.0.0-alpha.14) (2022-01-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support relativeTime query constraint on Postgres ([#7747](https://github.com/parse-community/parse-server/issues/7747)) ([16b1b2a](https://github.com/parse-community/parse-server/commit/16b1b2a19714535ca805f2dbb3b561d8f6a519a7))
|
||||
|
||||
# [5.0.0-alpha.13](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.12...5.0.0-alpha.13) (2021-12-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* node engine compatibility did not include node 16 ([#7739](https://github.com/parse-community/parse-server/issues/7739)) ([ea7c014](https://github.com/parse-community/parse-server/commit/ea7c01400f992a1263543706fe49b6174758a2d6))
|
||||
|
||||
# [5.0.0-alpha.12](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.11...5.0.0-alpha.12) (2021-12-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding or modifying a nested property requires addField permissions ([#7679](https://github.com/parse-community/parse-server/issues/7679)) ([6a6248b](https://github.com/parse-community/parse-server/commit/6a6248b6cb2e732d17131e18e659943b894ed2f1))
|
||||
|
||||
# [5.0.0-alpha.11](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.10...5.0.0-alpha.11) (2021-11-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* upgrade mime from 2.5.2 to 3.0.0 ([#7725](https://github.com/parse-community/parse-server/issues/7725)) ([f5ef98b](https://github.com/parse-community/parse-server/commit/f5ef98bde32083403c0e30a12162fcc1e52cac37))
|
||||
|
||||
# [5.0.0-alpha.10](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.9...5.0.0-alpha.10) (2021-11-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* upgrade parse from 3.3.1 to 3.4.0 ([#7723](https://github.com/parse-community/parse-server/issues/7723)) ([d4c1f47](https://github.com/parse-community/parse-server/commit/d4c1f473073764cb0570c633fc4a30669c2ce889))
|
||||
|
||||
# [5.0.0-alpha.9](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.8...5.0.0-alpha.9) (2021-11-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* unable to use objectId size higher than 19 on GraphQL API ([#7627](https://github.com/parse-community/parse-server/issues/7627)) ([ed86c80](https://github.com/parse-community/parse-server/commit/ed86c807721cc52a1a5a9dea0b768717eec269ed))
|
||||
|
||||
# [5.0.0-alpha.8](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.7...5.0.0-alpha.8) (2021-11-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for Node 16 ([#7707](https://github.com/parse-community/parse-server/issues/7707)) ([45cc58c](https://github.com/parse-community/parse-server/commit/45cc58c7e5e640a46c5d508019a3aa81242964b1))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Removes official Node 15 support which has reached it end-of-life date. ([45cc58c](45cc58c))
|
||||
|
||||
# [5.0.0-alpha.7](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.6...5.0.0-alpha.7) (2021-11-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* node engine range has no upper limit to exclude incompatible node versions ([#7692](https://github.com/parse-community/parse-server/issues/7692)) ([573558d](https://github.com/parse-community/parse-server/commit/573558d3adcbcc6222c92003829867e1a73eef94))
|
||||
|
||||
# [5.0.0-alpha.6](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.5...5.0.0-alpha.6) (2021-11-10)
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* refactor: allow ES import for cloud string if package type is module ([b64640c](https://github.com/parse-community/parse-server/commit/b64640c5705f733798783e68d216e957044ef23c))
|
||||
|
||||
# [5.0.0-alpha.5](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.4...5.0.0-alpha.5) (2021-11-01)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,124 @@
|
||||
# [5.4.0-beta.1](https://github.com/parse-community/parse-server/compare/5.3.0...5.4.0-beta.1) (2022-10-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) [skip release] ([#8187](https://github.com/parse-community/parse-server/issues/8187)) ([8c8ec71](https://github.com/parse-community/parse-server/commit/8c8ec715739e0f851338cfed794409ebac66c51b))
|
||||
* brute force guessing of user sensitive data via search patterns (GHSA-2m6g-crv8-p3c6) ([#8146](https://github.com/parse-community/parse-server/issues/8146)) [skip release] ([4c0c7c7](https://github.com/parse-community/parse-server/commit/4c0c7c77b76257878b9bcb05ff9de01c9d790262))
|
||||
* certificate in Apple Game Center auth adapter not validated [skip release] ([#8058](https://github.com/parse-community/parse-server/issues/8058)) ([75af9a2](https://github.com/parse-community/parse-server/commit/75af9a26cc8e9e88a33d1e452c93a0ee6e509f17))
|
||||
* graphQL query ignores condition `equalTo` with value `false` ([#8032](https://github.com/parse-community/parse-server/issues/8032)) ([7f5a15d](https://github.com/parse-community/parse-server/commit/7f5a15d5df0dfa3515e9f73709d6a49663545f9b))
|
||||
* internal indices for classes `_Idempotency` and `_Role` are not protected in defined schema ([#8121](https://github.com/parse-community/parse-server/issues/8121)) ([c16f529](https://github.com/parse-community/parse-server/commit/c16f529f74f92154401bf662f634b3c5fa45e18e))
|
||||
* invalid file request not properly handled [skip release] ([#8062](https://github.com/parse-community/parse-server/issues/8062)) ([4c9e956](https://github.com/parse-community/parse-server/commit/4c9e95674ad081f13062e8cd30b77b1962d5df57))
|
||||
* liveQuery with `containedIn` not working when object field is an array ([#8128](https://github.com/parse-community/parse-server/issues/8128)) ([1d9605b](https://github.com/parse-community/parse-server/commit/1d9605bc93009263d3811df4d4249034ba6eb8c4))
|
||||
* protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [skip release] ([#8076](https://github.com/parse-community/parse-server/issues/8076)) ([9fd4516](https://github.com/parse-community/parse-server/commit/9fd4516cde5c742f9f29dd05468b4a43a85639a6))
|
||||
* push notifications `badge` doesn't update with Installation beforeSave trigger ([#8162](https://github.com/parse-community/parse-server/issues/8162)) ([3c75c2b](https://github.com/parse-community/parse-server/commit/3c75c2ba4851fae96a8c19b11a3efde03816c9a1))
|
||||
* query aggregation pipeline cannot handle value of type `Date` when `directAccess: true` ([#8167](https://github.com/parse-community/parse-server/issues/8167)) ([e424137](https://github.com/parse-community/parse-server/commit/e4241374061caef66538de15112fb6bbafb1f5bb))
|
||||
* relation constraints in compound queries `Parse.Query.or`, `Parse.Query.and` not working ([#8203](https://github.com/parse-community/parse-server/issues/8203)) ([28f0d26](https://github.com/parse-community/parse-server/commit/28f0d2667787d2ac68726607b811d6f0ef62b9f1))
|
||||
* security upgrade undici from 5.6.0 to 5.8.0 ([#8108](https://github.com/parse-community/parse-server/issues/8108)) ([4aa016b](https://github.com/parse-community/parse-server/commit/4aa016b7322467422b9fdf05d8e29b9ecf910da7))
|
||||
* server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) [skip release] ([#8238](https://github.com/parse-community/parse-server/issues/8238)) ([c03908f](https://github.com/parse-community/parse-server/commit/c03908f74e5c9eed834874a89df6c89c1a1e849f))
|
||||
* session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) [skip release] ([#8180](https://github.com/parse-community/parse-server/issues/8180)) ([37fed30](https://github.com/parse-community/parse-server/commit/37fed3062ccc3ef1dfd49a9fc53318e72b3e4aff))
|
||||
* sorting by non-existing value throws `INVALID_SERVER_ERROR` on Postgres ([#8157](https://github.com/parse-community/parse-server/issues/8157)) ([3b775a1](https://github.com/parse-community/parse-server/commit/3b775a1fb8a1878714e3451191438963d688f1b0))
|
||||
* updating object includes unchanged keys in client response for certain key types ([#8159](https://github.com/parse-community/parse-server/issues/8159)) ([37af1d7](https://github.com/parse-community/parse-server/commit/37af1d78fce5a15039ffe3af7b323c1f1e8582fc))
|
||||
|
||||
### Features
|
||||
|
||||
* add convenience access to Parse Server configuration in Cloud Code via `Parse.Server` ([#8244](https://github.com/parse-community/parse-server/issues/8244)) ([9f11115](https://github.com/parse-community/parse-server/commit/9f111158edf7fd57a65db0c4f9244b37e58cf293))
|
||||
* add option to change the default value of the `Parse.Query.limit()` constraint ([#8152](https://github.com/parse-community/parse-server/issues/8152)) ([0388956](https://github.com/parse-community/parse-server/commit/038895680894984e569dff54bf5c7b31094f3891))
|
||||
* add support for MongoDB 6 ([#8242](https://github.com/parse-community/parse-server/issues/8242)) ([aba0081](https://github.com/parse-community/parse-server/commit/aba0081ce1a166a93de57f3928c19a05562b5cc1))
|
||||
* add support for Postgres 15 ([#8215](https://github.com/parse-community/parse-server/issues/8215)) ([2feb6c4](https://github.com/parse-community/parse-server/commit/2feb6c46080946c984daa351187fa07cd582355d))
|
||||
* liveQuery support for unsorted distance queries ([#8221](https://github.com/parse-community/parse-server/issues/8221)) ([0f763da](https://github.com/parse-community/parse-server/commit/0f763da17d646b2fec2cd980d3857e46072a8a07))
|
||||
|
||||
# [5.3.0-beta.1](https://github.com/parse-community/parse-server/compare/5.2.1...5.3.0-beta.1) (2022-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* afterSave trigger removes pointer in Parse object ([#7913](https://github.com/parse-community/parse-server/issues/7913)) ([47d796e](https://github.com/parse-community/parse-server/commit/47d796ea58f65e71612ce37149be692abc9ea97f))
|
||||
* auto-release process may fail if optional back-merging task fails ([#8051](https://github.com/parse-community/parse-server/issues/8051)) ([cf925e7](https://github.com/parse-community/parse-server/commit/cf925e75e87a6989f41e2e2abb2aba4332b1e79f))
|
||||
* custom database options are not passed to MongoDB GridFS ([#7911](https://github.com/parse-community/parse-server/issues/7911)) ([b1e5565](https://github.com/parse-community/parse-server/commit/b1e5565b22f2eff229571fe9a9500314bd30965b))
|
||||
* depreciate allowClientClassCreation defaulting to true ([#7925](https://github.com/parse-community/parse-server/issues/7925)) ([38ed96a](https://github.com/parse-community/parse-server/commit/38ed96ace534d639db007aa7dd5387b2da8f03ae))
|
||||
* errors in GraphQL do not show the original error but a general `Unexpected Error` ([#8045](https://github.com/parse-community/parse-server/issues/8045)) ([0d81887](https://github.com/parse-community/parse-server/commit/0d818879c217f9c56100a5f59868fa37e6d24b71))
|
||||
* interrupted WebSocket connection not closed by LiveQuery server ([#8012](https://github.com/parse-community/parse-server/issues/8012)) ([2d5221e](https://github.com/parse-community/parse-server/commit/2d5221e48012fb7781c0406d543a922d313075ea))
|
||||
* live query role cache does not clear when a user is added to a role ([#8026](https://github.com/parse-community/parse-server/issues/8026)) ([199dfc1](https://github.com/parse-community/parse-server/commit/199dfc17226d85a78ab85f24362cce740f4ada39))
|
||||
* peer dependency mismatch for GraphQL dependencies ([#7934](https://github.com/parse-community/parse-server/issues/7934)) ([0a6faa8](https://github.com/parse-community/parse-server/commit/0a6faa81fa97f8620e7fd05e8c7bbdb4b7da9578))
|
||||
* return correct response when revert is used in beforeSave ([#7839](https://github.com/parse-community/parse-server/issues/7839)) ([19900fc](https://github.com/parse-community/parse-server/commit/19900fcdf8c9f29a674fb62cf6e4b3341d796891))
|
||||
* security upgrade @parse/fs-files-adapter from 1.2.1 to 1.2.2 ([#7948](https://github.com/parse-community/parse-server/issues/7948)) ([3a70fda](https://github.com/parse-community/parse-server/commit/3a70fda6798d4143f21046439b5eaf232a31bdb6))
|
||||
* security upgrade moment from 2.29.1 to 2.29.2 ([#7931](https://github.com/parse-community/parse-server/issues/7931)) ([731c550](https://github.com/parse-community/parse-server/commit/731c5507144bbacff236097e7a2a03bfe54f6e10))
|
||||
* security upgrade parse push adapter from 4.1.0 to 4.1.2 ([#7893](https://github.com/parse-community/parse-server/issues/7893)) ([93667b4](https://github.com/parse-community/parse-server/commit/93667b4e8402bf13b46c4d3ef12cec6532fd9da7))
|
||||
* websocket connection of LiveQuery interrupts frequently ([#8048](https://github.com/parse-community/parse-server/issues/8048)) ([03caae1](https://github.com/parse-community/parse-server/commit/03caae1e611f28079cdddbbe433daaf69e3f595c))
|
||||
|
||||
### Features
|
||||
|
||||
* add MongoDB 5.1 compatibility ([#7682](https://github.com/parse-community/parse-server/issues/7682)) ([022a856](https://github.com/parse-community/parse-server/commit/022a85619d8a2c57a2f2938e245e4d8a47c15276))
|
||||
* add MongoDB 5.2 support ([#7894](https://github.com/parse-community/parse-server/issues/7894)) ([5bfa716](https://github.com/parse-community/parse-server/commit/5bfa7160d9e35b237cbae1016ed86724aa99f8d7))
|
||||
* add support for Node 17 and 18 ([#7896](https://github.com/parse-community/parse-server/issues/7896)) ([3e9f292](https://github.com/parse-community/parse-server/commit/3e9f292d840334244934cee9a34545ac86313549))
|
||||
* align file trigger syntax with class trigger; use the new syntax `Parse.Cloud.beforeSave(Parse.File, (request) => {})`, the old syntax `Parse.Cloud.beforeSaveFile((request) => {})` has been deprecated ([#7966](https://github.com/parse-community/parse-server/issues/7966)) ([c6dcad8](https://github.com/parse-community/parse-server/commit/c6dcad8d167d44912dbd416d328519314c0809bd))
|
||||
* replace GraphQL Apollo with GraphQL Yoga ([#7967](https://github.com/parse-community/parse-server/issues/7967)) ([1aa2204](https://github.com/parse-community/parse-server/commit/1aa2204aebfdbe273d54d6d56c6029f7c34aab14))
|
||||
* selectively enable / disable default authentication adapters ([#7953](https://github.com/parse-community/parse-server/issues/7953)) ([c1e808f](https://github.com/parse-community/parse-server/commit/c1e808f9e807fc49508acbde0d8b3f2b901a1638))
|
||||
* upgrade mongodb from 4.4.1 to 4.5.0 ([#7991](https://github.com/parse-community/parse-server/issues/7991)) ([e692b5d](https://github.com/parse-community/parse-server/commit/e692b5dd8214cdb0ce79bedd30d9aa3cf4de76a5))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* reduce database operations when using the constant parameter in Cloud Function validation ([#7892](https://github.com/parse-community/parse-server/issues/7892)) ([041197f](https://github.com/parse-community/parse-server/commit/041197fb4ca1cd7cf18dc426ce38647267823668))
|
||||
|
||||
# [5.2.0-beta.2](https://github.com/parse-community/parse-server/compare/5.2.0-beta.1...5.2.0-beta.2) (2022-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security bump minimist from 1.2.5 to 1.2.6 ([#7884](https://github.com/parse-community/parse-server/issues/7884)) ([c5cf282](https://github.com/parse-community/parse-server/commit/c5cf282d11ffdc023764f8e7539a2bd6bc246fe1))
|
||||
* sensitive keyword detection may produce false positives ([#7881](https://github.com/parse-community/parse-server/issues/7881)) ([0d6f9e9](https://github.com/parse-community/parse-server/commit/0d6f9e951d9e186e95e96d8869066ce7022bad02))
|
||||
|
||||
# [5.2.0-beta.1](https://github.com/parse-community/parse-server/compare/5.1.1...5.2.0-beta.1) (2022-03-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improved LiveQuery error logging with additional information ([#7837](https://github.com/parse-community/parse-server/issues/7837)) ([443a509](https://github.com/parse-community/parse-server/commit/443a5099059538d379fe491793a5871fcbb4f377))
|
||||
|
||||
# [5.0.0-beta.10](https://github.com/parse-community/parse-server/compare/5.0.0-beta.9...5.0.0-beta.10) (2022-03-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding or modifying a nested property requires addField permissions ([#7679](https://github.com/parse-community/parse-server/issues/7679)) ([6a6248b](https://github.com/parse-community/parse-server/commit/6a6248b6cb2e732d17131e18e659943b894ed2f1))
|
||||
* bump nanoid from 3.1.25 to 3.2.0 ([#7781](https://github.com/parse-community/parse-server/issues/7781)) ([f5f63bf](https://github.com/parse-community/parse-server/commit/f5f63bfc64d3481ed944ceb5e9f50b33dccd1ce9))
|
||||
* bump node-fetch from 2.6.1 to 3.1.1 ([#7782](https://github.com/parse-community/parse-server/issues/7782)) ([9082351](https://github.com/parse-community/parse-server/commit/90823514113a1a085ebc818f7109b3fd7591346f))
|
||||
* node engine compatibility did not include node 16 ([#7739](https://github.com/parse-community/parse-server/issues/7739)) ([ea7c014](https://github.com/parse-community/parse-server/commit/ea7c01400f992a1263543706fe49b6174758a2d6))
|
||||
* node engine range has no upper limit to exclude incompatible node versions ([#7692](https://github.com/parse-community/parse-server/issues/7692)) ([573558d](https://github.com/parse-community/parse-server/commit/573558d3adcbcc6222c92003829867e1a73eef94))
|
||||
* package.json & package-lock.json to reduce vulnerabilities ([#7823](https://github.com/parse-community/parse-server/issues/7823)) ([5ca2288](https://github.com/parse-community/parse-server/commit/5ca228882332b65f3ac05407e6e4da1ee3ef3749))
|
||||
* schema cache not cleared in some cases ([#7678](https://github.com/parse-community/parse-server/issues/7678)) ([5af6e5d](https://github.com/parse-community/parse-server/commit/5af6e5dfaa129b1a350afcba4fb381b21c4cc35d))
|
||||
* security upgrade follow-redirects from 1.14.6 to 1.14.7 ([#7769](https://github.com/parse-community/parse-server/issues/7769)) ([8f5a861](https://github.com/parse-community/parse-server/commit/8f5a8618cfa7ed9a2a239a095abffa8f3fd8d31a))
|
||||
* security upgrade follow-redirects from 1.14.7 to 1.14.8 ([#7801](https://github.com/parse-community/parse-server/issues/7801)) ([70088a9](https://github.com/parse-community/parse-server/commit/70088a95a78393da2a4ac68be81e63107747626a))
|
||||
* security vulnerability that allows remote code execution (GHSA-p6h4-93qp-jhcm) ([#7844](https://github.com/parse-community/parse-server/issues/7844)) ([e569f40](https://github.com/parse-community/parse-server/commit/e569f402b1fd8648fb0d1523b71b2a03273902a5))
|
||||
* server crash using GraphQL due to missing @apollo/client peer dependency ([#7787](https://github.com/parse-community/parse-server/issues/7787)) ([08089d6](https://github.com/parse-community/parse-server/commit/08089d6fcbb215412448ce7d92b21b9fe6c929f2))
|
||||
* unable to use objectId size higher than 19 on GraphQL API ([#7627](https://github.com/parse-community/parse-server/issues/7627)) ([ed86c80](https://github.com/parse-community/parse-server/commit/ed86c807721cc52a1a5a9dea0b768717eec269ed))
|
||||
* upgrade mime from 2.5.2 to 3.0.0 ([#7725](https://github.com/parse-community/parse-server/issues/7725)) ([f5ef98b](https://github.com/parse-community/parse-server/commit/f5ef98bde32083403c0e30a12162fcc1e52cac37))
|
||||
* upgrade parse from 3.3.1 to 3.4.0 ([#7723](https://github.com/parse-community/parse-server/issues/7723)) ([d4c1f47](https://github.com/parse-community/parse-server/commit/d4c1f473073764cb0570c633fc4a30669c2ce889))
|
||||
* upgrade winston from 3.5.0 to 3.5.1 ([#7820](https://github.com/parse-community/parse-server/issues/7820)) ([4af253d](https://github.com/parse-community/parse-server/commit/4af253d1f8654a6f57b5137ad310cdacadc922cc))
|
||||
|
||||
### Features
|
||||
|
||||
* add Cloud Code context to `ParseObject.fetch` ([#7779](https://github.com/parse-community/parse-server/issues/7779)) ([315290d](https://github.com/parse-community/parse-server/commit/315290d16110110938f80a6b779cc2d1db58c552))
|
||||
* add Idempotency to Postgres ([#7750](https://github.com/parse-community/parse-server/issues/7750)) ([0c3feaa](https://github.com/parse-community/parse-server/commit/0c3feaaa1751964c0db89f25674935c3354b1538))
|
||||
* add support for Node 16 ([#7707](https://github.com/parse-community/parse-server/issues/7707)) ([45cc58c](https://github.com/parse-community/parse-server/commit/45cc58c7e5e640a46c5d508019a3aa81242964b1))
|
||||
* bump required node engine to >=12.22.10 ([#7846](https://github.com/parse-community/parse-server/issues/7846)) ([5ace99d](https://github.com/parse-community/parse-server/commit/5ace99d542a11e422af46d9fd6b1d3d2513b34cf))
|
||||
* support `postgresql` protocol in database URI ([#7757](https://github.com/parse-community/parse-server/issues/7757)) ([caf4a23](https://github.com/parse-community/parse-server/commit/caf4a2341f554b28e3918c53e7e897a3ca47bf8b))
|
||||
* support relativeTime query constraint on Postgres ([#7747](https://github.com/parse-community/parse-server/issues/7747)) ([16b1b2a](https://github.com/parse-community/parse-server/commit/16b1b2a19714535ca805f2dbb3b561d8f6a519a7))
|
||||
* upgrade to MongoDB Node.js driver 4.x for MongoDB 5.0 support ([#7794](https://github.com/parse-community/parse-server/issues/7794)) ([f88aa2a](https://github.com/parse-community/parse-server/commit/f88aa2a62a533e5344d1c13dd38c5a0b283a480a))
|
||||
|
||||
### Reverts
|
||||
|
||||
* refactor: allow ES import for cloud string if package type is module ([b64640c](https://github.com/parse-community/parse-server/commit/b64640c5705f733798783e68d216e957044ef23c))
|
||||
* update node engine to 2.22.0 ([#7827](https://github.com/parse-community/parse-server/issues/7827)) ([f235412](https://github.com/parse-community/parse-server/commit/f235412c1b6c2b173b7531f285429ea7214b56a2))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This requires Node.js version >=12.22.10. ([5ace99d](5ace99d))
|
||||
* The MongoDB GridStore adapter has been removed. By default, Parse Server already uses GridFS, so if you do not manually use the GridStore adapter, you can ignore this change. ([f88aa2a](f88aa2a))
|
||||
* Removes official Node 15 support which has reached it end-of-life date. ([45cc58c](45cc58c))
|
||||
|
||||
# [5.0.0-beta.9](https://github.com/parse-community/parse-server/compare/5.0.0-beta.8...5.0.0-beta.9) (2022-03-12)
|
||||
|
||||
|
||||
|
||||
+281
-1813
File diff suppressed because it is too large
Load Diff
+5
-8
@@ -1,4 +1,4 @@
|
||||
'use strict'
|
||||
'use strict';
|
||||
|
||||
const CiVersionCheck = require('./CiVersionCheck');
|
||||
const mongoVersionList = require('mongodb-version-list');
|
||||
@@ -14,9 +14,8 @@ async function check() {
|
||||
* Check the MongoDB versions used in test environments.
|
||||
*/
|
||||
async function checkMongoDbVersions() {
|
||||
|
||||
const releasedVersions = await new Promise((resolve, reject) => {
|
||||
mongoVersionList(function(error, versions) {
|
||||
mongoVersionList(function (error, versions) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
@@ -31,14 +30,12 @@ async function checkMongoDbVersions() {
|
||||
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
|
||||
ciVersionKey: 'MONGODB_VERSION',
|
||||
releasedVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.path,
|
||||
latestComponent: CiVersionCheck.versionComponents.minor,
|
||||
ignoreReleasedVersions: [
|
||||
'<4.0.0', // Versions reached their MongoDB end-of-life support date
|
||||
'~4.1.0', // Development release according to MongoDB support
|
||||
'~4.3.0', // Development release according to MongoDB support
|
||||
'~4.7.0', // Development release according to MongoDB support
|
||||
|
||||
'4.0.26', // Temporarily disabled because not yet available for download via mongodb-runner
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
@@ -47,7 +44,6 @@ async function checkMongoDbVersions() {
|
||||
* Check the Nodejs versions used in test environments.
|
||||
*/
|
||||
async function checkNodeVersions() {
|
||||
|
||||
const allVersions = await allNodeVersions();
|
||||
const releasedVersions = allVersions.versions;
|
||||
|
||||
@@ -62,7 +58,8 @@ async function checkNodeVersions() {
|
||||
ignoreReleasedVersions: [
|
||||
'<12.0.0', // These versions have reached their end-of-life support date
|
||||
'>=13.0.0 <14.0.0', // These versions have reached their end-of-life support date
|
||||
'>=16.0.0', // This version has not been officially released yet
|
||||
'>=15.0.0 <16.0.0', // These versions have reached their end-of-life support date
|
||||
'>=19.0.0', // These versions are not officially supported yet
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
|
||||
+12
-8
@@ -75,17 +75,21 @@ class NodeEngineCheck {
|
||||
|
||||
// For each file
|
||||
for (const file of files) {
|
||||
|
||||
// Get node version
|
||||
const contentString = await fs.readFile(file, 'utf-8');
|
||||
const contentJson = JSON.parse(contentString);
|
||||
const version = ((contentJson || {}).engines || {}).node;
|
||||
try {
|
||||
const contentJson = JSON.parse(contentString);
|
||||
const version = ((contentJson || {}).engines || {}).node;
|
||||
|
||||
// Add response
|
||||
response.push({
|
||||
file: file,
|
||||
nodeVersion: version
|
||||
});
|
||||
// Add response
|
||||
response.push({
|
||||
file: file,
|
||||
nodeVersion: version
|
||||
});
|
||||
} catch(e) {
|
||||
console.log(`Ignoring file because it is not valid JSON: ${file}`);
|
||||
core.warning(`Ignoring file because it is not valid JSON: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If results should be cleaned by removing undefined node versions
|
||||
|
||||
Generated
+1470
-2005
File diff suppressed because it is too large
Load Diff
+45
-47
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "5.0.0",
|
||||
"version": "5.4.2",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -19,55 +19,56 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@apollographql/graphql-playground-html": "1.6.29",
|
||||
"@graphql-tools/links": "6.2.5",
|
||||
"@graphql-tools/stitch": "6.2.4",
|
||||
"@graphql-tools/utils": "6.2.4",
|
||||
"@parse/fs-files-adapter": "1.2.0",
|
||||
"@parse/push-adapter": "3.4.1",
|
||||
"apollo-server-express": "2.25.2",
|
||||
"@graphql-yoga/node": "2.6.0",
|
||||
"@graphql-tools/utils": "8.12.0",
|
||||
"@graphql-tools/merge": "8.3.6",
|
||||
"@graphql-tools/schema": "9.0.4",
|
||||
"@parse/fs-files-adapter": "1.2.2",
|
||||
"@parse/push-adapter": "4.1.2",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.19.0",
|
||||
"body-parser": "1.20.1",
|
||||
"commander": "5.1.0",
|
||||
"cors": "2.8.5",
|
||||
"deepcopy": "2.1.0",
|
||||
"express": "4.17.1",
|
||||
"follow-redirects": "1.14.8",
|
||||
"graphql": "15.6.0",
|
||||
"express": "4.18.1",
|
||||
"follow-redirects": "1.15.2",
|
||||
"graphql": "16.6.0",
|
||||
"graphql-list-fields": "2.0.2",
|
||||
"graphql-relay": "0.7.0",
|
||||
"graphql-tag": "2.12.5",
|
||||
"graphql-upload": "11.0.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"graphql-relay": "0.10.0",
|
||||
"intersect": "1.0.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"jwks-rsa": "1.12.3",
|
||||
"ldapjs": "2.3.1",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
"jwks-rsa": "2.1.4",
|
||||
"ldapjs": "2.3.3",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "5.1.1",
|
||||
"mime": "2.5.2",
|
||||
"mongodb": "3.6.11",
|
||||
"lru-cache": "7.12.0",
|
||||
"mime": "3.0.0",
|
||||
"mongodb": "4.10.0",
|
||||
"mustache": "4.2.0",
|
||||
"parse": "3.3.1",
|
||||
"pg-monitor": "1.4.1",
|
||||
"pg-promise": "10.11.0",
|
||||
"parse": "3.4.2",
|
||||
"pg-monitor": "1.5.0",
|
||||
"pg-promise": "10.12.0",
|
||||
"pluralize": "8.0.0",
|
||||
"redis": "3.1.2",
|
||||
"semver": "7.3.5",
|
||||
"subscriptions-transport-ws": "0.10.0",
|
||||
"semver": "7.3.8",
|
||||
"subscriptions-transport-ws": "0.11.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "8.3.2",
|
||||
"winston": "3.3.3",
|
||||
"winston-daily-rotate-file": "4.5.5",
|
||||
"ws": "8.2.2"
|
||||
"uuid": "9.0.0",
|
||||
"winston": "3.8.1",
|
||||
"winston-daily-rotate-file": "4.7.1",
|
||||
"ws": "8.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "1.2.6",
|
||||
"graphql-tag": "2.12.6",
|
||||
"@actions/core": "1.9.1",
|
||||
"@apollo/client": "3.6.1",
|
||||
"@babel/cli": "7.10.0",
|
||||
"@babel/core": "7.10.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.10.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.9.0",
|
||||
"@babel/preset-env": "7.10.0",
|
||||
"@parse/minami": "1.0.0",
|
||||
"@saithodev/semantic-release-backmerge": "2.1.2",
|
||||
"@semantic-release/changelog": "5.0.1",
|
||||
"@semantic-release/commit-analyzer": "8.0.1",
|
||||
"@semantic-release/git": "9.0.0",
|
||||
@@ -75,12 +76,7 @@
|
||||
"@semantic-release/npm": "7.1.3",
|
||||
"@semantic-release/release-notes-generator": "9.0.3",
|
||||
"all-node-versions": "8.0.0",
|
||||
"apollo-cache-inmemory": "1.6.6",
|
||||
"apollo-client": "2.6.10",
|
||||
"apollo-link": "1.2.14",
|
||||
"apollo-link-http": "1.5.17",
|
||||
"apollo-link-ws": "1.0.20",
|
||||
"apollo-upload-client": "13.0.0",
|
||||
"apollo-upload-client": "16.0.0",
|
||||
"apollo-utilities": "1.3.4",
|
||||
"babel-eslint": "10.1.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
@@ -90,7 +86,7 @@
|
||||
"eslint-plugin-flowtype": "5.1.3",
|
||||
"flow-bin": "0.119.1",
|
||||
"form-data": "3.0.0",
|
||||
"husky": "4.2.5",
|
||||
"husky": "4.3.8",
|
||||
"jasmine": "3.5.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"jsdoc": "3.6.3",
|
||||
@@ -101,7 +97,7 @@
|
||||
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
|
||||
"mongodb-runner": "4.8.1",
|
||||
"mongodb-version-list": "1.0.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-fetch": "3.2.10",
|
||||
"nyc": "15.1.0",
|
||||
"prettier": "2.0.5",
|
||||
"semantic-release": "17.4.6",
|
||||
@@ -119,15 +115,17 @@
|
||||
"test:mongodb:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
|
||||
"test:mongodb:testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine",
|
||||
"test:mongodb": "npm run test:mongodb:runnerstart --dbversion=$npm_config_dbversion && npm run test:mongodb:testonly --dbversion=$npm_config_dbversion",
|
||||
"test:mongodb:4.0.27": "npm run test:mongodb --dbversion=4.0.27",
|
||||
"test:mongodb:4.2.17": "npm run test:mongodb --dbversion=4.2.17",
|
||||
"test:mongodb:4.4.10": "npm run test:mongodb --dbversion=4.4.10",
|
||||
"test:mongodb:4.0.28": "npm run test:mongodb --dbversion=4.0.28",
|
||||
"test:mongodb:4.2.19": "npm run test:mongodb --dbversion=4.2.19",
|
||||
"test:mongodb:4.4.13": "npm run test:mongodb --dbversion=4.4.13",
|
||||
"test:mongodb:5.3.2": "npm run test:mongodb --dbversion=5.3.2",
|
||||
"test:mongodb:6.0.2": "npm run test:mongodb --dbversion=6.0.2",
|
||||
"posttest:mongodb": "mongodb-runner stop",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.4.10} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.4.10} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine",
|
||||
"test": "npm run testonly",
|
||||
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.4.10} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.4.10} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 nyc jasmine",
|
||||
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 nyc jasmine",
|
||||
"start": "node ./bin/parse-server",
|
||||
"prettier": "prettier --write {src,spec}/{**/*,*}.js",
|
||||
"prepare": "npm run build",
|
||||
@@ -135,7 +133,7 @@
|
||||
"madge:circular": "node_modules/.bin/madge ./src --circular"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.10 <16"
|
||||
"node": ">=12.22.10 <19"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "bin/parse-server"
|
||||
|
||||
+2
-5
@@ -28,7 +28,7 @@ async function config() {
|
||||
console.log(`Running on branch: ${branch}`);
|
||||
|
||||
// Set changelog file
|
||||
const changelogFile = `./changelogs/CHANGELOG_${branch}.md`;
|
||||
const changelogFile = `./changelogs/CHANGELOG_release.md`;
|
||||
console.log(`Changelog file output to: ${changelogFile}`);
|
||||
|
||||
// Load template file contents
|
||||
@@ -41,10 +41,7 @@ async function config() {
|
||||
{ name: 'beta', prerelease: true },
|
||||
'next-major',
|
||||
// Long-Term-Support branches
|
||||
// { name: 'release-1', range: '1.x.x', channel: '1.x' },
|
||||
// { name: 'release-2', range: '2.x.x', channel: '2.x' },
|
||||
// { name: 'release-3', range: '3.x.x', channel: '3.x' },
|
||||
// { name: 'release-4', range: '4.x.x', channel: '4.x' },
|
||||
{ name: 'release-5.x.x', range: '5.x.x', channel: '5.x.x' },
|
||||
],
|
||||
dryRun: false,
|
||||
debug: true,
|
||||
|
||||
@@ -23,23 +23,25 @@ const nestedOptionTypes = [
|
||||
'PagesRoute',
|
||||
'PasswordPolicyOptions',
|
||||
'SecurityOptions',
|
||||
'SchemaOptions',
|
||||
];
|
||||
|
||||
/** The prefix of environment variables for nested options. */
|
||||
const nestedOptionEnvPrefix = {
|
||||
'AccountLockoutOptions' : 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
|
||||
'CustomPagesOptions' : 'PARSE_SERVER_CUSTOM_PAGES_',
|
||||
'AccountLockoutOptions': 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
|
||||
'CustomPagesOptions': 'PARSE_SERVER_CUSTOM_PAGES_',
|
||||
'DatabaseOptions': 'PARSE_SERVER_DATABASE_',
|
||||
'FileUploadOptions' : 'PARSE_SERVER_FILE_UPLOAD_',
|
||||
'IdempotencyOptions' : 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
|
||||
'LiveQueryOptions' : 'PARSE_SERVER_LIVEQUERY_',
|
||||
'LiveQueryServerOptions' : 'PARSE_LIVE_QUERY_SERVER_',
|
||||
'PagesCustomUrlsOptions' : 'PARSE_SERVER_PAGES_CUSTOM_URL_',
|
||||
'PagesOptions' : 'PARSE_SERVER_PAGES_',
|
||||
'FileUploadOptions': 'PARSE_SERVER_FILE_UPLOAD_',
|
||||
'IdempotencyOptions': 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
|
||||
'LiveQueryOptions': 'PARSE_SERVER_LIVEQUERY_',
|
||||
'LiveQueryServerOptions': 'PARSE_LIVE_QUERY_SERVER_',
|
||||
'PagesCustomUrlsOptions': 'PARSE_SERVER_PAGES_CUSTOM_URL_',
|
||||
'PagesOptions': 'PARSE_SERVER_PAGES_',
|
||||
'PagesRoute': 'PARSE_SERVER_PAGES_ROUTE_',
|
||||
'ParseServerOptions' : 'PARSE_SERVER_',
|
||||
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_',
|
||||
'ParseServerOptions': 'PARSE_SERVER_',
|
||||
'PasswordPolicyOptions': 'PARSE_SERVER_PASSWORD_POLICY_',
|
||||
'SecurityOptions': 'PARSE_SERVER_SECURITY_',
|
||||
'SchemaOptions': 'PARSE_SERVER_SCHEMA_',
|
||||
};
|
||||
|
||||
function last(array) {
|
||||
@@ -50,7 +52,7 @@ const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
function toENV(key) {
|
||||
let str = '';
|
||||
let previousIsUpper = false;
|
||||
for(let i = 0; i < key.length; i++) {
|
||||
for (let i = 0; i < key.length; i++) {
|
||||
const char = key[i];
|
||||
if (letters.indexOf(char) >= 0) {
|
||||
if (!previousIsUpper) {
|
||||
@@ -273,8 +275,8 @@ function inject(t, list) {
|
||||
return { results, comments };
|
||||
}
|
||||
|
||||
const makeRequire = function(variableName, module, t) {
|
||||
const decl = t.variableDeclarator(t.identifier(variableName), t.callExpression(t.identifier('require'), [t.stringLiteral(module)]));
|
||||
const makeRequire = function (variableName, module, t) {
|
||||
const decl = t.variableDeclarator(t.identifier(variableName), t.callExpression(t.identifier('require'), [t.stringLiteral(module)]));
|
||||
return t.variableDeclaration('var', [decl])
|
||||
}
|
||||
let docs = ``;
|
||||
@@ -283,14 +285,14 @@ const plugin = function (babel) {
|
||||
const moduleExports = t.memberExpression(t.identifier('module'), t.identifier('exports'));
|
||||
return {
|
||||
visitor: {
|
||||
ImportDeclaration: function(path) {
|
||||
ImportDeclaration: function (path) {
|
||||
path.remove();
|
||||
},
|
||||
Program: function(path) {
|
||||
Program: function (path) {
|
||||
// Inject the parser's loader
|
||||
path.unshiftContainer('body', makeRequire('parsers', './parsers', t));
|
||||
},
|
||||
ExportDeclaration: function(path) {
|
||||
ExportDeclaration: function (path) {
|
||||
// Export declaration on an interface
|
||||
if (path.node && path.node.declaration && path.node.declaration.type == 'InterfaceDeclaration') {
|
||||
const { results, comments } = inject(t, doInterface(path.node.declaration));
|
||||
@@ -313,6 +315,6 @@ Do not edit manually, but update Options/index.js
|
||||
`
|
||||
|
||||
const babel = require("@babel/core");
|
||||
const res = babel.transformFileSync('./src/Options/index.js', { plugins: [ plugin, '@babel/transform-flow-strip-types' ], babelrc: false, auxiliaryCommentBefore, sourceMaps: false });
|
||||
const res = babel.transformFileSync('./src/Options/index.js', { plugins: [plugin, '@babel/transform-flow-strip-types'], babelrc: false, auxiliaryCommentBefore, sourceMaps: false });
|
||||
require('fs').writeFileSync('./src/Options/Definitions.js', res.code + '\n');
|
||||
require('fs').writeFileSync('./src/Options/docs.js', docs);
|
||||
|
||||
+3
-1
@@ -18,10 +18,13 @@
|
||||
"it_only_db": true,
|
||||
"it_only_mongodb_version": true,
|
||||
"it_only_postgres_version": true,
|
||||
"it_only_node_version": true,
|
||||
"fit_only_mongodb_version": true,
|
||||
"fit_only_node_version": true,
|
||||
"it_exclude_mongodb_version": true,
|
||||
"it_exclude_postgres_version": true,
|
||||
"fit_exclude_mongodb_version": true,
|
||||
"fit_exclude_node_version": true,
|
||||
"it_exclude_dbs": true,
|
||||
"describe_only_db": true,
|
||||
"describe_only": true,
|
||||
@@ -31,7 +34,6 @@
|
||||
"jequal": true,
|
||||
"create": true,
|
||||
"arrayContains": true,
|
||||
"expectAsync": true,
|
||||
"databaseAdapter": true
|
||||
},
|
||||
"rules": {
|
||||
|
||||
+15
-14
@@ -326,22 +326,24 @@ describe('AudiencesRouter', () => {
|
||||
{ name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }) },
|
||||
{ useMasterKey: true }
|
||||
).then(audience => {
|
||||
database.collection('test__Audience').updateOne(
|
||||
{ _id: audience.objectId },
|
||||
{
|
||||
$set: {
|
||||
times_used: 1,
|
||||
_last_used: now,
|
||||
},
|
||||
},
|
||||
{},
|
||||
error => {
|
||||
expect(error).toEqual(null);
|
||||
database
|
||||
.collection('test__Audience')
|
||||
.updateOne(
|
||||
{ _id: audience.objectId },
|
||||
{
|
||||
$set: {
|
||||
times_used: 1,
|
||||
_last_used: now,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
expect(result).toBeTruthy();
|
||||
database
|
||||
.collection('test__Audience')
|
||||
.find({ _id: audience.objectId })
|
||||
.toArray((error, rows) => {
|
||||
expect(error).toEqual(null);
|
||||
expect(error).toEqual(undefined);
|
||||
expect(rows[0]['times_used']).toEqual(1);
|
||||
expect(rows[0]['_last_used']).toEqual(now);
|
||||
Parse._request(
|
||||
@@ -361,8 +363,7 @@ describe('AudiencesRouter', () => {
|
||||
done.fail(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -419,6 +419,29 @@ describe('AuthenticationProviders', function () {
|
||||
expect(providerOptions).toEqual(options.facebook);
|
||||
});
|
||||
|
||||
it('should throw error when Facebook request appId is wrong data type', async () => {
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
return Promise.resolve({ id: 'a' });
|
||||
});
|
||||
const options = {
|
||||
facebook: {
|
||||
appIds: 'abcd',
|
||||
appSecret: 'secret_sauce',
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
access_token: 'badtoken',
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'facebook',
|
||||
options
|
||||
);
|
||||
await expectAsync(adapter.validateAppId(appIds, authData, providerOptions)).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Facebook appSecret for validating appIds', async () => {
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
@@ -441,6 +464,29 @@ describe('AuthenticationProviders', function () {
|
||||
expect(httpsRequest.get.calls.first().args[0].includes('appsecret_proof')).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw error when Facebook request appId is wrong data type', async () => {
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
return Promise.resolve({ id: 'a' });
|
||||
});
|
||||
const options = {
|
||||
facebook: {
|
||||
appIds: 'abcd',
|
||||
appSecret: 'secret_sauce',
|
||||
},
|
||||
};
|
||||
const authData = {
|
||||
access_token: 'badtoken',
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'facebook',
|
||||
options
|
||||
);
|
||||
await expectAsync(adapter.validateAppId(appIds, authData, providerOptions)).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Facebook appSecret for validating auth data', async () => {
|
||||
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
@@ -477,6 +523,34 @@ describe('AuthenticationProviders', function () {
|
||||
expect(appIds).toEqual(['a', 'b']);
|
||||
expect(providerOptions).toEqual(options.custom);
|
||||
});
|
||||
|
||||
it('can disable provider', async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
myoauth: {
|
||||
enabled: false,
|
||||
module: path.resolve(__dirname, 'support/myoauth'), // relative path as it's run from src
|
||||
},
|
||||
},
|
||||
});
|
||||
const provider = getMockMyOauthProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
await expectAsync(Parse.User._logInWith('myoauth')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, 'This authentication method is unsupported.')
|
||||
);
|
||||
});
|
||||
|
||||
it('can depreciate', async () => {
|
||||
const Deprecator = require('../lib/Deprecator/Deprecator');
|
||||
const spy = spyOn(Deprecator, 'logRuntimeDeprecation').and.callFake(() => {});
|
||||
const provider = getMockMyOauthProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
await Parse.User._logInWith('myoauth');
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
usage: 'auth.myoauth',
|
||||
solution: 'auth.myoauth.enabled: true',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('instagram auth adapter', () => {
|
||||
@@ -522,6 +596,7 @@ describe('instagram auth adapter', () => {
|
||||
describe('google auth adapter', () => {
|
||||
const google = require('../lib/Adapters/Auth/google');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
it('should throw error with missing id_token', async () => {
|
||||
try {
|
||||
@@ -544,7 +619,7 @@ describe('google auth adapter', () => {
|
||||
// it('should throw error if public key used to encode token is not available', async () => {
|
||||
// const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
|
||||
// try {
|
||||
// spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
// spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
|
||||
// await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
|
||||
// fail();
|
||||
@@ -563,7 +638,7 @@ describe('google auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await google.validateAuthData(
|
||||
@@ -579,7 +654,7 @@ describe('google auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -603,7 +678,7 @@ describe('google auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -625,7 +700,7 @@ describe('google auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -1328,6 +1403,7 @@ describe('apple signin auth adapter', () => {
|
||||
const apple = require('../lib/Adapters/Auth/apple');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const util = require('util');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
it('(using client id as string) should throw error with missing id_token', async () => {
|
||||
try {
|
||||
@@ -1362,7 +1438,7 @@ describe('apple signin auth adapter', () => {
|
||||
it('should throw error if public key used to encode token is not available', async () => {
|
||||
const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
|
||||
try {
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
|
||||
await apple.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
@@ -1384,7 +1460,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
@@ -1401,7 +1477,7 @@ describe('apple signin auth adapter', () => {
|
||||
|
||||
it('should not verify invalid id_token', async () => {
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1438,7 +1514,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1460,7 +1536,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1482,7 +1558,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1502,7 +1578,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1530,7 +1606,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1559,7 +1635,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1631,7 +1707,7 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'a_different_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
@@ -1652,8 +1728,44 @@ describe('apple signin auth adapter', () => {
|
||||
|
||||
describe('Apple Game Center Auth adapter', () => {
|
||||
const gcenter = require('../lib/Adapters/Auth/gcenter');
|
||||
const fs = require('fs');
|
||||
const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
|
||||
const testCert2 = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
|
||||
|
||||
it('can load adapter', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl:
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('validateAuthData should validate', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl:
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
// real token is used
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
@@ -1664,48 +1776,149 @@ describe('Apple Game Center Auth adapter', () => {
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'cloud.xtralife.gamecenterauth',
|
||||
};
|
||||
|
||||
try {
|
||||
await gcenter.validateAuthData(authData);
|
||||
} catch (e) {
|
||||
fail();
|
||||
}
|
||||
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
|
||||
await gcenter.validateAuthData(authData);
|
||||
});
|
||||
|
||||
it('validateAuthData invalid signature id', async () => {
|
||||
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
|
||||
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-6.cer'] = testCert2;
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
{}
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature: '1234',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'cloud.xtralife.gamecenterauth',
|
||||
bundleId: 'com.example.com',
|
||||
};
|
||||
|
||||
try {
|
||||
await gcenter.validateAuthData(authData);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Apple Game Center - invalid signature');
|
||||
}
|
||||
await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature')
|
||||
);
|
||||
});
|
||||
|
||||
it('validateAuthData invalid public key url', async () => {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'invalid.com',
|
||||
timestamp: 1565257031287,
|
||||
signature: '1234',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'cloud.xtralife.gamecenterauth',
|
||||
it('validateAuthData invalid public key http url', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl:
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
const publicKeyUrls = [
|
||||
'example.com',
|
||||
'http://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
'https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg',
|
||||
'https://example.com/ \\.apple.com/public_key.cer',
|
||||
'https://example.com/ &.apple.com/public_key.cer',
|
||||
];
|
||||
await Promise.all(
|
||||
publicKeyUrls.map(publicKeyUrl =>
|
||||
expectAsync(
|
||||
gcenter.validateAuthData({
|
||||
id: 'G:1965586982',
|
||||
timestamp: 1565257031287,
|
||||
publicKeyUrl,
|
||||
signature: '1234',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'com.example.com',
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.SCRIPT_FAILED,
|
||||
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await gcenter.validateAuthData(authData);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: invalid.com');
|
||||
}
|
||||
it('should not validate Symantec Cert', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl:
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
expect(() =>
|
||||
gcenter.verifyPublicKeyIssuer(
|
||||
testCert,
|
||||
'https://static.gc.apple.com/public-key/gc-prod-4.cer'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('adapter should load default cert', async () => {
|
||||
const options = {
|
||||
gcenter: {},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
const previous = new Date();
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
|
||||
const duration = new Date().getTime() - previous.getTime();
|
||||
expect(duration <= 1).toBe(true);
|
||||
});
|
||||
|
||||
it('adapter should throw', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl: 'https://example.com',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await expectAsync(
|
||||
adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
)
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1761,6 +1974,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const facebook = require('../lib/Adapters/Auth/facebook');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const util = require('util');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
// TODO: figure out a way to run this test alongside facebook classic tests
|
||||
xit('(using client id as string) should throw error with missing id_token', async () => {
|
||||
@@ -1799,7 +2013,7 @@ describe('facebook limited auth adapter', () => {
|
||||
header: { kid: '789', alg: 'RS256' },
|
||||
};
|
||||
try {
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
@@ -1823,7 +2037,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
@@ -1845,7 +2059,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -1887,7 +2101,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -1914,7 +2128,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -1941,7 +2155,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -1966,7 +2180,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -1999,7 +2213,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -2033,7 +2247,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
@@ -2119,7 +2333,7 @@ describe('facebook limited auth adapter', () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
|
||||
@@ -737,7 +737,8 @@ describe('cloud validator', () => {
|
||||
});
|
||||
|
||||
it('basic beforeSaveFile skipWithMasterKey', async done => {
|
||||
Parse.Cloud.beforeSaveFile(
|
||||
Parse.Cloud.beforeSave(
|
||||
Parse.File,
|
||||
() => {
|
||||
throw 'beforeSaveFile should have resolved using master key.';
|
||||
},
|
||||
@@ -1431,7 +1432,7 @@ describe('cloud validator', () => {
|
||||
});
|
||||
|
||||
it('validate beforeSaveFile', async done => {
|
||||
Parse.Cloud.beforeSaveFile(() => {}, validatorSuccess);
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {}, validatorSuccess);
|
||||
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
const result = await file.save({ useMasterKey: true });
|
||||
@@ -1440,7 +1441,7 @@ describe('cloud validator', () => {
|
||||
});
|
||||
|
||||
it('validate beforeSaveFile fail', async done => {
|
||||
Parse.Cloud.beforeSaveFile(() => {}, validatorFail);
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {}, validatorFail);
|
||||
try {
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
@@ -1452,7 +1453,7 @@ describe('cloud validator', () => {
|
||||
});
|
||||
|
||||
it('validate afterSaveFile', async done => {
|
||||
Parse.Cloud.afterSaveFile(() => {}, validatorSuccess);
|
||||
Parse.Cloud.afterSave(Parse.File, () => {}, validatorSuccess);
|
||||
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
const result = await file.save({ useMasterKey: true });
|
||||
@@ -1461,7 +1462,7 @@ describe('cloud validator', () => {
|
||||
});
|
||||
|
||||
it('validate afterSaveFile fail', async done => {
|
||||
Parse.Cloud.beforeSaveFile(() => {}, validatorFail);
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {}, validatorFail);
|
||||
try {
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
@@ -1473,7 +1474,7 @@ describe('cloud validator', () => {
|
||||
});
|
||||
|
||||
it('validate beforeDeleteFile', async done => {
|
||||
Parse.Cloud.beforeDeleteFile(() => {}, validatorSuccess);
|
||||
Parse.Cloud.beforeDelete(Parse.File, () => {}, validatorSuccess);
|
||||
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save();
|
||||
@@ -1482,7 +1483,7 @@ describe('cloud validator', () => {
|
||||
});
|
||||
|
||||
it('validate beforeDeleteFile fail', async done => {
|
||||
Parse.Cloud.beforeDeleteFile(() => {}, validatorFail);
|
||||
Parse.Cloud.beforeDelete(Parse.File, () => {}, validatorFail);
|
||||
try {
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save();
|
||||
@@ -1495,7 +1496,7 @@ describe('cloud validator', () => {
|
||||
});
|
||||
|
||||
it('validate afterDeleteFile', async done => {
|
||||
Parse.Cloud.afterDeleteFile(() => {}, validatorSuccess);
|
||||
Parse.Cloud.afterDelete(Parse.File, () => {}, validatorSuccess);
|
||||
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save();
|
||||
@@ -1504,7 +1505,7 @@ describe('cloud validator', () => {
|
||||
});
|
||||
|
||||
it('validate afterDeleteFile fail', async done => {
|
||||
Parse.Cloud.afterDeleteFile(() => {}, validatorFail);
|
||||
Parse.Cloud.afterDelete(Parse.File, () => {}, validatorFail);
|
||||
try {
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save();
|
||||
|
||||
+276
-42
@@ -50,6 +50,16 @@ describe('Cloud Code', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can get config', () => {
|
||||
const config = Parse.Server;
|
||||
let currentConfig = Config.get('test');
|
||||
expect(Object.keys(config)).toEqual(Object.keys(currentConfig));
|
||||
config.silent = false;
|
||||
Parse.Server = config;
|
||||
currentConfig = Config.get('test');
|
||||
expect(currentConfig.silent).toBeFalse();
|
||||
});
|
||||
|
||||
it('show warning on duplicate cloud functions', done => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'warn').and.callFake(() => {});
|
||||
@@ -1494,6 +1504,136 @@ describe('Cloud Code', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('before save can revert fields', async () => {
|
||||
Parse.Cloud.beforeSave('TestObject', ({ object }) => {
|
||||
object.revert('foo');
|
||||
return object;
|
||||
});
|
||||
|
||||
Parse.Cloud.afterSave('TestObject', ({ object }) => {
|
||||
expect(object.get('foo')).toBeUndefined();
|
||||
return object;
|
||||
});
|
||||
|
||||
const obj = new TestObject();
|
||||
obj.set('foo', 'bar');
|
||||
await obj.save();
|
||||
|
||||
expect(obj.get('foo')).toBeUndefined();
|
||||
await obj.fetch();
|
||||
|
||||
expect(obj.get('foo')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('before save can revert fields with existing object', async () => {
|
||||
Parse.Cloud.beforeSave(
|
||||
'TestObject',
|
||||
({ object }) => {
|
||||
object.revert('foo');
|
||||
return object;
|
||||
},
|
||||
{
|
||||
skipWithMasterKey: true,
|
||||
}
|
||||
);
|
||||
|
||||
Parse.Cloud.afterSave(
|
||||
'TestObject',
|
||||
({ object }) => {
|
||||
expect(object.get('foo')).toBe('bar');
|
||||
return object;
|
||||
},
|
||||
{
|
||||
skipWithMasterKey: true,
|
||||
}
|
||||
);
|
||||
|
||||
const obj = new TestObject();
|
||||
obj.set('foo', 'bar');
|
||||
await obj.save(null, { useMasterKey: true });
|
||||
|
||||
expect(obj.get('foo')).toBe('bar');
|
||||
obj.set('foo', 'yolo');
|
||||
await obj.save();
|
||||
expect(obj.get('foo')).toBe('bar');
|
||||
});
|
||||
|
||||
it('can unset in afterSave', async () => {
|
||||
Parse.Cloud.beforeSave('TestObject', ({ object }) => {
|
||||
if (!object.existed()) {
|
||||
object.set('secret', true);
|
||||
return object;
|
||||
}
|
||||
object.revert('secret');
|
||||
});
|
||||
|
||||
Parse.Cloud.afterSave('TestObject', ({ object }) => {
|
||||
object.unset('secret');
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeFind(
|
||||
'TestObject',
|
||||
({ query }) => {
|
||||
query.exclude('secret');
|
||||
},
|
||||
{
|
||||
skipWithMasterKey: true,
|
||||
}
|
||||
);
|
||||
|
||||
const obj = new TestObject();
|
||||
await obj.save();
|
||||
expect(obj.get('secret')).toBeUndefined();
|
||||
await obj.fetch();
|
||||
expect(obj.get('secret')).toBeUndefined();
|
||||
await obj.fetch({ useMasterKey: true });
|
||||
expect(obj.get('secret')).toBe(true);
|
||||
});
|
||||
|
||||
it('should revert in beforeSave', async () => {
|
||||
Parse.Cloud.beforeSave('MyObject', ({ object }) => {
|
||||
if (!object.existed()) {
|
||||
object.set('count', 0);
|
||||
return object;
|
||||
}
|
||||
object.revert('count');
|
||||
return object;
|
||||
});
|
||||
const obj = await new Parse.Object('MyObject').save();
|
||||
expect(obj.get('count')).toBe(0);
|
||||
obj.set('count', 10);
|
||||
await obj.save();
|
||||
expect(obj.get('count')).toBe(0);
|
||||
await obj.fetch();
|
||||
expect(obj.get('count')).toBe(0);
|
||||
});
|
||||
|
||||
it('pointer should not be cleared by triggers', async () => {
|
||||
Parse.Cloud.afterSave('MyObject', () => {});
|
||||
const foo = await new Parse.Object('Test', { foo: 'bar' }).save();
|
||||
const obj = await new Parse.Object('MyObject', { foo }).save();
|
||||
const foo2 = obj.get('foo');
|
||||
expect(foo2.get('foo')).toBe('bar');
|
||||
});
|
||||
|
||||
it('can set a pointer in triggers', async () => {
|
||||
Parse.Cloud.beforeSave('MyObject', () => {});
|
||||
Parse.Cloud.afterSave(
|
||||
'MyObject',
|
||||
async ({ object }) => {
|
||||
const foo = await new Parse.Object('Test', { foo: 'bar' }).save();
|
||||
object.set({ foo });
|
||||
await object.save(null, { useMasterKey: true });
|
||||
},
|
||||
{
|
||||
skipWithMasterKey: true,
|
||||
}
|
||||
);
|
||||
const obj = await new Parse.Object('MyObject').save();
|
||||
const foo2 = obj.get('foo');
|
||||
expect(foo2.get('foo')).toBe('bar');
|
||||
});
|
||||
|
||||
it('beforeSave should not sanitize database', async done => {
|
||||
const { adapter } = Config.get(Parse.applicationId).database;
|
||||
const spy = spyOn(adapter, 'findOneAndUpdate').and.callThrough();
|
||||
@@ -1860,6 +2000,36 @@ describe('afterSave hooks', () => {
|
||||
const myObject = new MyObject();
|
||||
myObject.save().then(() => done());
|
||||
});
|
||||
|
||||
it('should unset in afterSave', async () => {
|
||||
Parse.Cloud.afterSave(
|
||||
'MyObject',
|
||||
({ object }) => {
|
||||
object.unset('secret');
|
||||
},
|
||||
{
|
||||
skipWithMasterKey: true,
|
||||
}
|
||||
);
|
||||
const obj = new Parse.Object('MyObject');
|
||||
obj.set('secret', 'bar');
|
||||
await obj.save();
|
||||
expect(obj.get('secret')).toBeUndefined();
|
||||
await obj.fetch();
|
||||
expect(obj.get('secret')).toBe('bar');
|
||||
});
|
||||
|
||||
it('should unset', async () => {
|
||||
Parse.Cloud.beforeSave('MyObject', ({ object }) => {
|
||||
object.set('secret', 'hidden');
|
||||
});
|
||||
|
||||
Parse.Cloud.afterSave('MyObject', ({ object }) => {
|
||||
object.unset('secret');
|
||||
});
|
||||
const obj = await new Parse.Object('MyObject').save();
|
||||
expect(obj.get('secret')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforeDelete hooks', () => {
|
||||
@@ -1999,6 +2169,25 @@ describe('beforeFind hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have object found with nested relational data query', async () => {
|
||||
const obj1 = Parse.Object.extend('TestObject');
|
||||
const obj2 = Parse.Object.extend('TestObject2');
|
||||
let item2 = new obj2();
|
||||
item2 = await item2.save();
|
||||
let item1 = new obj1();
|
||||
const relation = item1.relation('rel');
|
||||
relation.add(item2);
|
||||
item1 = await item1.save();
|
||||
Parse.Cloud.beforeFind('TestObject', req => {
|
||||
const additionalQ = new Parse.Query('TestObject');
|
||||
additionalQ.equalTo('rel', item2);
|
||||
return Parse.Query.and(req.query, additionalQ);
|
||||
});
|
||||
const q = new Parse.Query('TestObject');
|
||||
const res = await q.first();
|
||||
expect(res.id).toEqual(item1.id);
|
||||
});
|
||||
|
||||
it('should use the modified exclude query', async () => {
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
const q = req.query;
|
||||
@@ -3205,13 +3394,28 @@ describe('afterLogin hook', () => {
|
||||
const query = new Parse.Query(TestObject);
|
||||
await query.find({ context: { a: 'a' } });
|
||||
});
|
||||
|
||||
it('beforeFind and afterFind should have access to context while making fetch call', async () => {
|
||||
Parse.Cloud.beforeFind('TestObject', req => {
|
||||
expect(req.context.a).toEqual('a');
|
||||
expect(req.context.b).toBeUndefined();
|
||||
req.context.b = 'b';
|
||||
});
|
||||
Parse.Cloud.afterFind('TestObject', req => {
|
||||
expect(req.context.a).toEqual('a');
|
||||
expect(req.context.b).toEqual('b');
|
||||
});
|
||||
const obj = new TestObject();
|
||||
await obj.save();
|
||||
await obj.fetch({ context: { a: 'a' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveFile hooks', () => {
|
||||
it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSaveFile(() => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
const newFile = new Parse.File('some-file.txt');
|
||||
newFile._url = 'http://www.somewhere.com/parse/files/some-app-id/some-file.txt';
|
||||
return newFile;
|
||||
@@ -3226,7 +3430,7 @@ describe('saveFile hooks', () => {
|
||||
|
||||
it('beforeSaveFile should throw error', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSaveFile(() => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw new Parse.Error(400, 'some-error-message');
|
||||
});
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
@@ -3240,8 +3444,8 @@ describe('saveFile hooks', () => {
|
||||
it('beforeSaveFile should change values of uploaded file by editing fileObject directly', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSaveFile(async req => {
|
||||
expect(req.triggerName).toEqual('beforeSaveFile');
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
expect(req.triggerName).toEqual('beforeSave');
|
||||
expect(req.master).toBe(true);
|
||||
req.file.addMetadata('foo', 'bar');
|
||||
req.file.addTag('tagA', 'some-tag');
|
||||
@@ -3269,8 +3473,8 @@ describe('saveFile hooks', () => {
|
||||
it('beforeSaveFile should change values by returning new fileObject', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSaveFile(async req => {
|
||||
expect(req.triggerName).toEqual('beforeSaveFile');
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
expect(req.triggerName).toEqual('beforeSave');
|
||||
expect(req.fileSize).toBe(3);
|
||||
const newFile = new Parse.File('donald_duck.pdf', [4, 5, 6], 'application/pdf');
|
||||
newFile.setMetadata({ foo: 'bar' });
|
||||
@@ -3303,8 +3507,8 @@ describe('saveFile hooks', () => {
|
||||
it('beforeSaveFile should contain metadata and tags saved from client', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSaveFile(async req => {
|
||||
expect(req.triggerName).toEqual('beforeSaveFile');
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
expect(req.triggerName).toEqual('beforeSave');
|
||||
expect(req.fileSize).toBe(3);
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
expect(req.file.name()).toBe('popeye.txt');
|
||||
@@ -3332,7 +3536,7 @@ describe('saveFile hooks', () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const config = Config.get('test');
|
||||
config.filesController.options.preserveFileName = true;
|
||||
Parse.Cloud.beforeSaveFile(async ({ file }) => {
|
||||
Parse.Cloud.beforeSave(Parse.File, async ({ file }) => {
|
||||
expect(file.name()).toBe('popeye.txt');
|
||||
const fileData = await file.getData();
|
||||
const newFile = new Parse.File('2020-04-01.txt', { base64: fileData });
|
||||
@@ -3346,13 +3550,13 @@ describe('saveFile hooks', () => {
|
||||
it('afterSaveFile should set fileSize to null if beforeSave returns an already saved file', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSaveFile(req => {
|
||||
Parse.Cloud.beforeSave(Parse.File, req => {
|
||||
expect(req.fileSize).toBe(3);
|
||||
const newFile = new Parse.File('some-file.txt');
|
||||
newFile._url = 'http://www.somewhere.com/parse/files/some-app-id/some-file.txt';
|
||||
return newFile;
|
||||
});
|
||||
Parse.Cloud.afterSaveFile(req => {
|
||||
Parse.Cloud.afterSave(Parse.File, req => {
|
||||
expect(req.fileSize).toBe(null);
|
||||
});
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
@@ -3365,7 +3569,7 @@ describe('saveFile hooks', () => {
|
||||
|
||||
it('afterSaveFile should throw error', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.afterSaveFile(async () => {
|
||||
Parse.Cloud.afterSave(Parse.File, async () => {
|
||||
throw new Parse.Error(400, 'some-error-message');
|
||||
});
|
||||
const filename = 'donald_duck.pdf';
|
||||
@@ -3379,11 +3583,11 @@ describe('saveFile hooks', () => {
|
||||
|
||||
it('afterSaveFile should call with fileObject', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSaveFile(async req => {
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
req.file.setTags({ tagA: 'some-tag' });
|
||||
req.file.setMetadata({ foo: 'bar' });
|
||||
});
|
||||
Parse.Cloud.afterSaveFile(async req => {
|
||||
Parse.Cloud.afterSave(Parse.File, async req => {
|
||||
expect(req.master).toBe(true);
|
||||
expect(req.file._tags).toEqual({ tagA: 'some-tag' });
|
||||
expect(req.file._metadata).toEqual({ foo: 'bar' });
|
||||
@@ -3395,13 +3599,13 @@ describe('saveFile hooks', () => {
|
||||
|
||||
it('afterSaveFile should change fileSize when file data changes', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSaveFile(async req => {
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
expect(req.fileSize).toBe(3);
|
||||
expect(req.master).toBe(true);
|
||||
const newFile = new Parse.File('donald_duck.pdf', [4, 5, 6, 7, 8, 9], 'application/pdf');
|
||||
return newFile;
|
||||
});
|
||||
Parse.Cloud.afterSaveFile(async req => {
|
||||
Parse.Cloud.afterSave(Parse.File, async req => {
|
||||
expect(req.fileSize).toBe(6);
|
||||
expect(req.master).toBe(true);
|
||||
done();
|
||||
@@ -3412,7 +3616,7 @@ describe('saveFile hooks', () => {
|
||||
|
||||
it('beforeDeleteFile should call with fileObject', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeDeleteFile(req => {
|
||||
Parse.Cloud.beforeDelete(Parse.File, req => {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
expect(req.file._name).toEqual('popeye.txt');
|
||||
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
|
||||
@@ -3424,7 +3628,7 @@ describe('saveFile hooks', () => {
|
||||
|
||||
it('beforeDeleteFile should throw error', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeDeleteFile(() => {
|
||||
Parse.Cloud.beforeDelete(Parse.File, () => {
|
||||
throw new Error('some error message');
|
||||
});
|
||||
const file = new Parse.File('popeye.txt');
|
||||
@@ -3438,12 +3642,12 @@ describe('saveFile hooks', () => {
|
||||
|
||||
it('afterDeleteFile should call with fileObject', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeDeleteFile(req => {
|
||||
Parse.Cloud.beforeDelete(Parse.File, req => {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
expect(req.file._name).toEqual('popeye.txt');
|
||||
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
|
||||
});
|
||||
Parse.Cloud.afterDeleteFile(req => {
|
||||
Parse.Cloud.afterDelete(Parse.File, req => {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
expect(req.file._name).toEqual('popeye.txt');
|
||||
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
|
||||
@@ -3455,7 +3659,7 @@ describe('saveFile hooks', () => {
|
||||
|
||||
it('beforeSaveFile should not change file if nothing is returned', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSaveFile(() => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
return;
|
||||
});
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
@@ -3464,7 +3668,7 @@ describe('saveFile hooks', () => {
|
||||
});
|
||||
|
||||
it('throw custom error from beforeSaveFile', async done => {
|
||||
Parse.Cloud.beforeSaveFile(() => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail');
|
||||
});
|
||||
try {
|
||||
@@ -3478,7 +3682,7 @@ describe('saveFile hooks', () => {
|
||||
});
|
||||
|
||||
it('throw empty error from beforeSaveFile', async done => {
|
||||
Parse.Cloud.beforeSaveFile(() => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw null;
|
||||
});
|
||||
try {
|
||||
@@ -3490,6 +3694,55 @@ describe('saveFile hooks', () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('legacy hooks', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
const triggers = {
|
||||
beforeSaveFile(req) {
|
||||
req.file.setTags({ tagA: 'some-tag' });
|
||||
req.file.setMetadata({ foo: 'bar' });
|
||||
expect(req.triggerName).toEqual('beforeSave');
|
||||
expect(req.master).toBe(true);
|
||||
},
|
||||
afterSaveFile(req) {
|
||||
expect(req.master).toBe(true);
|
||||
expect(req.file._tags).toEqual({ tagA: 'some-tag' });
|
||||
expect(req.file._metadata).toEqual({ foo: 'bar' });
|
||||
},
|
||||
beforeDeleteFile(req) {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
expect(req.file._name).toEqual('popeye.txt');
|
||||
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
|
||||
expect(req.fileSize).toBe(null);
|
||||
},
|
||||
afterDeleteFile(req) {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
expect(req.file._name).toEqual('popeye.txt');
|
||||
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
|
||||
},
|
||||
};
|
||||
|
||||
for (const key in triggers) {
|
||||
spyOn(triggers, key).and.callThrough();
|
||||
Parse.Cloud[key](triggers[key]);
|
||||
}
|
||||
|
||||
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save({ useMasterKey: true });
|
||||
await new Parse.File('popeye.txt', [1, 2, 3], 'text/plain').destroy({ useMasterKey: true });
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
for (const key in triggers) {
|
||||
expect(triggers[key]).toHaveBeenCalled();
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
`DeprecationWarning: Parse.Cloud.${key} is deprecated and will be removed in a future version. Use Parse.Cloud.${key.replace(
|
||||
'File',
|
||||
''
|
||||
)}(Parse.File, (request) => {})`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendEmail', () => {
|
||||
@@ -3516,23 +3769,4 @@ describe('sendEmail', () => {
|
||||
'Failed to send email because no mail adapter is configured for Parse Server.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should have object found with nested relational data query', async () => {
|
||||
const obj1 = Parse.Object.extend('TestObject');
|
||||
const obj2 = Parse.Object.extend('TestObject2');
|
||||
let item2 = new obj2();
|
||||
item2 = await item2.save();
|
||||
let item1 = new obj1();
|
||||
const relation = item1.relation('rel');
|
||||
relation.add(item2);
|
||||
item1 = await item1.save();
|
||||
Parse.Cloud.beforeFind('TestObject', req => {
|
||||
const additionalQ = new Parse.Query('TestObject');
|
||||
additionalQ.equalTo('rel', item2);
|
||||
return Parse.Query.and(req.query, additionalQ);
|
||||
});
|
||||
const q = new Parse.Query('TestObject');
|
||||
const res = await q.first();
|
||||
expect(res.id).toEqual(item1.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -432,6 +432,42 @@ describe('DefinedSchemas', () => {
|
||||
expect(testSchema.indexes).toBeUndefined();
|
||||
expect(userSchema.indexes).toEqual(expectedIndexes);
|
||||
});
|
||||
|
||||
it('should detect protected indexes for _User class', () => {
|
||||
const definedSchema = new DefinedSchemas({}, {});
|
||||
const protectedUserIndexes = ['_id_', 'case_insensitive_email', 'username_1', 'email_1'];
|
||||
protectedUserIndexes.forEach(field => {
|
||||
expect(definedSchema.isProtectedIndex('_User', field)).toEqual(true);
|
||||
});
|
||||
expect(definedSchema.isProtectedIndex('_User', 'test')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should detect protected indexes for _Role class', () => {
|
||||
const definedSchema = new DefinedSchemas({}, {});
|
||||
expect(definedSchema.isProtectedIndex('_Role', 'name_1')).toEqual(true);
|
||||
expect(definedSchema.isProtectedIndex('_Role', 'test')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should detect protected indexes for _Idempotency class', () => {
|
||||
const definedSchema = new DefinedSchemas({}, {});
|
||||
expect(definedSchema.isProtectedIndex('_Idempotency', 'reqId_1')).toEqual(true);
|
||||
expect(definedSchema.isProtectedIndex('_Idempotency', 'test')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should not detect protected indexes on user defined class', () => {
|
||||
const definedSchema = new DefinedSchemas({}, {});
|
||||
const protectedIndexes = [
|
||||
'case_insensitive_email',
|
||||
'username_1',
|
||||
'email_1',
|
||||
'reqId_1',
|
||||
'name_1',
|
||||
];
|
||||
protectedIndexes.forEach(field => {
|
||||
expect(definedSchema.isProtectedIndex('ExampleClass', field)).toEqual(false);
|
||||
});
|
||||
expect(definedSchema.isProtectedIndex('ExampleClass', '_id_')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ClassLevelPermissions', () => {
|
||||
|
||||
@@ -3,7 +3,6 @@ const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapte
|
||||
.WinstonLoggerAdapter;
|
||||
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
|
||||
.GridFSBucketAdapter;
|
||||
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
|
||||
const Config = require('../lib/Config');
|
||||
const FilesController = require('../lib/Controllers/FilesController').default;
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
@@ -24,8 +23,8 @@ const mockAdapter = {
|
||||
describe('FilesController', () => {
|
||||
it('should properly expand objects', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
const filesController = new FilesController(gridStoreAdapter);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
const result = filesController.expandFilesInObject(config, function () {});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
@@ -44,6 +43,20 @@ describe('FilesController', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it_only_db('mongo')('should pass databaseOptions to GridFSBucketAdapter', async () => {
|
||||
await reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/parse',
|
||||
filesAdapter: null,
|
||||
databaseAdapter: null,
|
||||
databaseOptions: {
|
||||
retryWrites: true,
|
||||
},
|
||||
});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
expect(config.database.adapter._mongoOptions.retryWrites).toBeTrue();
|
||||
expect(config.filesController.adapter._mongoOptions.retryWrites).toBeTrue();
|
||||
});
|
||||
|
||||
it('should create a server log on failure', done => {
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
@@ -88,19 +101,19 @@ describe('FilesController', () => {
|
||||
|
||||
it('should add a unique hash to the file name when the preserveFileName option is false', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridStoreAdapter, 'createFile');
|
||||
gridStoreAdapter.createFile.and.returnValue(Promise.resolve());
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridFSAdapter, 'createFile');
|
||||
gridFSAdapter.createFile.and.returnValue(Promise.resolve());
|
||||
const fileName = 'randomFileName.pdf';
|
||||
const regexEscapedFileName = fileName.replace(/\./g, '\\$&');
|
||||
const filesController = new FilesController(gridStoreAdapter, null, {
|
||||
const filesController = new FilesController(gridFSAdapter, null, {
|
||||
preserveFileName: false,
|
||||
});
|
||||
|
||||
filesController.createFile(config, fileName);
|
||||
|
||||
expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toMatch(
|
||||
expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toMatch(
|
||||
`^.{32}_${regexEscapedFileName}$`
|
||||
);
|
||||
|
||||
@@ -109,42 +122,42 @@ describe('FilesController', () => {
|
||||
|
||||
it('should not add a unique hash to the file name when the preserveFileName option is true', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridStoreAdapter, 'createFile');
|
||||
gridStoreAdapter.createFile.and.returnValue(Promise.resolve());
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridFSAdapter, 'createFile');
|
||||
gridFSAdapter.createFile.and.returnValue(Promise.resolve());
|
||||
const fileName = 'randomFileName.pdf';
|
||||
const filesController = new FilesController(gridStoreAdapter, null, {
|
||||
const filesController = new FilesController(gridFSAdapter, null, {
|
||||
preserveFileName: true,
|
||||
});
|
||||
|
||||
filesController.createFile(config, fileName);
|
||||
|
||||
expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName);
|
||||
expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should handle adapter without getMetadata', async () => {
|
||||
const gridStoreAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
gridStoreAdapter.getMetadata = null;
|
||||
const filesController = new FilesController(gridStoreAdapter);
|
||||
const gridFSAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
gridFSAdapter.getMetadata = null;
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
|
||||
const result = await filesController.getMetadata();
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should reject slashes in file names', done => {
|
||||
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
const fileName = 'foo/randomFileName.pdf';
|
||||
expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null);
|
||||
expect(gridFSAdapter.validateFilename(fileName)).not.toBe(null);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should also reject slashes in file names', done => {
|
||||
const gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse');
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
const fileName = 'foo/randomFileName.pdf';
|
||||
expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null);
|
||||
expect(gridFSAdapter.validateFilename(fileName)).not.toBe(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
|
||||
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
|
||||
.GridFSBucketAdapter;
|
||||
const { randomString } = require('../lib/cryptoUtils');
|
||||
@@ -14,25 +13,13 @@ async function expectMissingFile(gfsAdapter, name) {
|
||||
}
|
||||
}
|
||||
|
||||
describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
|
||||
describe_only_db('mongo')('GridFSBucket', () => {
|
||||
beforeEach(async () => {
|
||||
const gsAdapter = new GridStoreAdapter(databaseURI);
|
||||
const gsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
const db = await gsAdapter._connect();
|
||||
await db.dropDatabase();
|
||||
});
|
||||
|
||||
it('a file created in GridStore should be available in GridFS', async () => {
|
||||
const gsAdapter = new GridStoreAdapter(databaseURI);
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
await expectMissingFile(gfsAdapter, 'myFileName');
|
||||
const originalString = 'abcdefghi';
|
||||
await gsAdapter.createFile('myFileName', originalString);
|
||||
const gsResult = await gsAdapter.getFileData('myFileName');
|
||||
expect(gsResult.toString('utf8')).toBe(originalString);
|
||||
const gfsResult = await gfsAdapter.getFileData('myFileName');
|
||||
expect(gfsResult.toString('utf8')).toBe(originalString);
|
||||
});
|
||||
|
||||
it('should save an encrypted file that can only be decrypted by a GridFS adapter with the encryptionKey', async () => {
|
||||
const unencryptedAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
const encryptedAdapter = new GridFSBucketAdapter(
|
||||
@@ -451,7 +438,7 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
|
||||
await db.admin().serverStatus();
|
||||
expect(false).toBe(true);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('topology was destroyed');
|
||||
expect(e.message).toEqual('Client must be connected before running operations');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
const MongoClient = require('mongodb').MongoClient;
|
||||
const GridStore = require('mongodb').GridStore;
|
||||
|
||||
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
|
||||
const Config = require('../lib/Config');
|
||||
const FilesController = require('../lib/Controllers/FilesController').default;
|
||||
|
||||
// Small additional tests to improve overall coverage
|
||||
describe_only_db('mongo')('GridStoreAdapter', () => {
|
||||
it('should properly instanciate the GridStore when deleting a file', async done => {
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridStoreAdapter = new GridStoreAdapter(databaseURI);
|
||||
const db = await gridStoreAdapter._connect();
|
||||
await db.dropDatabase();
|
||||
const filesController = new FilesController(gridStoreAdapter, Parse.applicationId, {});
|
||||
|
||||
// save original unlink before redefinition
|
||||
const originalUnlink = GridStore.prototype.unlink;
|
||||
|
||||
let gridStoreMode;
|
||||
|
||||
// new unlink method that will capture the mode in which GridStore was opened
|
||||
GridStore.prototype.unlink = function () {
|
||||
// restore original unlink during first call
|
||||
GridStore.prototype.unlink = originalUnlink;
|
||||
|
||||
gridStoreMode = this.mode;
|
||||
|
||||
return originalUnlink.call(this);
|
||||
};
|
||||
|
||||
filesController
|
||||
.createFile(config, 'myFilename.txt', 'my file content', 'text/plain')
|
||||
.then(myFile => {
|
||||
return MongoClient.connect(databaseURI)
|
||||
.then(client => {
|
||||
const database = client.db(client.s.options.dbName);
|
||||
// Verify the existance of the fs.files document
|
||||
return database
|
||||
.collection('fs.files')
|
||||
.count()
|
||||
.then(count => {
|
||||
expect(count).toEqual(1);
|
||||
return { database, client };
|
||||
});
|
||||
})
|
||||
.then(({ database, client }) => {
|
||||
// Verify the existance of the fs.files document
|
||||
return database
|
||||
.collection('fs.chunks')
|
||||
.count()
|
||||
.then(count => {
|
||||
expect(count).toEqual(1);
|
||||
return client.close();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return filesController.deleteFile(config, myFile.name);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return MongoClient.connect(databaseURI)
|
||||
.then(client => {
|
||||
const database = client.db(client.s.options.dbName);
|
||||
// Verify the existance of the fs.files document
|
||||
return database
|
||||
.collection('fs.files')
|
||||
.count()
|
||||
.then(count => {
|
||||
expect(count).toEqual(0);
|
||||
return { database, client };
|
||||
});
|
||||
})
|
||||
.then(({ database, client }) => {
|
||||
// Verify the existance of the fs.files document
|
||||
return database
|
||||
.collection('fs.chunks')
|
||||
.count()
|
||||
.then(count => {
|
||||
expect(count).toEqual(0);
|
||||
return client.close();
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// Verify that gridStore was opened in read only mode
|
||||
expect(gridStoreMode).toEqual('r');
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(fail);
|
||||
});
|
||||
|
||||
it('handleShutdown, close connection', async () => {
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
const gridStoreAdapter = new GridStoreAdapter(databaseURI);
|
||||
|
||||
const db = await gridStoreAdapter._connect();
|
||||
const status = await db.admin().serverStatus();
|
||||
expect(status.connections.current > 0).toEqual(true);
|
||||
|
||||
await gridStoreAdapter.handleShutdown();
|
||||
try {
|
||||
await db.admin().serverStatus();
|
||||
expect(false).toBe(true);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('topology was destroyed');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -6,11 +6,14 @@ const rest = require('../lib/rest');
|
||||
const auth = require('../lib/Auth');
|
||||
const uuid = require('uuid');
|
||||
|
||||
describe_only_db('mongo')('Idempotency', () => {
|
||||
describe('Idempotency', () => {
|
||||
// Parameters
|
||||
/** Enable TTL expiration simulated by removing entry instead of waiting for MongoDB TTL monitor which
|
||||
runs only every 60s, so it can take up to 119s until entry removal - ain't nobody got time for that */
|
||||
const SIMULATE_TTL = true;
|
||||
const ttl = 2;
|
||||
const maxTimeOut = 4000;
|
||||
|
||||
// Helpers
|
||||
async function deleteRequestEntry(reqId) {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
@@ -38,9 +41,14 @@ describe_only_db('mongo')('Idempotency', () => {
|
||||
}
|
||||
await setup({
|
||||
paths: ['functions/.*', 'jobs/.*', 'classes/.*', 'users', 'installations'],
|
||||
ttl: 30,
|
||||
ttl: ttl,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
|
||||
});
|
||||
|
||||
// Tests
|
||||
it('should enforce idempotency for cloud code function', async () => {
|
||||
let counter = 0;
|
||||
@@ -56,7 +64,7 @@ describe_only_db('mongo')('Idempotency', () => {
|
||||
'X-Parse-Request-Id': 'abc-123',
|
||||
},
|
||||
};
|
||||
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(30);
|
||||
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(ttl);
|
||||
await request(params);
|
||||
await request(params).then(fail, e => {
|
||||
expect(e.status).toEqual(400);
|
||||
@@ -83,12 +91,38 @@ describe_only_db('mongo')('Idempotency', () => {
|
||||
if (SIMULATE_TTL) {
|
||||
await deleteRequestEntry('abc-123');
|
||||
} else {
|
||||
await new Promise(resolve => setTimeout(resolve, 130000));
|
||||
await new Promise(resolve => setTimeout(resolve, maxTimeOut));
|
||||
}
|
||||
await expectAsync(request(params)).toBeResolved();
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it_only_db('postgres')(
|
||||
'should delete request entry when postgress ttl function is called',
|
||||
async () => {
|
||||
const client = Config.get(Parse.applicationId).database.adapter._client;
|
||||
let counter = 0;
|
||||
Parse.Cloud.define('myFunction', () => {
|
||||
counter++;
|
||||
});
|
||||
const params = {
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/myFunction',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Request-Id': 'abc-123',
|
||||
},
|
||||
};
|
||||
await expectAsync(request(params)).toBeResolved();
|
||||
await expectAsync(request(params)).toBeRejected();
|
||||
await new Promise(resolve => setTimeout(resolve, maxTimeOut));
|
||||
await client.one('SELECT idempotency_delete_expired_records()');
|
||||
await expectAsync(request(params)).toBeResolved();
|
||||
expect(counter).toBe(2);
|
||||
}
|
||||
);
|
||||
|
||||
it('should enforce idempotency for cloud code jobs', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.job('myJob', () => {
|
||||
|
||||
+3
-118
@@ -158,78 +158,6 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.connection = { remoteAddress: 'ip3' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.connection = { remoteAddress: 'ip1' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.socket = { remoteAddress: 'ip3' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.socket = { remoteAddress: 'ip1' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if the connection.socket.remoteAddress does not belong to masterKeyIps list', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.connection = { socket: { remoteAddress: 'ip3' } };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
});
|
||||
fakeReq.connection = { socket: { remoteAddress: 'ip1' } };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow any ip to use masterKey if masterKeyIps is empty', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
@@ -243,52 +171,9 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed if xff header does belong to masterKeyIps', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1'],
|
||||
});
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed if xff header with one ip does belong to masterKeyIps', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1'],
|
||||
});
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
fakeReq.headers['x-forwarded-for'] = 'ip1';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if xff header does not belong to masterKeyIps', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip4'],
|
||||
});
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should not succeed if xff header is empty and masterKeyIps is set', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1'],
|
||||
});
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
fakeReq.headers['x-forwarded-for'] = '';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
it('can set trust proxy', async () => {
|
||||
const server = await reconfigureServer({ trustProxy: 1 });
|
||||
expect(server.app.parent.settings['trust proxy']).toBe(1);
|
||||
});
|
||||
|
||||
it('should properly expose the headers', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default;
|
||||
const { MongoClient } = require('mongodb');
|
||||
const { MongoClient, Collection } = require('mongodb');
|
||||
const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||
const request = require('../lib/request');
|
||||
const Config = require('../lib/Config');
|
||||
@@ -101,7 +101,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
done.fail('Find succeeded despite taking too long!');
|
||||
},
|
||||
err => {
|
||||
expect(err.name).toEqual('MongoError');
|
||||
expect(err.name).toEqual('MongoServerError');
|
||||
expect(err.code).toEqual(50);
|
||||
expect(err.message).toMatch('operation exceeded time limit');
|
||||
done();
|
||||
@@ -283,7 +283,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
await adapter.database.admin().serverStatus();
|
||||
expect(false).toBe(true);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('topology was destroyed');
|
||||
expect(e.message).toEqual('Client must be connected before running operations');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
|
||||
});
|
||||
|
||||
it('should use index for caseInsensitive query', async () => {
|
||||
it_only_mongodb_version('<5.1>=6')('should use index for caseInsensitive query', async () => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'Bugs');
|
||||
user.set('password', 'Bunny');
|
||||
@@ -342,6 +342,40 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
expect(postIndexPlan.executionStats.executionStages.stage).toBe('FETCH');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<6')('should use index for caseInsensitive query', async () => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'Bugs');
|
||||
user.set('password', 'Bunny');
|
||||
await user.signUp();
|
||||
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
await database.adapter.dropAllIndexes('_User');
|
||||
|
||||
const preIndexPlan = await database.find(
|
||||
'_User',
|
||||
{ username: 'bugs' },
|
||||
{ caseInsensitive: true, explain: true }
|
||||
);
|
||||
|
||||
const schema = await new Parse.Schema('_User').get();
|
||||
|
||||
await database.adapter.ensureIndex(
|
||||
'_User',
|
||||
schema,
|
||||
['username'],
|
||||
'case_insensitive_username',
|
||||
true
|
||||
);
|
||||
|
||||
const postIndexPlan = await database.find(
|
||||
'_User',
|
||||
{ username: 'bugs' },
|
||||
{ caseInsensitive: true, explain: true }
|
||||
);
|
||||
expect(preIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('COLLSCAN');
|
||||
expect(postIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
|
||||
});
|
||||
|
||||
it('should delete field without index', async () => {
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
const obj = new Parse.Object('MyObject');
|
||||
@@ -392,8 +426,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
|
||||
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
await request({
|
||||
method: 'POST',
|
||||
@@ -412,9 +445,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
|
||||
let found = false;
|
||||
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
|
||||
Collection.prototype.findOneAndUpdate.calls.all().forEach(call => {
|
||||
found = true;
|
||||
expect(call.args[2].session.transaction.state).not.toBe('NO_TRANSACTION');
|
||||
expect(call.args[2].session.transaction.state).toBe('TRANSACTION_COMMITTED');
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
@@ -423,8 +456,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
|
||||
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
await request({
|
||||
method: 'POST',
|
||||
@@ -443,9 +475,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
|
||||
let found = false;
|
||||
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
|
||||
Collection.prototype.findOneAndUpdate.calls.all().forEach(call => {
|
||||
found = true;
|
||||
expect(call.args[2].session).toBe(undefined);
|
||||
expect(call.args[2].session).toBeFalsy();
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
@@ -454,8 +486,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
|
||||
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
await request({
|
||||
method: 'POST',
|
||||
@@ -473,9 +504,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
|
||||
let found = false;
|
||||
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
|
||||
Collection.prototype.findOneAndUpdate.calls.all().forEach(call => {
|
||||
found = true;
|
||||
expect(call.args[2].session).toBe(undefined);
|
||||
expect(call.args[2].session).toBeFalsy();
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
@@ -484,8 +515,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
|
||||
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
await request({
|
||||
method: 'PUT',
|
||||
@@ -495,30 +525,28 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
|
||||
let found = false;
|
||||
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
|
||||
Collection.prototype.findOneAndUpdate.calls.all().forEach(call => {
|
||||
found = true;
|
||||
expect(call.args[2].session).toBe(undefined);
|
||||
expect(call.args[2].session).toBeFalsy();
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
it('should not use transactions when using SDK insert', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'insert').and.callThrough();
|
||||
spyOn(Collection.prototype, 'insertOne').and.callThrough();
|
||||
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const calls = databaseAdapter.database.serverConfig.insert.calls.all();
|
||||
const calls = Collection.prototype.insertOne.calls.all();
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
calls.forEach(call => {
|
||||
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
|
||||
expect(call.args[1].session).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not use transactions when using SDK update', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'update').and.callThrough();
|
||||
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
@@ -526,26 +554,25 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
myObject.set('myAttribute', 'myValue');
|
||||
await myObject.save();
|
||||
|
||||
const calls = databaseAdapter.database.serverConfig.update.calls.all();
|
||||
const calls = Collection.prototype.findOneAndUpdate.calls.all();
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
calls.forEach(call => {
|
||||
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
|
||||
expect(call.args[2].session).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not use transactions when using SDK delete', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'remove').and.callThrough();
|
||||
spyOn(Collection.prototype, 'deleteMany').and.callThrough();
|
||||
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
await myObject.destroy();
|
||||
|
||||
const calls = databaseAdapter.database.serverConfig.remove.calls.all();
|
||||
const calls = Collection.prototype.deleteMany.calls.all();
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
calls.forEach(call => {
|
||||
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
|
||||
expect(call.args[1].session).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+12
-11
@@ -4,6 +4,7 @@
|
||||
const transform = require('../lib/Adapters/Storage/Mongo/MongoTransform');
|
||||
const dd = require('deep-diff');
|
||||
const mongodb = require('mongodb');
|
||||
const Utils = require('../lib/Utils');
|
||||
|
||||
describe('parseObjectToMongoObjectForCreate', () => {
|
||||
it('a basic number', done => {
|
||||
@@ -592,7 +593,7 @@ describe('relativeTimeToDate', () => {
|
||||
describe('In the future', () => {
|
||||
it('should parse valid natural time', () => {
|
||||
const text = 'in 1 year 2 weeks 12 days 10 hours 24 minutes 30 seconds';
|
||||
const { result, status, info } = transform.relativeTimeToDate(text, now);
|
||||
const { result, status, info } = Utils.relativeTimeToDate(text, now);
|
||||
expect(result.toISOString()).toBe('2018-10-22T23:52:46.617Z');
|
||||
expect(status).toBe('success');
|
||||
expect(info).toBe('future');
|
||||
@@ -602,7 +603,7 @@ describe('relativeTimeToDate', () => {
|
||||
describe('In the past', () => {
|
||||
it('should parse valid natural time', () => {
|
||||
const text = '2 days 12 hours 1 minute 12 seconds ago';
|
||||
const { result, status, info } = transform.relativeTimeToDate(text, now);
|
||||
const { result, status, info } = Utils.relativeTimeToDate(text, now);
|
||||
expect(result.toISOString()).toBe('2017-09-24T01:27:04.617Z');
|
||||
expect(status).toBe('success');
|
||||
expect(info).toBe('past');
|
||||
@@ -612,7 +613,7 @@ describe('relativeTimeToDate', () => {
|
||||
describe('From now', () => {
|
||||
it('should equal current time', () => {
|
||||
const text = 'now';
|
||||
const { result, status, info } = transform.relativeTimeToDate(text, now);
|
||||
const { result, status, info } = Utils.relativeTimeToDate(text, now);
|
||||
expect(result.toISOString()).toBe('2017-09-26T13:28:16.617Z');
|
||||
expect(status).toBe('success');
|
||||
expect(info).toBe('present');
|
||||
@@ -621,54 +622,54 @@ describe('relativeTimeToDate', () => {
|
||||
|
||||
describe('Error cases', () => {
|
||||
it('should error if string is completely gibberish', () => {
|
||||
expect(transform.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
|
||||
status: 'error',
|
||||
info: "Time should either start with 'in' or end with 'ago'",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if string contains neither `ago` nor `in`', () => {
|
||||
expect(transform.relativeTimeToDate('12 hours 1 minute')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('12 hours 1 minute')).toEqual({
|
||||
status: 'error',
|
||||
info: "Time should either start with 'in' or end with 'ago'",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if there are missing units or numbers', () => {
|
||||
expect(transform.relativeTimeToDate('in 12 hours 1')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('in 12 hours 1')).toEqual({
|
||||
status: 'error',
|
||||
info: 'Invalid time string. Dangling unit or number.',
|
||||
});
|
||||
|
||||
expect(transform.relativeTimeToDate('12 hours minute ago')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('12 hours minute ago')).toEqual({
|
||||
status: 'error',
|
||||
info: 'Invalid time string. Dangling unit or number.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error on floating point numbers', () => {
|
||||
expect(transform.relativeTimeToDate('in 12.3 hours')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('in 12.3 hours')).toEqual({
|
||||
status: 'error',
|
||||
info: "'12.3' is not an integer.",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if numbers are invalid', () => {
|
||||
expect(transform.relativeTimeToDate('12 hours 123a minute ago')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('12 hours 123a minute ago')).toEqual({
|
||||
status: 'error',
|
||||
info: "'123a' is not an integer.",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error on invalid interval units', () => {
|
||||
expect(transform.relativeTimeToDate('4 score 7 years ago')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('4 score 7 years ago')).toEqual({
|
||||
status: 'error',
|
||||
info: "Invalid interval: 'score'",
|
||||
});
|
||||
});
|
||||
|
||||
it("should error when string contains 'ago' and 'in'", () => {
|
||||
expect(transform.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
|
||||
status: 'error',
|
||||
info: "Time cannot have both 'in' and 'ago'",
|
||||
});
|
||||
|
||||
+146
-49
@@ -951,57 +951,154 @@ describe('miscellaneous', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the updated fields on PUT', done => {
|
||||
it('return the updated fields on PUT', async () => {
|
||||
const obj = new Parse.Object('GameScore');
|
||||
obj
|
||||
.save({ a: 'hello', c: 1, d: ['1'], e: ['1'], f: ['1', '2'] })
|
||||
.then(() => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Installation-Id': 'yolo',
|
||||
};
|
||||
request({
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore/' + obj.id,
|
||||
body: JSON.stringify({
|
||||
a: 'b',
|
||||
c: { __op: 'Increment', amount: 2 },
|
||||
d: { __op: 'Add', objects: ['2'] },
|
||||
e: { __op: 'AddUnique', objects: ['1', '2'] },
|
||||
f: { __op: 'Remove', objects: ['2'] },
|
||||
selfThing: {
|
||||
__type: 'Pointer',
|
||||
className: 'GameScore',
|
||||
objectId: obj.id,
|
||||
},
|
||||
}),
|
||||
}).then(response => {
|
||||
try {
|
||||
const body = response.data;
|
||||
expect(body.a).toBeUndefined();
|
||||
expect(body.c).toEqual(3); // 2+1
|
||||
expect(body.d.length).toBe(2);
|
||||
expect(body.d.indexOf('1') > -1).toBe(true);
|
||||
expect(body.d.indexOf('2') > -1).toBe(true);
|
||||
expect(body.e.length).toBe(2);
|
||||
expect(body.e.indexOf('1') > -1).toBe(true);
|
||||
expect(body.e.indexOf('2') > -1).toBe(true);
|
||||
expect(body.f.length).toBe(1);
|
||||
expect(body.f.indexOf('1') > -1).toBe(true);
|
||||
// return nothing on other self
|
||||
expect(body.selfThing).toBeUndefined();
|
||||
// updatedAt is always set
|
||||
expect(body.updatedAt).not.toBeUndefined();
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
}
|
||||
done();
|
||||
});
|
||||
const pointer = new Parse.Object('Child');
|
||||
await pointer.save();
|
||||
obj.set(
|
||||
'point',
|
||||
new Parse.GeoPoint({
|
||||
latitude: 37.4848,
|
||||
longitude: -122.1483,
|
||||
})
|
||||
.catch(done.fail);
|
||||
);
|
||||
obj.set('array', ['obj1', 'obj2']);
|
||||
obj.set('objects', { a: 'b' });
|
||||
obj.set('string', 'abc');
|
||||
obj.set('bool', true);
|
||||
obj.set('number', 1);
|
||||
obj.set('date', new Date());
|
||||
obj.set('pointer', pointer);
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Installation-Id': 'yolo',
|
||||
};
|
||||
const saveResponse = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore',
|
||||
body: JSON.stringify({
|
||||
a: 'hello',
|
||||
c: 1,
|
||||
d: ['1'],
|
||||
e: ['1'],
|
||||
f: ['1', '2'],
|
||||
...obj.toJSON(),
|
||||
}),
|
||||
});
|
||||
expect(Object.keys(saveResponse.data).sort()).toEqual(['createdAt', 'objectId']);
|
||||
obj.id = saveResponse.data.objectId;
|
||||
const response = await request({
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore/' + obj.id,
|
||||
body: JSON.stringify({
|
||||
a: 'b',
|
||||
c: { __op: 'Increment', amount: 2 },
|
||||
d: { __op: 'Add', objects: ['2'] },
|
||||
e: { __op: 'AddUnique', objects: ['1', '2'] },
|
||||
f: { __op: 'Remove', objects: ['2'] },
|
||||
selfThing: {
|
||||
__type: 'Pointer',
|
||||
className: 'GameScore',
|
||||
objectId: obj.id,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const body = response.data;
|
||||
expect(Object.keys(body).sort()).toEqual(['c', 'd', 'e', 'f', 'updatedAt']);
|
||||
expect(body.a).toBeUndefined();
|
||||
expect(body.c).toEqual(3); // 2+1
|
||||
expect(body.d.length).toBe(2);
|
||||
expect(body.d.indexOf('1') > -1).toBe(true);
|
||||
expect(body.d.indexOf('2') > -1).toBe(true);
|
||||
expect(body.e.length).toBe(2);
|
||||
expect(body.e.indexOf('1') > -1).toBe(true);
|
||||
expect(body.e.indexOf('2') > -1).toBe(true);
|
||||
expect(body.f.length).toBe(1);
|
||||
expect(body.f.indexOf('1') > -1).toBe(true);
|
||||
expect(body.selfThing).toBeUndefined();
|
||||
expect(body.updatedAt).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should response should not change with triggers', async () => {
|
||||
const obj = new Parse.Object('GameScore');
|
||||
const pointer = new Parse.Object('Child');
|
||||
Parse.Cloud.beforeSave('GameScore', request => {
|
||||
return request.object;
|
||||
});
|
||||
Parse.Cloud.afterSave('GameScore', request => {
|
||||
return request.object;
|
||||
});
|
||||
await pointer.save();
|
||||
obj.set(
|
||||
'point',
|
||||
new Parse.GeoPoint({
|
||||
latitude: 37.4848,
|
||||
longitude: -122.1483,
|
||||
})
|
||||
);
|
||||
obj.set('array', ['obj1', 'obj2']);
|
||||
obj.set('objects', { a: 'b' });
|
||||
obj.set('string', 'abc');
|
||||
obj.set('bool', true);
|
||||
obj.set('number', 1);
|
||||
obj.set('date', new Date());
|
||||
obj.set('pointer', pointer);
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Installation-Id': 'yolo',
|
||||
};
|
||||
const saveResponse = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore',
|
||||
body: JSON.stringify({
|
||||
a: 'hello',
|
||||
c: 1,
|
||||
d: ['1'],
|
||||
e: ['1'],
|
||||
f: ['1', '2'],
|
||||
...obj.toJSON(),
|
||||
}),
|
||||
});
|
||||
expect(Object.keys(saveResponse.data).sort()).toEqual(['createdAt', 'objectId']);
|
||||
obj.id = saveResponse.data.objectId;
|
||||
const response = await request({
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore/' + obj.id,
|
||||
body: JSON.stringify({
|
||||
a: 'b',
|
||||
c: { __op: 'Increment', amount: 2 },
|
||||
d: { __op: 'Add', objects: ['2'] },
|
||||
e: { __op: 'AddUnique', objects: ['1', '2'] },
|
||||
f: { __op: 'Remove', objects: ['2'] },
|
||||
selfThing: {
|
||||
__type: 'Pointer',
|
||||
className: 'GameScore',
|
||||
objectId: obj.id,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const body = response.data;
|
||||
expect(Object.keys(body).sort()).toEqual(['c', 'd', 'e', 'f', 'updatedAt']);
|
||||
expect(body.a).toBeUndefined();
|
||||
expect(body.c).toEqual(3); // 2+1
|
||||
expect(body.d.length).toBe(2);
|
||||
expect(body.d.indexOf('1') > -1).toBe(true);
|
||||
expect(body.d.indexOf('2') > -1).toBe(true);
|
||||
expect(body.e.length).toBe(2);
|
||||
expect(body.e.indexOf('1') > -1).toBe(true);
|
||||
expect(body.e.indexOf('2') > -1).toBe(true);
|
||||
expect(body.f.length).toBe(1);
|
||||
expect(body.f.indexOf('1') > -1).toBe(true);
|
||||
expect(body.selfThing).toBeUndefined();
|
||||
expect(body.updatedAt).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('test cloud function error handling', done => {
|
||||
|
||||
+237
-10
@@ -654,7 +654,236 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
xdescribe('Gridstore Range tests', () => {
|
||||
describe('getting files', () => {
|
||||
it('does not crash on file request with invalid app ID', async () => {
|
||||
const res1 = await request({
|
||||
url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt',
|
||||
}).catch(e => e);
|
||||
expect(res1.status).toBe(403);
|
||||
expect(res1.data).toEqual({ code: 119, error: 'Invalid application ID.' });
|
||||
// Ensure server did not crash
|
||||
const res2 = await request({ url: 'http://localhost:8378/1/health' });
|
||||
expect(res2.status).toEqual(200);
|
||||
expect(res2.data).toEqual({ status: 'ok' });
|
||||
});
|
||||
|
||||
it('does not crash on file request with invalid path', async () => {
|
||||
const res1 = await request({
|
||||
url: 'http://localhost:8378/1/files/invalid-id//invalid-path/%20/invalid-file.txt',
|
||||
}).catch(e => e);
|
||||
expect(res1.status).toBe(403);
|
||||
expect(res1.data).toEqual({ error: 'unauthorized' });
|
||||
// Ensure server did not crash
|
||||
const res2 = await request({ url: 'http://localhost:8378/1/health' });
|
||||
expect(res2.status).toEqual(200);
|
||||
expect(res2.data).toEqual({ status: 'ok' });
|
||||
});
|
||||
|
||||
it('does not crash on file metadata request with invalid app ID', async () => {
|
||||
const res1 = await request({
|
||||
url: `http://localhost:8378/1/files/invalid-id/metadata/invalid-file.txt`,
|
||||
});
|
||||
expect(res1.status).toBe(200);
|
||||
expect(res1.data).toEqual({});
|
||||
// Ensure server did not crash
|
||||
const res2 = await request({ url: 'http://localhost:8378/1/health' });
|
||||
expect(res2.status).toEqual(200);
|
||||
expect(res2.data).toEqual({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
|
||||
describe_only_db('mongo')('Gridstore Range', () => {
|
||||
it('supports bytes range out of range', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: repeat('argle bargle', 100),
|
||||
});
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
Range: 'bytes=15000-18000',
|
||||
},
|
||||
});
|
||||
expect(file.headers['content-range']).toBe('bytes 1212-1212/1212');
|
||||
});
|
||||
|
||||
it('supports bytes range if end greater than start', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: repeat('argle bargle', 100),
|
||||
});
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
Range: 'bytes=15000-100',
|
||||
},
|
||||
});
|
||||
expect(file.headers['content-range']).toBe('bytes 100-1212/1212');
|
||||
});
|
||||
|
||||
it('supports bytes range if end is undefined', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: repeat('argle bargle', 100),
|
||||
});
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
Range: 'bytes=100-',
|
||||
},
|
||||
});
|
||||
expect(file.headers['content-range']).toBe('bytes 100-1212/1212');
|
||||
});
|
||||
|
||||
it('supports bytes range if start and end undefined', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: repeat('argle bargle', 100),
|
||||
});
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
Range: 'bytes=abc-efs',
|
||||
},
|
||||
}).catch(e => e);
|
||||
expect(file.headers['content-range']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('supports bytes range if start and end undefined', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: repeat('argle bargle', 100),
|
||||
});
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
},
|
||||
}).catch(e => e);
|
||||
expect(file.headers['content-range']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('supports bytes range if end is greater than size', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: repeat('argle bargle', 100),
|
||||
});
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
Range: 'bytes=0-2000',
|
||||
},
|
||||
}).catch(e => e);
|
||||
expect(file.headers['content-range']).toBe('bytes 0-1212/1212');
|
||||
});
|
||||
|
||||
it('supports bytes range if end is greater than size', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: repeat('argle bargle', 100),
|
||||
});
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
Range: 'bytes=0-2000',
|
||||
},
|
||||
}).catch(e => e);
|
||||
expect(file.headers['content-range']).toBe('bytes 0-1212/1212');
|
||||
});
|
||||
|
||||
it('supports bytes range with 0 length', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: 'a',
|
||||
}).catch(e => e);
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
Range: 'bytes=-2000',
|
||||
},
|
||||
}).catch(e => e);
|
||||
expect(file.headers['content-range']).toBe('bytes 0-1/1');
|
||||
});
|
||||
|
||||
it('supports range requests', done => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
@@ -743,7 +972,7 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
xit('supports getting last n bytes', done => {
|
||||
it('supports getting last n bytes', done => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -841,8 +1070,8 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails to stream unknown file', done => {
|
||||
request({
|
||||
it('fails to stream unknown file', async () => {
|
||||
const response = await request({
|
||||
url: 'http://localhost:8378/1/files/test/file.txt',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
@@ -850,12 +1079,10 @@ describe('Parse.File testing', () => {
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
Range: 'bytes=13-240',
|
||||
},
|
||||
}).then(response => {
|
||||
expect(response.status).toBe(404);
|
||||
const body = response.text;
|
||||
expect(body).toEqual('File not found.');
|
||||
done();
|
||||
});
|
||||
}).catch(e => e);
|
||||
expect(response.status).toBe(404);
|
||||
const body = response.text;
|
||||
expect(body).toEqual('File not found.');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ describe('ParseGraphQLSchema', () => {
|
||||
it('should load a brand new GraphQL Schema if Parse Schema changes', async () => {
|
||||
await parseGraphQLSchema.load();
|
||||
const parseClasses = parseGraphQLSchema.parseClasses;
|
||||
const parseCachedClasses = parseGraphQLSchema.parseCachedClasses;
|
||||
const parseClassTypes = parseGraphQLSchema.parseClassTypes;
|
||||
const graphQLSchema = parseGraphQLSchema.graphQLSchema;
|
||||
const graphQLTypes = parseGraphQLSchema.graphQLTypes;
|
||||
@@ -70,7 +69,6 @@ describe('ParseGraphQLSchema', () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
await parseGraphQLSchema.load();
|
||||
expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses);
|
||||
expect(parseCachedClasses).not.toBe(parseGraphQLSchema.parseCachedClasses);
|
||||
expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes);
|
||||
expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema);
|
||||
expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes);
|
||||
@@ -94,7 +92,6 @@ describe('ParseGraphQLSchema', () => {
|
||||
});
|
||||
await parseGraphQLSchema.load();
|
||||
const parseClasses = parseGraphQLSchema.parseClasses;
|
||||
const parseCachedClasses = parseGraphQLSchema.parseCachedClasses;
|
||||
const parseClassTypes = parseGraphQLSchema.parseClassTypes;
|
||||
const graphQLSchema = parseGraphQLSchema.graphQLSchema;
|
||||
const graphQLTypes = parseGraphQLSchema.graphQLTypes;
|
||||
@@ -109,7 +106,6 @@ describe('ParseGraphQLSchema', () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
await parseGraphQLSchema.load();
|
||||
expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses);
|
||||
expect(parseCachedClasses).not.toBe(parseGraphQLSchema.parseCachedClasses);
|
||||
expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes);
|
||||
expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema);
|
||||
expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes);
|
||||
|
||||
+352
-124
@@ -1,7 +1,7 @@
|
||||
const http = require('http');
|
||||
const express = require('express');
|
||||
const req = require('../lib/request');
|
||||
const fetch = require('node-fetch');
|
||||
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
|
||||
const FormData = require('form-data');
|
||||
const ws = require('ws');
|
||||
require('./helper');
|
||||
@@ -9,13 +9,17 @@ const { updateCLP } = require('./support/dev');
|
||||
|
||||
const pluralize = require('pluralize');
|
||||
const { getMainDefinition } = require('apollo-utilities');
|
||||
const { ApolloLink, split } = require('apollo-link');
|
||||
const { createHttpLink } = require('apollo-link-http');
|
||||
const { InMemoryCache } = require('apollo-cache-inmemory');
|
||||
const { createUploadLink } = require('apollo-upload-client');
|
||||
const { SubscriptionClient } = require('subscriptions-transport-ws');
|
||||
const { WebSocketLink } = require('apollo-link-ws');
|
||||
const ApolloClient = require('apollo-client').default;
|
||||
const { WebSocketLink } = require('@apollo/client/link/ws');
|
||||
const { mergeSchemas } = require('@graphql-tools/schema');
|
||||
const {
|
||||
ApolloClient,
|
||||
InMemoryCache,
|
||||
ApolloLink,
|
||||
split,
|
||||
createHttpLink,
|
||||
} = require('@apollo/client/core');
|
||||
const gql = require('graphql-tag');
|
||||
const { toGlobalId } = require('graphql-relay');
|
||||
const {
|
||||
@@ -29,7 +33,7 @@ const {
|
||||
} = require('graphql');
|
||||
const { ParseServer } = require('../');
|
||||
const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer');
|
||||
const ReadPreference = require('mongodb').ReadPreference;
|
||||
const { ReadPreference, Collection } = require('mongodb');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
function handleError(e) {
|
||||
@@ -95,6 +99,24 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('_getServer', () => {
|
||||
it('should only return new server on schema changes', async () => {
|
||||
parseGraphQLServer.server = undefined;
|
||||
const server1 = await parseGraphQLServer._getServer();
|
||||
const server2 = await parseGraphQLServer._getServer();
|
||||
expect(server1).toBe(server2);
|
||||
|
||||
// Trigger a schema change
|
||||
const obj = new Parse.Object('SomeClass');
|
||||
await obj.save();
|
||||
|
||||
const server3 = await parseGraphQLServer._getServer();
|
||||
const server4 = await parseGraphQLServer._getServer();
|
||||
expect(server3).not.toBe(server2);
|
||||
expect(server3).toBe(server4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_getGraphQLOptions', () => {
|
||||
const req = {
|
||||
info: new Object(),
|
||||
@@ -103,11 +125,15 @@ describe('ParseGraphQLServer', () => {
|
||||
};
|
||||
|
||||
it("should return schema and context with req's info, config and auth", async () => {
|
||||
const options = await parseGraphQLServer._getGraphQLOptions(req);
|
||||
const options = await parseGraphQLServer._getGraphQLOptions();
|
||||
expect(options.multipart).toEqual({
|
||||
fileSize: 20971520,
|
||||
});
|
||||
expect(options.schema).toEqual(parseGraphQLServer.parseGraphQLSchema.graphQLSchema);
|
||||
expect(options.context.info).toEqual(req.info);
|
||||
expect(options.context.config).toEqual(req.config);
|
||||
expect(options.context.auth).toEqual(req.auth);
|
||||
const contextResponse = options.context({ req });
|
||||
expect(contextResponse.info).toEqual(req.info);
|
||||
expect(contextResponse.config).toEqual(req.config);
|
||||
expect(contextResponse.auth).toEqual(req.auth);
|
||||
});
|
||||
|
||||
it('should load GraphQL schema in every call', async () => {
|
||||
@@ -464,7 +490,7 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should be cors enabled', async () => {
|
||||
it('should be cors enabled and scope the response within the source origin', async () => {
|
||||
let checked = false;
|
||||
const apolloClient = new ApolloClient({
|
||||
link: new ApolloLink((operation, forward) => {
|
||||
@@ -473,7 +499,7 @@ describe('ParseGraphQLServer', () => {
|
||||
const {
|
||||
response: { headers },
|
||||
} = context;
|
||||
expect(headers.get('access-control-allow-origin')).toEqual('*');
|
||||
expect(headers.get('access-control-allow-origin')).toEqual('http://example.com');
|
||||
checked = true;
|
||||
return response;
|
||||
});
|
||||
@@ -483,7 +509,7 @@ describe('ParseGraphQLServer', () => {
|
||||
fetch,
|
||||
headers: {
|
||||
...headers,
|
||||
Origin: 'http://someorigin.com',
|
||||
Origin: 'http://example.com',
|
||||
},
|
||||
})
|
||||
),
|
||||
@@ -501,14 +527,25 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
|
||||
it('should handle Parse headers', async () => {
|
||||
let checked = false;
|
||||
const test = {
|
||||
context: ({ req: { info, config, auth } }) => {
|
||||
expect(req.info).toBeDefined();
|
||||
expect(req.config).toBeDefined();
|
||||
expect(req.auth).toBeDefined();
|
||||
return {
|
||||
info,
|
||||
config,
|
||||
auth,
|
||||
};
|
||||
},
|
||||
};
|
||||
const contextSpy = spyOn(test, 'context');
|
||||
const originalGetGraphQLOptions = parseGraphQLServer._getGraphQLOptions;
|
||||
parseGraphQLServer._getGraphQLOptions = async req => {
|
||||
expect(req.info).toBeDefined();
|
||||
expect(req.config).toBeDefined();
|
||||
expect(req.auth).toBeDefined();
|
||||
checked = true;
|
||||
return await originalGetGraphQLOptions.bind(parseGraphQLServer)(req);
|
||||
parseGraphQLServer._getGraphQLOptions = async () => {
|
||||
return {
|
||||
schema: await parseGraphQLServer.parseGraphQLSchema.load(),
|
||||
context: test.context,
|
||||
};
|
||||
};
|
||||
const health = (
|
||||
await apolloClient.query({
|
||||
@@ -520,7 +557,7 @@ describe('ParseGraphQLServer', () => {
|
||||
})
|
||||
).data.health;
|
||||
expect(health).toBeTruthy();
|
||||
expect(checked).toBeTruthy();
|
||||
expect(contextSpy).toHaveBeenCalledTimes(1);
|
||||
parseGraphQLServer._getGraphQLOptions = originalGetGraphQLOptions;
|
||||
});
|
||||
});
|
||||
@@ -2597,11 +2634,23 @@ describe('ParseGraphQLServer', () => {
|
||||
// "SecondaryObject:bBRgmzIRRM" < "SecondaryObject:nTMcuVbATY" true
|
||||
// base64("SecondaryObject:bBRgmzIRRM"") < base64(""SecondaryObject:nTMcuVbATY"") false
|
||||
// "U2Vjb25kYXJ5T2JqZWN0OmJCUmdteklSUk0=" < "U2Vjb25kYXJ5T2JqZWN0Om5UTWN1VmJBVFk=" false
|
||||
const originalIds = [
|
||||
getSecondaryObjectsResult.data.secondaryObject2.objectId,
|
||||
getSecondaryObjectsResult.data.secondaryObject4.objectId,
|
||||
];
|
||||
expect(
|
||||
findSecondaryObjectsResult.data.secondaryObjects.edges[0].node.objectId
|
||||
).toBeLessThan(
|
||||
findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId
|
||||
);
|
||||
).not.toBe(findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId);
|
||||
expect(
|
||||
originalIds.includes(
|
||||
findSecondaryObjectsResult.data.secondaryObjects.edges[0].node.objectId
|
||||
)
|
||||
).toBeTrue();
|
||||
expect(
|
||||
originalIds.includes(
|
||||
findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId
|
||||
)
|
||||
).toBeTrue();
|
||||
|
||||
const createPrimaryObjectResult = await apolloClient.mutate({
|
||||
mutation: gql`
|
||||
@@ -4458,8 +4507,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const databaseAdapter = parseServer.config.databaseController.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
@@ -4483,13 +4531,13 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
let foundGraphQLClassReadPreference = false;
|
||||
let foundUserClassReadPreference = false;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
|
||||
foundGraphQLClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY);
|
||||
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
|
||||
expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY);
|
||||
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
|
||||
foundUserClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY);
|
||||
expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4505,8 +4553,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const databaseAdapter = parseServer.config.databaseController.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
@@ -4530,13 +4577,13 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
let foundGraphQLClassReadPreference = false;
|
||||
let foundUserClassReadPreference = false;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
|
||||
foundGraphQLClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
|
||||
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
|
||||
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
|
||||
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
|
||||
foundUserClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
|
||||
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4549,8 +4596,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const databaseAdapter = parseServer.config.databaseController.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
@@ -4577,13 +4623,13 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
let foundGraphQLClassReadPreference = false;
|
||||
let foundUserClassReadPreference = false;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
|
||||
foundGraphQLClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
|
||||
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
|
||||
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
|
||||
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
|
||||
foundUserClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.NEAREST);
|
||||
expect(call.args[1].readPreference).toBe(ReadPreference.NEAREST);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5441,8 +5487,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const databaseAdapter = parseServer.config.databaseController.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
@@ -5467,13 +5512,13 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
let foundGraphQLClassReadPreference = false;
|
||||
let foundUserClassReadPreference = false;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
|
||||
foundGraphQLClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY);
|
||||
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
|
||||
expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY);
|
||||
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
|
||||
foundUserClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY);
|
||||
expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5486,8 +5531,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const databaseAdapter = parseServer.config.databaseController.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
@@ -5512,13 +5556,13 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
let foundGraphQLClassReadPreference = false;
|
||||
let foundUserClassReadPreference = false;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
|
||||
foundGraphQLClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
|
||||
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
|
||||
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
|
||||
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
|
||||
foundUserClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
|
||||
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5531,8 +5575,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const databaseAdapter = parseServer.config.databaseController.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
@@ -5559,13 +5602,13 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
let foundGraphQLClassReadPreference = false;
|
||||
let foundUserClassReadPreference = false;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
|
||||
foundGraphQLClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
|
||||
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
|
||||
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
|
||||
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
|
||||
foundUserClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.NEAREST);
|
||||
expect(call.args[1].readPreference).toBe(ReadPreference.NEAREST);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5579,8 +5622,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const databaseAdapter = parseServer.config.databaseController.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
await apolloClient.query({
|
||||
query: gql`
|
||||
@@ -5617,13 +5659,13 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
let foundGraphQLClassReadPreference = false;
|
||||
let foundUserClassReadPreference = false;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
|
||||
foundGraphQLClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
|
||||
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
|
||||
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
|
||||
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
|
||||
foundUserClassReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.NEAREST);
|
||||
expect(call.args[1].readPreference).toBe(ReadPreference.NEAREST);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6778,14 +6820,14 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
expect(queryResult.data.customers.edges.length).toEqual(1);
|
||||
} catch (e) {
|
||||
console.log(JSON.stringify(e));
|
||||
console.error(JSON.stringify(e));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Files Mutations', () => {
|
||||
describe('Create', () => {
|
||||
it('should return File object', async () => {
|
||||
it_only_node_version('<17')('should return File object', async () => {
|
||||
const clientMutationId = uuidv4();
|
||||
|
||||
parseServer = await global.reconfigureServer({
|
||||
@@ -9088,7 +9130,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result6[0].node.name).toEqual('imACountry3');
|
||||
});
|
||||
|
||||
it('should support files', async () => {
|
||||
it_only_node_version('<17')('should support files', async () => {
|
||||
try {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
@@ -9099,15 +9141,15 @@ describe('ParseGraphQLServer', () => {
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `
|
||||
mutation CreateFile($input: CreateFileInput!) {
|
||||
createFile(input: $input) {
|
||||
fileInfo {
|
||||
name
|
||||
url
|
||||
mutation CreateFile($input: CreateFileInput!) {
|
||||
createFile(input: $input) {
|
||||
fileInfo {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
upload: null,
|
||||
@@ -9168,46 +9210,46 @@ describe('ParseGraphQLServer', () => {
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `
|
||||
mutation CreateSomeObject(
|
||||
$fields1: CreateSomeClassFieldsInput
|
||||
$fields2: CreateSomeClassFieldsInput
|
||||
$fields3: CreateSomeClassFieldsInput
|
||||
) {
|
||||
createSomeClass1: createSomeClass(
|
||||
input: { fields: $fields1 }
|
||||
mutation CreateSomeObject(
|
||||
$fields1: CreateSomeClassFieldsInput
|
||||
$fields2: CreateSomeClassFieldsInput
|
||||
$fields3: CreateSomeClassFieldsInput
|
||||
) {
|
||||
someClass {
|
||||
id
|
||||
someField {
|
||||
name
|
||||
url
|
||||
createSomeClass1: createSomeClass(
|
||||
input: { fields: $fields1 }
|
||||
) {
|
||||
someClass {
|
||||
id
|
||||
someField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
createSomeClass2: createSomeClass(
|
||||
input: { fields: $fields2 }
|
||||
) {
|
||||
someClass {
|
||||
id
|
||||
someField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
createSomeClass3: createSomeClass(
|
||||
input: { fields: $fields3 }
|
||||
) {
|
||||
someClass {
|
||||
id
|
||||
someField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
createSomeClass2: createSomeClass(
|
||||
input: { fields: $fields2 }
|
||||
) {
|
||||
someClass {
|
||||
id
|
||||
someField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
createSomeClass3: createSomeClass(
|
||||
input: { fields: $fields3 }
|
||||
) {
|
||||
someClass {
|
||||
id
|
||||
someField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
`,
|
||||
variables: {
|
||||
fields1: {
|
||||
someField: { file: someFieldValue },
|
||||
@@ -9336,6 +9378,51 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it_only_node_version('<17')('should not upload if file is too large', async () => {
|
||||
parseGraphQLServer.parseServer.config.maxUploadSize = '1kb';
|
||||
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `
|
||||
mutation CreateFile($input: CreateFileInput!) {
|
||||
createFile(input: $input) {
|
||||
fileInfo {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
upload: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
body.append('map', JSON.stringify({ 1: ['variables.input.upload'] }));
|
||||
body.append(
|
||||
'1',
|
||||
Buffer.alloc(parseGraphQLServer._transformMaxUploadSizeToBytes('2kb'), 1),
|
||||
{
|
||||
filename: 'myFileName.txt',
|
||||
contentType: 'text/plain',
|
||||
}
|
||||
);
|
||||
|
||||
const res = await fetch('http://localhost:13377/graphql', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
const result = JSON.parse(await res.text());
|
||||
expect(res.status).toEqual(500);
|
||||
expect(result.errors[0].message).toEqual('File size limit exceeded: 1024 bytes');
|
||||
});
|
||||
|
||||
it('should support object values', async () => {
|
||||
try {
|
||||
const someObjectFieldValue = {
|
||||
@@ -9429,6 +9516,130 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should support where argument on object field that contains false boolean value or 0 number value', async () => {
|
||||
try {
|
||||
const someObjectFieldValue1 = {
|
||||
foo: { bar: true, baz: 100 },
|
||||
};
|
||||
|
||||
const someObjectFieldValue2 = {
|
||||
foo: { bar: false, baz: 0 },
|
||||
};
|
||||
|
||||
const object1 = new Parse.Object('SomeClass');
|
||||
await object1.save({
|
||||
someObjectField: someObjectFieldValue1,
|
||||
});
|
||||
const object2 = new Parse.Object('SomeClass');
|
||||
await object2.save({
|
||||
someObjectField: someObjectFieldValue2,
|
||||
});
|
||||
|
||||
const whereToObject1 = {
|
||||
someObjectField: {
|
||||
equalTo: { key: 'foo.bar', value: true },
|
||||
notEqualTo: { key: 'foo.baz', value: 0 },
|
||||
},
|
||||
};
|
||||
const whereToObject2 = {
|
||||
someObjectField: {
|
||||
notEqualTo: { key: 'foo.bar', value: true },
|
||||
equalTo: { key: 'foo.baz', value: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
const whereToAll = {
|
||||
someObjectField: {
|
||||
lessThan: { key: 'foo.baz', value: 101 },
|
||||
},
|
||||
};
|
||||
|
||||
const whereToNone = {
|
||||
someObjectField: {
|
||||
notEqualTo: { key: 'foo.bar', value: true },
|
||||
equalTo: { key: 'foo.baz', value: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const queryResult = await apolloClient.query({
|
||||
query: gql`
|
||||
query GetSomeObject(
|
||||
$id1: ID!
|
||||
$id2: ID!
|
||||
$whereToObject1: SomeClassWhereInput
|
||||
$whereToObject2: SomeClassWhereInput
|
||||
$whereToAll: SomeClassWhereInput
|
||||
$whereToNone: SomeClassWhereInput
|
||||
) {
|
||||
obj1: someClass(id: $id1) {
|
||||
id
|
||||
someObjectField
|
||||
}
|
||||
obj2: someClass(id: $id2) {
|
||||
id
|
||||
someObjectField
|
||||
}
|
||||
onlyObj1: someClasses(where: $whereToObject1) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
someObjectField
|
||||
}
|
||||
}
|
||||
}
|
||||
onlyObj2: someClasses(where: $whereToObject2) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
someObjectField
|
||||
}
|
||||
}
|
||||
}
|
||||
all: someClasses(where: $whereToAll) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
someObjectField
|
||||
}
|
||||
}
|
||||
}
|
||||
none: someClasses(where: $whereToNone) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
someObjectField
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id1: object1.id,
|
||||
id2: object2.id,
|
||||
whereToObject1,
|
||||
whereToObject2,
|
||||
whereToAll,
|
||||
whereToNone,
|
||||
},
|
||||
});
|
||||
|
||||
const { obj1, obj2, onlyObj1, onlyObj2, all, none } = queryResult.data;
|
||||
|
||||
expect(obj1.someObjectField).toEqual(someObjectFieldValue1);
|
||||
expect(obj2.someObjectField).toEqual(someObjectFieldValue2);
|
||||
|
||||
// Checks class query results
|
||||
expect(onlyObj1.edges.length).toEqual(1);
|
||||
expect(onlyObj1.edges[0].node.someObjectField).toEqual(someObjectFieldValue1);
|
||||
expect(onlyObj2.edges.length).toEqual(1);
|
||||
expect(onlyObj2.edges[0].node.someObjectField).toEqual(someObjectFieldValue2);
|
||||
expect(all.edges.length).toEqual(2);
|
||||
expect(none.edges.length).toEqual(0);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should support object composed queries', async () => {
|
||||
try {
|
||||
const someObjectFieldValue1 = {
|
||||
@@ -10373,7 +10584,7 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
|
||||
describe('Custom API', () => {
|
||||
describe('GraphQL Schema Based', () => {
|
||||
describe('SDL based', () => {
|
||||
let httpServer;
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -10496,7 +10707,7 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('SDL Based', () => {
|
||||
describe('GraphQL Schema Based', () => {
|
||||
let httpServer;
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -10559,6 +10770,12 @@ describe('ParseGraphQLServer', () => {
|
||||
},
|
||||
resolve: (p, { message }) => message,
|
||||
},
|
||||
errorQuery: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
resolve: () => {
|
||||
throw new Error('A test error');
|
||||
},
|
||||
},
|
||||
customQueryWithAutoTypeReturn: {
|
||||
type: SomeClassType,
|
||||
args: {
|
||||
@@ -10647,6 +10864,18 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result.data.customQuery).toEqual('hello');
|
||||
});
|
||||
|
||||
it('can forward original error of a custom query', async () => {
|
||||
await expectAsync(
|
||||
apolloClient.query({
|
||||
query: gql`
|
||||
query ErrorQuery {
|
||||
errorQuery
|
||||
}
|
||||
`,
|
||||
})
|
||||
).toBeRejectedWithError('A test error');
|
||||
});
|
||||
|
||||
it('can resolve a custom query with auto type return', async () => {
|
||||
const obj = new Parse.Object('SomeClass');
|
||||
await obj.save({ name: 'aname', type: 'robot' });
|
||||
@@ -10793,8 +11022,7 @@ describe('ParseGraphQLServer', () => {
|
||||
httpServer = http.createServer(expressApp);
|
||||
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
|
||||
graphQLPath: '/graphql',
|
||||
graphQLCustomTypeDefs: ({ autoSchema, stitchSchemas }) =>
|
||||
stitchSchemas({ subschemas: [autoSchema] }),
|
||||
graphQLCustomTypeDefs: ({ autoSchema }) => mergeSchemas({ schemas: [autoSchema] }),
|
||||
});
|
||||
|
||||
parseGraphQLServer.applyGraphQL(expressApp);
|
||||
|
||||
@@ -1239,6 +1239,56 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can use push with beforeSave', async () => {
|
||||
const input = {
|
||||
deviceToken: '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306',
|
||||
deviceType: 'ios',
|
||||
};
|
||||
await rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
const functions = {
|
||||
beforeSave() {},
|
||||
afterSave() {},
|
||||
};
|
||||
spyOn(functions, 'beforeSave').and.callThrough();
|
||||
spyOn(functions, 'afterSave').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.Installation, functions.beforeSave);
|
||||
Parse.Cloud.afterSave(Parse.Installation, functions.afterSave);
|
||||
await Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
});
|
||||
|
||||
await Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
});
|
||||
|
||||
await Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
const installation = await new Parse.Query(Parse.Installation).first({ useMasterKey: true });
|
||||
expect(installation.get('badge')).toEqual(3);
|
||||
expect(functions.beforeSave).not.toHaveBeenCalled();
|
||||
expect(functions.afterSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// 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
|
||||
|
||||
@@ -319,6 +319,41 @@ describe('ParseLiveQuery', function () {
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('can log on afterLiveQueryEvent throw', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
|
||||
let session = undefined;
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', ({ sessionToken }) => {
|
||||
session = sessionToken;
|
||||
/* eslint-disable no-undef */
|
||||
foo.bar();
|
||||
/* eslint-enable no-undef */
|
||||
});
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
const subscription = await query.subscribe();
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
await new Promise(resolve => subscription.on('error', resolve));
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Failed running afterLiveQueryEvent on class TestObject for event update with session ${session} with:\n Error: {"message":"foo is not defined","code":141}`
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle afterEvent sendEvent to false', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
@@ -566,6 +601,33 @@ describe('ParseLiveQuery', function () {
|
||||
await query.subscribe();
|
||||
});
|
||||
|
||||
it('can log on beforeConnect throw', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
let token = undefined;
|
||||
Parse.Cloud.beforeConnect(({ sessionToken }) => {
|
||||
token = sessionToken;
|
||||
/* eslint-disable no-undef */
|
||||
foo.bar();
|
||||
/* eslint-enable no-undef */
|
||||
});
|
||||
new Parse.Query(TestObject).subscribe();
|
||||
await new Promise(resolve => Parse.LiveQuery.on('error', resolve));
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Failed running beforeConnect for session ${token} with:\n Error: {"message":"foo is not defined","code":141}`
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle beforeSubscribe error', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
@@ -594,6 +656,34 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('can log on beforeSubscribe error', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
|
||||
Parse.Cloud.beforeSubscribe(TestObject, () => {
|
||||
/* eslint-disable no-undef */
|
||||
foo.bar();
|
||||
/* eslint-enable no-undef */
|
||||
});
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
const subscription = await query.subscribe();
|
||||
await new Promise(resolve => subscription.on('error', resolve));
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Failed running beforeSubscribe on TestObject for session undefined with:\n Error: {"message":"foo is not defined","code":141}`
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle mutate beforeSubscribe query', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
@@ -746,6 +836,50 @@ describe('ParseLiveQuery', function () {
|
||||
}
|
||||
});
|
||||
|
||||
it('LiveQuery should work with changing role', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['Chat'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
});
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username');
|
||||
user.setPassword('password');
|
||||
await user.signUp();
|
||||
|
||||
const role = new Parse.Role('Test', new Parse.ACL(user));
|
||||
await role.save();
|
||||
|
||||
const chatQuery = new Parse.Query('Chat');
|
||||
const subscription = await chatQuery.subscribe();
|
||||
subscription.on('create', () => {
|
||||
fail('should not call create as user is not part of role.');
|
||||
});
|
||||
|
||||
const object = new Parse.Object('Chat');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setRoleReadAccess(role, true);
|
||||
object.setACL(acl);
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save(null, { useMasterKey: true });
|
||||
role.getUsers().add(user);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await role.save();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
object.set('foo', 'yolo');
|
||||
await Promise.all([
|
||||
new Promise(resolve => {
|
||||
subscription.on('update', obj => {
|
||||
expect(obj.get('foo')).toBe('yolo');
|
||||
expect(obj.getACL().toJSON()).toEqual({ 'role:Test': { read: true } });
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
object.save(null, { useMasterKey: true }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('liveQuery on Session class', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: { classNames: [Parse.Session] },
|
||||
@@ -976,6 +1110,86 @@ describe('ParseLiveQuery', function () {
|
||||
}
|
||||
});
|
||||
|
||||
it('should strip out protected fields', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: { classNames: ['Test'] },
|
||||
startLiveQueryServer: true,
|
||||
});
|
||||
const obj1 = new Parse.Object('Test');
|
||||
obj1.set('foo', 'foo');
|
||||
obj1.set('bar', 'bar');
|
||||
obj1.set('qux', 'qux');
|
||||
await obj1.save();
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const schemaController = await config.database.loadSchema();
|
||||
await schemaController.updateClass(
|
||||
'Test',
|
||||
{},
|
||||
{
|
||||
get: { '*': true },
|
||||
find: { '*': true },
|
||||
update: { '*': true },
|
||||
protectedFields: {
|
||||
'*': ['foo'],
|
||||
},
|
||||
}
|
||||
);
|
||||
const object = await obj1.fetch();
|
||||
expect(object.get('foo')).toBe(undefined);
|
||||
expect(object.get('bar')).toBeDefined();
|
||||
expect(object.get('qux')).toBeDefined();
|
||||
|
||||
const subscription = await new Parse.Query('Test').subscribe();
|
||||
await Promise.all([
|
||||
new Promise(resolve => {
|
||||
subscription.on('update', (obj, original) => {
|
||||
expect(obj.get('foo')).toBe(undefined);
|
||||
expect(obj.get('bar')).toBeDefined();
|
||||
expect(obj.get('qux')).toBeDefined();
|
||||
expect(original.get('foo')).toBe(undefined);
|
||||
expect(original.get('bar')).toBeDefined();
|
||||
expect(original.get('qux')).toBeDefined();
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
obj1.save({ foo: 'abc' }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('can subscribe to query and return object with withinKilometers with last parameter on update', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
const firstPoint = new Parse.GeoPoint({ latitude: 40.0, longitude: -30.0 });
|
||||
object.set({ location: firstPoint });
|
||||
await object.save();
|
||||
|
||||
// unsorted will use $centerSphere operator
|
||||
const sorted = false;
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.withinKilometers(
|
||||
'location',
|
||||
new Parse.GeoPoint({ latitude: 40.0, longitude: -30.0 }),
|
||||
2,
|
||||
sorted
|
||||
);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('update', obj => {
|
||||
expect(obj.id).toBe(object.id);
|
||||
done();
|
||||
});
|
||||
|
||||
const secondPoint = new Parse.GeoPoint({ latitude: 40.0, longitude: -30.0 });
|
||||
object.set({ location: secondPoint });
|
||||
await object.save();
|
||||
});
|
||||
|
||||
afterEach(async function (done) {
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
client.close();
|
||||
|
||||
@@ -666,6 +666,23 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate with Date object (directAccess)', async () => {
|
||||
const rest = require('../lib/rest');
|
||||
const auth = require('../lib/Auth');
|
||||
const TestObject = Parse.Object.extend('TestObject');
|
||||
const date = new Date();
|
||||
await new TestObject({ date: date }).save(null, { useMasterKey: true });
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const resp = await rest.find(
|
||||
config,
|
||||
auth.master(config),
|
||||
'TestObject',
|
||||
{},
|
||||
{ pipeline: [{ $match: { date: { $lte: new Date() } } }] }
|
||||
);
|
||||
expect(resp.results.length).toBe(1);
|
||||
});
|
||||
|
||||
it('match comparison query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
|
||||
@@ -27,7 +27,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
await TestUtils.destroyAllDataPermanently(false);
|
||||
});
|
||||
|
||||
it('query find with hint string', async () => {
|
||||
it_only_mongodb_version('<5.1>=6')('query find with hint string', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
@@ -39,7 +39,18 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(explain.queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it('query find with hint object', async () => {
|
||||
it_only_mongodb_version('>=5.1<6')('query find with hint string', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
const explain = await collection._rawFind({ _id: object.id }, { hint: '_id_', explain: true });
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('IXSCAN');
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<5.1>=6')('query find with hint object', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
@@ -53,6 +64,20 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<6')('query find with hint object', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
const explain = await collection._rawFind(
|
||||
{ _id: object.id },
|
||||
{ hint: { _id: 1 }, explain: true }
|
||||
);
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('IXSCAN');
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<4.4')('query aggregate with hint string', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
@@ -73,7 +98,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=4.4')('query aggregate with hint string', async () => {
|
||||
it_only_mongodb_version('>=4.4<5.1')('query aggregate with hint string', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
@@ -97,6 +122,54 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<5.2')('query aggregate with hint string', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
|
||||
|
||||
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
hint: '_id_',
|
||||
explain: true,
|
||||
});
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.2')('query aggregate with hint string', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let queryPlanner = result[0].queryPlanner;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('GROUP');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
|
||||
|
||||
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
hint: '_id_',
|
||||
explain: true,
|
||||
});
|
||||
queryPlanner = result[0].queryPlanner;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('GROUP');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<4.4')('query aggregate with hint object', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
@@ -117,7 +190,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=4.4')('query aggregate with hint object', async () => {
|
||||
it_only_mongodb_version('>=4.4<5.1')('query aggregate with hint object', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
@@ -142,7 +215,57 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it('query find with hint (rest)', async () => {
|
||||
it_only_mongodb_version('>=5.1<5.2')('query aggregate with hint object', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
|
||||
|
||||
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
hint: { _id: 1 },
|
||||
explain: true,
|
||||
});
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.2')('query aggregate with hint object', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let queryPlanner = result[0].queryPlanner;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('GROUP');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
|
||||
|
||||
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
hint: { _id: 1 },
|
||||
explain: true,
|
||||
});
|
||||
queryPlanner = result[0].queryPlanner;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('GROUP');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<5.1>=6')('query find with hint (rest)', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
@@ -167,6 +290,31 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(explain.queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<6')('query find with hint (rest)', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
let explain = response.data.results;
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
|
||||
options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
explain = response.data.results;
|
||||
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<4.4')('query aggregate with hint (rest)', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
@@ -194,7 +342,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=4.4')('query aggregate with hint (rest)', async () => {
|
||||
it_only_mongodb_version('>=4.4<5.1')('query aggregate with hint (rest)', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
@@ -226,4 +374,70 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.1<5.2')('query aggregate with hint (rest)', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
let { queryPlanner } = response.data.results[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
|
||||
|
||||
options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=5.2')('query aggregate with hint (rest)', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
let queryPlanner = response.data.results[0].queryPlanner;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('GROUP');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
|
||||
|
||||
options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
queryPlanner = response.data.results[0].queryPlanner;
|
||||
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('GROUP');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
+102
-93
@@ -314,39 +314,57 @@ describe('Parse.Query testing', () => {
|
||||
equal(results.length, 0);
|
||||
});
|
||||
|
||||
it('query with limit', function (done) {
|
||||
const baz = new TestObject({ foo: 'baz' });
|
||||
const qux = new TestObject({ foo: 'qux' });
|
||||
Parse.Object.saveAll([baz, qux]).then(function () {
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.limit(1);
|
||||
query.find().then(function (results) {
|
||||
equal(results.length, 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('query without limit respects default limit', async () => {
|
||||
await reconfigureServer({ defaultLimit: 1 });
|
||||
const obj1 = new TestObject({ foo: 'baz' });
|
||||
const obj2 = new TestObject({ foo: 'qux' });
|
||||
await Parse.Object.saveAll([obj1, obj2]);
|
||||
const query = new Parse.Query(TestObject);
|
||||
const result = await query.find();
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
it('query with limit', async () => {
|
||||
const obj1 = new TestObject({ foo: 'baz' });
|
||||
const obj2 = new TestObject({ foo: 'qux' });
|
||||
await Parse.Object.saveAll([obj1, obj2]);
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.limit(1);
|
||||
const result = await query.find();
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
it('query with limit overrides default limit', async () => {
|
||||
await reconfigureServer({ defaultLimit: 2 });
|
||||
const obj1 = new TestObject({ foo: 'baz' });
|
||||
const obj2 = new TestObject({ foo: 'qux' });
|
||||
await Parse.Object.saveAll([obj1, obj2]);
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.limit(1);
|
||||
const result = await query.find();
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
it('query with limit equal to maxlimit', async () => {
|
||||
const baz = new TestObject({ foo: 'baz' });
|
||||
const qux = new TestObject({ foo: 'qux' });
|
||||
await reconfigureServer({ maxLimit: 1 });
|
||||
await Parse.Object.saveAll([baz, qux]);
|
||||
const obj1 = new TestObject({ foo: 'baz' });
|
||||
const obj2 = new TestObject({ foo: 'qux' });
|
||||
await Parse.Object.saveAll([obj1, obj2]);
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.limit(1);
|
||||
const results = await query.find();
|
||||
equal(results.length, 1);
|
||||
const result = await query.find();
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
it('query with limit exceeding maxlimit', async () => {
|
||||
const baz = new TestObject({ foo: 'baz' });
|
||||
const qux = new TestObject({ foo: 'qux' });
|
||||
await reconfigureServer({ maxLimit: 1 });
|
||||
await Parse.Object.saveAll([baz, qux]);
|
||||
const obj1 = new TestObject({ foo: 'baz' });
|
||||
const obj2 = new TestObject({ foo: 'qux' });
|
||||
await Parse.Object.saveAll([obj1, obj2]);
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.limit(2);
|
||||
const results = await query.find();
|
||||
equal(results.length, 1);
|
||||
const result = await query.find();
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
it('containedIn object array queries', function (done) {
|
||||
@@ -1715,6 +1733,16 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('order by non-existing string', async () => {
|
||||
const strings = ['a', 'b', 'c', 'd'];
|
||||
const makeBoxedNumber = function (num, i) {
|
||||
return new BoxedNumber({ number: num, string: strings[i] });
|
||||
};
|
||||
await Parse.Object.saveAll([3, 1, 3, 2].map(makeBoxedNumber));
|
||||
const results = await new Parse.Query(BoxedNumber).ascending('foo').find();
|
||||
expect(results.length).toBe(4);
|
||||
});
|
||||
|
||||
it('order by descending number then ascending string', function (done) {
|
||||
const strings = ['a', 'b', 'c', 'd'];
|
||||
const makeBoxedNumber = function (num, i) {
|
||||
@@ -4766,7 +4794,7 @@ describe('Parse.Query testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('should handle relative times correctly', function (done) {
|
||||
it('should handle relative times correctly', async () => {
|
||||
const now = Date.now();
|
||||
const obj1 = new Parse.Object('MyCustomObject', {
|
||||
name: 'obj1',
|
||||
@@ -4777,94 +4805,75 @@ describe('Parse.Query testing', () => {
|
||||
ttl: new Date(now - 2 * 24 * 60 * 60 * 1000), // 2 days ago
|
||||
});
|
||||
|
||||
Parse.Object.saveAll([obj1, obj2])
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: 'in 1 day' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(1);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: '1 day ago' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(1);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.lessThan('ttl', { $relativeTime: '5 days ago' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(0);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: '3 days ago' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(2);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: 'now' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(1);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: 'now' });
|
||||
q.lessThan('ttl', { $relativeTime: 'in 1 day' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(0);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: '1 year 3 weeks ago' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(2);
|
||||
})
|
||||
.then(done, done.fail);
|
||||
await Parse.Object.saveAll([obj1, obj2]);
|
||||
const q1 = new Parse.Query('MyCustomObject');
|
||||
q1.greaterThan('ttl', { $relativeTime: 'in 1 day' });
|
||||
const results1 = await q1.find({ useMasterKey: true });
|
||||
expect(results1.length).toBe(1);
|
||||
|
||||
const q2 = new Parse.Query('MyCustomObject');
|
||||
q2.greaterThan('ttl', { $relativeTime: '1 day ago' });
|
||||
const results2 = await q2.find({ useMasterKey: true });
|
||||
expect(results2.length).toBe(1);
|
||||
|
||||
const q3 = new Parse.Query('MyCustomObject');
|
||||
q3.lessThan('ttl', { $relativeTime: '5 days ago' });
|
||||
const results3 = await q3.find({ useMasterKey: true });
|
||||
expect(results3.length).toBe(0);
|
||||
|
||||
const q4 = new Parse.Query('MyCustomObject');
|
||||
q4.greaterThan('ttl', { $relativeTime: '3 days ago' });
|
||||
const results4 = await q4.find({ useMasterKey: true });
|
||||
expect(results4.length).toBe(2);
|
||||
|
||||
const q5 = new Parse.Query('MyCustomObject');
|
||||
q5.greaterThan('ttl', { $relativeTime: 'now' });
|
||||
const results5 = await q5.find({ useMasterKey: true });
|
||||
expect(results5.length).toBe(1);
|
||||
|
||||
const q6 = new Parse.Query('MyCustomObject');
|
||||
q6.greaterThan('ttl', { $relativeTime: 'now' });
|
||||
q6.lessThan('ttl', { $relativeTime: 'in 1 day' });
|
||||
const results6 = await q6.find({ useMasterKey: true });
|
||||
expect(results6.length).toBe(0);
|
||||
|
||||
const q7 = new Parse.Query('MyCustomObject');
|
||||
q7.greaterThan('ttl', { $relativeTime: '1 year 3 weeks ago' });
|
||||
const results7 = await q7.find({ useMasterKey: true });
|
||||
expect(results7.length).toBe(2);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('should error on invalid relative time', function (done) {
|
||||
it('should error on invalid relative time', async () => {
|
||||
const obj1 = new Parse.Object('MyCustomObject', {
|
||||
name: 'obj1',
|
||||
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
|
||||
});
|
||||
|
||||
await obj1.save({ useMasterKey: true });
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: '-12 bananas ago' });
|
||||
obj1
|
||||
.save({ useMasterKey: true })
|
||||
.then(() => q.find({ useMasterKey: true }))
|
||||
.then(done.fail, () => done());
|
||||
try {
|
||||
await q.find({ useMasterKey: true });
|
||||
fail('Should have thrown error');
|
||||
} catch (error) {
|
||||
expect(error.code).toBe(Parse.Error.INVALID_JSON);
|
||||
}
|
||||
});
|
||||
|
||||
it_only_db('mongo')('should error when using $relativeTime on non-Date field', function (done) {
|
||||
it('should error when using $relativeTime on non-Date field', async () => {
|
||||
const obj1 = new Parse.Object('MyCustomObject', {
|
||||
name: 'obj1',
|
||||
nonDateField: 'abcd',
|
||||
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
|
||||
});
|
||||
|
||||
await obj1.save({ useMasterKey: true });
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('nonDateField', { $relativeTime: '1 day ago' });
|
||||
obj1
|
||||
.save({ useMasterKey: true })
|
||||
.then(() => q.find({ useMasterKey: true }))
|
||||
.then(done.fail, () => done());
|
||||
try {
|
||||
await q.find({ useMasterKey: true });
|
||||
fail('Should have thrown error');
|
||||
} catch (error) {
|
||||
expect(error.code).toBe(Parse.Error.INVALID_JSON);
|
||||
}
|
||||
});
|
||||
|
||||
it('should match complex structure with dot notation when using matchesKeyInQuery', function (done) {
|
||||
|
||||
@@ -410,6 +410,49 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('or queries with base constraint on relation field', async () => {
|
||||
const ChildObject = Parse.Object.extend('ChildObject');
|
||||
const childObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
childObjects.push(new ChildObject({ x: i }));
|
||||
}
|
||||
await Parse.Object.saveAll(childObjects);
|
||||
const ParentObject = Parse.Object.extend('ParentObject');
|
||||
const parent = new ParentObject();
|
||||
parent.set('x', 4);
|
||||
const relation = parent.relation('toChilds');
|
||||
relation.add(childObjects[0]);
|
||||
relation.add(childObjects[1]);
|
||||
relation.add(childObjects[2]);
|
||||
|
||||
const parent2 = new ParentObject();
|
||||
parent2.set('x', 3);
|
||||
const relation2 = parent2.relation('toChilds');
|
||||
relation2.add(childObjects[0]);
|
||||
relation2.add(childObjects[1]);
|
||||
relation2.add(childObjects[2]);
|
||||
|
||||
const parents = [];
|
||||
parents.push(parent);
|
||||
parents.push(parent2);
|
||||
parents.push(new ParentObject());
|
||||
|
||||
await Parse.Object.saveAll(parents);
|
||||
const query1 = new Parse.Query(ParentObject);
|
||||
query1.equalTo('x', 4);
|
||||
const query2 = new Parse.Query(ParentObject);
|
||||
query2.equalTo('x', 3);
|
||||
|
||||
const query = Parse.Query.or(query1, query2);
|
||||
query.equalTo('toChilds', childObjects[2]);
|
||||
|
||||
const list = await query.find();
|
||||
const objectIds = list.map(item => item.id);
|
||||
expect(objectIds.indexOf(parent.id)).not.toBe(-1);
|
||||
expect(objectIds.indexOf(parent2.id)).not.toBe(-1);
|
||||
equal(list.length, 2, 'There should be 2 results');
|
||||
});
|
||||
|
||||
it('Get query on relation using un-fetched parent object', done => {
|
||||
// Setup data model
|
||||
const Wheel = Parse.Object.extend('Wheel');
|
||||
|
||||
@@ -15,35 +15,18 @@ describe('ParseServerRESTController', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle a get request', done => {
|
||||
RESTController.request('GET', '/classes/MyObject').then(
|
||||
res => {
|
||||
expect(res.results.length).toBe(0);
|
||||
done();
|
||||
},
|
||||
err => {
|
||||
console.log(err);
|
||||
jfail(err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
it('should handle a get request', async () => {
|
||||
const res = await RESTController.request('GET', '/classes/MyObject');
|
||||
expect(res.results.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle a get request with full serverURL mount path', done => {
|
||||
RESTController.request('GET', '/1/classes/MyObject').then(
|
||||
res => {
|
||||
expect(res.results.length).toBe(0);
|
||||
done();
|
||||
},
|
||||
err => {
|
||||
jfail(err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
it('should handle a get request with full serverURL mount path', async () => {
|
||||
const res = await RESTController.request('GET', '/1/classes/MyObject');
|
||||
expect(res.results.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle a POST batch without transaction', done => {
|
||||
RESTController.request('POST', 'batch', {
|
||||
it('should handle a POST batch without transaction', async () => {
|
||||
const res = await RESTController.request('POST', 'batch', {
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
@@ -59,20 +42,12 @@ describe('ParseServerRESTController', () => {
|
||||
path: '/classes/MyObject',
|
||||
},
|
||||
],
|
||||
}).then(
|
||||
res => {
|
||||
expect(res.length).toBe(3);
|
||||
done();
|
||||
},
|
||||
err => {
|
||||
jfail(err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
expect(res.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle a POST batch with transaction=false', done => {
|
||||
RESTController.request('POST', 'batch', {
|
||||
it('should handle a POST batch with transaction=false', async () => {
|
||||
const res = await RESTController.request('POST', 'batch', {
|
||||
requests: [
|
||||
{
|
||||
method: 'GET',
|
||||
@@ -89,16 +64,8 @@ describe('ParseServerRESTController', () => {
|
||||
},
|
||||
],
|
||||
transaction: false,
|
||||
}).then(
|
||||
res => {
|
||||
expect(res.length).toBe(3);
|
||||
done();
|
||||
},
|
||||
err => {
|
||||
jfail(err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
expect(res.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle response status', async () => {
|
||||
@@ -186,54 +153,40 @@ describe('ParseServerRESTController', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle a batch request with transaction = true', async done => {
|
||||
await reconfigureServer();
|
||||
it('should handle a batch request with transaction = true', async () => {
|
||||
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
|
||||
myObject
|
||||
.save()
|
||||
.then(() => {
|
||||
return myObject.destroy();
|
||||
})
|
||||
.then(() => {
|
||||
spyOn(databaseAdapter, 'createObject').and.callThrough();
|
||||
|
||||
return RESTController.request('POST', 'batch', {
|
||||
requests: [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/1/classes/MyObject',
|
||||
body: { key: 'value1' },
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/1/classes/MyObject',
|
||||
body: { key: 'value2' },
|
||||
},
|
||||
],
|
||||
transaction: true,
|
||||
}).then(response => {
|
||||
expect(response.length).toEqual(2);
|
||||
expect(response[0].success.objectId).toBeDefined();
|
||||
expect(response[0].success.createdAt).toBeDefined();
|
||||
expect(response[1].success.objectId).toBeDefined();
|
||||
expect(response[1].success.createdAt).toBeDefined();
|
||||
const query = new Parse.Query('MyObject');
|
||||
return query.find().then(results => {
|
||||
expect(databaseAdapter.createObject.calls.count() % 2).toBe(0);
|
||||
for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) {
|
||||
expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe(
|
||||
databaseAdapter.createObject.calls.argsFor(i + 1)[3]
|
||||
);
|
||||
}
|
||||
expect(results.map(result => result.get('key')).sort()).toEqual([
|
||||
'value1',
|
||||
'value2',
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(done.fail);
|
||||
await myObject.save();
|
||||
await myObject.destroy();
|
||||
spyOn(databaseAdapter, 'createObject').and.callThrough();
|
||||
const response = await RESTController.request('POST', 'batch', {
|
||||
requests: [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/1/classes/MyObject',
|
||||
body: { key: 'value1' },
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/1/classes/MyObject',
|
||||
body: { key: 'value2' },
|
||||
},
|
||||
],
|
||||
transaction: true,
|
||||
});
|
||||
expect(response.length).toEqual(2);
|
||||
expect(response[0].success.objectId).toBeDefined();
|
||||
expect(response[0].success.createdAt).toBeDefined();
|
||||
expect(response[1].success.objectId).toBeDefined();
|
||||
expect(response[1].success.createdAt).toBeDefined();
|
||||
const query = new Parse.Query('MyObject');
|
||||
const results = await query.find();
|
||||
expect(databaseAdapter.createObject.calls.count() % 2).toBe(0);
|
||||
for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) {
|
||||
expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe(
|
||||
databaseAdapter.createObject.calls.argsFor(i + 1)[3]
|
||||
);
|
||||
}
|
||||
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
|
||||
});
|
||||
|
||||
it('should not save anything when one operation fails in a transaction', async () => {
|
||||
@@ -560,21 +513,11 @@ describe('ParseServerRESTController', () => {
|
||||
});
|
||||
}
|
||||
|
||||
it('should handle a POST request', done => {
|
||||
RESTController.request('POST', '/classes/MyObject', { key: 'value' })
|
||||
.then(() => {
|
||||
return RESTController.request('GET', '/classes/MyObject');
|
||||
})
|
||||
.then(res => {
|
||||
expect(res.results.length).toBe(1);
|
||||
expect(res.results[0].key).toEqual('value');
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
it('should handle a POST request', async () => {
|
||||
await RESTController.request('POST', '/classes/MyObject', { key: 'value' });
|
||||
const res = await RESTController.request('GET', '/classes/MyObject');
|
||||
expect(res.results.length).toBe(1);
|
||||
expect(res.results[0].key).toEqual('value');
|
||||
});
|
||||
|
||||
it('should handle a POST request with context', async () => {
|
||||
@@ -593,125 +536,76 @@ describe('ParseServerRESTController', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('ensures sessionTokens are properly handled', done => {
|
||||
let userId;
|
||||
Parse.User.signUp('user', 'pass')
|
||||
.then(user => {
|
||||
userId = user.id;
|
||||
const sessionToken = user.getSessionToken();
|
||||
return RESTController.request('GET', '/users/me', undefined, {
|
||||
sessionToken,
|
||||
});
|
||||
})
|
||||
.then(res => {
|
||||
// Result is in JSON format
|
||||
expect(res.objectId).toEqual(userId);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
jfail(err);
|
||||
done();
|
||||
it('ensures sessionTokens are properly handled', async () => {
|
||||
const user = await Parse.User.signUp('user', 'pass');
|
||||
const sessionToken = user.getSessionToken();
|
||||
const res = await RESTController.request('GET', '/users/me', undefined, {
|
||||
sessionToken,
|
||||
});
|
||||
// Result is in JSON format
|
||||
expect(res.objectId).toEqual(user.id);
|
||||
});
|
||||
|
||||
it('ensures masterKey is properly handled', async () => {
|
||||
const user = await Parse.User.signUp('user', 'pass');
|
||||
const userId = user.id;
|
||||
await Parse.User.logOut();
|
||||
const res = await RESTController.request('GET', '/classes/_User', undefined, {
|
||||
useMasterKey: true,
|
||||
});
|
||||
expect(res.results.length).toBe(1);
|
||||
expect(res.results[0].objectId).toEqual(userId);
|
||||
});
|
||||
|
||||
it('ensures no user is created when passing an empty username', async () => {
|
||||
try {
|
||||
await RESTController.request('POST', '/classes/_User', {
|
||||
username: '',
|
||||
password: 'world',
|
||||
});
|
||||
fail('Success callback should not be called when passing an empty username.');
|
||||
} catch (err) {
|
||||
expect(err.code).toBe(Parse.Error.USERNAME_MISSING);
|
||||
expect(err.message).toBe('bad or missing username');
|
||||
}
|
||||
});
|
||||
|
||||
it('ensures masterKey is properly handled', done => {
|
||||
let userId;
|
||||
Parse.User.signUp('user', 'pass')
|
||||
.then(user => {
|
||||
userId = user.id;
|
||||
return Parse.User.logOut().then(() => {
|
||||
return RESTController.request('GET', '/classes/_User', undefined, {
|
||||
useMasterKey: true,
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(
|
||||
res => {
|
||||
expect(res.results.length).toBe(1);
|
||||
expect(res.results[0].objectId).toEqual(userId);
|
||||
done();
|
||||
},
|
||||
err => {
|
||||
jfail(err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
it('ensures no user is created when passing an empty password', async () => {
|
||||
try {
|
||||
await RESTController.request('POST', '/classes/_User', {
|
||||
username: 'hello',
|
||||
password: '',
|
||||
});
|
||||
fail('Success callback should not be called when passing an empty password.');
|
||||
} catch (err) {
|
||||
expect(err.code).toBe(Parse.Error.PASSWORD_MISSING);
|
||||
expect(err.message).toBe('password is required');
|
||||
}
|
||||
});
|
||||
|
||||
it('ensures no user is created when passing an empty username', done => {
|
||||
RESTController.request('POST', '/classes/_User', {
|
||||
username: '',
|
||||
password: 'world',
|
||||
}).then(
|
||||
() => {
|
||||
jfail(new Error('Success callback should not be called when passing an empty username.'));
|
||||
done();
|
||||
},
|
||||
err => {
|
||||
expect(err.code).toBe(Parse.Error.USERNAME_MISSING);
|
||||
expect(err.message).toBe('bad or missing username');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('ensures no user is created when passing an empty password', done => {
|
||||
RESTController.request('POST', '/classes/_User', {
|
||||
username: 'hello',
|
||||
password: '',
|
||||
}).then(
|
||||
() => {
|
||||
jfail(new Error('Success callback should not be called when passing an empty password.'));
|
||||
done();
|
||||
},
|
||||
err => {
|
||||
expect(err.code).toBe(Parse.Error.PASSWORD_MISSING);
|
||||
expect(err.message).toBe('password is required');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('ensures no session token is created on creating users', done => {
|
||||
RESTController.request('POST', '/classes/_User', {
|
||||
it('ensures no session token is created on creating users', async () => {
|
||||
const user = await RESTController.request('POST', '/classes/_User', {
|
||||
username: 'hello',
|
||||
password: 'world',
|
||||
})
|
||||
.then(user => {
|
||||
expect(user.sessionToken).toBeUndefined();
|
||||
const query = new Parse.Query('_Session');
|
||||
return query.find({ useMasterKey: true });
|
||||
})
|
||||
.then(sessions => {
|
||||
expect(sessions.length).toBe(0);
|
||||
done();
|
||||
}, done.fail);
|
||||
});
|
||||
expect(user.sessionToken).toBeUndefined();
|
||||
const query = new Parse.Query('_Session');
|
||||
const sessions = await query.find({ useMasterKey: true });
|
||||
expect(sessions.length).toBe(0);
|
||||
});
|
||||
|
||||
it('ensures a session token is created when passing installationId != cloud', done => {
|
||||
RESTController.request(
|
||||
it('ensures a session token is created when passing installationId != cloud', async () => {
|
||||
const user = await RESTController.request(
|
||||
'POST',
|
||||
'/classes/_User',
|
||||
{ username: 'hello', password: 'world' },
|
||||
{ installationId: 'my-installation' }
|
||||
)
|
||||
.then(user => {
|
||||
expect(user.sessionToken).not.toBeUndefined();
|
||||
const query = new Parse.Query('_Session');
|
||||
return query.find({ useMasterKey: true });
|
||||
})
|
||||
.then(
|
||||
sessions => {
|
||||
expect(sessions.length).toBe(1);
|
||||
expect(sessions[0].get('installationId')).toBe('my-installation');
|
||||
done();
|
||||
},
|
||||
err => {
|
||||
jfail(err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
);
|
||||
expect(user.sessionToken).not.toBeUndefined();
|
||||
const query = new Parse.Query('_Session');
|
||||
const sessions = await query.find({ useMasterKey: true });
|
||||
expect(sessions.length).toBe(1);
|
||||
expect(sessions[0].get('installationId')).toBe('my-installation');
|
||||
});
|
||||
|
||||
it('ensures logIn is saved with installationId', async () => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//
|
||||
|
||||
'use strict';
|
||||
const request = require('../lib/request');
|
||||
|
||||
function setupTestUsers() {
|
||||
const user1 = new Parse.User();
|
||||
@@ -135,4 +136,31 @@ describe('Parse.Session', () => {
|
||||
fail(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot edit session with known ID', async () => {
|
||||
await setupTestUsers();
|
||||
const [first, second] = await new Parse.Query(Parse.Session).find({ useMasterKey: true });
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': second.get('sessionToken'),
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const firstUser = first.get('user').id;
|
||||
const secondUser = second.get('user').id;
|
||||
const e = await request({
|
||||
method: 'PUT',
|
||||
headers,
|
||||
url: `http://localhost:8378/1/sessions/${first.id}`,
|
||||
body: JSON.stringify({
|
||||
foo: 'bar',
|
||||
user: { __type: 'Pointer', className: '_User', objectId: secondUser },
|
||||
}),
|
||||
}).catch(e => e.data);
|
||||
expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
|
||||
expect(e.error).toBe('Object not found.');
|
||||
await Parse.Object.fetchAll([first, second], { useMasterKey: true });
|
||||
expect(first.get('user').id).toBe(firstUser);
|
||||
expect(second.get('user').id).toBe(secondUser);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2926,7 +2926,7 @@ describe('Parse.User testing', () => {
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve(),
|
||||
};
|
||||
reconfigureServer({
|
||||
await reconfigureServer({
|
||||
appName: 'unused',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('ParseWebSocketServer', function () {
|
||||
ws.readyState = 0;
|
||||
ws.OPEN = 0;
|
||||
ws.ping = jasmine.createSpy('ping');
|
||||
ws.terminate = () => {};
|
||||
|
||||
parseWebSocketServer.onConnection(ws);
|
||||
|
||||
@@ -75,6 +76,61 @@ describe('ParseWebSocketServer', function () {
|
||||
expect(wssError).toBe('Invalid Packet');
|
||||
});
|
||||
|
||||
it('can handle ping/pong', async () => {
|
||||
const onConnectCallback = jasmine.createSpy('onConnectCallback');
|
||||
const http = require('http');
|
||||
const server = http.createServer();
|
||||
const parseWebSocketServer = new ParseWebSocketServer(server, onConnectCallback, {
|
||||
websocketTimeout: 10,
|
||||
}).server;
|
||||
|
||||
const ws = new EventEmitter();
|
||||
ws.readyState = 0;
|
||||
ws.OPEN = 0;
|
||||
ws.ping = jasmine.createSpy('ping');
|
||||
ws.terminate = jasmine.createSpy('terminate');
|
||||
|
||||
parseWebSocketServer.onConnection(ws);
|
||||
|
||||
expect(onConnectCallback).toHaveBeenCalled();
|
||||
expect(ws.waitingForPong).toBe(false);
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
expect(ws.ping).toHaveBeenCalled();
|
||||
expect(ws.waitingForPong).toBe(true);
|
||||
ws.emit('pong');
|
||||
expect(ws.waitingForPong).toBe(false);
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
expect(ws.waitingForPong).toBe(true);
|
||||
expect(ws.terminate).not.toHaveBeenCalled();
|
||||
server.close();
|
||||
});
|
||||
|
||||
it('closes interrupted connection', async () => {
|
||||
const onConnectCallback = jasmine.createSpy('onConnectCallback');
|
||||
const http = require('http');
|
||||
const server = http.createServer();
|
||||
const parseWebSocketServer = new ParseWebSocketServer(server, onConnectCallback, {
|
||||
websocketTimeout: 5,
|
||||
}).server;
|
||||
const ws = new EventEmitter();
|
||||
ws.readyState = 0;
|
||||
ws.OPEN = 0;
|
||||
ws.ping = jasmine.createSpy('ping');
|
||||
ws.terminate = jasmine.createSpy('terminate');
|
||||
|
||||
parseWebSocketServer.onConnection(ws);
|
||||
|
||||
// Make sure callback is called
|
||||
expect(onConnectCallback).toHaveBeenCalled();
|
||||
expect(ws.waitingForPong).toBe(false);
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
expect(ws.ping).toHaveBeenCalled();
|
||||
expect(ws.waitingForPong).toBe(true);
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
expect(ws.terminate).toHaveBeenCalled();
|
||||
server.close();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
jasmine.restoreLibrary('ws', 'Server');
|
||||
});
|
||||
|
||||
@@ -4,8 +4,7 @@ const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/Postgre
|
||||
const postgresURI =
|
||||
process.env.PARSE_SERVER_TEST_DATABASE_URI ||
|
||||
'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
|
||||
const ParseServer = require('../lib/index');
|
||||
const express = require('express');
|
||||
|
||||
//public schema
|
||||
const databaseOptions1 = {
|
||||
initOptions: {
|
||||
@@ -24,72 +23,56 @@ const GameScore = Parse.Object.extend({
|
||||
className: 'GameScore',
|
||||
});
|
||||
|
||||
function createParseServer(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const parseServer = new ParseServer.default(
|
||||
Object.assign({}, defaultConfiguration, options, {
|
||||
serverURL: 'http://localhost:12668/parse',
|
||||
serverStartComplete: error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
expect(Parse.applicationId).toEqual('test');
|
||||
const app = express();
|
||||
app.use('/parse', parseServer.app);
|
||||
|
||||
const server = app.listen(12668);
|
||||
Parse.serverURL = 'http://localhost:12668/parse';
|
||||
resolve(server);
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
describe_only_db('postgres')('Postgres database init options', () => {
|
||||
let server;
|
||||
|
||||
afterAll(done => {
|
||||
if (server) {
|
||||
Parse.serverURL = 'http://localhost:8378/1';
|
||||
server.close(done);
|
||||
}
|
||||
});
|
||||
|
||||
it('should create server with public schema databaseOptions', done => {
|
||||
it('should create server with public schema databaseOptions', async () => {
|
||||
const adapter = new PostgresStorageAdapter({
|
||||
uri: postgresURI,
|
||||
collectionPrefix: 'test_',
|
||||
databaseOptions: databaseOptions1,
|
||||
});
|
||||
|
||||
createParseServer({ databaseAdapter: adapter })
|
||||
.then(newServer => {
|
||||
server = newServer;
|
||||
const score = new GameScore({
|
||||
score: 1337,
|
||||
playerName: 'Sean Plott',
|
||||
cheatMode: false,
|
||||
});
|
||||
return score.save();
|
||||
})
|
||||
.then(async () => {
|
||||
await reconfigureServer();
|
||||
done();
|
||||
}, done.fail);
|
||||
await reconfigureServer({
|
||||
databaseAdapter: adapter,
|
||||
});
|
||||
const score = new GameScore({
|
||||
score: 1337,
|
||||
playerName: 'Sean Plott',
|
||||
cheatMode: false,
|
||||
});
|
||||
await score.save();
|
||||
});
|
||||
|
||||
it('should fail to create server if schema databaseOptions does not exist', done => {
|
||||
it('should create server using postgresql uri with public schema databaseOptions', async () => {
|
||||
const postgresURI2 = new URL(postgresURI);
|
||||
postgresURI2.protocol = 'postgresql:';
|
||||
const adapter = new PostgresStorageAdapter({
|
||||
uri: postgresURI2.toString(),
|
||||
collectionPrefix: 'test_',
|
||||
databaseOptions: databaseOptions1,
|
||||
});
|
||||
await reconfigureServer({
|
||||
databaseAdapter: adapter,
|
||||
});
|
||||
const score = new GameScore({
|
||||
score: 1337,
|
||||
playerName: 'Sean Plott',
|
||||
cheatMode: false,
|
||||
});
|
||||
await score.save();
|
||||
});
|
||||
|
||||
it('should fail to create server if schema databaseOptions does not exist', async () => {
|
||||
const adapter = new PostgresStorageAdapter({
|
||||
uri: postgresURI,
|
||||
collectionPrefix: 'test_',
|
||||
databaseOptions: databaseOptions2,
|
||||
});
|
||||
|
||||
createParseServer({ databaseAdapter: adapter }).then(done.fail, async () => {
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
try {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: adapter,
|
||||
});
|
||||
fail('Should have thrown error');
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,6 +149,135 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
|
||||
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
|
||||
});
|
||||
|
||||
it('$relativeTime should error on $eq', async () => {
|
||||
const tableName = '_User';
|
||||
const schema = {
|
||||
fields: {
|
||||
objectId: { type: 'String' },
|
||||
username: { type: 'String' },
|
||||
email: { type: 'String' },
|
||||
emailVerified: { type: 'Boolean' },
|
||||
createdAt: { type: 'Date' },
|
||||
updatedAt: { type: 'Date' },
|
||||
authData: { type: 'Object' },
|
||||
},
|
||||
};
|
||||
const client = adapter._client;
|
||||
await adapter.createTable(tableName, schema);
|
||||
await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [
|
||||
tableName,
|
||||
'objectId',
|
||||
'username',
|
||||
'Bugs',
|
||||
'Bunny',
|
||||
]);
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
await database.loadSchema({ clearCache: true });
|
||||
try {
|
||||
await database.find(
|
||||
tableName,
|
||||
{
|
||||
createdAt: {
|
||||
$eq: {
|
||||
$relativeTime: '12 days ago',
|
||||
},
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
fail('Should have thrown error');
|
||||
} catch (error) {
|
||||
expect(error.code).toBe(Parse.Error.INVALID_JSON);
|
||||
}
|
||||
await dropTable(client, tableName);
|
||||
});
|
||||
|
||||
it('$relativeTime should error on $ne', async () => {
|
||||
const tableName = '_User';
|
||||
const schema = {
|
||||
fields: {
|
||||
objectId: { type: 'String' },
|
||||
username: { type: 'String' },
|
||||
email: { type: 'String' },
|
||||
emailVerified: { type: 'Boolean' },
|
||||
createdAt: { type: 'Date' },
|
||||
updatedAt: { type: 'Date' },
|
||||
authData: { type: 'Object' },
|
||||
},
|
||||
};
|
||||
const client = adapter._client;
|
||||
await adapter.createTable(tableName, schema);
|
||||
await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [
|
||||
tableName,
|
||||
'objectId',
|
||||
'username',
|
||||
'Bugs',
|
||||
'Bunny',
|
||||
]);
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
await database.loadSchema({ clearCache: true });
|
||||
try {
|
||||
await database.find(
|
||||
tableName,
|
||||
{
|
||||
createdAt: {
|
||||
$ne: {
|
||||
$relativeTime: '12 days ago',
|
||||
},
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
fail('Should have thrown error');
|
||||
} catch (error) {
|
||||
expect(error.code).toBe(Parse.Error.INVALID_JSON);
|
||||
}
|
||||
await dropTable(client, tableName);
|
||||
});
|
||||
|
||||
it('$relativeTime should error on $exists', async () => {
|
||||
const tableName = '_User';
|
||||
const schema = {
|
||||
fields: {
|
||||
objectId: { type: 'String' },
|
||||
username: { type: 'String' },
|
||||
email: { type: 'String' },
|
||||
emailVerified: { type: 'Boolean' },
|
||||
createdAt: { type: 'Date' },
|
||||
updatedAt: { type: 'Date' },
|
||||
authData: { type: 'Object' },
|
||||
},
|
||||
};
|
||||
const client = adapter._client;
|
||||
await adapter.createTable(tableName, schema);
|
||||
await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [
|
||||
tableName,
|
||||
'objectId',
|
||||
'username',
|
||||
'Bugs',
|
||||
'Bunny',
|
||||
]);
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
await database.loadSchema({ clearCache: true });
|
||||
try {
|
||||
await database.find(
|
||||
tableName,
|
||||
{
|
||||
createdAt: {
|
||||
$exists: {
|
||||
$relativeTime: '12 days ago',
|
||||
},
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
fail('Should have thrown error');
|
||||
} catch (error) {
|
||||
expect(error.code).toBe(Parse.Error.INVALID_JSON);
|
||||
}
|
||||
await dropTable(client, tableName);
|
||||
});
|
||||
|
||||
it('should use index for caseInsensitive query using Postgres', async () => {
|
||||
const tableName = '_User';
|
||||
const schema = {
|
||||
@@ -426,9 +555,21 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
|
||||
},
|
||||
classLevelPermissions: undefined,
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
expect(adapter._onchange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Idempotency class should have function', async () => {
|
||||
await reconfigureServer();
|
||||
const adapter = Config.get('test').database.adapter;
|
||||
const client = adapter._client;
|
||||
const qs =
|
||||
"SELECT format('%I.%I(%s)', ns.nspname, p.proname, oidvectortypes(p.proargtypes)) FROM pg_proc p INNER JOIN pg_namespace ns ON (p.pronamespace = ns.oid) WHERE p.proname = 'idempotency_delete_expired_records'";
|
||||
const foundFunction = await client.one(qs);
|
||||
expect(foundFunction.format).toBe('public.idempotency_delete_expired_records()');
|
||||
await adapter.deleteIdempotencyFunction();
|
||||
await client.none(qs);
|
||||
});
|
||||
});
|
||||
|
||||
describe_only_db('postgres')('PostgresStorageAdapter shutdown', () => {
|
||||
@@ -438,4 +579,13 @@ describe_only_db('postgres')('PostgresStorageAdapter shutdown', () => {
|
||||
adapter.handleShutdown();
|
||||
expect(adapter._client.$pool.ending).toEqual(true);
|
||||
});
|
||||
|
||||
it('handleShutdown, close connection of postgresql uri', () => {
|
||||
const databaseURI2 = new URL(databaseURI);
|
||||
databaseURI2.protocol = 'postgresql:';
|
||||
const adapter = new PostgresStorageAdapter({ uri: databaseURI2.toString() });
|
||||
expect(adapter._client.$pool.ending).toEqual(false);
|
||||
adapter.handleShutdown();
|
||||
expect(adapter._client.$pool.ending).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
+24
-50
@@ -562,11 +562,7 @@ describe('PushController', () => {
|
||||
});
|
||||
const pushStatusId = await sendPush(payload, {}, config, auth);
|
||||
// it is enqueued so it can take time
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
await sleep(1000);
|
||||
Parse.serverURL = 'http://localhost:8378/1'; // GOOD url
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result).toBeDefined();
|
||||
@@ -767,7 +763,7 @@ describe('PushController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not schedule push when not configured', done => {
|
||||
it('should not schedule push when not configured', async () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
@@ -800,33 +796,20 @@ describe('PushController', () => {
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
reconfigureServer({
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Object.saveAll(installations)
|
||||
.then(() => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
})
|
||||
.then(() => new Promise(resolve => setTimeout(resolve, 300)));
|
||||
})
|
||||
.then(() => {
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find({ useMasterKey: true }).then(results => {
|
||||
expect(results.length).toBe(1);
|
||||
const pushStatus = results[0];
|
||||
expect(pushStatus.get('status')).not.toBe('scheduled');
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
fail('should not fail');
|
||||
done();
|
||||
});
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
await pushController.sendPush(payload, {}, config, auth);
|
||||
await sleep(1000);
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
const results = await query.find({ useMasterKey: true });
|
||||
expect(results.length).toBe(1);
|
||||
const pushStatus = results[0];
|
||||
expect(pushStatus.get('status')).not.toBe('scheduled');
|
||||
});
|
||||
|
||||
it('should schedule push when configured', done => {
|
||||
it('should schedule push when configured', async () => {
|
||||
const auth = {
|
||||
isMaster: true,
|
||||
};
|
||||
@@ -866,28 +849,19 @@ describe('PushController', () => {
|
||||
installation.set('deviceType', 'ios');
|
||||
installations.push(installation);
|
||||
}
|
||||
reconfigureServer({
|
||||
await reconfigureServer({
|
||||
push: { adapter: pushAdapter },
|
||||
scheduledPush: true,
|
||||
})
|
||||
.then(() => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
return Parse.Object.saveAll(installations)
|
||||
.then(() => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
})
|
||||
.then(() => new Promise(resolve => setTimeout(resolve, 300)));
|
||||
})
|
||||
.then(() => {
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find({ useMasterKey: true }).then(results => {
|
||||
expect(results.length).toBe(1);
|
||||
const pushStatus = results[0];
|
||||
expect(pushStatus.get('status')).toBe('scheduled');
|
||||
});
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.err);
|
||||
});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
await Parse.Object.saveAll(installations);
|
||||
await pushController.sendPush(payload, {}, config, auth);
|
||||
await sleep(1000);
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
const results = await query.find({ useMasterKey: true });
|
||||
expect(results.length).toBe(1);
|
||||
const pushStatus = results[0];
|
||||
expect(pushStatus.get('status')).toBe('scheduled');
|
||||
});
|
||||
|
||||
it('should not enqueue push when device token is not set', async () => {
|
||||
|
||||
@@ -583,6 +583,29 @@ describe('matchesQuery', function () {
|
||||
expect(matchesQuery(message, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('should support containedIn with array of pointers', () => {
|
||||
const message = {
|
||||
id: new Id('Message', 'O2'),
|
||||
profiles: [pointer('Profile', 'yeahaw'), pointer('Profile', 'yes')],
|
||||
};
|
||||
|
||||
let q = new Parse.Query('Message');
|
||||
q.containedIn('profiles', [
|
||||
Parse.Object.fromJSON({ className: 'Profile', objectId: 'no' }),
|
||||
Parse.Object.fromJSON({ className: 'Profile', objectId: 'yes' }),
|
||||
]);
|
||||
|
||||
expect(matchesQuery(message, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.containedIn('profiles', [
|
||||
Parse.Object.fromJSON({ className: 'Profile', objectId: 'no' }),
|
||||
Parse.Object.fromJSON({ className: 'Profile', objectId: 'nope' }),
|
||||
]);
|
||||
|
||||
expect(matchesQuery(message, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('should support notContainedIn with pointers', () => {
|
||||
let message = {
|
||||
id: new Id('Message', 'O1'),
|
||||
|
||||
+157
-215
@@ -1,9 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const Parse = require('parse/node');
|
||||
const ReadPreference = require('mongodb').ReadPreference;
|
||||
const { ReadPreference, Collection } = require('mongodb');
|
||||
const request = require('../lib/request');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
function waitForReplication() {
|
||||
return new Promise(function (resolve) {
|
||||
@@ -13,8 +12,6 @@ function waitForReplication() {
|
||||
|
||||
describe_only_db('mongo')('Read preference option', () => {
|
||||
it('should find in primary by default', done => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
@@ -22,7 +19,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
|
||||
Parse.Object.saveAll([obj0, obj1])
|
||||
.then(() => {
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
const query = new Parse.Query('MyObject');
|
||||
query.equalTo('boolKey', false);
|
||||
@@ -31,10 +28,10 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].get('boolKey')).toBe(false);
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY);
|
||||
expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -58,15 +55,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
databaseAdapter: new MongoStorageAdapter(adapterOptions),
|
||||
});
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
const query = new Parse.Query('MyObject');
|
||||
query.equalTo('boolKey', false);
|
||||
@@ -76,10 +71,10 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(results[0].get('boolKey')).toBe(false);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = true;
|
||||
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.NEAREST);
|
||||
expect(call.args[1].readPreference).toBe(ReadPreference.NEAREST);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -87,15 +82,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference in the beforeFind trigger', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'SECONDARY';
|
||||
@@ -110,9 +103,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(results[0].get('boolKey')).toBe(false);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -120,15 +113,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should check read preference as case insensitive', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'sEcOnDarY';
|
||||
@@ -144,9 +135,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(results[0].get('boolKey')).toBe(false);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -154,15 +145,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference in the beforeFind trigger even changing query', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.query.equalTo('boolKey', true);
|
||||
@@ -178,9 +167,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(results[0].get('boolKey')).toBe(true);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -188,15 +177,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference in the beforeFind trigger even returning query', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'SECONDARY';
|
||||
@@ -216,9 +203,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(results[0].get('boolKey')).toBe(true);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -226,15 +213,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference in the beforeFind trigger even returning promise', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'SECONDARY';
|
||||
@@ -253,9 +238,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(results[0].get('boolKey')).toBe(true);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -263,15 +248,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference to PRIMARY_PREFERRED', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'PRIMARY_PREFERRED';
|
||||
@@ -286,9 +269,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(results[0].get('boolKey')).toBe(false);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -296,15 +279,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference to SECONDARY_PREFERRED', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'SECONDARY_PREFERRED';
|
||||
@@ -319,9 +300,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(results[0].get('boolKey')).toBe(false);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -329,15 +310,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference to NEAREST', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'NEAREST';
|
||||
@@ -352,9 +331,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(results[0].get('boolKey')).toBe(false);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -362,15 +341,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference for GET', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'SECONDARY';
|
||||
@@ -383,9 +360,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(result.get('boolKey')).toBe(false);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -393,15 +370,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference for GET using API', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'SECONDARY';
|
||||
@@ -421,9 +396,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(body.boolKey).toBe(false);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -431,15 +406,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference for GET directly from API', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
await waitForReplication();
|
||||
|
||||
const response = await request({
|
||||
@@ -454,9 +427,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(response.data.boolKey).toBe(false);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -464,15 +437,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference for GET using API through the beforeFind overriding API option', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'SECONDARY_PREFERRED';
|
||||
@@ -491,9 +462,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(response.data.boolKey).toBe(false);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -501,15 +472,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference for FIND using API through beforeFind trigger', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'SECONDARY';
|
||||
@@ -528,9 +497,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(response.data.results.length).toEqual(2);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -538,15 +507,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference for FIND directly from API', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
await waitForReplication();
|
||||
|
||||
const response = await request({
|
||||
@@ -561,9 +528,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(response.data.results.length).toEqual(2);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -571,15 +538,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change read preference for FIND using API through the beforeFind overriding API option', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'SECONDARY_PREFERRED';
|
||||
@@ -598,9 +563,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(response.data.results.length).toEqual(2);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -608,15 +573,13 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
xit('should change read preference for count', done => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject');
|
||||
obj1.set('boolKey', true);
|
||||
|
||||
Parse.Object.saveAll([obj0, obj1]).then(() => {
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
req.readPreference = 'SECONDARY';
|
||||
@@ -631,9 +594,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
expect(result).toBe(1);
|
||||
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -659,17 +622,16 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
await waitForReplication();
|
||||
|
||||
// Spy on DB adapter
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'startSession').and.callThrough();
|
||||
spyOn(Collection.prototype, 'aggregate').and.callThrough();
|
||||
// Query
|
||||
const query = new Parse.Query('MyObject');
|
||||
const results = await query.aggregate([{ match: { boolKey: false } }]);
|
||||
// Validate
|
||||
expect(results.length).toBe(1);
|
||||
let readPreference = null;
|
||||
databaseAdapter.database.serverConfig.startSession.calls.all().forEach(call => {
|
||||
if (call.args[0].owner.ns.indexOf('MyObject') > -1) {
|
||||
readPreference = call.args[0].owner.operation.readPreference.mode;
|
||||
Collection.prototype.aggregate.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') > -1) {
|
||||
readPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
expect(readPreference).toEqual(ReadPreference.SECONDARY);
|
||||
@@ -685,8 +647,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
await waitForReplication();
|
||||
|
||||
// Spy on DB adapter
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
// Query
|
||||
const query = new Parse.Query('MyObject');
|
||||
query.equalTo('boolKey', false);
|
||||
@@ -695,9 +656,9 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
// Validate
|
||||
expect(results.length).toBe(1);
|
||||
let myObjectReadPreference = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
|
||||
myObjectReadPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY);
|
||||
@@ -713,8 +674,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
await waitForReplication();
|
||||
|
||||
// Spy on DB adapter
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'startSession').and.callThrough();
|
||||
spyOn(Collection.prototype, 'aggregate').and.callThrough();
|
||||
// Query
|
||||
const query = new Parse.Query('MyObject');
|
||||
query.readPreference('SECONDARY');
|
||||
@@ -722,17 +682,15 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
// Validate
|
||||
expect(results.length).toBe(1);
|
||||
let readPreference = null;
|
||||
databaseAdapter.database.serverConfig.startSession.calls.all().forEach(call => {
|
||||
if (call.args[0].owner.ns.indexOf('MyObject') > -1) {
|
||||
readPreference = call.args[0].owner.operation.readPreference.mode;
|
||||
Collection.prototype.aggregate.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject') > -1) {
|
||||
readPreference = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
expect(readPreference).toEqual(ReadPreference.SECONDARY);
|
||||
});
|
||||
|
||||
it('should find includes in same replica of readPreference by default', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject0');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject1');
|
||||
@@ -743,7 +701,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
obj2.set('myObject1', obj1);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1, obj2]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject2', req => {
|
||||
req.readPreference = 'SECONDARY';
|
||||
@@ -765,15 +723,15 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
let myObjectReadPreference0 = null;
|
||||
let myObjectReadPreference1 = null;
|
||||
let myObjectReadPreference2 = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -783,8 +741,6 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change includes read preference', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject0');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject1');
|
||||
@@ -795,7 +751,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
obj2.set('myObject1', obj1);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1, obj2]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject2', req => {
|
||||
req.readPreference = 'SECONDARY_PREFERRED';
|
||||
@@ -818,15 +774,15 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
let myObjectReadPreference0 = null;
|
||||
let myObjectReadPreference1 = null;
|
||||
let myObjectReadPreference2 = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -836,8 +792,6 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change includes read preference when finding through API', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject0');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject1');
|
||||
@@ -848,7 +802,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
obj2.set('myObject1', obj1);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1, obj2]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
await waitForReplication();
|
||||
|
||||
const response = await request({
|
||||
@@ -873,15 +827,15 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
let myObjectReadPreference0 = null;
|
||||
let myObjectReadPreference1 = null;
|
||||
let myObjectReadPreference2 = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -891,8 +845,6 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change includes read preference when getting through API', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject0');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject1');
|
||||
@@ -903,7 +855,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
obj2.set('myObject1', obj1);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1, obj2]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
await waitForReplication();
|
||||
|
||||
const response = await request({
|
||||
@@ -929,15 +881,15 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
let myObjectReadPreference0 = null;
|
||||
let myObjectReadPreference1 = null;
|
||||
let myObjectReadPreference2 = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -947,8 +899,6 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should find subqueries in same replica of readPreference by default', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject0');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject1');
|
||||
@@ -959,7 +909,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
obj2.set('myObject1', obj1);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1, obj2]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject2', req => {
|
||||
req.readPreference = 'SECONDARY';
|
||||
@@ -982,15 +932,15 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
let myObjectReadPreference0 = null;
|
||||
let myObjectReadPreference1 = null;
|
||||
let myObjectReadPreference2 = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1000,8 +950,6 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change subqueries read preference when using matchesQuery', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject0');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject1');
|
||||
@@ -1012,7 +960,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
obj2.set('myObject1', obj1);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1, obj2]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject2', req => {
|
||||
req.readPreference = 'SECONDARY_PREFERRED';
|
||||
@@ -1036,15 +984,15 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
let myObjectReadPreference0 = null;
|
||||
let myObjectReadPreference1 = null;
|
||||
let myObjectReadPreference2 = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1054,8 +1002,6 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change subqueries read preference when using doesNotMatchQuery', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject0');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject1');
|
||||
@@ -1066,7 +1012,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
obj2.set('myObject1', obj1);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1, obj2]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject2', req => {
|
||||
req.readPreference = 'SECONDARY_PREFERRED';
|
||||
@@ -1090,15 +1036,15 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
let myObjectReadPreference0 = null;
|
||||
let myObjectReadPreference1 = null;
|
||||
let myObjectReadPreference2 = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1108,8 +1054,6 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change subqueries read preference when using matchesKeyInQuery and doesNotMatchKeyInQuery', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject0');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject1');
|
||||
@@ -1120,7 +1064,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
obj2.set('myObject1', obj1);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1, obj2]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
|
||||
Parse.Cloud.beforeFind('MyObject2', req => {
|
||||
req.readPreference = 'SECONDARY_PREFERRED';
|
||||
@@ -1145,15 +1089,15 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
let myObjectReadPreference0 = null;
|
||||
let myObjectReadPreference1 = null;
|
||||
let myObjectReadPreference2 = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1163,8 +1107,6 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
});
|
||||
|
||||
it('should change subqueries read preference when using matchesKeyInQuery and doesNotMatchKeyInQuery to find through API', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
|
||||
const obj0 = new Parse.Object('MyObject0');
|
||||
obj0.set('boolKey', false);
|
||||
const obj1 = new Parse.Object('MyObject1');
|
||||
@@ -1175,7 +1117,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
obj2.set('myObject1', obj1);
|
||||
|
||||
await Parse.Object.saveAll([obj0, obj1, obj2]);
|
||||
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
|
||||
spyOn(Collection.prototype, 'find').and.callThrough();
|
||||
await waitForReplication();
|
||||
|
||||
const whereString = JSON.stringify({
|
||||
@@ -1215,15 +1157,15 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
let myObjectReadPreference0 = null;
|
||||
let myObjectReadPreference1 = null;
|
||||
let myObjectReadPreference2 = null;
|
||||
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
|
||||
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
|
||||
Collection.prototype.find.calls.all().forEach(call => {
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
|
||||
myObjectReadPreference0 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
|
||||
myObjectReadPreference1 = call.args[1].readPreference;
|
||||
}
|
||||
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
|
||||
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
|
||||
myObjectReadPreference2 = call.args[1].readPreference;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -191,6 +191,79 @@ describe('rest query', () => {
|
||||
expect(result.results.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('query internal field', async () => {
|
||||
const internalFields = [
|
||||
'_email_verify_token',
|
||||
'_perishable_token',
|
||||
'_tombstone',
|
||||
'_email_verify_token_expires_at',
|
||||
'_failed_login_count',
|
||||
'_account_lockout_expires_at',
|
||||
'_password_changed_at',
|
||||
'_password_history',
|
||||
];
|
||||
await Promise.all([
|
||||
...internalFields.map(field =>
|
||||
expectAsync(new Parse.Query(Parse.User).exists(field).find()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${field}`)
|
||||
)
|
||||
),
|
||||
...internalFields.map(field =>
|
||||
new Parse.Query(Parse.User).exists(field).find({ useMasterKey: true })
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('query protected field', async () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username1');
|
||||
user.setPassword('password');
|
||||
await user.signUp();
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const obj = new Parse.Object('Test');
|
||||
|
||||
obj.set('owner', user);
|
||||
obj.set('test', 'test');
|
||||
obj.set('zip', 1234);
|
||||
await obj.save();
|
||||
|
||||
const schema = await config.database.loadSchema();
|
||||
await schema.updateClass(
|
||||
'Test',
|
||||
{},
|
||||
{
|
||||
get: { '*': true },
|
||||
find: { '*': true },
|
||||
protectedFields: { [user.id]: ['zip'] },
|
||||
}
|
||||
);
|
||||
await Promise.all([
|
||||
new Parse.Query('Test').exists('test').find(),
|
||||
expectAsync(new Parse.Query('Test').exists('zip').find()).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
'This user is not allowed to query zip on class Test'
|
||||
)
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('query protected field with matchesQuery', async () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username1');
|
||||
user.setPassword('password');
|
||||
await user.signUp();
|
||||
const test = new Parse.Object('TestObject', { user });
|
||||
await test.save();
|
||||
const subQuery = new Parse.Query(Parse.User);
|
||||
subQuery.exists('_perishable_token');
|
||||
await expectAsync(
|
||||
new Parse.Query('TestObject').matchesQuery('user', subQuery).find()
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid key name: _perishable_token')
|
||||
);
|
||||
});
|
||||
|
||||
it('query with wrongly encoded parameter', done => {
|
||||
rest
|
||||
.create(config, nobody, 'TestParameterEncode', { foo: 'bar' })
|
||||
@@ -429,4 +502,25 @@ describe('RestQuery.each', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('test afterSave should not affect save response', async () => {
|
||||
Parse.Cloud.beforeSave('TestObject2', ({ object }) => {
|
||||
object.set('addedBeforeSave', true);
|
||||
});
|
||||
Parse.Cloud.afterSave('TestObject2', ({ object }) => {
|
||||
object.set('addedAfterSave', true);
|
||||
object.unset('initialToRemove');
|
||||
});
|
||||
const { response } = await rest.create(config, nobody, 'TestObject2', {
|
||||
initialSave: true,
|
||||
initialToRemove: true,
|
||||
});
|
||||
expect(Object.keys(response).sort()).toEqual([
|
||||
'addedAfterSave',
|
||||
'addedBeforeSave',
|
||||
'createdAt',
|
||||
'initialToRemove',
|
||||
'objectId',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+110
-84
@@ -89,10 +89,54 @@ describe('batch', () => {
|
||||
expect(internalURL).toEqual('/classes/Object');
|
||||
});
|
||||
|
||||
it('should handle a batch request without transaction', async done => {
|
||||
it('should return the proper url with no url provided', () => {
|
||||
const originalURL = '/parse/batch';
|
||||
const internalURL = batch.makeBatchRoutingPathFunction(
|
||||
originalURL,
|
||||
undefined,
|
||||
publicServerURL
|
||||
)('/parse/classes/Object');
|
||||
|
||||
expect(internalURL).toEqual('/classes/Object');
|
||||
});
|
||||
|
||||
it('should return the proper url with no public url provided', () => {
|
||||
const originalURL = '/parse/batch';
|
||||
const internalURL = batch.makeBatchRoutingPathFunction(
|
||||
originalURL,
|
||||
serverURLNaked,
|
||||
undefined
|
||||
)('/parse/classes/Object');
|
||||
|
||||
expect(internalURL).toEqual('/classes/Object');
|
||||
});
|
||||
|
||||
it('should return the proper url with bad url provided', () => {
|
||||
const originalURL = '/parse/batch';
|
||||
const internalURL = batch.makeBatchRoutingPathFunction(
|
||||
originalURL,
|
||||
'badurl.com',
|
||||
publicServerURL
|
||||
)('/parse/classes/Object');
|
||||
|
||||
expect(internalURL).toEqual('/classes/Object');
|
||||
});
|
||||
|
||||
it('should return the proper url with bad public url provided', () => {
|
||||
const originalURL = '/parse/batch';
|
||||
const internalURL = batch.makeBatchRoutingPathFunction(
|
||||
originalURL,
|
||||
serverURLNaked,
|
||||
'badurl.com'
|
||||
)('/parse/classes/Object');
|
||||
|
||||
expect(internalURL).toEqual('/classes/Object');
|
||||
});
|
||||
|
||||
it('should handle a batch request without transaction', async () => {
|
||||
spyOn(databaseAdapter, 'createObject').and.callThrough();
|
||||
|
||||
request({
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/batch',
|
||||
@@ -110,28 +154,25 @@ describe('batch', () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
}).then(response => {
|
||||
expect(response.data.length).toEqual(2);
|
||||
expect(response.data[0].success.objectId).toBeDefined();
|
||||
expect(response.data[0].success.createdAt).toBeDefined();
|
||||
expect(response.data[1].success.objectId).toBeDefined();
|
||||
expect(response.data[1].success.createdAt).toBeDefined();
|
||||
const query = new Parse.Query('MyObject');
|
||||
query.find().then(results => {
|
||||
expect(databaseAdapter.createObject.calls.count()).toBe(2);
|
||||
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toEqual(null);
|
||||
expect(databaseAdapter.createObject.calls.argsFor(1)[3]).toEqual(null);
|
||||
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
expect(response.data.length).toEqual(2);
|
||||
expect(response.data[0].success.objectId).toBeDefined();
|
||||
expect(response.data[0].success.createdAt).toBeDefined();
|
||||
expect(response.data[1].success.objectId).toBeDefined();
|
||||
expect(response.data[1].success.createdAt).toBeDefined();
|
||||
const query = new Parse.Query('MyObject');
|
||||
const results = await query.find();
|
||||
expect(databaseAdapter.createObject.calls.count()).toBe(2);
|
||||
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toEqual(null);
|
||||
expect(databaseAdapter.createObject.calls.argsFor(1)[3]).toEqual(null);
|
||||
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
|
||||
});
|
||||
|
||||
it('should handle a batch request with transaction = false', async done => {
|
||||
await reconfigureServer();
|
||||
it('should handle a batch request with transaction = false', async () => {
|
||||
spyOn(databaseAdapter, 'createObject').and.callThrough();
|
||||
|
||||
request({
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/batch',
|
||||
@@ -150,21 +191,18 @@ describe('batch', () => {
|
||||
],
|
||||
transaction: false,
|
||||
}),
|
||||
}).then(response => {
|
||||
expect(response.data.length).toEqual(2);
|
||||
expect(response.data[0].success.objectId).toBeDefined();
|
||||
expect(response.data[0].success.createdAt).toBeDefined();
|
||||
expect(response.data[1].success.objectId).toBeDefined();
|
||||
expect(response.data[1].success.createdAt).toBeDefined();
|
||||
const query = new Parse.Query('MyObject');
|
||||
query.find().then(results => {
|
||||
expect(databaseAdapter.createObject.calls.count()).toBe(2);
|
||||
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toEqual(null);
|
||||
expect(databaseAdapter.createObject.calls.argsFor(1)[3]).toEqual(null);
|
||||
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
expect(response.data.length).toEqual(2);
|
||||
expect(response.data[0].success.objectId).toBeDefined();
|
||||
expect(response.data[0].success.createdAt).toBeDefined();
|
||||
expect(response.data[1].success.objectId).toBeDefined();
|
||||
expect(response.data[1].success.createdAt).toBeDefined();
|
||||
const query = new Parse.Query('MyObject');
|
||||
const results = await query.find();
|
||||
expect(databaseAdapter.createObject.calls.count()).toBe(2);
|
||||
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toEqual(null);
|
||||
expect(databaseAdapter.createObject.calls.argsFor(1)[3]).toEqual(null);
|
||||
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -191,58 +229,45 @@ describe('batch', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle a batch request with transaction = true', async done => {
|
||||
await reconfigureServer();
|
||||
it('should handle a batch request with transaction = true', async () => {
|
||||
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
|
||||
myObject
|
||||
.save()
|
||||
.then(() => {
|
||||
return myObject.destroy();
|
||||
})
|
||||
.then(() => {
|
||||
spyOn(databaseAdapter, 'createObject').and.callThrough();
|
||||
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/batch',
|
||||
body: JSON.stringify({
|
||||
requests: [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/1/classes/MyObject',
|
||||
body: { key: 'value1' },
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/1/classes/MyObject',
|
||||
body: { key: 'value2' },
|
||||
},
|
||||
],
|
||||
transaction: true,
|
||||
}),
|
||||
}).then(response => {
|
||||
expect(response.data.length).toEqual(2);
|
||||
expect(response.data[0].success.objectId).toBeDefined();
|
||||
expect(response.data[0].success.createdAt).toBeDefined();
|
||||
expect(response.data[1].success.objectId).toBeDefined();
|
||||
expect(response.data[1].success.createdAt).toBeDefined();
|
||||
const query = new Parse.Query('MyObject');
|
||||
query.find().then(results => {
|
||||
expect(databaseAdapter.createObject.calls.count() % 2).toBe(0);
|
||||
for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) {
|
||||
expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe(
|
||||
databaseAdapter.createObject.calls.argsFor(i + 1)[3]
|
||||
);
|
||||
}
|
||||
expect(results.map(result => result.get('key')).sort()).toEqual([
|
||||
'value1',
|
||||
'value2',
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
await myObject.save();
|
||||
await myObject.destroy();
|
||||
spyOn(databaseAdapter, 'createObject').and.callThrough();
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/batch',
|
||||
body: JSON.stringify({
|
||||
requests: [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/1/classes/MyObject',
|
||||
body: { key: 'value1' },
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/1/classes/MyObject',
|
||||
body: { key: 'value2' },
|
||||
},
|
||||
],
|
||||
transaction: true,
|
||||
}),
|
||||
});
|
||||
expect(response.data.length).toEqual(2);
|
||||
expect(response.data[0].success.objectId).toBeDefined();
|
||||
expect(response.data[0].success.createdAt).toBeDefined();
|
||||
expect(response.data[1].success.objectId).toBeDefined();
|
||||
expect(response.data[1].success.createdAt).toBeDefined();
|
||||
const query = new Parse.Query('MyObject');
|
||||
const results = await query.find();
|
||||
expect(databaseAdapter.createObject.calls.count() % 2).toBe(0);
|
||||
for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) {
|
||||
expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe(
|
||||
databaseAdapter.createObject.calls.argsFor(i + 1)[3]
|
||||
);
|
||||
}
|
||||
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
|
||||
});
|
||||
|
||||
it('should not save anything when one operation fails in a transaction', async () => {
|
||||
@@ -350,6 +375,7 @@ describe('batch', () => {
|
||||
transaction: true,
|
||||
}),
|
||||
});
|
||||
fail();
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
const query = new Parse.Query('MyObject');
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
'use strict';
|
||||
const dns = require('dns');
|
||||
const semver = require('semver');
|
||||
const CurrentSpecReporter = require('./support/CurrentSpecReporter.js');
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
const SchemaCache = require('../lib/Adapters/Cache/SchemaCache').default;
|
||||
|
||||
// Ensure localhost resolves to ipv4 address first on node v17+
|
||||
if (dns.setDefaultResultOrder) {
|
||||
dns.setDefaultResultOrder('ipv4first');
|
||||
}
|
||||
|
||||
// Sets up a Parse API server for testing.
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
|
||||
jasmine.getEnv().addReporter(new CurrentSpecReporter());
|
||||
@@ -455,6 +461,15 @@ global.it_only_postgres_version = version => {
|
||||
}
|
||||
};
|
||||
|
||||
global.it_only_node_version = version => {
|
||||
const envVersion = process.version;
|
||||
if (!envVersion || semver.satisfies(envVersion, version)) {
|
||||
return it;
|
||||
} else {
|
||||
return xit;
|
||||
}
|
||||
};
|
||||
|
||||
global.fit_only_mongodb_version = version => {
|
||||
const envVersion = process.env.MONGODB_VERSION;
|
||||
if (!envVersion || semver.satisfies(envVersion, version)) {
|
||||
@@ -464,6 +479,15 @@ global.fit_only_mongodb_version = version => {
|
||||
}
|
||||
};
|
||||
|
||||
global.fit_only_node_version = version => {
|
||||
const envVersion = process.version;
|
||||
if (!envVersion || semver.satisfies(envVersion, version)) {
|
||||
return fit;
|
||||
} else {
|
||||
return xit;
|
||||
}
|
||||
};
|
||||
|
||||
global.it_exclude_mongodb_version = version => {
|
||||
const envVersion = process.env.MONGODB_VERSION;
|
||||
if (!envVersion || !semver.satisfies(envVersion, version)) {
|
||||
@@ -482,6 +506,15 @@ global.it_exclude_postgres_version = version => {
|
||||
}
|
||||
};
|
||||
|
||||
global.it_exclude_node_version = version => {
|
||||
const envVersion = process.env.NODE_VERSION;
|
||||
if (!envVersion || !semver.satisfies(envVersion, version)) {
|
||||
return it;
|
||||
} else {
|
||||
return xit;
|
||||
}
|
||||
};
|
||||
|
||||
global.fit_exclude_mongodb_version = version => {
|
||||
const envVersion = process.env.MONGODB_VERSION;
|
||||
if (!envVersion || !semver.satisfies(envVersion, version)) {
|
||||
@@ -491,6 +524,15 @@ global.fit_exclude_mongodb_version = version => {
|
||||
}
|
||||
};
|
||||
|
||||
global.fit_exclude_node_version = version => {
|
||||
const envVersion = process.env.NODE_VERSION;
|
||||
if (!envVersion || !semver.satisfies(envVersion, version)) {
|
||||
return fit;
|
||||
} else {
|
||||
return xit;
|
||||
}
|
||||
};
|
||||
|
||||
global.fit_exclude_dbs = excluded => {
|
||||
if (excluded.indexOf(process.env.PARSE_SERVER_TEST_DB) >= 0) {
|
||||
return xit;
|
||||
|
||||
+22
-1
@@ -462,6 +462,26 @@ describe('server', () => {
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('fails if default limit is negative', async () => {
|
||||
await expectAsync(reconfigureServer({ defaultLimit: -1 })).toBeRejectedWith(
|
||||
'Default limit must be a value greater than 0.'
|
||||
);
|
||||
});
|
||||
|
||||
it('fails if default limit is wrong type', async () => {
|
||||
for (const value of ['invalid', {}, [], true]) {
|
||||
await expectAsync(reconfigureServer({ defaultLimit: value })).toBeRejectedWith(
|
||||
'Default limit must be a number.'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('fails if default limit is zero', async () => {
|
||||
await expectAsync(reconfigureServer({ defaultLimit: 0 })).toBeRejectedWith(
|
||||
'Default limit must be a value greater than 0.'
|
||||
);
|
||||
});
|
||||
|
||||
it('fails if maxLimit is negative', done => {
|
||||
reconfigureServer({ maxLimit: -100 }).catch(error => {
|
||||
expect(error).toEqual('Max limit must be a value greater than 0.');
|
||||
@@ -533,6 +553,7 @@ describe('server', () => {
|
||||
|
||||
it('should not fail when Google signin is introduced without the optional clientId', done => {
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authUtils = require('../lib/Adapters/Auth/utils');
|
||||
|
||||
reconfigureServer({
|
||||
auth: { google: {} },
|
||||
@@ -545,7 +566,7 @@ describe('server', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
const user = new Parse.User();
|
||||
user
|
||||
|
||||
@@ -1866,6 +1866,34 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nested documents', () => {
|
||||
beforeAll(async () => {
|
||||
const testSchema = new Parse.Schema('test_7371');
|
||||
testSchema.setCLP({
|
||||
create: { ['*']: true },
|
||||
update: { ['*']: true },
|
||||
addField: {},
|
||||
});
|
||||
testSchema.addObject('a');
|
||||
await testSchema.save();
|
||||
});
|
||||
|
||||
it('addField permission not required for adding a nested property', async () => {
|
||||
const obj = new Parse.Object('test_7371');
|
||||
obj.set('a', {});
|
||||
await obj.save();
|
||||
obj.set('a.b', 2);
|
||||
await obj.save();
|
||||
});
|
||||
it('addField permission not required for modifying a nested property', async () => {
|
||||
const obj = new Parse.Object('test_7371');
|
||||
obj.set('a', { b: 1 });
|
||||
await obj.save();
|
||||
obj.set('a.b', 2);
|
||||
await obj.save();
|
||||
});
|
||||
});
|
||||
|
||||
it('should aceept class-level permission with userid of any length', async done => {
|
||||
await global.reconfigureServer({
|
||||
customIdSize: 11,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEvDCCA6SgAwIBAgIQXRHxNXkw1L9z5/3EZ/T/hDANBgkqhkiG9w0BAQsFADB/
|
||||
MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAd
|
||||
BgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMDAuBgNVBAMTJ1N5bWFudGVj
|
||||
IENsYXNzIDMgU0hBMjU2IENvZGUgU2lnbmluZyBDQTAeFw0xODA5MTcwMDAwMDBa
|
||||
Fw0xOTA5MTcyMzU5NTlaMHMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9y
|
||||
bmlhMRIwEAYDVQQHDAlDdXBlcnRpbm8xFDASBgNVBAoMC0FwcGxlLCBJbmMuMQ8w
|
||||
DQYDVQQLDAZHQyBTUkUxFDASBgNVBAMMC0FwcGxlLCBJbmMuMIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA06fwIi8fgKrTQu7cBcFkJVF6+Tqvkg7MKJTM
|
||||
IOYPPQtPF3AZYPsbUoRKAD7/JXrxxOSVJ7vU1mP77tYG8TcUteZ3sAwvt2dkRbm7
|
||||
ZO6DcmSggv1Dg4k3goNw4GYyCY4Z2/8JSmsQ80Iv/UOOwynpBziEeZmJ4uck6zlA
|
||||
17cDkH48LBpKylaqthym5bFs9gj11pto7mvyb5BTcVuohwi6qosvbs/4VGbC2Nsz
|
||||
ie416nUZfv+xxoXH995gxR2mw5cDdeCew7pSKxEhvYjT2nVdQF0q/hnPMFnOaEyT
|
||||
q79n3gwFXyt0dy8eP6KBF7EW9J6b7ubu/j7h+tQfxPM+gTXOBQIDAQABo4IBPjCC
|
||||
ATowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
|
||||
AwMwYQYDVR0gBFowWDBWBgZngQwBBAEwTDAjBggrBgEFBQcCARYXaHR0cHM6Ly9k
|
||||
LnN5bWNiLmNvbS9jcHMwJQYIKwYBBQUHAgIwGQwXaHR0cHM6Ly9kLnN5bWNiLmNv
|
||||
bS9ycGEwHwYDVR0jBBgwFoAUljtT8Hkzl699g+8uK8zKt4YecmYwKwYDVR0fBCQw
|
||||
IjAgoB6gHIYaaHR0cDovL3N2LnN5bWNiLmNvbS9zdi5jcmwwVwYIKwYBBQUHAQEE
|
||||
SzBJMB8GCCsGAQUFBzABhhNodHRwOi8vc3Yuc3ltY2QuY29tMCYGCCsGAQUFBzAC
|
||||
hhpodHRwOi8vc3Yuc3ltY2IuY29tL3N2LmNydDANBgkqhkiG9w0BAQsFAAOCAQEA
|
||||
I/j/PcCNPebSAGrcqSFBSa2mmbusOX01eVBg8X0G/z8Z+ZWUfGFzDG0GQf89MPxV
|
||||
woec+nZuqui7o9Bg8s8JbHV0TC52X14CbTj9w/qBF748WbH9gAaTkrJYPm+MlNhu
|
||||
tjEuQdNl/YXVMvQW4O8UMHTi09GyJQ0NC4q92Wxvx1m/qzjvTLvrXHGQ9pEHhPyz
|
||||
vfBLxQkWpNoCNKU7UeESyH06XOrGc9MsII9deeKsDJp9a0jtx+pP4MFVtFME9SSQ
|
||||
tMBs0It7WwEf7qcRLpialxKwY2EzQ9g4WnANHqo18PrDBE10TFpZPzUh7JhMViVr
|
||||
EEbl0YdElmF8Hlamah/yNw==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,42 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIHbDCCBVSgAwIBAgIQAwuBj1pc45FkhpmTbIvZOjANBgkqhkiG9w0BAQsFADBp
|
||||
MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMT
|
||||
OERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0
|
||||
IDIwMjEgQ0ExMB4XDTIxMDcyOTAwMDAwMFoXDTIyMDcyODIzNTk1OVowcTELMAkG
|
||||
A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCUN1cGVydGlu
|
||||
bzETMBEGA1UEChMKQXBwbGUgSW5jLjEPMA0GA1UECxMGR0MgU1JFMRMwEQYDVQQD
|
||||
EwpBcHBsZSBJbmMuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGXC
|
||||
hfNKtSFUkayI4RGDl1T7cTqs9Ni6vnwJpU/9nTT3BWWxZ2Yng4muIhMeA3oZfDZu
|
||||
T1ShS5y3CQV9/9SaUU1NNfnPxenvrrE8xSn8a9bo2adTrn9ASrEMqRD6bp+fS5Cp
|
||||
kFHYH+VD5a8XTyOuDGpQyqIpUpYqGABXITWrEpjnpAw1IjMaeNO9sYJkWuLdw0gg
|
||||
IMpBqmiiJXHgasl8D59S93PVHD1xkEjZcPT9NEWJXSRHUW+Xe+JUhrFSzEfjyWNS
|
||||
spgJrnVtv4ec30Uz0qUC683lkfE446VPiIyo3xmjh3rs3G75JYJd5925YVM0uz1U
|
||||
Wn0VmOTN5s81V6CBdYRc3J0sCGd5QEmDo4pwPwCMej+fT6fktIXUWZ1i/ycI1//m
|
||||
Vc4kkuyiJ2msv8GSACPG6XkL+zKTjYC+GElj/WCX+hVJKzsYtL51zRr4KNnqhG7/
|
||||
GK5kJ9eVTgTEKqdB0DZ7ZpOD3EoE2D9kj4zaoq/7r6Syi7Efw230zDMQyIJnoUQc
|
||||
GDWUR2ZPQ+U+aUOKdWpgbhy4vOzTi24hOVcACbvc/CFTQ2gI7SfCSao9WLVqqGO5
|
||||
waHhoOidTYY9Ey2PQvYHqXm5R2Ol+3V+GQl0NkiDt5kc7OpYIm7cDyQ04ZaHnUDt
|
||||
ZljI5N1fdlhYVKntEzX4sNhcx1pNB1C/T5Wfw68CAwEAAaOCAgYwggICMB8GA1Ud
|
||||
IwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRS7TCGHb7iPnHR
|
||||
/odXPWLpAxPVKjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMw
|
||||
gbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp
|
||||
Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j
|
||||
cmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0
|
||||
ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMD4GA1UdIAQ3
|
||||
MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQu
|
||||
Y29tL0NQUzCBlAYIKwYBBQUHAQEEgYcwgYQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9v
|
||||
Y3NwLmRpZ2ljZXJ0LmNvbTBcBggrBgEFBQcwAoZQaHR0cDovL2NhY2VydHMuZGln
|
||||
aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hB
|
||||
Mzg0MjAyMUNBMS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEA
|
||||
uk71YLf55ne94hEeQtYsjCn38Tw3h78CH195J8H4T4r2p7p9MPjrA2zz+ZXza+kb
|
||||
z5OTZ9k1/nu9vKnh4ljZS33uTh5AcdWhQNUeSuByjhVu+YTnVKqVYH/jaZXEFFe/
|
||||
4/n23Shn2xN5jtkCEwYeqEaO6+8uBCFQldnUgbSag2Le9s/lICUJvGsKTAUhEGrK
|
||||
R4u4OyJGGk8JO5Ozbnoe1AGBK9pKMWOAl+SY/b/CLLTgypwZwD/6xszM1MhcfzPS
|
||||
aBbJ7MX2Uiq91/PNJdPnZI/PoqAQEzDL+5MZnwKwNpeC1rH8ZhlCn1BXbxI5jemw
|
||||
Tfo2U6cDN1ObJ4LBzsVioWA0KoNnp4eWkMmbGGH5iWRcwoCjhkzot8VvXoll0uSe
|
||||
F9v1RMOCM+Vcr++MYdJxdoQDNMunEoUnpHQbreHSLMcwPUhSNO4+EtZA86hob2u0
|
||||
6yMXdAi9pEs9Aj13LAW74MCDrToCzoa2ZaisvxbRfQSpXryUQEnqpuQqCVjglxaJ
|
||||
FIMhV0DRWIaLF9vhv6zF9kL77qr+arLd/wJlXubtD/P9tJZRlEh6/0iHvyyH2+Rg
|
||||
u05//UQ7ex/j15PLFSVkQXIFPpN1ZgN0FrJKAJOL+MWiB5RncKxjin8Y9xfC3XKS
|
||||
fbV6c7J9AGi8bE8aFMM2ISg7v/dOQzcLPPScWbe5cTg=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -109,6 +109,67 @@ describe('Vulnerabilities', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('denies expanding existing object with polluted keys', async () => {
|
||||
const obj = await new Parse.Object('RCE', { a: { foo: [] } }).save();
|
||||
await reconfigureServer({
|
||||
requestKeywordDenylist: ['foo'],
|
||||
});
|
||||
obj.addUnique('a.foo', 'abc');
|
||||
await expectAsync(obj.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Prohibited keyword in request data: "foo".`)
|
||||
);
|
||||
});
|
||||
|
||||
it('denies creating a cloud trigger with polluted data', async () => {
|
||||
Parse.Cloud.beforeSave('TestObject', ({ object }) => {
|
||||
object.set('obj', {
|
||||
constructor: {
|
||||
prototype: {
|
||||
dummy: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
await expectAsync(new Parse.Object('TestObject').save()).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
'Prohibited keyword in request data: {"key":"constructor"}.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('denies creating a hook with polluted data', async () => {
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const port = 34567;
|
||||
const hookServerURL = 'http://localhost:' + port;
|
||||
const app = express();
|
||||
app.use(bodyParser.json({ type: '*/*' }));
|
||||
const server = await new Promise(resolve => {
|
||||
const res = app.listen(port, undefined, () => resolve(res));
|
||||
});
|
||||
app.post('/BeforeSave', function (req, res) {
|
||||
const object = Parse.Object.fromJSON(req.body.object);
|
||||
object.set('hello', 'world');
|
||||
object.set('obj', {
|
||||
constructor: {
|
||||
prototype: {
|
||||
dummy: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
res.json({ success: object });
|
||||
});
|
||||
await Parse.Hooks.createTrigger('TestObject', 'beforeSave', hookServerURL + '/BeforeSave');
|
||||
await expectAsync(new Parse.Object('TestObject').save()).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
'Prohibited keyword in request data: {"key":"constructor"}.'
|
||||
)
|
||||
);
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
});
|
||||
|
||||
it('allows BSON type code data in write request with custom denylist', async () => {
|
||||
await reconfigureServer({
|
||||
requestKeywordDenylist: [],
|
||||
@@ -279,5 +340,57 @@ describe('Vulnerabilities', () => {
|
||||
expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
|
||||
expect(text.error).toBe('Prohibited keyword in request data: {"value":"aValue[123]*"}.');
|
||||
});
|
||||
|
||||
it('denies BSON type code data in file metadata', async () => {
|
||||
const str = 'Hello World!';
|
||||
const data = [];
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
data.push(str.charCodeAt(i));
|
||||
}
|
||||
const file = new Parse.File('hello.txt', data, 'text/plain');
|
||||
file.addMetadata('obj', {
|
||||
_bsontype: 'Code',
|
||||
code: 'delete Object.prototype.evalFunctions',
|
||||
});
|
||||
await expectAsync(file.save()).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
`Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('denies BSON type code data in file tags', async () => {
|
||||
const str = 'Hello World!';
|
||||
const data = [];
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
data.push(str.charCodeAt(i));
|
||||
}
|
||||
const file = new Parse.File('hello.txt', data, 'text/plain');
|
||||
file.addTag('obj', {
|
||||
_bsontype: 'Code',
|
||||
code: 'delete Object.prototype.evalFunctions',
|
||||
});
|
||||
await expectAsync(file.save()).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
`Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ignore non-matches', () => {
|
||||
it('ignores write request that contains only fraction of denied keyword', async () => {
|
||||
await reconfigureServer({
|
||||
requestKeywordDenylist: [{ key: 'abc' }],
|
||||
});
|
||||
// Initially saving an object executes the keyword detection in RestWrite.js
|
||||
const obj = new TestObject({ a: { b: { c: 0 } } });
|
||||
await expectAsync(obj.save()).toBeResolved();
|
||||
// Modifying a nested key executes the keyword detection in DatabaseController.js
|
||||
obj.increment('a.b.c');
|
||||
await expectAsync(obj.save()).toBeResolved();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ const Parse = require('parse/node').Parse;
|
||||
const jwksClient = require('jwks-rsa');
|
||||
const util = require('util');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authUtils = require('./utils');
|
||||
|
||||
const TOKEN_ISSUER = 'https://appleid.apple.com';
|
||||
|
||||
@@ -30,21 +31,12 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
return key;
|
||||
};
|
||||
|
||||
const getHeaderFromToken = token => {
|
||||
const decodedToken = jwt.decode(token, { complete: true });
|
||||
if (!decodedToken) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
|
||||
}
|
||||
|
||||
return decodedToken.header;
|
||||
};
|
||||
|
||||
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
|
||||
if (!token) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
|
||||
}
|
||||
|
||||
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
|
||||
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
|
||||
const ONE_HOUR_IN_MS = 3600000;
|
||||
let jwtClaims;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ const jwksClient = require('jwks-rsa');
|
||||
const util = require('util');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const httpsRequest = require('./httpsRequest');
|
||||
const authUtils = require('./utils');
|
||||
|
||||
const TOKEN_ISSUER = 'https://facebook.com';
|
||||
|
||||
@@ -32,22 +33,23 @@ function validateGraphToken(authData, options) {
|
||||
});
|
||||
}
|
||||
|
||||
function validateGraphAppId(appIds, authData, options) {
|
||||
async function validateGraphAppId(appIds, authData, options) {
|
||||
var access_token = authData.access_token;
|
||||
if (process.env.TESTING && access_token === 'test') {
|
||||
return Promise.resolve();
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(appIds)) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.');
|
||||
}
|
||||
if (!appIds.length) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Facebook auth is not configured.');
|
||||
}
|
||||
return graphRequest(
|
||||
'app?access_token=' + access_token + getAppSecretPath(authData, options)
|
||||
).then(data => {
|
||||
if (data && appIds.indexOf(data.id) != -1) {
|
||||
return;
|
||||
}
|
||||
const data = await graphRequest(
|
||||
`app?access_token=${access_token}${getAppSecretPath(authData, options)}`
|
||||
);
|
||||
if (!data || !appIds.includes(data.id)) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Facebook auth is invalid for this user.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
@@ -72,21 +74,12 @@ const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
return key;
|
||||
};
|
||||
|
||||
const getHeaderFromToken = token => {
|
||||
const decodedToken = jwt.decode(token, { complete: true });
|
||||
if (!decodedToken) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'provided token does not decode as JWT');
|
||||
}
|
||||
|
||||
return decodedToken.header;
|
||||
};
|
||||
|
||||
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
|
||||
if (!token) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'id token is invalid for this user.');
|
||||
}
|
||||
|
||||
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
|
||||
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
|
||||
const ONE_HOUR_IN_MS = 3600000;
|
||||
let jwtClaims;
|
||||
|
||||
|
||||
+100
-26
@@ -14,20 +14,17 @@ const authData = {
|
||||
const { Parse } = require('parse/node');
|
||||
const crypto = require('crypto');
|
||||
const https = require('https');
|
||||
const url = require('url');
|
||||
|
||||
const { pki } = require('node-forge');
|
||||
const ca = { cert: null, url: null };
|
||||
const cache = {}; // (publicKey -> cert) cache
|
||||
|
||||
function verifyPublicKeyUrl(publicKeyUrl) {
|
||||
const parsedUrl = url.parse(publicKeyUrl);
|
||||
if (parsedUrl.protocol !== 'https:') {
|
||||
try {
|
||||
const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
|
||||
return regex.test(publicKeyUrl);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
const hostnameParts = parsedUrl.hostname.split('.');
|
||||
const length = hostnameParts.length;
|
||||
const domainParts = hostnameParts.slice(length - 2, length);
|
||||
const domain = domainParts.join('.');
|
||||
return domain === 'apple.com';
|
||||
}
|
||||
|
||||
function convertX509CertToPEM(X509Cert) {
|
||||
@@ -40,7 +37,7 @@ function convertX509CertToPEM(X509Cert) {
|
||||
return pemPreFix + certBody + pemPostFix;
|
||||
}
|
||||
|
||||
function getAppleCertificate(publicKeyUrl) {
|
||||
async function getAppleCertificate(publicKeyUrl) {
|
||||
if (!verifyPublicKeyUrl(publicKeyUrl)) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
@@ -50,26 +47,59 @@ function getAppleCertificate(publicKeyUrl) {
|
||||
if (cache[publicKeyUrl]) {
|
||||
return cache[publicKeyUrl];
|
||||
}
|
||||
const url = new URL(publicKeyUrl);
|
||||
const headOptions = {
|
||||
hostname: url.hostname,
|
||||
path: url.pathname,
|
||||
method: 'HEAD',
|
||||
};
|
||||
const cert_headers = await new Promise((resolve, reject) =>
|
||||
https.get(headOptions, res => resolve(res.headers)).on('error', reject)
|
||||
);
|
||||
const validContentTypes = ['application/x-x509-ca-cert', 'application/pkix-cert'];
|
||||
if (
|
||||
!validContentTypes.includes(cert_headers['content-type']) ||
|
||||
cert_headers['content-length'] == null ||
|
||||
cert_headers['content-length'] > 10000
|
||||
) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
|
||||
);
|
||||
}
|
||||
const { certificate, headers } = await getCertificate(publicKeyUrl);
|
||||
if (headers['cache-control']) {
|
||||
const expire = headers['cache-control'].match(/max-age=([0-9]+)/);
|
||||
if (expire) {
|
||||
cache[publicKeyUrl] = certificate;
|
||||
// we'll expire the cache entry later, as per max-age
|
||||
setTimeout(() => {
|
||||
delete cache[publicKeyUrl];
|
||||
}, parseInt(expire[1], 10) * 1000);
|
||||
}
|
||||
}
|
||||
return verifyPublicKeyIssuer(certificate, publicKeyUrl);
|
||||
}
|
||||
|
||||
function getCertificate(url, buffer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
https
|
||||
.get(publicKeyUrl, res => {
|
||||
let data = '';
|
||||
.get(url, res => {
|
||||
const data = [];
|
||||
res.on('data', chunk => {
|
||||
data += chunk.toString('base64');
|
||||
data.push(chunk);
|
||||
});
|
||||
res.on('end', () => {
|
||||
const cert = convertX509CertToPEM(data);
|
||||
if (res.headers['cache-control']) {
|
||||
var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
|
||||
if (expire) {
|
||||
cache[publicKeyUrl] = cert;
|
||||
// we'll expire the cache entry later, as per max-age
|
||||
setTimeout(() => {
|
||||
delete cache[publicKeyUrl];
|
||||
}, parseInt(expire[1], 10) * 1000);
|
||||
}
|
||||
if (buffer) {
|
||||
resolve({ certificate: Buffer.concat(data), headers: res.headers });
|
||||
return;
|
||||
}
|
||||
resolve(cert);
|
||||
let cert = '';
|
||||
for (const chunk of data) {
|
||||
cert += chunk.toString('base64');
|
||||
}
|
||||
const certificate = convertX509CertToPEM(cert);
|
||||
resolve({ certificate, headers: res.headers });
|
||||
});
|
||||
})
|
||||
.on('error', reject);
|
||||
@@ -100,6 +130,30 @@ function verifySignature(publicKey, authData) {
|
||||
}
|
||||
}
|
||||
|
||||
function verifyPublicKeyIssuer(cert, publicKeyUrl) {
|
||||
const publicKeyCert = pki.certificateFromPem(cert);
|
||||
if (!ca.cert) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
|
||||
);
|
||||
}
|
||||
try {
|
||||
if (!ca.cert.verify(publicKeyCert)) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
|
||||
);
|
||||
}
|
||||
return cert;
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills if this user id is valid.
|
||||
async function validateAuthData(authData) {
|
||||
if (!authData.id) {
|
||||
@@ -111,11 +165,31 @@ async function validateAuthData(authData) {
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills if this app id is valid.
|
||||
function validateAppId() {
|
||||
return Promise.resolve();
|
||||
async function validateAppId(appIds, authData, options = {}) {
|
||||
if (!options.rootCertificateUrl) {
|
||||
options.rootCertificateUrl =
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
|
||||
}
|
||||
if (ca.url === options.rootCertificateUrl) {
|
||||
return;
|
||||
}
|
||||
const { certificate, headers } = await getCertificate(options.rootCertificateUrl, true);
|
||||
if (
|
||||
headers['content-type'] !== 'application/x-pem-file' ||
|
||||
headers['content-length'] == null ||
|
||||
headers['content-length'] > 10000
|
||||
) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
|
||||
);
|
||||
}
|
||||
ca.cert = pki.certificateFromPem(certificate);
|
||||
ca.url = options.rootCertificateUrl;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId,
|
||||
validateAuthData,
|
||||
cache,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ var Parse = require('parse/node').Parse;
|
||||
|
||||
const https = require('https');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authUtils = require('./utils');
|
||||
|
||||
const TOKEN_ISSUER = 'accounts.google.com';
|
||||
const HTTPS_TOKEN_ISSUER = 'https://accounts.google.com';
|
||||
@@ -51,22 +52,12 @@ function getGoogleKeyByKeyId(keyId) {
|
||||
});
|
||||
}
|
||||
|
||||
function getHeaderFromToken(token) {
|
||||
const decodedToken = jwt.decode(token, { complete: true });
|
||||
|
||||
if (!decodedToken) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
|
||||
}
|
||||
|
||||
return decodedToken.header;
|
||||
}
|
||||
|
||||
async function verifyIdToken({ id_token: token, id }, { clientId }) {
|
||||
if (!token) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
|
||||
}
|
||||
|
||||
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
|
||||
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
|
||||
let jwtClaims;
|
||||
const googleKey = await getGoogleKeyByKeyId(keyId);
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
*/
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
const url = require('url');
|
||||
const querystring = require('querystring');
|
||||
const httpsRequest = require('./httpsRequest');
|
||||
|
||||
@@ -112,7 +111,7 @@ function requestTokenInfo(options, access_token) {
|
||||
if (!options || !options.tokenIntrospectionEndpointUrl) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, MISSING_URL);
|
||||
}
|
||||
const parsedUrl = url.parse(options.tokenIntrospectionEndpointUrl);
|
||||
const parsedUrl = new URL(options.tokenIntrospectionEndpointUrl);
|
||||
const postData = querystring.stringify({
|
||||
token: access_token,
|
||||
});
|
||||
|
||||
@@ -13,17 +13,18 @@ function validateAuthData(authData) {
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills if this app id is valid.
|
||||
function validateAppId(appIds, authData) {
|
||||
var access_token = authData.access_token;
|
||||
async function validateAppId(appIds, authData) {
|
||||
const access_token = authData.access_token;
|
||||
if (!Array.isArray(appIds)) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.');
|
||||
}
|
||||
if (!appIds.length) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is not configured.');
|
||||
}
|
||||
return request('me', access_token).then(data => {
|
||||
if (data && appIds.indexOf(data.id) != -1) {
|
||||
return;
|
||||
}
|
||||
const data = await request('me', access_token);
|
||||
if (!data || !appIds.includes(data.id)) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is invalid for this user.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// A promisey wrapper for Spotify API requests.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Parse = require('parse/node').Parse;
|
||||
const getHeaderFromToken = token => {
|
||||
const decodedToken = jwt.decode(token, { complete: true });
|
||||
if (!decodedToken) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
|
||||
}
|
||||
|
||||
return decodedToken.header;
|
||||
};
|
||||
module.exports = {
|
||||
getHeaderFromToken,
|
||||
};
|
||||
@@ -5,7 +5,7 @@ export class LRUCache {
|
||||
constructor({ ttl = defaults.cacheTTL, maxSize = defaults.cacheMaxSize }) {
|
||||
this.cache = new LRU({
|
||||
max: maxSize,
|
||||
maxAge: ttl,
|
||||
ttl,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
GridFSBucketAdapter
|
||||
Stores files in Mongo using GridStore
|
||||
Stores files in Mongo using GridFS
|
||||
Requires the database adapter to be based on mongoclient
|
||||
|
||||
@flow weak
|
||||
@@ -228,22 +228,35 @@ export class GridFSBucketAdapter extends FilesAdapter {
|
||||
const partialstart = parts[0];
|
||||
const partialend = parts[1];
|
||||
|
||||
const start = parseInt(partialstart, 10);
|
||||
const end = partialend ? parseInt(partialend, 10) : files[0].length - 1;
|
||||
const fileLength = files[0].length;
|
||||
const fileStart = parseInt(partialstart, 10);
|
||||
const fileEnd = partialend ? parseInt(partialend, 10) : fileLength;
|
||||
|
||||
res.writeHead(206, {
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': end - start + 1,
|
||||
'Content-Range': 'bytes ' + start + '-' + end + '/' + files[0].length,
|
||||
'Content-Type': contentType,
|
||||
});
|
||||
let start = Math.min(fileStart || 0, fileEnd, fileLength);
|
||||
let end = Math.max(fileStart || 0, fileEnd) + 1 || fileLength;
|
||||
if (isNaN(fileStart)) {
|
||||
start = fileLength - end + 1;
|
||||
end = fileLength;
|
||||
}
|
||||
end = Math.min(end, fileLength);
|
||||
start = Math.max(start, 0);
|
||||
|
||||
res.status(206);
|
||||
res.header('Accept-Ranges', 'bytes');
|
||||
res.header('Content-Length', end - start);
|
||||
res.header('Content-Range', 'bytes ' + start + '-' + end + '/' + fileLength);
|
||||
res.header('Content-Type', contentType);
|
||||
const stream = bucket.openDownloadStreamByName(filename);
|
||||
stream.start(start);
|
||||
if (end) {
|
||||
stream.end(end);
|
||||
}
|
||||
stream.on('data', chunk => {
|
||||
res.write(chunk);
|
||||
});
|
||||
stream.on('error', () => {
|
||||
res.sendStatus(404);
|
||||
stream.on('error', (e) => {
|
||||
res.status(404);
|
||||
res.send(e.message);
|
||||
});
|
||||
stream.on('end', () => {
|
||||
res.end();
|
||||
|
||||
@@ -1,181 +1,4 @@
|
||||
/**
|
||||
GridStoreAdapter
|
||||
Stores files in Mongo using GridStore
|
||||
Requires the database adapter to be based on mongoclient
|
||||
(GridStore is deprecated, Please use GridFSBucket instead)
|
||||
|
||||
@flow weak
|
||||
*/
|
||||
|
||||
// @flow-disable-next
|
||||
import { MongoClient, GridStore, Db } from 'mongodb';
|
||||
import { FilesAdapter, validateFilename } from './FilesAdapter';
|
||||
import defaults from '../../defaults';
|
||||
|
||||
export class GridStoreAdapter extends FilesAdapter {
|
||||
_databaseURI: string;
|
||||
_connectionPromise: Promise<Db>;
|
||||
_mongoOptions: Object;
|
||||
|
||||
constructor(mongoDatabaseURI = defaults.DefaultMongoURI, mongoOptions = {}) {
|
||||
super();
|
||||
this._databaseURI = mongoDatabaseURI;
|
||||
|
||||
const defaultMongoOptions = {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
};
|
||||
this._mongoOptions = Object.assign(defaultMongoOptions, mongoOptions);
|
||||
}
|
||||
|
||||
_connect() {
|
||||
if (!this._connectionPromise) {
|
||||
this._connectionPromise = MongoClient.connect(this._databaseURI, this._mongoOptions).then(
|
||||
client => {
|
||||
this._client = client;
|
||||
return client.db(client.s.options.dbName);
|
||||
}
|
||||
);
|
||||
}
|
||||
return this._connectionPromise;
|
||||
}
|
||||
|
||||
// For a given config object, filename, and data, store a file
|
||||
// Returns a promise
|
||||
createFile(filename: string, data) {
|
||||
return this._connect()
|
||||
.then(database => {
|
||||
const gridStore = new GridStore(database, filename, 'w');
|
||||
return gridStore.open();
|
||||
})
|
||||
.then(gridStore => {
|
||||
return gridStore.write(data);
|
||||
})
|
||||
.then(gridStore => {
|
||||
return gridStore.close();
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(filename: string) {
|
||||
return this._connect()
|
||||
.then(database => {
|
||||
const gridStore = new GridStore(database, filename, 'r');
|
||||
return gridStore.open();
|
||||
})
|
||||
.then(gridStore => {
|
||||
return gridStore.unlink();
|
||||
})
|
||||
.then(gridStore => {
|
||||
return gridStore.close();
|
||||
});
|
||||
}
|
||||
|
||||
getFileData(filename: string) {
|
||||
return this._connect()
|
||||
.then(database => {
|
||||
return GridStore.exist(database, filename).then(() => {
|
||||
const gridStore = new GridStore(database, filename, 'r');
|
||||
return gridStore.open();
|
||||
});
|
||||
})
|
||||
.then(gridStore => {
|
||||
return gridStore.read();
|
||||
});
|
||||
}
|
||||
|
||||
getFileLocation(config, filename) {
|
||||
return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename);
|
||||
}
|
||||
|
||||
async handleFileStream(filename: string, req, res, contentType) {
|
||||
const stream = await this._connect().then(database => {
|
||||
return GridStore.exist(database, filename).then(() => {
|
||||
const gridStore = new GridStore(database, filename, 'r');
|
||||
return gridStore.open();
|
||||
});
|
||||
});
|
||||
handleRangeRequest(stream, req, res, contentType);
|
||||
}
|
||||
|
||||
handleShutdown() {
|
||||
if (!this._client) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._client.close(false);
|
||||
}
|
||||
|
||||
validateFilename(filename) {
|
||||
return validateFilename(filename);
|
||||
}
|
||||
}
|
||||
|
||||
// handleRangeRequest is licensed under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/).
|
||||
// Author: LEROIB at weightingformypizza (https://weightingformypizza.wordpress.com/2015/06/24/stream-html5-media-content-like-video-audio-from-mongodb-using-express-and-gridstore/).
|
||||
function handleRangeRequest(stream, req, res, contentType) {
|
||||
const buffer_size = 1024 * 1024; //1024Kb
|
||||
// Range request, partial stream the file
|
||||
const parts = req
|
||||
.get('Range')
|
||||
.replace(/bytes=/, '')
|
||||
.split('-');
|
||||
let [start, end] = parts;
|
||||
const notEnded = !end && end !== 0;
|
||||
const notStarted = !start && start !== 0;
|
||||
// No end provided, we want all bytes
|
||||
if (notEnded) {
|
||||
end = stream.length - 1;
|
||||
}
|
||||
// No start provided, we're reading backwards
|
||||
if (notStarted) {
|
||||
start = stream.length - end;
|
||||
end = start + end - 1;
|
||||
}
|
||||
|
||||
// Data exceeds the buffer_size, cap
|
||||
if (end - start >= buffer_size) {
|
||||
end = start + buffer_size - 1;
|
||||
}
|
||||
|
||||
const contentLength = end - start + 1;
|
||||
|
||||
res.writeHead(206, {
|
||||
'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': contentLength,
|
||||
'Content-Type': contentType,
|
||||
});
|
||||
|
||||
stream.seek(start, function () {
|
||||
// Get gridFile stream
|
||||
const gridFileStream = stream.stream(true);
|
||||
let bufferAvail = 0;
|
||||
let remainingBytesToWrite = contentLength;
|
||||
let totalBytesWritten = 0;
|
||||
// Write to response
|
||||
gridFileStream.on('data', function (data) {
|
||||
bufferAvail += data.length;
|
||||
if (bufferAvail > 0) {
|
||||
// slice returns the same buffer if overflowing
|
||||
// safe to call in any case
|
||||
const buffer = data.slice(0, remainingBytesToWrite);
|
||||
// Write the buffer
|
||||
res.write(buffer);
|
||||
// Increment total
|
||||
totalBytesWritten += buffer.length;
|
||||
// Decrement remaining
|
||||
remainingBytesToWrite -= data.length;
|
||||
// Decrement the available buffer
|
||||
bufferAvail -= buffer.length;
|
||||
}
|
||||
// In case of small slices, all values will be good at that point
|
||||
// we've written enough, end...
|
||||
if (totalBytesWritten >= contentLength) {
|
||||
stream.close();
|
||||
res.end();
|
||||
this.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default GridStoreAdapter;
|
||||
// Note: GridStore was replaced by GridFSBucketAdapter by default in 2018 by @flovilmart
|
||||
throw new Error(
|
||||
'GridStoreAdapter: GridStore is no longer supported by parse server and mongodb, use GridFSBucketAdapter instead.'
|
||||
);
|
||||
|
||||
@@ -177,7 +177,7 @@ class MongoSchemaCollection {
|
||||
insertSchema(schema: any) {
|
||||
return this._collection
|
||||
.insertOne(schema)
|
||||
.then(result => mongoSchemaToParseSchema(result.ops[0]))
|
||||
.then(() => mongoSchemaToParseSchema(schema))
|
||||
.catch(error => {
|
||||
if (error.code === 11000) {
|
||||
//Mongo's duplicate key error
|
||||
|
||||
@@ -479,6 +479,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.insertOne(mongoObject, transactionalSession))
|
||||
.then(() => ({ ops: [mongoObject] }))
|
||||
.catch(error => {
|
||||
if (error.code === 11000) {
|
||||
// Duplicate value
|
||||
@@ -517,8 +518,8 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
})
|
||||
.catch(err => this.handleError(err))
|
||||
.then(
|
||||
({ result }) => {
|
||||
if (result.n === 0) {
|
||||
({ deletedCount }) => {
|
||||
if (deletedCount === 0) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
}
|
||||
return Promise.resolve();
|
||||
@@ -951,6 +952,9 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// an operator in it (like $gt, $lt, etc). Because of this I felt it was easier to make this a
|
||||
// recursive method to traverse down to the "leaf node" which is going to be the string.
|
||||
_convertToDate(value: any): any {
|
||||
if (value instanceof Date) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return new Date(value);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import log from '../../../logger';
|
||||
import _ from 'lodash';
|
||||
var mongodb = require('mongodb');
|
||||
var Parse = require('parse/node').Parse;
|
||||
const Utils = require('../../../Utils');
|
||||
|
||||
const transformKey = (className, fieldName, schema) => {
|
||||
// Check if the schema is known since it's a built-in field.
|
||||
@@ -634,133 +635,6 @@ function transformTopLevelAtom(atom, field) {
|
||||
}
|
||||
}
|
||||
|
||||
function relativeTimeToDate(text, now = new Date()) {
|
||||
text = text.toLowerCase();
|
||||
|
||||
let parts = text.split(' ');
|
||||
|
||||
// Filter out whitespace
|
||||
parts = parts.filter(part => part !== '');
|
||||
|
||||
const future = parts[0] === 'in';
|
||||
const past = parts[parts.length - 1] === 'ago';
|
||||
|
||||
if (!future && !past && text !== 'now') {
|
||||
return {
|
||||
status: 'error',
|
||||
info: "Time should either start with 'in' or end with 'ago'",
|
||||
};
|
||||
}
|
||||
|
||||
if (future && past) {
|
||||
return {
|
||||
status: 'error',
|
||||
info: "Time cannot have both 'in' and 'ago'",
|
||||
};
|
||||
}
|
||||
|
||||
// strip the 'ago' or 'in'
|
||||
if (future) {
|
||||
parts = parts.slice(1);
|
||||
} else {
|
||||
// past
|
||||
parts = parts.slice(0, parts.length - 1);
|
||||
}
|
||||
|
||||
if (parts.length % 2 !== 0 && text !== 'now') {
|
||||
return {
|
||||
status: 'error',
|
||||
info: 'Invalid time string. Dangling unit or number.',
|
||||
};
|
||||
}
|
||||
|
||||
const pairs = [];
|
||||
while (parts.length) {
|
||||
pairs.push([parts.shift(), parts.shift()]);
|
||||
}
|
||||
|
||||
let seconds = 0;
|
||||
for (const [num, interval] of pairs) {
|
||||
const val = Number(num);
|
||||
if (!Number.isInteger(val)) {
|
||||
return {
|
||||
status: 'error',
|
||||
info: `'${num}' is not an integer.`,
|
||||
};
|
||||
}
|
||||
|
||||
switch (interval) {
|
||||
case 'yr':
|
||||
case 'yrs':
|
||||
case 'year':
|
||||
case 'years':
|
||||
seconds += val * 31536000; // 365 * 24 * 60 * 60
|
||||
break;
|
||||
|
||||
case 'wk':
|
||||
case 'wks':
|
||||
case 'week':
|
||||
case 'weeks':
|
||||
seconds += val * 604800; // 7 * 24 * 60 * 60
|
||||
break;
|
||||
|
||||
case 'd':
|
||||
case 'day':
|
||||
case 'days':
|
||||
seconds += val * 86400; // 24 * 60 * 60
|
||||
break;
|
||||
|
||||
case 'hr':
|
||||
case 'hrs':
|
||||
case 'hour':
|
||||
case 'hours':
|
||||
seconds += val * 3600; // 60 * 60
|
||||
break;
|
||||
|
||||
case 'min':
|
||||
case 'mins':
|
||||
case 'minute':
|
||||
case 'minutes':
|
||||
seconds += val * 60;
|
||||
break;
|
||||
|
||||
case 'sec':
|
||||
case 'secs':
|
||||
case 'second':
|
||||
case 'seconds':
|
||||
seconds += val;
|
||||
break;
|
||||
|
||||
default:
|
||||
return {
|
||||
status: 'error',
|
||||
info: `Invalid interval: '${interval}'`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const milliseconds = seconds * 1000;
|
||||
if (future) {
|
||||
return {
|
||||
status: 'success',
|
||||
info: 'future',
|
||||
result: new Date(now.valueOf() + milliseconds),
|
||||
};
|
||||
} else if (past) {
|
||||
return {
|
||||
status: 'success',
|
||||
info: 'past',
|
||||
result: new Date(now.valueOf() - milliseconds),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'success',
|
||||
info: 'present',
|
||||
result: new Date(now.valueOf()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -813,7 +687,7 @@ function transformConstraint(constraint, field, count = false) {
|
||||
);
|
||||
}
|
||||
|
||||
const parserResult = relativeTimeToDate(val.$relativeTime);
|
||||
const parserResult = Utils.relativeTimeToDate(val.$relativeTime);
|
||||
if (parserResult.status === 'success') {
|
||||
answer[key] = parserResult.result;
|
||||
break;
|
||||
@@ -1556,7 +1430,6 @@ module.exports = {
|
||||
transformUpdate,
|
||||
transformWhere,
|
||||
mongoObjectToParseObject,
|
||||
relativeTimeToDate,
|
||||
transformConstraint,
|
||||
transformPointerString,
|
||||
};
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
const url = require('url');
|
||||
const fs = require('fs');
|
||||
function getDatabaseOptionsFromURI(uri) {
|
||||
const databaseOptions = {};
|
||||
|
||||
const parsedURI = url.parse(uri);
|
||||
const queryParams = parseQueryParams(parsedURI.query);
|
||||
const authParts = parsedURI.auth ? parsedURI.auth.split(':') : [];
|
||||
const parsedURI = new URL(uri);
|
||||
const queryParams = parseQueryParams(parsedURI.searchParams.toString());
|
||||
|
||||
databaseOptions.host = parsedURI.hostname || 'localhost';
|
||||
databaseOptions.port = parsedURI.port ? parseInt(parsedURI.port) : 5432;
|
||||
databaseOptions.database = parsedURI.pathname ? parsedURI.pathname.substr(1) : undefined;
|
||||
|
||||
databaseOptions.user = authParts.length > 0 ? authParts[0] : '';
|
||||
databaseOptions.password = authParts.length > 1 ? authParts[1] : '';
|
||||
databaseOptions.user = parsedURI.username;
|
||||
databaseOptions.password = parsedURI.password;
|
||||
|
||||
if (queryParams.ssl && queryParams.ssl.toLowerCase() === 'true') {
|
||||
databaseOptions.ssl = true;
|
||||
|
||||
@@ -7,12 +7,14 @@ import _ from 'lodash';
|
||||
// @flow-disable-next
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import sql from './sql';
|
||||
import { StorageAdapter } from '../StorageAdapter';
|
||||
import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter';
|
||||
const Utils = require('../../../Utils');
|
||||
|
||||
const PostgresRelationDoesNotExistError = '42P01';
|
||||
const PostgresDuplicateRelationError = '42P07';
|
||||
const PostgresDuplicateColumnError = '42701';
|
||||
const PostgresMissingColumnError = '42703';
|
||||
const PostgresDuplicateObjectError = '42710';
|
||||
const PostgresUniqueIndexViolationError = '23505';
|
||||
const logger = require('../../../logger');
|
||||
|
||||
@@ -22,9 +24,6 @@ const debug = function (...args: any) {
|
||||
log.debug.apply(log, args);
|
||||
};
|
||||
|
||||
import { StorageAdapter } from '../StorageAdapter';
|
||||
import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter';
|
||||
|
||||
const parseTypeToPostgresType = type => {
|
||||
switch (type.type) {
|
||||
case 'String':
|
||||
@@ -92,6 +91,22 @@ const toPostgresValue = value => {
|
||||
return value;
|
||||
};
|
||||
|
||||
const toPostgresValueCastType = value => {
|
||||
const postgresValue = toPostgresValue(value);
|
||||
let castType;
|
||||
switch (typeof postgresValue) {
|
||||
case 'number':
|
||||
castType = 'double precision';
|
||||
break;
|
||||
case 'boolean':
|
||||
castType = 'boolean';
|
||||
break;
|
||||
default:
|
||||
castType = undefined;
|
||||
}
|
||||
return castType;
|
||||
};
|
||||
|
||||
const transformValue = value => {
|
||||
if (typeof value === 'object' && value.__type === 'Pointer') {
|
||||
return value.objectId;
|
||||
@@ -370,9 +385,17 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
|
||||
);
|
||||
} else {
|
||||
if (fieldName.indexOf('.') >= 0) {
|
||||
const constraintFieldName = transformDotField(fieldName);
|
||||
const castType = toPostgresValueCastType(fieldValue.$ne);
|
||||
const constraintFieldName = castType
|
||||
? `CAST ((${transformDotField(fieldName)}) AS ${castType})`
|
||||
: transformDotField(fieldName);
|
||||
patterns.push(
|
||||
`(${constraintFieldName} <> $${index} OR ${constraintFieldName} IS NULL)`
|
||||
`(${constraintFieldName} <> $${index + 1} OR ${constraintFieldName} IS NULL)`
|
||||
);
|
||||
} else if (typeof fieldValue.$ne === 'object' && fieldValue.$ne.$relativeTime) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'
|
||||
);
|
||||
} else {
|
||||
patterns.push(`($${index}:name <> $${index + 1} OR $${index}:name IS NULL)`);
|
||||
@@ -397,8 +420,17 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
|
||||
index += 1;
|
||||
} else {
|
||||
if (fieldName.indexOf('.') >= 0) {
|
||||
const castType = toPostgresValueCastType(fieldValue.$eq);
|
||||
const constraintFieldName = castType
|
||||
? `CAST ((${transformDotField(fieldName)}) AS ${castType})`
|
||||
: transformDotField(fieldName);
|
||||
values.push(fieldValue.$eq);
|
||||
patterns.push(`${transformDotField(fieldName)} = $${index++}`);
|
||||
patterns.push(`${constraintFieldName} = $${index++}`);
|
||||
} else if (typeof fieldValue.$eq === 'object' && fieldValue.$eq.$relativeTime) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'
|
||||
);
|
||||
} else {
|
||||
values.push(fieldName, fieldValue.$eq);
|
||||
patterns.push(`$${index}:name = $${index + 1}`);
|
||||
@@ -513,7 +545,12 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
|
||||
}
|
||||
|
||||
if (typeof fieldValue.$exists !== 'undefined') {
|
||||
if (fieldValue.$exists) {
|
||||
if (typeof fieldValue.$exists === 'object' && fieldValue.$exists.$relativeTime) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'
|
||||
);
|
||||
} else if (fieldValue.$exists) {
|
||||
patterns.push(`$${index}:name IS NOT NULL`);
|
||||
} else {
|
||||
patterns.push(`$${index}:name IS NULL`);
|
||||
@@ -757,24 +794,33 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
|
||||
Object.keys(ParseToPosgresComparator).forEach(cmp => {
|
||||
if (fieldValue[cmp] || fieldValue[cmp] === 0) {
|
||||
const pgComparator = ParseToPosgresComparator[cmp];
|
||||
const postgresValue = toPostgresValue(fieldValue[cmp]);
|
||||
let constraintFieldName;
|
||||
let postgresValue = toPostgresValue(fieldValue[cmp]);
|
||||
|
||||
if (fieldName.indexOf('.') >= 0) {
|
||||
let castType;
|
||||
switch (typeof postgresValue) {
|
||||
case 'number':
|
||||
castType = 'double precision';
|
||||
break;
|
||||
case 'boolean':
|
||||
castType = 'boolean';
|
||||
break;
|
||||
default:
|
||||
castType = undefined;
|
||||
}
|
||||
const castType = toPostgresValueCastType(fieldValue[cmp]);
|
||||
constraintFieldName = castType
|
||||
? `CAST ((${transformDotField(fieldName)}) AS ${castType})`
|
||||
: transformDotField(fieldName);
|
||||
} else {
|
||||
if (typeof postgresValue === 'object' && postgresValue.$relativeTime) {
|
||||
if (schema.fields[fieldName].type !== 'Date') {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'$relativeTime can only be used with Date field'
|
||||
);
|
||||
}
|
||||
const parserResult = Utils.relativeTimeToDate(postgresValue.$relativeTime);
|
||||
if (parserResult.status === 'success') {
|
||||
postgresValue = toPostgresValue(parserResult.result);
|
||||
} else {
|
||||
console.error('Error while parsing relative date', parserResult);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
`bad $relativeTime (${postgresValue.$relativeTime}) value. ${parserResult.info}`
|
||||
);
|
||||
}
|
||||
}
|
||||
constraintFieldName = `$${index++}:name`;
|
||||
values.push(fieldName);
|
||||
}
|
||||
@@ -873,15 +919,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
'CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )'
|
||||
)
|
||||
.catch(error => {
|
||||
if (
|
||||
error.code === PostgresDuplicateRelationError ||
|
||||
error.code === PostgresUniqueIndexViolationError ||
|
||||
error.code === PostgresDuplicateObjectError
|
||||
) {
|
||||
// Table already exists, must have been created by a different request. Ignore error.
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2416,6 +2454,11 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
? fieldNames.map((fieldName, index) => `lower($${index + 3}:name) varchar_pattern_ops`)
|
||||
: fieldNames.map((fieldName, index) => `$${index + 3}:name`);
|
||||
const qs = `CREATE INDEX IF NOT EXISTS $1:name ON $2:name (${constraintPatterns.join()})`;
|
||||
const setIdempotencyFunction =
|
||||
options.setIdempotencyFunction !== undefined ? options.setIdempotencyFunction : false;
|
||||
if (setIdempotencyFunction) {
|
||||
await this.ensureIdempotencyFunctionExists(options);
|
||||
}
|
||||
await conn.none(qs, [indexNameOptions.name, className, ...fieldNames]).catch(error => {
|
||||
if (
|
||||
error.code === PostgresDuplicateRelationError &&
|
||||
@@ -2436,6 +2479,24 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIdempotencyFunction(options?: Object = {}): Promise<any> {
|
||||
const conn = options.conn !== undefined ? options.conn : this._client;
|
||||
const qs = 'DROP FUNCTION IF EXISTS idempotency_delete_expired_records()';
|
||||
return conn.none(qs).catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
async ensureIdempotencyFunctionExists(options?: Object = {}): Promise<any> {
|
||||
const conn = options.conn !== undefined ? options.conn : this._client;
|
||||
const ttlOptions = options.ttl !== undefined ? `${options.ttl} seconds` : '60 seconds';
|
||||
const qs =
|
||||
'CREATE OR REPLACE FUNCTION idempotency_delete_expired_records() RETURNS void LANGUAGE plpgsql AS $$ BEGIN DELETE FROM "_Idempotency" WHERE expire < NOW() - INTERVAL $1; END; $$;';
|
||||
return conn.none(qs, [ttlOptions]).catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function convertPolygonToSQL(polygon) {
|
||||
|
||||
@@ -230,6 +230,15 @@ Auth.prototype.cacheRoles = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
Auth.prototype.clearRoleCache = function (sessionToken) {
|
||||
if (!this.cacheController) {
|
||||
return false;
|
||||
}
|
||||
this.cacheController.role.del(this.user.id);
|
||||
this.cacheController.user.del(sessionToken);
|
||||
return true;
|
||||
};
|
||||
|
||||
Auth.prototype.getRolesByIds = async function (ins) {
|
||||
const results = [];
|
||||
// Build an OR query across all parentRoles
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
PagesOptions,
|
||||
SecurityOptions,
|
||||
SchemaOptions,
|
||||
ParseServerOptions,
|
||||
} from './Options/Definitions';
|
||||
import { isBoolean, isString } from 'lodash';
|
||||
|
||||
@@ -63,6 +64,7 @@ export class Config {
|
||||
revokeSessionOnPasswordReset,
|
||||
expireInactiveSessions,
|
||||
sessionLength,
|
||||
defaultLimit,
|
||||
maxLimit,
|
||||
emailVerifyTokenValidityDuration,
|
||||
accountLockout,
|
||||
@@ -110,6 +112,7 @@ export class Config {
|
||||
}
|
||||
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
|
||||
this.validateMasterKeyIps(masterKeyIps);
|
||||
this.validateDefaultLimit(defaultLimit);
|
||||
this.validateMaxLimit(maxLimit);
|
||||
this.validateAllowHeaders(allowHeaders);
|
||||
this.validateIdempotencyOptions(idempotencyOptions);
|
||||
@@ -453,6 +456,18 @@ export class Config {
|
||||
}
|
||||
}
|
||||
|
||||
static validateDefaultLimit(defaultLimit) {
|
||||
if (defaultLimit == null) {
|
||||
defaultLimit = ParseServerOptions.defaultLimit.default;
|
||||
}
|
||||
if (typeof defaultLimit !== 'number') {
|
||||
throw 'Default limit must be a number.';
|
||||
}
|
||||
if (defaultLimit <= 0) {
|
||||
throw 'Default limit must be a value greater than 0.';
|
||||
}
|
||||
}
|
||||
|
||||
static validateMaxLimit(maxLimit) {
|
||||
if (maxLimit <= 0) {
|
||||
throw 'Max limit must be a value greater than 0.';
|
||||
|
||||
@@ -11,9 +11,11 @@ import intersect from 'intersect';
|
||||
// @flow-disable-next
|
||||
import deepcopy from 'deepcopy';
|
||||
import logger from '../logger';
|
||||
import Utils from '../Utils';
|
||||
import * as SchemaController from './SchemaController';
|
||||
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
|
||||
import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||
import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter';
|
||||
import SchemaCache from '../Adapters/Cache/SchemaCache';
|
||||
import type { LoadSchemaOptions } from './types';
|
||||
import type { ParseServerOptions } from '../Options';
|
||||
@@ -53,31 +55,27 @@ const transformObjectACL = ({ ACL, ...result }) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const specialQuerykeys = [
|
||||
'$and',
|
||||
'$or',
|
||||
'$nor',
|
||||
'_rperm',
|
||||
'_wperm',
|
||||
'_perishable_token',
|
||||
const specialQueryKeys = ['$and', '$or', '$nor', '_rperm', '_wperm'];
|
||||
const specialMasterQueryKeys = [
|
||||
...specialQueryKeys,
|
||||
'_email_verify_token',
|
||||
'_perishable_token',
|
||||
'_tombstone',
|
||||
'_email_verify_token_expires_at',
|
||||
'_account_lockout_expires_at',
|
||||
'_failed_login_count',
|
||||
'_account_lockout_expires_at',
|
||||
'_password_changed_at',
|
||||
'_password_history',
|
||||
];
|
||||
|
||||
const isSpecialQueryKey = key => {
|
||||
return specialQuerykeys.indexOf(key) >= 0;
|
||||
};
|
||||
|
||||
const validateQuery = (query: any): void => {
|
||||
const validateQuery = (query: any, isMaster: boolean, update: boolean): void => {
|
||||
if (query.ACL) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
|
||||
}
|
||||
|
||||
if (query.$or) {
|
||||
if (query.$or instanceof Array) {
|
||||
query.$or.forEach(validateQuery);
|
||||
query.$or.forEach(value => validateQuery(value, isMaster, update));
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.');
|
||||
}
|
||||
@@ -85,7 +83,7 @@ const validateQuery = (query: any): void => {
|
||||
|
||||
if (query.$and) {
|
||||
if (query.$and instanceof Array) {
|
||||
query.$and.forEach(validateQuery);
|
||||
query.$and.forEach(value => validateQuery(value, isMaster, update));
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.');
|
||||
}
|
||||
@@ -93,7 +91,7 @@ const validateQuery = (query: any): void => {
|
||||
|
||||
if (query.$nor) {
|
||||
if (query.$nor instanceof Array && query.$nor.length > 0) {
|
||||
query.$nor.forEach(validateQuery);
|
||||
query.$nor.forEach(value => validateQuery(value, isMaster, update));
|
||||
} else {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
@@ -113,7 +111,11 @@ const validateQuery = (query: any): void => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
|
||||
if (
|
||||
!key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/) &&
|
||||
((!specialQueryKeys.includes(key) && !isMaster && !update) ||
|
||||
(update && isMaster && !specialMasterQueryKeys.includes(key)))
|
||||
) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`);
|
||||
}
|
||||
});
|
||||
@@ -125,7 +127,7 @@ const filterSensitiveData = (
|
||||
aclGroup: any[],
|
||||
auth: any,
|
||||
operation: any,
|
||||
schema: SchemaController.SchemaController,
|
||||
schema: SchemaController.SchemaController | any,
|
||||
className: string,
|
||||
protectedFields: null | Array<any>,
|
||||
object: any
|
||||
@@ -134,7 +136,8 @@ const filterSensitiveData = (
|
||||
if (auth && auth.user) userId = auth.user.id;
|
||||
|
||||
// replace protectedFields when using pointer-permissions
|
||||
const perms = schema.getClassLevelPermissions(className);
|
||||
const perms =
|
||||
schema && schema.getClassLevelPermissions ? schema.getClassLevelPermissions(className) : {};
|
||||
if (perms) {
|
||||
const isReadOperation = ['get', 'find'].indexOf(operation) > -1;
|
||||
|
||||
@@ -205,27 +208,24 @@ const filterSensitiveData = (
|
||||
perms.protectedFields.temporaryKeys.forEach(k => delete object[k]);
|
||||
}
|
||||
|
||||
if (!isUserClass) {
|
||||
return object;
|
||||
if (isUserClass) {
|
||||
object.password = object._hashed_password;
|
||||
delete object._hashed_password;
|
||||
delete object.sessionToken;
|
||||
}
|
||||
|
||||
object.password = object._hashed_password;
|
||||
delete object._hashed_password;
|
||||
|
||||
delete object.sessionToken;
|
||||
|
||||
if (isMaster) {
|
||||
return object;
|
||||
}
|
||||
delete object._email_verify_token;
|
||||
delete object._perishable_token;
|
||||
delete object._perishable_token_expires_at;
|
||||
delete object._tombstone;
|
||||
delete object._email_verify_token_expires_at;
|
||||
delete object._failed_login_count;
|
||||
delete object._account_lockout_expires_at;
|
||||
delete object._password_changed_at;
|
||||
delete object._password_history;
|
||||
for (const key in object) {
|
||||
if (key.charAt(0) === '_') {
|
||||
delete object[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUserClass) {
|
||||
return object;
|
||||
}
|
||||
|
||||
if (aclGroup.indexOf(object.objectId) > -1) {
|
||||
return object;
|
||||
@@ -364,12 +364,14 @@ class DatabaseController {
|
||||
schemaPromise: ?Promise<SchemaController.SchemaController>;
|
||||
_transactionalSession: ?any;
|
||||
options: ParseServerOptions;
|
||||
idempotencyOptions: any;
|
||||
|
||||
constructor(adapter: StorageAdapter, options: ParseServerOptions) {
|
||||
this.adapter = adapter;
|
||||
// We don't want a mutable this.schema, because then you could have
|
||||
// one request that uses different schemas for different parts of
|
||||
// it. Instead, use loadSchema to get a schema.
|
||||
this.options = options || {};
|
||||
this.idempotencyOptions = this.options.idempotencyOptions || {};
|
||||
// Prevent mutable this.schema, otherwise one request could use
|
||||
// multiple schemas, so instead use loadSchema to get a schema.
|
||||
this.schemaPromise = null;
|
||||
this._transactionalSession = null;
|
||||
this.options = options;
|
||||
@@ -510,7 +512,7 @@ class DatabaseController {
|
||||
if (acl) {
|
||||
query = addWriteACL(query, acl);
|
||||
}
|
||||
validateQuery(query);
|
||||
validateQuery(query, isMaster, true);
|
||||
return schemaController
|
||||
.getOneSchema(className, true)
|
||||
.catch(error => {
|
||||
@@ -756,7 +758,7 @@ class DatabaseController {
|
||||
if (acl) {
|
||||
query = addWriteACL(query, acl);
|
||||
}
|
||||
validateQuery(query);
|
||||
validateQuery(query, isMaster, false);
|
||||
return schemaController
|
||||
.getOneSchema(className)
|
||||
.catch(error => {
|
||||
@@ -862,7 +864,7 @@ class DatabaseController {
|
||||
if (object[field] && object[field].__op && object[field].__op === 'Delete') {
|
||||
return false;
|
||||
}
|
||||
return schemaFields.indexOf(field) < 0;
|
||||
return schemaFields.indexOf(getRootFieldName(field)) < 0;
|
||||
});
|
||||
if (newKeys.length > 0) {
|
||||
// adds a marker that new field is being adding during update
|
||||
@@ -927,32 +929,32 @@ class DatabaseController {
|
||||
reduceInRelation(className: string, query: any, schema: any): Promise<any> {
|
||||
// Search for an in-relation or equal-to-relation
|
||||
// Make it sequential for now, not sure of paralleization side effects
|
||||
const promises = [];
|
||||
if (query['$or']) {
|
||||
const ors = query['$or'];
|
||||
return Promise.all(
|
||||
ors.map((aQuery, index) => {
|
||||
promises.push(
|
||||
...ors.map((aQuery, index) => {
|
||||
return this.reduceInRelation(className, aQuery, schema).then(aQuery => {
|
||||
query['$or'][index] = aQuery;
|
||||
});
|
||||
})
|
||||
).then(() => {
|
||||
return Promise.resolve(query);
|
||||
});
|
||||
);
|
||||
}
|
||||
if (query['$and']) {
|
||||
const ors = query['$and'];
|
||||
return Promise.all(
|
||||
ors.map((aQuery, index) => {
|
||||
const ands = query['$and'];
|
||||
promises.push(
|
||||
...ands.map((aQuery, index) => {
|
||||
return this.reduceInRelation(className, aQuery, schema).then(aQuery => {
|
||||
query['$and'][index] = aQuery;
|
||||
});
|
||||
})
|
||||
).then(() => {
|
||||
return Promise.resolve(query);
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
const promises = Object.keys(query).map(key => {
|
||||
const otherKeys = Object.keys(query).map(key => {
|
||||
if (key === '$and' || key === '$or') {
|
||||
return;
|
||||
}
|
||||
const t = schema.getExpectedType(className, key);
|
||||
if (!t || t.type !== 'Relation') {
|
||||
return Promise.resolve(query);
|
||||
@@ -1014,7 +1016,7 @@ class DatabaseController {
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return Promise.all([...promises, ...otherKeys]).then(() => {
|
||||
return Promise.resolve(query);
|
||||
});
|
||||
}
|
||||
@@ -1205,6 +1207,9 @@ class DatabaseController {
|
||||
`Invalid field name: ${fieldName}.`
|
||||
);
|
||||
}
|
||||
if (!schema.fields[fieldName.split('.')[0]] && fieldName !== 'score') {
|
||||
delete sort[fieldName];
|
||||
}
|
||||
});
|
||||
return (isMaster
|
||||
? Promise.resolve()
|
||||
@@ -1248,7 +1253,7 @@ class DatabaseController {
|
||||
query = addReadACL(query, aclGroup);
|
||||
}
|
||||
}
|
||||
validateQuery(query);
|
||||
validateQuery(query, isMaster, false);
|
||||
if (count) {
|
||||
if (!classExists) {
|
||||
return 0;
|
||||
@@ -1529,14 +1534,17 @@ class DatabaseController {
|
||||
}
|
||||
|
||||
addProtectedFields(
|
||||
schema: SchemaController.SchemaController,
|
||||
schema: SchemaController.SchemaController | any,
|
||||
className: string,
|
||||
query: any = {},
|
||||
aclGroup: any[] = [],
|
||||
auth: any = {},
|
||||
queryOptions: FullQueryOptions = {}
|
||||
): null | string[] {
|
||||
const perms = schema.getClassLevelPermissions(className);
|
||||
const perms =
|
||||
schema && schema.getClassLevelPermissions
|
||||
? schema.getClassLevelPermissions(className)
|
||||
: schema;
|
||||
if (!perms) return null;
|
||||
|
||||
const protectedFields = perms.protectedFields;
|
||||
@@ -1681,9 +1689,7 @@ class DatabaseController {
|
||||
};
|
||||
await this.loadSchema().then(schema => schema.enforceClassExists('_User'));
|
||||
await this.loadSchema().then(schema => schema.enforceClassExists('_Role'));
|
||||
if (this.adapter instanceof MongoStorageAdapter) {
|
||||
await this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency'));
|
||||
}
|
||||
await this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency'));
|
||||
|
||||
await this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']).catch(error => {
|
||||
logger.warn('Unable to ensure uniqueness for usernames: ', error);
|
||||
@@ -1719,18 +1725,28 @@ class DatabaseController {
|
||||
logger.warn('Unable to ensure uniqueness for role name: ', error);
|
||||
throw error;
|
||||
});
|
||||
if (this.adapter instanceof MongoStorageAdapter) {
|
||||
await this.adapter
|
||||
.ensureUniqueness('_Idempotency', requiredIdempotencyFields, ['reqId'])
|
||||
.catch(error => {
|
||||
logger.warn('Unable to ensure uniqueness for idempotency request ID: ', error);
|
||||
throw error;
|
||||
});
|
||||
|
||||
await this.adapter
|
||||
.ensureIndex('_Idempotency', requiredIdempotencyFields, ['expire'], 'ttl', false, {
|
||||
await this.adapter
|
||||
.ensureUniqueness('_Idempotency', requiredIdempotencyFields, ['reqId'])
|
||||
.catch(error => {
|
||||
logger.warn('Unable to ensure uniqueness for idempotency request ID: ', error);
|
||||
throw error;
|
||||
});
|
||||
|
||||
const isMongoAdapter = this.adapter instanceof MongoStorageAdapter;
|
||||
const isPostgresAdapter = this.adapter instanceof PostgresStorageAdapter;
|
||||
if (isMongoAdapter || isPostgresAdapter) {
|
||||
let options = {};
|
||||
if (isMongoAdapter) {
|
||||
options = {
|
||||
ttl: 0,
|
||||
})
|
||||
};
|
||||
} else if (isPostgresAdapter) {
|
||||
options = this.idempotencyOptions;
|
||||
options.setIdempotencyFunction = true;
|
||||
}
|
||||
await this.adapter
|
||||
.ensureIndex('_Idempotency', requiredIdempotencyFields, ['expire'], 'ttl', false, options)
|
||||
.catch(error => {
|
||||
logger.warn('Unable to create TTL index for idempotency expire date: ', error);
|
||||
throw error;
|
||||
@@ -1752,8 +1768,12 @@ class DatabaseController {
|
||||
if (this.options && this.options.requestKeywordDenylist) {
|
||||
// Scan request data for denied keywords
|
||||
for (const keyword of this.options.requestKeywordDenylist) {
|
||||
const isMatch = (a, b) => (typeof a === 'string' && new RegExp(a).test(b)) || a === b;
|
||||
if (isMatch(firstKey, keyword.key)) {
|
||||
const match = Utils.objectContainsKeyValue(
|
||||
{ [firstKey]: true, [nextPath]: true },
|
||||
keyword.key,
|
||||
true
|
||||
);
|
||||
if (match) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
|
||||
@@ -1793,9 +1813,11 @@ class DatabaseController {
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
static _validateQuery: any => void;
|
||||
static _validateQuery: (any, boolean, boolean) => void;
|
||||
static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
|
||||
}
|
||||
|
||||
module.exports = DatabaseController;
|
||||
// Expose validateQuery for tests
|
||||
module.exports._validateQuery = validateQuery;
|
||||
module.exports.filterSensitiveData = filterSensitiveData;
|
||||
|
||||
@@ -56,6 +56,13 @@ export class LiveQueryController {
|
||||
return false;
|
||||
}
|
||||
|
||||
clearCachedRoles(user: any) {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
return this.liveQueryPublisher.onClearCachedRoles(user);
|
||||
}
|
||||
|
||||
_makePublisherRequest(currentObject: any, originalObject: any, classLevelPermissions: ?any): any {
|
||||
const req = {
|
||||
object: currentObject,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Parse } from 'parse/node';
|
||||
import AdaptableController from './AdaptableController';
|
||||
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
|
||||
import url from 'url';
|
||||
|
||||
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
|
||||
const LOG_STRING_TRUNCATE_LENGTH = 1000;
|
||||
@@ -38,15 +37,16 @@ export class LoggerController extends AdaptableController {
|
||||
});
|
||||
}
|
||||
|
||||
maskSensitiveUrl(urlString) {
|
||||
const urlObj = url.parse(urlString, true);
|
||||
const query = urlObj.query;
|
||||
maskSensitiveUrl(path) {
|
||||
const urlString = 'http://localhost' + path; // prepend dummy string to make a real URL
|
||||
const urlObj = new URL(urlString);
|
||||
const query = urlObj.searchParams;
|
||||
let sanitizedQuery = '?';
|
||||
|
||||
for (const key in query) {
|
||||
for (const [key, value] of query) {
|
||||
if (key !== 'password') {
|
||||
// normal value
|
||||
sanitizedQuery += key + '=' + query[key] + '&';
|
||||
sanitizedQuery += key + '=' + value + '&';
|
||||
} else {
|
||||
// password value, redact it
|
||||
sanitizedQuery += key + '=' + '********' + '&';
|
||||
|
||||
@@ -150,9 +150,15 @@ const defaultColumns: { [string]: SchemaFields } = Object.freeze({
|
||||
},
|
||||
});
|
||||
|
||||
// fields required for read or write operations on their respective classes.
|
||||
const requiredColumns = Object.freeze({
|
||||
_Product: ['productIdentifier', 'icon', 'order', 'title', 'subtitle'],
|
||||
_Role: ['name', 'ACL'],
|
||||
read: {
|
||||
_User: ['username'],
|
||||
},
|
||||
write: {
|
||||
_Product: ['productIdentifier', 'icon', 'order', 'title', 'subtitle'],
|
||||
_Role: ['name', 'ACL'],
|
||||
},
|
||||
});
|
||||
|
||||
const invalidColumns = ['length'];
|
||||
@@ -1269,7 +1275,7 @@ export default class SchemaController {
|
||||
|
||||
// Validates that all the properties are set for the object
|
||||
validateRequiredColumns(className: string, object: any, query: any) {
|
||||
const columns = requiredColumns[className];
|
||||
const columns = requiredColumns.write[className];
|
||||
if (!columns || columns.length == 0) {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
@@ -1600,4 +1606,5 @@ export {
|
||||
convertSchemaToAdapterSchema,
|
||||
VolatileClassesSchemas,
|
||||
SchemaController,
|
||||
requiredColumns,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import authDataManager from '../Adapters/Auth';
|
||||
import { ParseServerOptions } from '../Options';
|
||||
import { loadAdapter } from '../Adapters/AdapterLoader';
|
||||
import defaults from '../defaults';
|
||||
import url from 'url';
|
||||
// Controllers
|
||||
import { LoggerController } from './LoggerController';
|
||||
import { FilesController } from './FilesController';
|
||||
@@ -92,12 +91,20 @@ export function getLoggerController(options: ParseServerOptions): LoggerControll
|
||||
}
|
||||
|
||||
export function getFilesController(options: ParseServerOptions): FilesController {
|
||||
const { appId, databaseURI, filesAdapter, databaseAdapter, preserveFileName, fileKey } = options;
|
||||
const {
|
||||
appId,
|
||||
databaseURI,
|
||||
databaseOptions = {},
|
||||
filesAdapter,
|
||||
databaseAdapter,
|
||||
preserveFileName,
|
||||
fileKey,
|
||||
} = options;
|
||||
if (!filesAdapter && databaseAdapter) {
|
||||
throw 'When using an explicit database adapter, you must also use an explicit filesAdapter.';
|
||||
}
|
||||
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
|
||||
return new GridFSBucketAdapter(databaseURI, {}, fileKey);
|
||||
return new GridFSBucketAdapter(databaseURI, databaseOptions, fileKey);
|
||||
});
|
||||
return new FilesController(filesControllerAdapter, appId, {
|
||||
preserveFileName,
|
||||
@@ -220,13 +227,14 @@ export function getAuthDataManager(options: ParseServerOptions) {
|
||||
export function getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions) {
|
||||
let protocol;
|
||||
try {
|
||||
const parsedURI = url.parse(databaseURI);
|
||||
const parsedURI = new URL(databaseURI);
|
||||
protocol = parsedURI.protocol ? parsedURI.protocol.toLowerCase() : null;
|
||||
} catch (e) {
|
||||
/* */
|
||||
}
|
||||
switch (protocol) {
|
||||
case 'postgres:':
|
||||
case 'postgresql:':
|
||||
return new PostgresStorageAdapter({
|
||||
uri: databaseURI,
|
||||
collectionPrefix,
|
||||
|
||||
@@ -23,4 +23,5 @@ module.exports = [
|
||||
"Additionally, the environment variable 'PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS' will be deprecated and renamed to 'PARSE_SERVER_DIRECT_ACCESS' in a future version; it is currently possible to use either one.",
|
||||
},
|
||||
{ optionKey: 'enforcePrivateUsers', changeNewDefault: 'true' },
|
||||
{ optionKey: 'allowClientClassCreation', changeNewDefault: 'false' },
|
||||
];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Parse from 'parse/node';
|
||||
import { GraphQLSchema, GraphQLObjectType, DocumentNode, GraphQLNamedType } from 'graphql';
|
||||
import { stitchSchemas } from '@graphql-tools/stitch';
|
||||
import { mergeSchemas } from '@graphql-tools/schema';
|
||||
import { mergeTypeDefs } from '@graphql-tools/merge';
|
||||
import { isDeepStrictEqual } from 'util';
|
||||
import { SchemaDirectiveVisitor } from '@graphql-tools/utils';
|
||||
import requiredParameter from '../requiredParameter';
|
||||
import * as defaultGraphQLTypes from './loaders/defaultGraphQLTypes';
|
||||
import * as parseClassTypes from './loaders/parseClassTypes';
|
||||
@@ -89,14 +89,19 @@ class ParseGraphQLSchema {
|
||||
this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs;
|
||||
this.appId = params.appId || requiredParameter('You must provide the appId!');
|
||||
this.schemaCache = SchemaCache;
|
||||
this.logCache = {};
|
||||
}
|
||||
|
||||
async load() {
|
||||
const { parseGraphQLConfig } = await this._initializeSchemaAndConfig();
|
||||
const parseClasses = await this._getClassesForSchema(parseGraphQLConfig);
|
||||
const parseClassesArray = await this._getClassesForSchema(parseGraphQLConfig);
|
||||
const functionNames = await this._getFunctionNames();
|
||||
const functionNamesString = JSON.stringify(functionNames);
|
||||
const functionNamesString = functionNames.join();
|
||||
|
||||
const parseClasses = parseClassesArray.reduce((acc, clazz) => {
|
||||
acc[clazz.className] = clazz;
|
||||
return acc;
|
||||
}, {});
|
||||
if (
|
||||
!this._hasSchemaInputChanged({
|
||||
parseClasses,
|
||||
@@ -127,7 +132,7 @@ class ParseGraphQLSchema {
|
||||
defaultRelaySchema.load(this);
|
||||
schemaTypes.load(this);
|
||||
|
||||
this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach(
|
||||
this._getParseClassesWithConfig(parseClassesArray, parseGraphQLConfig).forEach(
|
||||
([parseClass, parseClassConfig]) => {
|
||||
// Some times schema return the _auth_data_ field
|
||||
// it will lead to unstable graphql generation order
|
||||
@@ -155,7 +160,7 @@ class ParseGraphQLSchema {
|
||||
}
|
||||
);
|
||||
|
||||
defaultGraphQLTypes.loadArrayResult(this, parseClasses);
|
||||
defaultGraphQLTypes.loadArrayResult(this, parseClassesArray);
|
||||
defaultGraphQLQueries.load(this);
|
||||
defaultGraphQLMutations.load(this);
|
||||
|
||||
@@ -198,9 +203,8 @@ class ParseGraphQLSchema {
|
||||
|
||||
if (this.graphQLCustomTypeDefs) {
|
||||
schemaDirectives.load(this);
|
||||
|
||||
if (typeof this.graphQLCustomTypeDefs.getTypeMap === 'function') {
|
||||
// In following code we use underscore attr to avoid js var un ref
|
||||
// In following code we use underscore attr to keep the direct variable reference
|
||||
const customGraphQLSchemaTypeMap = this.graphQLCustomTypeDefs._typeMap;
|
||||
const findAndReplaceLastType = (parent, key) => {
|
||||
if (parent[key].name) {
|
||||
@@ -275,51 +279,18 @@ class ParseGraphQLSchema {
|
||||
this.graphQLSchema = await this.graphQLCustomTypeDefs({
|
||||
directivesDefinitionsSchema: this.graphQLSchemaDirectivesDefinitions,
|
||||
autoSchema: this.graphQLAutoSchema,
|
||||
stitchSchemas,
|
||||
graphQLSchemaDirectives: this.graphQLSchemaDirectives,
|
||||
});
|
||||
} else {
|
||||
this.graphQLSchema = stitchSchemas({
|
||||
schemas: [
|
||||
this.graphQLSchemaDirectivesDefinitions,
|
||||
this.graphQLAutoSchema,
|
||||
this.graphQLSchema = mergeSchemas({
|
||||
schemas: [this.graphQLAutoSchema],
|
||||
typeDefs: mergeTypeDefs([
|
||||
this.graphQLCustomTypeDefs,
|
||||
],
|
||||
mergeDirectives: true,
|
||||
this.graphQLSchemaDirectivesDefinitions,
|
||||
]),
|
||||
});
|
||||
this.graphQLSchema = this.graphQLSchemaDirectives(this.graphQLSchema);
|
||||
}
|
||||
|
||||
// Only merge directive when string schema provided
|
||||
const graphQLSchemaTypeMap = this.graphQLSchema.getTypeMap();
|
||||
Object.keys(graphQLSchemaTypeMap).forEach(graphQLSchemaTypeName => {
|
||||
const graphQLSchemaType = graphQLSchemaTypeMap[graphQLSchemaTypeName];
|
||||
if (
|
||||
typeof graphQLSchemaType.getFields === 'function' &&
|
||||
this.graphQLCustomTypeDefs.definitions
|
||||
) {
|
||||
const graphQLCustomTypeDef = this.graphQLCustomTypeDefs.definitions.find(
|
||||
definition => definition.name.value === graphQLSchemaTypeName
|
||||
);
|
||||
if (graphQLCustomTypeDef) {
|
||||
const graphQLSchemaTypeFieldMap = graphQLSchemaType.getFields();
|
||||
Object.keys(graphQLSchemaTypeFieldMap).forEach(graphQLSchemaTypeFieldName => {
|
||||
const graphQLSchemaTypeField = graphQLSchemaTypeFieldMap[graphQLSchemaTypeFieldName];
|
||||
if (!graphQLSchemaTypeField.astNode) {
|
||||
const astNode = graphQLCustomTypeDef.fields.find(
|
||||
field => field.name.value === graphQLSchemaTypeFieldName
|
||||
);
|
||||
if (astNode) {
|
||||
graphQLSchemaTypeField.astNode = astNode;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
SchemaDirectiveVisitor.visitSchemaDirectives(
|
||||
this.graphQLSchema,
|
||||
this.graphQLSchemaDirectives
|
||||
);
|
||||
} else {
|
||||
this.graphQLSchema = this.graphQLAutoSchema;
|
||||
}
|
||||
@@ -327,6 +298,14 @@ class ParseGraphQLSchema {
|
||||
return this.graphQLSchema;
|
||||
}
|
||||
|
||||
_logOnce(severity, message) {
|
||||
if (this.logCache[message]) {
|
||||
return;
|
||||
}
|
||||
this.log[severity](message);
|
||||
this.logCache[message] = true;
|
||||
}
|
||||
|
||||
addGraphQLType(type, throwError = false, ignoreReserved = false, ignoreConnection = false) {
|
||||
if (
|
||||
(!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) ||
|
||||
@@ -337,7 +316,7 @@ class ParseGraphQLSchema {
|
||||
if (throwError) {
|
||||
throw new Error(message);
|
||||
}
|
||||
this.log.warn(message);
|
||||
this._logOnce('warn', message);
|
||||
return undefined;
|
||||
}
|
||||
this.graphQLTypes.push(type);
|
||||
@@ -353,7 +332,7 @@ class ParseGraphQLSchema {
|
||||
if (throwError) {
|
||||
throw new Error(message);
|
||||
}
|
||||
this.log.warn(message);
|
||||
this._logOnce('warn', message);
|
||||
return undefined;
|
||||
}
|
||||
this.graphQLQueries[fieldName] = field;
|
||||
@@ -369,7 +348,7 @@ class ParseGraphQLSchema {
|
||||
if (throwError) {
|
||||
throw new Error(message);
|
||||
}
|
||||
this.log.warn(message);
|
||||
this._logOnce('warn', message);
|
||||
return undefined;
|
||||
}
|
||||
this.graphQLMutations[fieldName] = field;
|
||||
@@ -478,7 +457,8 @@ class ParseGraphQLSchema {
|
||||
if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(functionName)) {
|
||||
return true;
|
||||
} else {
|
||||
this.log.warn(
|
||||
this._logOnce(
|
||||
'warn',
|
||||
`Function ${functionName} could not be added to the auto schema because GraphQL names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/.`
|
||||
);
|
||||
return false;
|
||||
@@ -500,29 +480,17 @@ class ParseGraphQLSchema {
|
||||
const { parseClasses, parseGraphQLConfig, functionNamesString } = params;
|
||||
|
||||
// First init
|
||||
if (!this.parseCachedClasses || !this.graphQLSchema) {
|
||||
const thisParseClassesObj = parseClasses.reduce((acc, clzz) => {
|
||||
acc[clzz.className] = clzz;
|
||||
return acc;
|
||||
}, {});
|
||||
this.parseCachedClasses = thisParseClassesObj;
|
||||
if (!this.graphQLSchema) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const newParseCachedClasses = parseClasses.reduce((acc, clzz) => {
|
||||
acc[clzz.className] = clzz;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (
|
||||
isDeepStrictEqual(this.parseGraphQLConfig, parseGraphQLConfig) &&
|
||||
this.functionNamesString === functionNamesString &&
|
||||
isDeepStrictEqual(this.parseCachedClasses, newParseCachedClasses)
|
||||
isDeepStrictEqual(this.parseClasses, parseClasses)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.parseCachedClasses = newParseCachedClasses;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import corsMiddleware from 'cors';
|
||||
import bodyParser from 'body-parser';
|
||||
import { graphqlUploadExpress } from 'graphql-upload';
|
||||
import { graphqlExpress } from 'apollo-server-express/dist/expressApollo';
|
||||
import { renderPlaygroundPage } from '@apollographql/graphql-playground-html';
|
||||
import { createServer, renderGraphiQL } from '@graphql-yoga/node';
|
||||
import { execute, subscribe } from 'graphql';
|
||||
import { SubscriptionServer } from 'subscriptions-transport-ws';
|
||||
import { handleParseErrors, handleParseHeaders } from '../middlewares';
|
||||
@@ -32,18 +29,20 @@ class ParseGraphQLServer {
|
||||
});
|
||||
}
|
||||
|
||||
async _getGraphQLOptions(req) {
|
||||
async _getGraphQLOptions() {
|
||||
try {
|
||||
return {
|
||||
schema: await this.parseGraphQLSchema.load(),
|
||||
context: {
|
||||
info: req.info,
|
||||
config: req.config,
|
||||
auth: req.auth,
|
||||
},
|
||||
formatError: error => {
|
||||
// Allow to console.log here to debug
|
||||
return error;
|
||||
context: ({ req: { info, config, auth } }) => ({
|
||||
info,
|
||||
config,
|
||||
auth,
|
||||
}),
|
||||
maskedErrors: false,
|
||||
multipart: {
|
||||
fileSize: this._transformMaxUploadSizeToBytes(
|
||||
this.parseServer.config.maxUploadSize || '20mb'
|
||||
),
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -52,6 +51,17 @@ class ParseGraphQLServer {
|
||||
}
|
||||
}
|
||||
|
||||
async _getServer() {
|
||||
const schemaRef = this.parseGraphQLSchema.graphQLSchema;
|
||||
const newSchemaRef = await this.parseGraphQLSchema.load();
|
||||
if (schemaRef === newSchemaRef && this._server) {
|
||||
return this._server;
|
||||
}
|
||||
const options = await this._getGraphQLOptions();
|
||||
this._server = createServer(options);
|
||||
return this._server;
|
||||
}
|
||||
|
||||
_transformMaxUploadSizeToBytes(maxUploadSize) {
|
||||
const unitMap = {
|
||||
kb: 1,
|
||||
@@ -70,22 +80,13 @@ class ParseGraphQLServer {
|
||||
requiredParameter('You must provide an Express.js app instance!');
|
||||
}
|
||||
|
||||
app.use(
|
||||
this.config.graphQLPath,
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: this._transformMaxUploadSizeToBytes(
|
||||
this.parseServer.config.maxUploadSize || '20mb'
|
||||
),
|
||||
})
|
||||
);
|
||||
app.use(this.config.graphQLPath, corsMiddleware());
|
||||
app.use(this.config.graphQLPath, bodyParser.json());
|
||||
app.use(this.config.graphQLPath, handleParseHeaders);
|
||||
app.use(this.config.graphQLPath, handleParseErrors);
|
||||
app.use(
|
||||
this.config.graphQLPath,
|
||||
graphqlExpress(async req => await this._getGraphQLOptions(req))
|
||||
);
|
||||
app.use(this.config.graphQLPath, async (req, res) => {
|
||||
const server = await this._getServer();
|
||||
return server(req, res);
|
||||
});
|
||||
}
|
||||
|
||||
applyPlayground(app) {
|
||||
@@ -98,14 +99,13 @@ class ParseGraphQLServer {
|
||||
(_req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.write(
|
||||
renderPlaygroundPage({
|
||||
renderGraphiQL({
|
||||
endpoint: this.config.graphQLPath,
|
||||
version: '1.7.25',
|
||||
subscriptionEndpoint: this.config.subscriptionsPath,
|
||||
headers: {
|
||||
headers: JSON.stringify({
|
||||
'X-Parse-Application-Id': this.parseServer.config.appId,
|
||||
'X-Parse-Master-Key': this.parseServer.config.masterKey,
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
res.end();
|
||||
|
||||
@@ -12,9 +12,7 @@ const needToGetAllKeys = (fields, keys, parseClasses) =>
|
||||
if (fields[key[0]]) {
|
||||
if (fields[key[0]].type === 'Relation') return false;
|
||||
if (fields[key[0]].type === 'Pointer') {
|
||||
const subClass = parseClasses.find(
|
||||
({ className: parseClassName }) => fields[key[0]].targetClass === parseClassName
|
||||
);
|
||||
const subClass = parseClasses[fields[key[0]].targetClass];
|
||||
if (subClass && subClass.fields[key[1]]) {
|
||||
// Current sub key is not custom
|
||||
return false;
|
||||
@@ -48,17 +46,11 @@ const getObject = async (
|
||||
) => {
|
||||
const options = {};
|
||||
try {
|
||||
if (
|
||||
!needToGetAllKeys(
|
||||
parseClasses.find(({ className: parseClassName }) => className === parseClassName).fields,
|
||||
keys,
|
||||
parseClasses
|
||||
)
|
||||
) {
|
||||
if (!needToGetAllKeys(parseClasses[className].fields, keys, parseClasses)) {
|
||||
options.keys = keys;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.error(e);
|
||||
}
|
||||
if (include) {
|
||||
options.include = include;
|
||||
@@ -165,13 +157,7 @@ const findObjects = async (
|
||||
// Silently replace the limit on the query with the max configured
|
||||
options.limit = config.maxLimit;
|
||||
}
|
||||
if (
|
||||
!needToGetAllKeys(
|
||||
parseClasses.find(({ className: parseClassName }) => className === parseClassName).fields,
|
||||
keys,
|
||||
parseClasses
|
||||
)
|
||||
) {
|
||||
if (!needToGetAllKeys(parseClasses[className].fields, keys, parseClasses)) {
|
||||
options.keys = keys;
|
||||
}
|
||||
if (includeAll === true) {
|
||||
@@ -286,7 +272,7 @@ const calculateSkipAndLimit = (skipInput, first, after, last, before, maxLimit)
|
||||
}
|
||||
|
||||
if ((skip || 0) >= before) {
|
||||
// If the before index is less then the skip, no objects will be returned
|
||||
// If the before index is less than the skip, no objects will be returned
|
||||
limit = 0;
|
||||
} else if ((!limit && limit !== 0) || (skip || 0) + limit > before) {
|
||||
// If there is no limit set, the limit is calculated. Or, if the limit (plus skip) is bigger than the before index, the new limit is set.
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
GraphQLUnionType,
|
||||
} from 'graphql';
|
||||
import { toGlobalId } from 'graphql-relay';
|
||||
import { GraphQLUpload } from '@graphql-tools/links';
|
||||
|
||||
class TypeValidationError extends Error {
|
||||
constructor(value, type) {
|
||||
@@ -223,6 +222,11 @@ const DATE = new GraphQLScalarType({
|
||||
},
|
||||
});
|
||||
|
||||
const GraphQLUpload = new GraphQLScalarType({
|
||||
name: 'Upload',
|
||||
description: 'The Upload scalar type represents a file upload.',
|
||||
});
|
||||
|
||||
const BYTES = new GraphQLScalarType({
|
||||
name: 'Bytes',
|
||||
description:
|
||||
@@ -1190,8 +1194,8 @@ const ELEMENT = new GraphQLObjectType({
|
||||
// Default static union type, we update types and resolveType function later
|
||||
let ARRAY_RESULT;
|
||||
|
||||
const loadArrayResult = (parseGraphQLSchema, parseClasses) => {
|
||||
const classTypes = parseClasses
|
||||
const loadArrayResult = (parseGraphQLSchema, parseClassesArray) => {
|
||||
const classTypes = parseClassesArray
|
||||
.filter(parseClass =>
|
||||
parseGraphQLSchema.parseClassTypes[parseClass.className].classGraphQLOutputType ? true : false
|
||||
)
|
||||
@@ -1206,12 +1210,12 @@ const loadArrayResult = (parseGraphQLSchema, parseClasses) => {
|
||||
resolveType: value => {
|
||||
if (value.__type === 'Object' && value.className && value.objectId) {
|
||||
if (parseGraphQLSchema.parseClassTypes[value.className]) {
|
||||
return parseGraphQLSchema.parseClassTypes[value.className].classGraphQLOutputType;
|
||||
return parseGraphQLSchema.parseClassTypes[value.className].classGraphQLOutputType.name;
|
||||
} else {
|
||||
return ELEMENT;
|
||||
return ELEMENT.name;
|
||||
}
|
||||
} else {
|
||||
return ELEMENT;
|
||||
return ELEMENT.name;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1265,6 +1269,7 @@ const load = parseGraphQLSchema => {
|
||||
};
|
||||
|
||||
export {
|
||||
GraphQLUpload,
|
||||
TypeValidationError,
|
||||
parseStringValue,
|
||||
parseIntValue,
|
||||
|
||||
@@ -39,7 +39,7 @@ const load = parseGraphQLSchema => {
|
||||
}
|
||||
},
|
||||
obj => {
|
||||
return parseGraphQLSchema.parseClassTypes[obj.className].classGraphQLOutputType;
|
||||
return parseGraphQLSchema.parseClassTypes[obj.className].classGraphQLOutputType.name;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,43 +1,33 @@
|
||||
import { GraphQLNonNull } from 'graphql';
|
||||
import { mutationWithClientMutationId } from 'graphql-relay';
|
||||
import { GraphQLUpload } from '@graphql-tools/links';
|
||||
import Parse from 'parse/node';
|
||||
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
|
||||
import logger from '../../logger';
|
||||
|
||||
const handleUpload = async (upload, config) => {
|
||||
const { createReadStream, filename, mimetype } = await upload;
|
||||
let data = null;
|
||||
if (createReadStream) {
|
||||
const stream = createReadStream();
|
||||
data = await new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
stream
|
||||
.on('error', reject)
|
||||
.on('data', chunk => chunks.push(chunk))
|
||||
.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
});
|
||||
}
|
||||
const data = Buffer.from(await upload.arrayBuffer());
|
||||
const fileName = upload.name;
|
||||
const type = upload.type;
|
||||
|
||||
if (!data || !data.length) {
|
||||
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.');
|
||||
}
|
||||
|
||||
if (filename.length > 128) {
|
||||
if (fileName.length > 128) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.');
|
||||
}
|
||||
|
||||
if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
|
||||
if (!fileName.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename contains invalid characters.');
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
fileInfo: await config.filesController.createFile(config, filename, data, mimetype),
|
||||
fileInfo: await config.filesController.createFile(config, fileName, data, type),
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error('Error creating a file: ', e);
|
||||
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `Could not store file: ${filename}.`);
|
||||
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `Could not store file: ${fileName}.`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -48,7 +38,7 @@ const load = parseGraphQLSchema => {
|
||||
inputFields: {
|
||||
upload: {
|
||||
description: 'This is the new file to be created and uploaded.',
|
||||
type: new GraphQLNonNull(GraphQLUpload),
|
||||
type: new GraphQLNonNull(defaultGraphQLTypes.GraphQLUpload),
|
||||
},
|
||||
},
|
||||
outputFields: {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { SchemaDirectiveVisitor } from '@graphql-tools/utils';
|
||||
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
|
||||
import { FunctionsRouter } from '../../Routers/FunctionsRouter';
|
||||
|
||||
export const definitions = gql`
|
||||
export const definitions = `
|
||||
directive @resolve(to: String) on FIELD_DEFINITION
|
||||
directive @mock(with: Any!) on FIELD_DEFINITION
|
||||
`;
|
||||
@@ -10,46 +9,48 @@ export const definitions = gql`
|
||||
const load = parseGraphQLSchema => {
|
||||
parseGraphQLSchema.graphQLSchemaDirectivesDefinitions = definitions;
|
||||
|
||||
class ResolveDirectiveVisitor extends SchemaDirectiveVisitor {
|
||||
visitFieldDefinition(field) {
|
||||
field.resolve = async (_source, args, context) => {
|
||||
try {
|
||||
const { config, auth, info } = context;
|
||||
|
||||
let functionName = field.name;
|
||||
if (this.args.to) {
|
||||
functionName = this.args.to;
|
||||
}
|
||||
|
||||
return (
|
||||
await FunctionsRouter.handleCloudFunction({
|
||||
params: {
|
||||
functionName,
|
||||
},
|
||||
config,
|
||||
auth,
|
||||
info,
|
||||
body: args,
|
||||
})
|
||||
).response.result;
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
const resolveDirective = schema =>
|
||||
mapSchema(schema, {
|
||||
[MapperKind.OBJECT_FIELD]: fieldConfig => {
|
||||
const directive = getDirective(schema, fieldConfig, 'resolve')?.[0];
|
||||
if (directive) {
|
||||
const { to: targetCloudFunction } = directive;
|
||||
fieldConfig.resolve = async (_source, args, context, gqlInfo) => {
|
||||
try {
|
||||
const { config, auth, info } = context;
|
||||
const functionName = targetCloudFunction || gqlInfo.fieldName;
|
||||
return (
|
||||
await FunctionsRouter.handleCloudFunction({
|
||||
params: {
|
||||
functionName,
|
||||
},
|
||||
config,
|
||||
auth,
|
||||
info,
|
||||
body: args,
|
||||
})
|
||||
).response.result;
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return fieldConfig;
|
||||
},
|
||||
});
|
||||
|
||||
parseGraphQLSchema.graphQLSchemaDirectives.resolve = ResolveDirectiveVisitor;
|
||||
const mockDirective = schema =>
|
||||
mapSchema(schema, {
|
||||
[MapperKind.OBJECT_FIELD]: fieldConfig => {
|
||||
const directive = getDirective(schema, fieldConfig, 'mock')?.[0];
|
||||
if (directive) {
|
||||
const { with: mockValue } = directive;
|
||||
fieldConfig.resolve = async () => mockValue;
|
||||
}
|
||||
return fieldConfig;
|
||||
},
|
||||
});
|
||||
|
||||
class MockDirectiveVisitor extends SchemaDirectiveVisitor {
|
||||
visitFieldDefinition(field) {
|
||||
field.resolve = () => {
|
||||
return this.args.with;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
parseGraphQLSchema.graphQLSchemaDirectives.mock = MockDirectiveVisitor;
|
||||
parseGraphQLSchema.graphQLSchemaDirectives = schema => mockDirective(resolveDirective(schema));
|
||||
};
|
||||
|
||||
export { load };
|
||||
|
||||
@@ -29,19 +29,19 @@ const SCHEMA_FIELD = new GraphQLInterfaceType({
|
||||
},
|
||||
resolveType: value =>
|
||||
({
|
||||
String: SCHEMA_STRING_FIELD,
|
||||
Number: SCHEMA_NUMBER_FIELD,
|
||||
Boolean: SCHEMA_BOOLEAN_FIELD,
|
||||
Array: SCHEMA_ARRAY_FIELD,
|
||||
Object: SCHEMA_OBJECT_FIELD,
|
||||
Date: SCHEMA_DATE_FIELD,
|
||||
File: SCHEMA_FILE_FIELD,
|
||||
GeoPoint: SCHEMA_GEO_POINT_FIELD,
|
||||
Polygon: SCHEMA_POLYGON_FIELD,
|
||||
Bytes: SCHEMA_BYTES_FIELD,
|
||||
Pointer: SCHEMA_POINTER_FIELD,
|
||||
Relation: SCHEMA_RELATION_FIELD,
|
||||
ACL: SCHEMA_ACL_FIELD,
|
||||
String: SCHEMA_STRING_FIELD.name,
|
||||
Number: SCHEMA_NUMBER_FIELD.name,
|
||||
Boolean: SCHEMA_BOOLEAN_FIELD.name,
|
||||
Array: SCHEMA_ARRAY_FIELD.name,
|
||||
Object: SCHEMA_OBJECT_FIELD.name,
|
||||
Date: SCHEMA_DATE_FIELD.name,
|
||||
File: SCHEMA_FILE_FIELD.name,
|
||||
GeoPoint: SCHEMA_GEO_POINT_FIELD.name,
|
||||
Polygon: SCHEMA_POLYGON_FIELD.name,
|
||||
Bytes: SCHEMA_BYTES_FIELD.name,
|
||||
Pointer: SCHEMA_POINTER_FIELD.name,
|
||||
Relation: SCHEMA_RELATION_FIELD.name,
|
||||
ACL: SCHEMA_ACL_FIELD.name,
|
||||
}[value.type]),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Parse from 'parse/node';
|
||||
import { ApolloError } from 'apollo-server-core';
|
||||
import { GraphQLYogaError } from '@graphql-yoga/node';
|
||||
|
||||
export function enforceMasterKeyAccess(auth) {
|
||||
if (!auth.isMaster) {
|
||||
@@ -16,7 +16,7 @@ export function toGraphQLError(error) {
|
||||
code = Parse.Error.INTERNAL_SERVER_ERROR;
|
||||
message = 'Internal server error';
|
||||
}
|
||||
return new ApolloError(message, code);
|
||||
return new GraphQLYogaError(message, { code });
|
||||
}
|
||||
|
||||
export const extractKeysAndInclude = selectedFields => {
|
||||
|
||||
@@ -14,7 +14,7 @@ const transformTypes = async (
|
||||
classGraphQLUpdateType,
|
||||
config: { isCreateEnabled, isUpdateEnabled },
|
||||
} = parseGraphQLSchema.parseClassTypes[className];
|
||||
const parseClass = parseGraphQLSchema.parseClasses.find(clazz => clazz.className === className);
|
||||
const parseClass = parseGraphQLSchema.parseClasses[className];
|
||||
if (fields) {
|
||||
const classGraphQLCreateTypeFields =
|
||||
isCreateEnabled && classGraphQLCreateType ? classGraphQLCreateType.getFields() : null;
|
||||
|
||||
@@ -51,7 +51,7 @@ const transformQueryConstraintInputToParse = (
|
||||
parentConstraints,
|
||||
parseClasses
|
||||
) => {
|
||||
const fields = parseClasses.find(parseClass => parseClass.className === className).fields;
|
||||
const fields = parseClasses[className].fields;
|
||||
if (parentFieldName === 'id' && className) {
|
||||
Object.keys(constraints).forEach(constraintName => {
|
||||
const constraintValue = constraints[constraintName];
|
||||
@@ -108,7 +108,7 @@ const transformQueryConstraintInputToParse = (
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
if (fieldValue.key && fieldValue.value && parentConstraints && parentFieldName) {
|
||||
if (fieldValue.key && fieldValue.value !== undefined && parentConstraints && parentFieldName) {
|
||||
delete parentConstraints[parentFieldName];
|
||||
parentConstraints[`${parentFieldName}.${fieldValue.key}`] = {
|
||||
...parentConstraints[`${parentFieldName}.${fieldValue.key}`],
|
||||
|
||||
@@ -19,6 +19,13 @@ class ParseCloudCodePublisher {
|
||||
this._onCloudCodeMessage(Parse.applicationId + 'afterDelete', request);
|
||||
}
|
||||
|
||||
onClearCachedRoles(user: Parse.Object) {
|
||||
this.parsePublisher.publish(
|
||||
Parse.applicationId + 'clearCache',
|
||||
JSON.stringify({ userId: user.id })
|
||||
);
|
||||
}
|
||||
|
||||
// Request is the request object from cloud code functions. request.object is a ParseObject.
|
||||
_onCloudCodeMessage(type: string, request: any): void {
|
||||
logger.verbose(
|
||||
@@ -33,6 +40,9 @@ class ParseCloudCodePublisher {
|
||||
if (request.original) {
|
||||
message.originalParseObject = request.original._toFullJSON();
|
||||
}
|
||||
if (request.classLevelPermissions) {
|
||||
message.classLevelPermissions = request.classLevelPermissions;
|
||||
}
|
||||
this.parsePublisher.publish(type, JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,18 @@ import { ParsePubSub } from './ParsePubSub';
|
||||
import SchemaController from '../Controllers/SchemaController';
|
||||
import _ from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { runLiveQueryEventHandlers, getTrigger, runTrigger, toJSONwithObjects } from '../triggers';
|
||||
import {
|
||||
runLiveQueryEventHandlers,
|
||||
getTrigger,
|
||||
runTrigger,
|
||||
resolveError,
|
||||
toJSONwithObjects,
|
||||
} from '../triggers';
|
||||
import { getAuthForSessionToken, Auth } from '../Auth';
|
||||
import { getCacheController } from '../Controllers';
|
||||
import { getCacheController, getDatabaseController } from '../Controllers';
|
||||
import LRU from 'lru-cache';
|
||||
import UserRouter from '../Routers/UsersRouter';
|
||||
import DatabaseController from '../Controllers/DatabaseController';
|
||||
|
||||
class ParseLiveQueryServer {
|
||||
clients: Map;
|
||||
@@ -58,7 +65,7 @@ class ParseLiveQueryServer {
|
||||
// The main benefit is to be able to reuse the same user / session token resolution.
|
||||
this.authCache = new LRU({
|
||||
max: 500, // 500 concurrent
|
||||
maxAge: config.cacheTimeout,
|
||||
ttl: config.cacheTimeout,
|
||||
});
|
||||
// Initialize websocket server
|
||||
this.parseWebSocketServer = new ParseWebSocketServer(
|
||||
@@ -71,6 +78,7 @@ class ParseLiveQueryServer {
|
||||
this.subscriber = ParsePubSub.createSubscriber(config);
|
||||
this.subscriber.subscribe(Parse.applicationId + 'afterSave');
|
||||
this.subscriber.subscribe(Parse.applicationId + 'afterDelete');
|
||||
this.subscriber.subscribe(Parse.applicationId + 'clearCache');
|
||||
// Register message handler for subscriber. When publisher get messages, it will publish message
|
||||
// to the subscribers and the handler will be called.
|
||||
this.subscriber.on('message', (channel, messageStr) => {
|
||||
@@ -82,6 +90,10 @@ class ParseLiveQueryServer {
|
||||
logger.error('unable to parse message', messageStr, e);
|
||||
return;
|
||||
}
|
||||
if (channel === Parse.applicationId + 'clearCache') {
|
||||
this._clearCachedRoles(message.userId);
|
||||
return;
|
||||
}
|
||||
this._inflateParseObject(message);
|
||||
if (channel === Parse.applicationId + 'afterSave') {
|
||||
this._onAfterSave(message);
|
||||
@@ -185,23 +197,18 @@ class ParseLiveQueryServer {
|
||||
if (res.object && typeof res.object.toJSON === 'function') {
|
||||
deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
|
||||
}
|
||||
if (
|
||||
(deletedParseObject.className === '_User' ||
|
||||
deletedParseObject.className === '_Session') &&
|
||||
!client.hasMasterKey
|
||||
) {
|
||||
delete deletedParseObject.sessionToken;
|
||||
delete deletedParseObject.authData;
|
||||
}
|
||||
client.pushDelete(requestId, deletedParseObject);
|
||||
} catch (error) {
|
||||
Client.pushError(
|
||||
client.parseWebSocket,
|
||||
error.code || Parse.Error.SCRIPT_FAILED,
|
||||
error.message || error,
|
||||
false,
|
||||
requestId
|
||||
await this._filterSensitiveData(
|
||||
classLevelPermissions,
|
||||
res,
|
||||
client,
|
||||
requestId,
|
||||
op,
|
||||
subscription.query
|
||||
);
|
||||
client.pushDelete(requestId, deletedParseObject);
|
||||
} catch (e) {
|
||||
const error = resolveError(e);
|
||||
Client.pushError(client.parseWebSocket, error.code, error.message, false, requestId);
|
||||
logger.error(
|
||||
`Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` +
|
||||
JSON.stringify(error)
|
||||
@@ -344,28 +351,21 @@ class ParseLiveQueryServer {
|
||||
res.original.className || className
|
||||
);
|
||||
}
|
||||
if (
|
||||
(currentParseObject.className === '_User' ||
|
||||
currentParseObject.className === '_Session') &&
|
||||
!client.hasMasterKey
|
||||
) {
|
||||
delete currentParseObject.sessionToken;
|
||||
delete originalParseObject?.sessionToken;
|
||||
delete currentParseObject.authData;
|
||||
delete originalParseObject?.authData;
|
||||
}
|
||||
await this._filterSensitiveData(
|
||||
classLevelPermissions,
|
||||
res,
|
||||
client,
|
||||
requestId,
|
||||
op,
|
||||
subscription.query
|
||||
);
|
||||
const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
|
||||
if (client[functionName]) {
|
||||
client[functionName](requestId, currentParseObject, originalParseObject);
|
||||
}
|
||||
} catch (error) {
|
||||
Client.pushError(
|
||||
client.parseWebSocket,
|
||||
error.code || Parse.Error.SCRIPT_FAILED,
|
||||
error.message || error,
|
||||
false,
|
||||
requestId
|
||||
);
|
||||
} catch (e) {
|
||||
const error = resolveError(e);
|
||||
Client.pushError(client.parseWebSocket, error.code, error.message, false, requestId);
|
||||
logger.error(
|
||||
`Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` +
|
||||
JSON.stringify(error)
|
||||
@@ -478,6 +478,32 @@ class ParseLiveQueryServer {
|
||||
return matchesQuery(parseObject, subscription.query);
|
||||
}
|
||||
|
||||
async _clearCachedRoles(userId: string) {
|
||||
try {
|
||||
const validTokens = await new Parse.Query(Parse.Session)
|
||||
.equalTo('user', Parse.User.createWithoutData(userId))
|
||||
.find({ useMasterKey: true });
|
||||
await Promise.all(
|
||||
validTokens.map(async token => {
|
||||
const sessionToken = token.get('sessionToken');
|
||||
const authPromise = this.authCache.get(sessionToken);
|
||||
if (!authPromise) {
|
||||
return;
|
||||
}
|
||||
const [auth1, auth2] = await Promise.all([
|
||||
authPromise,
|
||||
getAuthForSessionToken({ cacheController: this.cacheController, sessionToken }),
|
||||
]);
|
||||
auth1.auth?.clearRoleCache(sessionToken);
|
||||
auth2.auth?.clearRoleCache(sessionToken);
|
||||
this.authCache.del(sessionToken);
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
logger.verbose(`Could not clear role cache. ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
getAuthForSessionToken(sessionToken: ?string): Promise<{ auth: ?Auth, userId: ?string }> {
|
||||
if (!sessionToken) {
|
||||
return Promise.resolve({});
|
||||
@@ -550,6 +576,54 @@ class ParseLiveQueryServer {
|
||||
// return rolesQuery.find({useMasterKey:true});
|
||||
}
|
||||
|
||||
async _filterSensitiveData(
|
||||
classLevelPermissions: ?any,
|
||||
res: any,
|
||||
client: any,
|
||||
requestId: number,
|
||||
op: string,
|
||||
query: any
|
||||
) {
|
||||
const subscriptionInfo = client.getSubscriptionInfo(requestId);
|
||||
const aclGroup = ['*'];
|
||||
let clientAuth;
|
||||
if (typeof subscriptionInfo !== 'undefined') {
|
||||
const { userId, auth } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken);
|
||||
if (userId) {
|
||||
aclGroup.push(userId);
|
||||
}
|
||||
clientAuth = auth;
|
||||
}
|
||||
const filter = obj => {
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
let protectedFields = classLevelPermissions?.protectedFields || [];
|
||||
if (!client.hasMasterKey && !Array.isArray(protectedFields)) {
|
||||
protectedFields = getDatabaseController(this.config).addProtectedFields(
|
||||
classLevelPermissions,
|
||||
res.object.className,
|
||||
query,
|
||||
aclGroup,
|
||||
clientAuth
|
||||
);
|
||||
}
|
||||
return DatabaseController.filterSensitiveData(
|
||||
client.hasMasterKey,
|
||||
aclGroup,
|
||||
clientAuth,
|
||||
op,
|
||||
classLevelPermissions,
|
||||
res.object.className,
|
||||
protectedFields,
|
||||
obj,
|
||||
query
|
||||
);
|
||||
};
|
||||
res.object = filter(res.object);
|
||||
res.original = filter(res.original);
|
||||
}
|
||||
|
||||
_getCLPOperation(query: any) {
|
||||
return typeof query === 'object' &&
|
||||
Object.keys(query).length == 1 &&
|
||||
@@ -584,7 +658,6 @@ class ParseLiveQueryServer {
|
||||
if (!acl_has_roles) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const roleNames = await auth.getUserRoles();
|
||||
// Finally, see if any of the user's roles allow them read access
|
||||
for (const role of roleNames) {
|
||||
@@ -681,13 +754,9 @@ class ParseLiveQueryServer {
|
||||
logger.info(`Create new client: ${parseWebsocket.clientId}`);
|
||||
client.pushConnect();
|
||||
runLiveQueryEventHandlers(req);
|
||||
} catch (error) {
|
||||
Client.pushError(
|
||||
parseWebsocket,
|
||||
error.code || Parse.Error.SCRIPT_FAILED,
|
||||
error.message || error,
|
||||
false
|
||||
);
|
||||
} catch (e) {
|
||||
const error = resolveError(e);
|
||||
Client.pushError(parseWebsocket, error.code, error.message, false);
|
||||
logger.error(
|
||||
`Failed running beforeConnect for session ${request.sessionToken} with:\n Error: ` +
|
||||
JSON.stringify(error)
|
||||
@@ -827,16 +896,11 @@ class ParseLiveQueryServer {
|
||||
installationId: client.installationId,
|
||||
});
|
||||
} catch (e) {
|
||||
Client.pushError(
|
||||
parseWebsocket,
|
||||
e.code || Parse.Error.SCRIPT_FAILED,
|
||||
e.message || e,
|
||||
false,
|
||||
request.requestId
|
||||
);
|
||||
const error = resolveError(e);
|
||||
Client.pushError(parseWebsocket, error.code, error.message, false, request.requestId);
|
||||
logger.error(
|
||||
`Failed running beforeSubscribe on ${className} for session ${request.sessionToken} with:\n Error: ` +
|
||||
JSON.stringify(e)
|
||||
JSON.stringify(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ export class ParseWebSocketServer {
|
||||
logger.info('Parse LiveQuery Server started running');
|
||||
};
|
||||
wss.onConnection = ws => {
|
||||
ws.waitingForPong = false;
|
||||
ws.on('pong', () => {
|
||||
ws.waitingForPong = false;
|
||||
});
|
||||
ws.on('error', error => {
|
||||
logger.error(error.message);
|
||||
logger.error(inspect(ws, false));
|
||||
@@ -21,10 +25,12 @@ export class ParseWebSocketServer {
|
||||
onConnect(new ParseWebSocket(ws));
|
||||
// Send ping to client periodically
|
||||
const pingIntervalId = setInterval(() => {
|
||||
if (ws.readyState == ws.OPEN) {
|
||||
if (!ws.waitingForPong) {
|
||||
ws.ping();
|
||||
ws.waitingForPong = true;
|
||||
} else {
|
||||
clearInterval(pingIntervalId);
|
||||
ws.terminate();
|
||||
}
|
||||
}, config.websocketTimeout || 10 * 1000);
|
||||
};
|
||||
|
||||
@@ -103,8 +103,18 @@ function contains(haystack: Array, needle: any): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(needle)) {
|
||||
for (const need of needle) {
|
||||
if (contains(haystack, need)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return haystack.indexOf(needle) > -1;
|
||||
}
|
||||
/**
|
||||
@@ -334,9 +344,25 @@ function matchesKeyConstraints(object, key, constraints) {
|
||||
return true;
|
||||
}
|
||||
case '$geoWithin': {
|
||||
const points = compareTo.$polygon.map(geoPoint => [geoPoint.latitude, geoPoint.longitude]);
|
||||
const polygon = new Parse.Polygon(points);
|
||||
return polygon.containsPoint(object[key]);
|
||||
if (compareTo.$polygon) {
|
||||
const points = compareTo.$polygon.map(geoPoint => [
|
||||
geoPoint.latitude,
|
||||
geoPoint.longitude,
|
||||
]);
|
||||
const polygon = new Parse.Polygon(points);
|
||||
return polygon.containsPoint(object[key]);
|
||||
}
|
||||
if (compareTo.$centerSphere) {
|
||||
const [WGS84Point, maxDistance] = compareTo.$centerSphere;
|
||||
const centerPoint = new Parse.GeoPoint({
|
||||
latitude: WGS84Point[1],
|
||||
longitude: WGS84Point[0],
|
||||
});
|
||||
const point = new Parse.GeoPoint(object[key]);
|
||||
const distance = point.radiansTo(centerPoint);
|
||||
return distance <= maxDistance;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '$geoIntersects': {
|
||||
const polygon = new Parse.Polygon(object[key].coordinates);
|
||||
|
||||
@@ -19,7 +19,7 @@ class SessionTokenCache {
|
||||
constructor(timeout: number = 30 * 24 * 60 * 60 * 1000, maxSize: number = 10000) {
|
||||
this.cache = new LRU({
|
||||
max: maxSize,
|
||||
maxAge: timeout,
|
||||
ttl: timeout,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+73
-41
@@ -5,6 +5,51 @@ Do not edit manually, but update Options/index.js
|
||||
*/
|
||||
var parsers = require('./parsers');
|
||||
|
||||
module.exports.SchemaOptions = {
|
||||
afterMigration: {
|
||||
env: 'PARSE_SERVER_SCHEMA_AFTER_MIGRATION',
|
||||
help: 'Execute a callback after running schema migrations.',
|
||||
},
|
||||
beforeMigration: {
|
||||
env: 'PARSE_SERVER_SCHEMA_BEFORE_MIGRATION',
|
||||
help: 'Execute a callback before running schema migrations.',
|
||||
},
|
||||
definitions: {
|
||||
env: 'PARSE_SERVER_SCHEMA_DEFINITIONS',
|
||||
help:
|
||||
'Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema',
|
||||
required: true,
|
||||
action: parsers.objectParser,
|
||||
default: [],
|
||||
},
|
||||
deleteExtraFields: {
|
||||
env: 'PARSE_SERVER_SCHEMA_DELETE_EXTRA_FIELDS',
|
||||
help:
|
||||
'Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
lockSchemas: {
|
||||
env: 'PARSE_SERVER_SCHEMA_LOCK_SCHEMAS',
|
||||
help:
|
||||
'Is true if Parse Server will reject any attempts to modify the schema while the server is running.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
recreateModifiedFields: {
|
||||
env: 'PARSE_SERVER_SCHEMA_RECREATE_MODIFIED_FIELDS',
|
||||
help:
|
||||
'Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
strict: {
|
||||
env: 'PARSE_SERVER_SCHEMA_STRICT',
|
||||
help: 'Is true if Parse Server should exit if schema update fail.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
module.exports.ParseServerOptions = {
|
||||
accountLockout: {
|
||||
env: 'PARSE_SERVER_ACCOUNT_LOCKOUT',
|
||||
@@ -50,7 +95,7 @@ module.exports.ParseServerOptions = {
|
||||
env: 'PARSE_SERVER_AUTH_PROVIDERS',
|
||||
help:
|
||||
'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication',
|
||||
action: parsers.objectParser,
|
||||
action: parsers.arrayParser,
|
||||
},
|
||||
cacheAdapter: {
|
||||
env: 'PARSE_SERVER_CACHE_ADAPTER',
|
||||
@@ -95,7 +140,8 @@ module.exports.ParseServerOptions = {
|
||||
},
|
||||
databaseAdapter: {
|
||||
env: 'PARSE_SERVER_DATABASE_ADAPTER',
|
||||
help: 'Adapter module for the database',
|
||||
help:
|
||||
'Adapter module for the database; any options that are not explicitly described here are passed directly to the database client.',
|
||||
action: parsers.moduleOrObjectParser,
|
||||
},
|
||||
databaseOptions: {
|
||||
@@ -109,6 +155,12 @@ module.exports.ParseServerOptions = {
|
||||
required: true,
|
||||
default: 'mongodb://localhost:27017/parse',
|
||||
},
|
||||
defaultLimit: {
|
||||
env: 'PARSE_SERVER_DEFAULT_LIMIT',
|
||||
help: 'Default value for limit option on queries, defaults to `100`.',
|
||||
action: parsers.numberParser('defaultLimit'),
|
||||
default: 100,
|
||||
},
|
||||
directAccess: {
|
||||
env: 'PARSE_SERVER_DIRECT_ACCESS',
|
||||
help:
|
||||
@@ -385,6 +437,11 @@ module.exports.ParseServerOptions = {
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
schema: {
|
||||
env: 'PARSE_SERVER_SCHEMA',
|
||||
help: 'Defined schema',
|
||||
action: parsers.objectParser,
|
||||
},
|
||||
security: {
|
||||
env: 'PARSE_SERVER_SECURITY',
|
||||
help: 'The security options to identify and report weak security settings.',
|
||||
@@ -420,6 +477,13 @@ module.exports.ParseServerOptions = {
|
||||
help: 'Starts the liveQuery server',
|
||||
action: parsers.booleanParser,
|
||||
},
|
||||
trustProxy: {
|
||||
env: 'PARSE_SERVER_TRUST_PROXY',
|
||||
help:
|
||||
'The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.',
|
||||
action: parsers.objectParser,
|
||||
default: [],
|
||||
},
|
||||
userSensitiveFields: {
|
||||
env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS',
|
||||
help:
|
||||
@@ -464,45 +528,6 @@ module.exports.SecurityOptions = {
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
module.exports.SchemaOptions = {
|
||||
definitions: {
|
||||
help: 'The schema definitions.',
|
||||
default: [],
|
||||
},
|
||||
strict: {
|
||||
env: 'PARSE_SERVER_SCHEMA_STRICT',
|
||||
help: 'Is true if Parse Server should exit if schema update fail.',
|
||||
action: parsers.booleanParser,
|
||||
default: true,
|
||||
},
|
||||
deleteExtraFields: {
|
||||
env: 'PARSE_SERVER_SCHEMA_DELETE_EXTRA_FIELDS',
|
||||
help:
|
||||
'Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
recreateModifiedFields: {
|
||||
env: 'PARSE_SERVER_SCHEMA_RECREATE_MODIFIED_FIELDS',
|
||||
help:
|
||||
'Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
lockSchemas: {
|
||||
env: 'PARSE_SERVER_SCHEMA_LOCK',
|
||||
help:
|
||||
'Is true if Parse Server will reject any attempts to modify the schema while the server is running.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
beforeMigration: {
|
||||
help: 'Execute a callback before running schema migrations.',
|
||||
},
|
||||
afterMigration: {
|
||||
help: 'Execute a callback after running schema migrations.',
|
||||
},
|
||||
};
|
||||
module.exports.PagesOptions = {
|
||||
customRoutes: {
|
||||
env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES',
|
||||
@@ -865,3 +890,10 @@ module.exports.DatabaseOptions = {
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
module.exports.AuthAdapter = {
|
||||
enabled: {
|
||||
help: 'Is `true` if the auth adapter is enabled, `false` otherwise.',
|
||||
action: parsers.booleanParser,
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
|
||||
+21
-2
@@ -1,3 +1,14 @@
|
||||
/**
|
||||
* @interface SchemaOptions
|
||||
* @property {Function} afterMigration Execute a callback after running schema migrations.
|
||||
* @property {Function} beforeMigration Execute a callback before running schema migrations.
|
||||
* @property {Any} definitions Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema
|
||||
* @property {Boolean} deleteExtraFields Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.
|
||||
* @property {Boolean} lockSchemas Is true if Parse Server will reject any attempts to modify the schema while the server is running.
|
||||
* @property {Boolean} recreateModifiedFields Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.
|
||||
* @property {Boolean} strict Is true if Parse Server should exit if schema update fail.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface ParseServerOptions
|
||||
* @property {AccountLockoutOptions} accountLockout The account lockout policy for failed login attempts.
|
||||
@@ -8,7 +19,7 @@
|
||||
* @property {Adapter<AnalyticsAdapter>} analyticsAdapter Adapter module for the analytics
|
||||
* @property {String} appId Your Parse Application ID
|
||||
* @property {String} appName Sets the app name
|
||||
* @property {Any} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
|
||||
* @property {AuthAdapter[]} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
|
||||
* @property {Adapter<CacheAdapter>} cacheAdapter Adapter module for the cache
|
||||
* @property {Number} cacheMaxSize Sets the maximum size for the in memory cache, defaults to 10000
|
||||
* @property {Number} cacheTTL Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)
|
||||
@@ -17,9 +28,10 @@
|
||||
* @property {Number|Boolean} cluster Run with cluster, optionally set the number of processes default to os.cpus().length
|
||||
* @property {String} collectionPrefix A collection prefix for the classes
|
||||
* @property {CustomPagesOptions} customPages custom pages for password validation and reset
|
||||
* @property {Adapter<StorageAdapter>} databaseAdapter Adapter module for the database
|
||||
* @property {Adapter<StorageAdapter>} databaseAdapter Adapter module for the database; any options that are not explicitly described here are passed directly to the database client.
|
||||
* @property {DatabaseOptions} databaseOptions Options to pass to the database client
|
||||
* @property {String} databaseURI The full URI to your database. Supported databases are mongodb or postgres.
|
||||
* @property {Number} defaultLimit Default value for limit option on queries, defaults to `100`.
|
||||
* @property {Boolean} directAccess Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.<br><br>If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.<br><br>⚠️ In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`.
|
||||
* @property {String} dotNetKey Key for Unity and .Net SDK
|
||||
* @property {Adapter<MailAdapter>} emailAdapter Adapter module for email sending
|
||||
@@ -68,6 +80,7 @@
|
||||
* @property {String} restAPIKey Key for REST calls
|
||||
* @property {Boolean} 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.
|
||||
* @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false.
|
||||
* @property {SchemaOptions} schema Defined schema
|
||||
* @property {SecurityOptions} security The security options to identify and report weak security settings.
|
||||
* @property {Function} serverCloseComplete Callback when server has closed
|
||||
* @property {Function} serverStartComplete Callback when server has started
|
||||
@@ -75,6 +88,7 @@
|
||||
* @property {Number} sessionLength Session duration, in seconds, defaults to 1 year
|
||||
* @property {Boolean} silent Disables console output
|
||||
* @property {Boolean} startLiveQueryServer Starts the liveQuery server
|
||||
* @property {Any} trustProxy The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
|
||||
* @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields
|
||||
* @property {Boolean} verbose Set the logging to verbose
|
||||
* @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process.<br><br>Default is `false`.
|
||||
@@ -196,3 +210,8 @@
|
||||
* @interface DatabaseOptions
|
||||
* @property {Boolean} enableSchemaHooks Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface AuthAdapter
|
||||
* @property {Boolean} enabled Is `true` if the auth adapter is enabled, `false` otherwise.
|
||||
*/
|
||||
|
||||
+41
-4
@@ -8,7 +8,28 @@ import { MailAdapter } from '../Adapters/Email/MailAdapter';
|
||||
import { PubSubAdapter } from '../Adapters/PubSub/PubSubAdapter';
|
||||
import { WSSAdapter } from '../Adapters/WebSocketServer/WSSAdapter';
|
||||
import { CheckGroup } from '../Security/CheckGroup';
|
||||
import type { SchemaOptions } from '../SchemaMigrations/Migrations';
|
||||
|
||||
export interface SchemaOptions {
|
||||
/* Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema
|
||||
:DEFAULT: [] */
|
||||
definitions: any;
|
||||
/* Is true if Parse Server should exit if schema update fail.
|
||||
:DEFAULT: false */
|
||||
strict: ?boolean;
|
||||
/* Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.
|
||||
:DEFAULT: false */
|
||||
deleteExtraFields: ?boolean;
|
||||
/* Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.
|
||||
:DEFAULT: false */
|
||||
recreateModifiedFields: ?boolean;
|
||||
/* Is true if Parse Server will reject any attempts to modify the schema while the server is running.
|
||||
:DEFAULT: false */
|
||||
lockSchemas: ?boolean;
|
||||
/* Execute a callback before running schema migrations. */
|
||||
beforeMigration: ?() => void | Promise<void>;
|
||||
/* Execute a callback after running schema migrations. */
|
||||
afterMigration: ?() => void | Promise<void>;
|
||||
}
|
||||
|
||||
type Adapter<T> = string | any | T;
|
||||
type NumberOrBoolean = number | boolean;
|
||||
@@ -71,7 +92,7 @@ export interface ParseServerOptions {
|
||||
/* Options to pass to the database client
|
||||
:ENV: PARSE_SERVER_DATABASE_OPTIONS */
|
||||
databaseOptions: ?DatabaseOptions;
|
||||
/* Adapter module for the database */
|
||||
/* Adapter module for the database; any options that are not explicitly described here are passed directly to the database client. */
|
||||
databaseAdapter: ?Adapter<StorageAdapter>;
|
||||
/* Full path to your cloud code main.js */
|
||||
cloud: ?string;
|
||||
@@ -119,7 +140,7 @@ export interface ParseServerOptions {
|
||||
allowCustomObjectId: ?boolean;
|
||||
/* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
|
||||
:ENV: PARSE_SERVER_AUTH_PROVIDERS */
|
||||
auth: ?any;
|
||||
auth: ?(AuthAdapter[]);
|
||||
/* Max file size for uploads, defaults to 20mb
|
||||
:DEFAULT: 20mb */
|
||||
maxUploadSize: ?string;
|
||||
@@ -173,6 +194,9 @@ export interface ParseServerOptions {
|
||||
/* Session duration, in seconds, defaults to 1 year
|
||||
:DEFAULT: 31536000 */
|
||||
sessionLength: ?number;
|
||||
/* Default value for limit option on queries, defaults to `100`.
|
||||
:DEFAULT: 100 */
|
||||
defaultLimit: ?number;
|
||||
/* Max value for limit option on queries, defaults to unlimited */
|
||||
maxLimit: ?number;
|
||||
/* Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.
|
||||
@@ -214,6 +238,9 @@ export interface ParseServerOptions {
|
||||
cluster: ?NumberOrBoolean;
|
||||
/* middleware for express server, can be string or function */
|
||||
middleware: ?((() => void) | string);
|
||||
/* The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
|
||||
:DEFAULT: false */
|
||||
trustProxy: ?any;
|
||||
/* Starts the liveQuery server */
|
||||
startLiveQueryServer: ?boolean;
|
||||
/* Live query server configuration options (will start the liveQuery server) */
|
||||
@@ -246,7 +273,9 @@ export interface ParseServerOptions {
|
||||
playgroundPath: ?string;
|
||||
/* Callback when server has started */
|
||||
serverStartComplete: ?(error: ?Error) => void;
|
||||
/* Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema */
|
||||
/* Defined schema
|
||||
:ENV: PARSE_SERVER_SCHEMA
|
||||
*/
|
||||
schema: ?SchemaOptions;
|
||||
/* Callback when server has closed */
|
||||
serverCloseComplete: ?() => void;
|
||||
@@ -483,3 +512,11 @@ export interface DatabaseOptions {
|
||||
:DEFAULT: false */
|
||||
enableSchemaHooks: ?boolean;
|
||||
}
|
||||
|
||||
export interface AuthAdapter {
|
||||
/* Is `true` if the auth adapter is enabled, `false` otherwise.
|
||||
:DEFAULT: true
|
||||
:ENV:
|
||||
*/
|
||||
enabled: ?boolean;
|
||||
}
|
||||
|
||||
@@ -304,6 +304,9 @@ class ParseServer {
|
||||
options
|
||||
);
|
||||
}
|
||||
if (options.trustProxy) {
|
||||
app.set('trust proxy', options.trustProxy);
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
if (!process.env.TESTING) {
|
||||
configureListeners(this);
|
||||
@@ -374,6 +377,16 @@ class ParseServer {
|
||||
|
||||
function addParseCloud() {
|
||||
const ParseCloud = require('./cloud-code/Parse.Cloud');
|
||||
Object.defineProperty(Parse, 'Server', {
|
||||
get() {
|
||||
return Config.get(Parse.applicationId);
|
||||
},
|
||||
set(newVal) {
|
||||
newVal.appId = Parse.applicationId;
|
||||
Config.put(newVal);
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
Object.assign(Parse.Cloud, ParseCloud);
|
||||
global.Parse = Parse;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const Config = require('./Config');
|
||||
const Auth = require('./Auth');
|
||||
const RESTController = require('parse/lib/node/RESTController');
|
||||
const URL = require('url');
|
||||
const Parse = require('parse/node');
|
||||
|
||||
function getSessionToken(options) {
|
||||
@@ -38,9 +37,9 @@ function ParseServerRESTController(applicationId, router) {
|
||||
if (!config) {
|
||||
config = Config.get(applicationId);
|
||||
}
|
||||
const serverURL = URL.parse(config.serverURL);
|
||||
if (path.indexOf(serverURL.path) === 0) {
|
||||
path = path.slice(serverURL.path.length, path.length);
|
||||
const serverURL = new URL(config.serverURL);
|
||||
if (path.indexOf(serverURL.pathname) === 0) {
|
||||
path = path.slice(serverURL.pathname.length, path.length);
|
||||
}
|
||||
|
||||
if (path[0] !== '/') {
|
||||
|
||||
@@ -202,6 +202,9 @@ RestQuery.prototype.execute = function (executeOptions) {
|
||||
.then(() => {
|
||||
return this.buildRestWhere();
|
||||
})
|
||||
.then(() => {
|
||||
return this.denyProtectedFields();
|
||||
})
|
||||
.then(() => {
|
||||
return this.handleIncludeAll();
|
||||
})
|
||||
@@ -688,6 +691,30 @@ RestQuery.prototype.runCount = function () {
|
||||
});
|
||||
};
|
||||
|
||||
RestQuery.prototype.denyProtectedFields = async function () {
|
||||
if (this.auth.isMaster) {
|
||||
return;
|
||||
}
|
||||
const schemaController = await this.config.database.loadSchema();
|
||||
const protectedFields =
|
||||
this.config.database.addProtectedFields(
|
||||
schemaController,
|
||||
this.className,
|
||||
this.restWhere,
|
||||
this.findOptions.acl,
|
||||
this.auth,
|
||||
this.findOptions
|
||||
) || [];
|
||||
for (const key of protectedFields) {
|
||||
if (this.restWhere[key]) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
`This user is not allowed to query ${key} on class ${this.className}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Augments this.response with all pointers on an object
|
||||
RestQuery.prototype.handleIncludeAll = function () {
|
||||
if (!this.includeAll) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user