mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d141b822ad | ||
|
|
be4c7e23c6 | ||
|
|
328918178f | ||
|
|
3dd99dd80e | ||
|
|
150627328f | ||
|
|
a318e7bbaf | ||
|
|
832702dffd | ||
|
|
18b63d1da7 | ||
|
|
f59d46c9e9 | ||
|
|
acba5cf16d | ||
|
|
2e4119e23d | ||
|
|
2f53c80298 | ||
|
|
4f15539ac2 | ||
|
|
9e43bc2fa0 | ||
|
|
177891ea0e | ||
|
|
65e5879e42 | ||
|
|
cf1b59e97b | ||
|
|
d9e347d741 | ||
|
|
c4ce59cbd1 | ||
|
|
34833e42ee | ||
|
|
0f1979f814 | ||
|
|
4ce135a4fe | ||
|
|
6613872ce2 | ||
|
|
ce34747e8a | ||
|
|
d05cfcdb95 | ||
|
|
c793bb88e7 | ||
|
|
87cab09b6a | ||
|
|
94d558edbb | ||
|
|
22d2446dfe | ||
|
|
3f5b2900a6 | ||
|
|
40c196153b | ||
|
|
656bca6132 | ||
|
|
29044aa9c3 | ||
|
|
33358a6234 | ||
|
|
a74b7c2f74 | ||
|
|
e9ab6b055d | ||
|
|
b3b76de71b | ||
|
|
bdca9f4ce3 | ||
|
|
e5d610e5e4 | ||
|
|
5477848518 | ||
|
|
a49e323d5a | ||
|
|
992b3c79f5 | ||
|
|
e76123b482 | ||
|
|
f5bfe4571e | ||
|
|
4450ecbc01 | ||
|
|
46e2760a46 | ||
|
|
8414ad3f32 | ||
|
|
70e6914173 | ||
|
|
f7eee19dba | ||
|
|
c99adfa771 | ||
|
|
301459d216 | ||
|
|
7a0949cf79 | ||
|
|
39a074fd0e | ||
|
|
ab181ad1d6 | ||
|
|
9ed42d7fb5 | ||
|
|
8b97988f0d | ||
|
|
aa2cd51b70 | ||
|
|
cf6966fa10 | ||
|
|
fe2b5e4b7c | ||
|
|
9092874a9a | ||
|
|
8adc054592 | ||
|
|
c114dc8831 | ||
|
|
8f7a8f4c9d | ||
|
|
f7f5e7e203 | ||
|
|
1c37a7cd07 | ||
|
|
5a35e6ff0d | ||
|
|
a7bd180cdd | ||
|
|
eb0311b570 | ||
|
|
fc92faac75 | ||
|
|
62b3426b14 | ||
|
|
40810b48eb | ||
|
|
bd82d8ede1 | ||
|
|
9d461df0c0 | ||
|
|
f3bcc9365c | ||
|
|
3d57072c1f | ||
|
|
ffa4974158 | ||
|
|
76c7a6fbd0 | ||
|
|
6c79f6a69e | ||
|
|
0eac5dc6d4 | ||
|
|
a4990dcd29 | ||
|
|
b1bb1fcfeb | ||
|
|
8895fbb82b | ||
|
|
d0d30c4f13 | ||
|
|
df00cbebe3 | ||
|
|
892040dc2f | ||
|
|
b7815ed80a | ||
|
|
d19acf1c1a | ||
|
|
ee7246733d | ||
|
|
49388df693 | ||
|
|
99fcf45e55 | ||
|
|
db9941c5a6 | ||
|
|
1412666f75 | ||
|
|
6323368d3f | ||
|
|
65c2d2ced2 | ||
|
|
5a26426f23 | ||
|
|
65a6dd180e | ||
|
|
e22b73d4b7 | ||
|
|
679c61db63 | ||
|
|
fded5be186 | ||
|
|
8f3b694e39 | ||
|
|
0a8670dc22 | ||
|
|
b48c27049a | ||
|
|
536bb2f1ae | ||
|
|
b458dcec91 | ||
|
|
b2761fb378 | ||
|
|
40dd82ff19 | ||
|
|
e32cc7db93 | ||
|
|
ab82635b0d | ||
|
|
6f527440c2 | ||
|
|
c312e261b6 | ||
|
|
fe3dc0d14d | ||
|
|
9b34b0263b | ||
|
|
e373f09040 | ||
|
|
a9a9772b05 | ||
|
|
c8d8e9fbad | ||
|
|
f29d9720e9 | ||
|
|
4b1d46fb09 | ||
|
|
6e52ec7558 | ||
|
|
dd45eb2a11 | ||
|
|
ed499e32a2 | ||
|
|
6e66b20e28 | ||
|
|
6c16021a1f | ||
|
|
7336afc562 | ||
|
|
7d622f06a4 | ||
|
|
9af9115f9d | ||
|
|
c8227cd140 | ||
|
|
37245f62ce | ||
|
|
ccb14970cb | ||
|
|
ebea057e5b | ||
|
|
130d29074e | ||
|
|
9d3c1c6918 | ||
|
|
f535ee6ec2 | ||
|
|
2054a88f19 | ||
|
|
2d79c0835b | ||
|
|
739a3a9957 | ||
|
|
5bbf9cade9 | ||
|
|
4eb5f28b04 | ||
|
|
ed3248f9c6 | ||
|
|
2546cc8572 | ||
|
|
7cb266b207 | ||
|
|
735669a86a | ||
|
|
fd8a11bc27 | ||
|
|
60c5a73d25 | ||
|
|
3e983c41ff | ||
|
|
d27dfa3464 | ||
|
|
d9c3c02e7d | ||
|
|
6728da1e35 | ||
|
|
43194b1bbe | ||
|
|
a8cb2a4ef1 | ||
|
|
46dbecdec1 | ||
|
|
42581225f1 | ||
|
|
2458a8c58d | ||
|
|
50eed3cffe | ||
|
|
81304be649 | ||
|
|
e90a5183ec | ||
|
|
963613ec3f | ||
|
|
70ada3e522 | ||
|
|
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 | ||
|
|
37fed3062c | ||
|
|
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 | ||
|
|
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 | ||
|
|
e8eb546c90 | ||
|
|
0fd600cf24 | ||
|
|
35cd6910de | ||
|
|
7844442840 | ||
|
|
42c9543189 | ||
|
|
86832b9b95 | ||
|
|
e26beb1f5c | ||
|
|
4c9e95674a | ||
|
|
1a04a347cf | ||
|
|
75af9a26cc | ||
|
|
4c2aa63fd2 | ||
|
|
4a1039679c | ||
|
|
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 |
@@ -6,8 +6,9 @@
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": "12"
|
||||
}
|
||||
"node": "14",
|
||||
},
|
||||
"exclude": ["proposal-dynamic-import"]
|
||||
}]
|
||||
],
|
||||
"sourceMaps": "inline"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
*.md
|
||||
PATENTS
|
||||
LICENSE
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.gitignore
|
||||
|
||||
+6
-2
@@ -5,13 +5,14 @@
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parser": "@babel/eslint-parser",
|
||||
"plugins": [
|
||||
"flowtype"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
"sourceType": "module",
|
||||
"requireConfigFile": false
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
@@ -24,5 +25,8 @@
|
||||
"space-infix-ops": "error",
|
||||
"no-useless-escape": "off",
|
||||
"require-atomic-updates": "off"
|
||||
},
|
||||
"globals": {
|
||||
"Parse": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,3 +8,4 @@
|
||||
|
||||
[options]
|
||||
suppress_comment= \\(.\\|\n\\)*\\@flow-disable-next
|
||||
esproposal.optional_chaining=enable
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
### New Pull Request Checklist
|
||||
<!--
|
||||
Please check the following boxes [x] before submitting your issue.
|
||||
Click the "Preview" tab for better readability.
|
||||
Thanks for contributing to Parse Server!
|
||||
-->
|
||||
## Pull Request
|
||||
|
||||
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
|
||||
- [ ] I am creating this PR in reference to an [issue](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
- Report security issues [confidentially](https://github.com/parse-community/parse-server/security/policy).
|
||||
- Any contribution is under this [license](https://github.com/parse-community/parse-server/blob/alpha/LICENSE).
|
||||
- Link this pull request to an [issue](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
|
||||
### Issue Description
|
||||
<!-- Add a brief description of the issue this PR solves. -->
|
||||
## Issue
|
||||
<!-- Add the link to the issue that this PR closes. -->
|
||||
|
||||
Related issue: FILL_THIS_OUT
|
||||
Closes: FILL_THIS_OUT
|
||||
|
||||
### Approach
|
||||
<!-- Add a description of the approach in this PR. -->
|
||||
## Approach
|
||||
<!-- Describe the changes in this PR. -->
|
||||
|
||||
### TODOs before merging
|
||||
<!--
|
||||
Add TODOs that need to be completed before merging this PR.
|
||||
Delete suggested TODOs that do not apply to this PR.
|
||||
-->
|
||||
## Tasks
|
||||
<!-- Delete tasks that don't apply. -->
|
||||
|
||||
- [ ] Add tests
|
||||
- [ ] Add changes to documentation (guides, repository pages, in-code descriptions)
|
||||
- [ ] Add changes to documentation (guides, repository pages, code comments)
|
||||
- [ ] Add [security check](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md#security-checks)
|
||||
- [ ] Add new Parse Error codes to Parse JS SDK <!-- no hard-coded error codes in Parse Server -->
|
||||
- [x] A changelog entry is created automatically using the pull request title (do not manually add a changelog entry)
|
||||
|
||||
+103
-55
@@ -1,18 +1,39 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [ release, alpha, beta, next-major ]
|
||||
branches: [ release*, alpha, beta ]
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
env:
|
||||
NODE_VERSION: 16.13.0
|
||||
NODE_VERSION: 19.3.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-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
@@ -33,7 +54,7 @@ jobs:
|
||||
check-lint:
|
||||
name: Lint
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
@@ -50,10 +71,31 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: npm run lint
|
||||
check-definitions:
|
||||
name: Check Definitions
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: CI Definitions Check
|
||||
run: npm run ci:definitionsCheck
|
||||
check-circular:
|
||||
name: Circular Dependencies
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
@@ -73,7 +115,7 @@ jobs:
|
||||
check-docker:
|
||||
name: Docker Build
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
@@ -90,74 +132,71 @@ jobs:
|
||||
check-lock-file-version:
|
||||
name: NPM Lock File Version
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Check NPM lock file version
|
||||
uses: mansona/npm-lockfile-version@v1
|
||||
with:
|
||||
version: 1
|
||||
version: 2
|
||||
check-mongo:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: MongoDB 5.0, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 5.0.3
|
||||
- name: MongoDB 4.2, ReplicaSet
|
||||
MONGODB_VERSION: 4.2.19
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: MongoDB 4.4, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.4.10
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: MongoDB 4.4, ReplicaSet
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: MongoDB 4.2, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.2.17
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: MongoDB 5, ReplicaSet
|
||||
MONGODB_VERSION: 5.3.2
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: MongoDB 4.0, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.0.27
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: MongoDB 6, ReplicaSet
|
||||
MONGODB_VERSION: 6.0.2
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: MongoDB 4.0, Standalone, MMAPv1
|
||||
MONGODB_VERSION: 4.0.27
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: mmapv1
|
||||
NODE_VERSION: 16.13.0
|
||||
NODE_VERSION: 19.3.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: 16.13.0
|
||||
- name: Node 12
|
||||
MONGODB_VERSION: 4.4.10
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 12.22.7
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: Node 14
|
||||
MONGODB_VERSION: 4.4.10
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 14.18.1
|
||||
NODE_VERSION: 14.21.1
|
||||
- name: Node 16
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 16.18.1
|
||||
- name: Node 18
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 18.12.1
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
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"
|
||||
- name: Fix git protocol for Node 14
|
||||
if: ${{ startsWith(matrix.NODE_VERSION, '14.') }}
|
||||
run: sudo git config --system url."https://github".insteadOf "ssh://git@github"
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
@@ -183,26 +222,32 @@ jobs:
|
||||
include:
|
||||
- name: PostgreSQL 11, PostGIS 3.0
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.0
|
||||
NODE_VERSION: 16.13.0
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 11, PostGIS 3.1
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.1
|
||||
NODE_VERSION: 16.13.0
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 11, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.2
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: PostgreSQL 12, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:12-3.2
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: PostgreSQL 13, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.2
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: PostgreSQL 14, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:14-3.2
|
||||
NODE_VERSION: 16.13.0
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 11, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 12, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:12-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 13, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 14, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:14-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 15, PostGIS 3.3
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.3
|
||||
NODE_VERSION: 19.3.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
@@ -245,3 +290,6 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
# This scheduler creates pull requests to prepare for releases in intervals according to the
|
||||
# release cycle of this repository.
|
||||
|
||||
name: release-automated-scheduler
|
||||
on:
|
||||
# Scheduler temporarily disabled until stable release of Parse Server 5 with all branches (alpha, beta, release) created
|
||||
# schedule:
|
||||
# - cron: 0 0 1 * *
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
create-pr-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout beta branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: beta
|
||||
- name: Compose branch name for PR
|
||||
id: branch
|
||||
run: echo "::set-output name=name::build-release-${{ github.run_id }}${{ github.run_number }}"
|
||||
- name: Create branch
|
||||
run: |
|
||||
git config --global user.email ${{ github.actor }}@users.noreply.github.com
|
||||
git config --global user.name ${{ github.actor }}
|
||||
git checkout -b ${{ steps.branch.outputs.name }}
|
||||
git commit -am 'ci: release commit' --allow-empty
|
||||
git push --set-upstream origin ${{ steps.branch.outputs.name }}
|
||||
- name: Create PR
|
||||
uses: k3rnels-actions/pr-update@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr_title: "build: release"
|
||||
pr_source: ${{ steps.branch.outputs.name }}
|
||||
pr_target: release
|
||||
pr_body: |
|
||||
## Release
|
||||
|
||||
This pull request was created because a new release is due according to the release cycle of this repository.
|
||||
Just resolve any conflicts and it's good to merge. Any version increment will be done by release automation.
|
||||
|
||||
*⚠️ Use `Merge commit` to merge this pull request. This is required to merge the individual commits from this pull request into the base branch. Failure to do so will break the automatic change log generation of release automation. Do not use "Squash and merge"!*
|
||||
create-pr-beta:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-pr-release
|
||||
steps:
|
||||
- name: Checkout alpha branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: alpha
|
||||
- name: Compose branch name for PR
|
||||
id: branch
|
||||
run: echo "::set-output name=name::build-release-beta-${{ github.run_id }}${{ github.run_number }}"
|
||||
- name: Create branch
|
||||
run: |
|
||||
git config --global user.email ${{ github.actor }}@users.noreply.github.com
|
||||
git config --global user.name ${{ github.actor }}
|
||||
git checkout -b ${{ steps.branch.outputs.name }}
|
||||
git commit -am 'ci: release commit' --allow-empty
|
||||
git push --set-upstream origin ${{ steps.branch.outputs.name }}
|
||||
- name: Create PR
|
||||
uses: k3rnels-actions/pr-update@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr_title: "build: release beta"
|
||||
pr_source: ${{ steps.branch.outputs.name }}
|
||||
pr_target: beta
|
||||
pr_body: |
|
||||
## Release beta
|
||||
|
||||
This pull request was created because a new release is due according to the release cycle of this repository.
|
||||
Just resolve any conflicts and it's good to merge. Any version increment will be done by release automation.
|
||||
|
||||
*⚠️ Use `Merge commit` to merge this pull request. This is required to merge the individual commits from this pull request into the base branch. Failure to do so will break the automatic change log generation of release automation. Do not use "Squash and merge"!*
|
||||
@@ -1,7 +1,7 @@
|
||||
name: release-automated
|
||||
on:
|
||||
push:
|
||||
branches: [ release, alpha, beta, next-major ]
|
||||
branches: [ release, alpha, beta, next-major, 'release-[0-9]+.x.x' ]
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 18.1.0
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
env:
|
||||
REGISTRY: docker.io
|
||||
IMAGE_NAME: parseplatform/parse-server
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -86,14 +86,14 @@ jobs:
|
||||
docs:
|
||||
needs: release
|
||||
if: needs.release.outputs.current_tag != '' && github.ref == 'refs/heads/release'
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 18.1.0
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
|
||||
@@ -14,7 +14,7 @@ env:
|
||||
IMAGE_NAME: parseplatform/parse-server
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
# Upgrading Parse Server to version 2.3.0
|
||||
|
||||
Parse Server version 2.3.0 begins using unique indexes to ensure the User's username and email are unique. This is not a backwards incompatible change, but it may in some cases cause a significant performance regression until the index finishes building. Building the unique index before upgrading your Parse Server version will eliminate the performance impact, and is a recommended step before upgrading any app to Parse Server 2.3.0. New apps starting with version 2.3.0 do not need to take any steps before beginning their project.
|
||||
|
||||
If you are using MongoDB in Cluster or Replica Set mode, we recommend reading Mongo's [documentation on index building](https://docs.mongodb.com/v3.0/tutorial/build-indexes-on-replica-sets/) first. If you are not using these features, you can execute the following commands from the Mongo shell to build the unique index. You may also want to create a backup first.
|
||||
|
||||
```js
|
||||
// Select the database that your Parse App uses
|
||||
use parse;
|
||||
|
||||
// Select the collection your Parse App uses for users. For migrated apps, this probably includes a collectionPrefix.
|
||||
var coll = db['your_prefix:_User'];
|
||||
|
||||
// You can check if the indexes already exists by running coll.getIndexes()
|
||||
coll.getIndexes();
|
||||
|
||||
// The indexes you want should look like this. If they already exist, you can skip creating them.
|
||||
{
|
||||
"v" : 1,
|
||||
"unique" : true,
|
||||
"key" : {
|
||||
"username" : 1
|
||||
},
|
||||
"name" : "username_1",
|
||||
"ns" : "parse.your_prefix:_User",
|
||||
"background" : true,
|
||||
"sparse" : true
|
||||
}
|
||||
|
||||
{
|
||||
"v" : 1,
|
||||
"unique" : true,
|
||||
"key" : {
|
||||
"email" : 1
|
||||
},
|
||||
"name" : "email_1",
|
||||
"ns" : "parse.your_prefix:_User",
|
||||
"background" : true,
|
||||
"sparse" : true
|
||||
}
|
||||
|
||||
// Create the username index.
|
||||
// "background: true" is mandatory and avoids downtime while the index builds.
|
||||
// "sparse: true" is also mandatory because Parse Server uses sparse indexes.
|
||||
coll.ensureIndex({ username: 1 }, { background: true, unique: true, sparse: true });
|
||||
|
||||
// Create the email index.
|
||||
// "background: true" is still mandatory.
|
||||
// "sparse: true" is also mandatory both because Parse Server uses sparse indexes, and because email addresses are not required by the Parse API.
|
||||
coll.ensureIndex({ email: 1 }, { background: true, unique: true, sparse: true });
|
||||
```
|
||||
|
||||
There are some issues you may run into during this process:
|
||||
|
||||
## Mongo complains that the index already exists, but with different options
|
||||
|
||||
In this case, you will need to remove the incorrect index. If your app relies on the existence of the index in order to be performant, you can create a new index, with "-1" for the direction of the field, so that it counts as different options. Then, drop the conflicting index, and create the unique index.
|
||||
|
||||
## There is already non-unique data in the username or email field
|
||||
|
||||
This is possible if you have explicitly set some user's emails to null. If this is bogus data, and those null fields should be unset, you can unset the null emails with this command. If your app relies on the difference between null and unset emails, you will need to upgrade your app to treat null and unset emails the same before building the index and upgrading to Parse Server 2.3.0.
|
||||
|
||||
```js
|
||||
coll.update({ email: { $exists: true, $eq: null } }, { $unset: { email: '' } }, { multi: true })
|
||||
```
|
||||
|
||||
## There is already non-unique data in the username or email field, and it's not nulls
|
||||
|
||||
This is possible due to a race condition in previous versions of Parse Server. If you have this problem, it is unlikely that you have a lot of rows with duplicate data. We recommend you clean up the data manually, by removing or modifying the offending rows.
|
||||
|
||||
This command, can be used to find the duplicate data:
|
||||
|
||||
```js
|
||||
coll.aggregate([
|
||||
{$match: {"username": {"$ne": null}}},
|
||||
{$group: {_id: "$username", uniqueIds: {$addToSet: "$_id"}, count: {$sum: 1}}},
|
||||
{$match: {count: {"$gt": 1}}},
|
||||
{$project: {id: "$uniqueIds", username: "$_id", _id : 0} },
|
||||
{$unwind: "$id" },
|
||||
{$out: '_duplicates'} // Save the list of duplicates to a new, "_duplicates" collection. Remove this line to just output the list.
|
||||
], {allowDiskUse:true})
|
||||
```
|
||||
@@ -1,226 +0,0 @@
|
||||
# Upgrading Parse Server to version 3.0.0
|
||||
|
||||
parse-server 3.0.0 comes also with the JS SDK version 2.0 which requires a migration. Follow the migration guide [here](https://github.com/parse-community/Parse-SDK-JS/blob/master/2.0.0.md).
|
||||
|
||||
With the 3.0.0, parse-server has completely revamped it's cloud code interface. Gone are the backbone style calls, welcome promises and async/await.
|
||||
|
||||
In order to leverage those new nodejs features, you'll need to run at least node 8.10. We've dropped the support of node 6 a few months ago, so if you haven't made the change yet, now would be a really good time to take the plunge.
|
||||
|
||||
## Migrating Cloud Code Hooks
|
||||
|
||||
### Synchronous validations:
|
||||
|
||||
```js
|
||||
// before
|
||||
Parse.Cloud.beforeSave('MyClassName', function(request, response) {
|
||||
if (!passesValidation(request.object)) {
|
||||
response.error('Ooops something went wrong');
|
||||
} else {
|
||||
response.success();
|
||||
}
|
||||
});
|
||||
|
||||
// after
|
||||
Parse.Cloud.beforeSave('MyClassName', (request) => {
|
||||
if (!passesValidation(request.object)) {
|
||||
throw 'Ooops something went wrong';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
All methods are wrapped in promises, so you can freely `throw` `Error`, `Parse.Error` or `string` to mark an error.
|
||||
|
||||
### Asynchronous validations:
|
||||
|
||||
For asynchronous code, you can use promises or async / await.
|
||||
|
||||
Consider the following beforeSave call that would replace the contents of a fileURL with a proper copy of the image as a Parse.File.
|
||||
|
||||
```js
|
||||
// before
|
||||
Parse.Cloud.beforeSave('Post', function(request, response) {
|
||||
Parse.Cloud.httpRequest({
|
||||
url: request.object.get('fileURL'),
|
||||
success: function(contents) {
|
||||
const file = new Parse.File('image.png', { base64: contents.buffer.toString('base64') });
|
||||
file.save({
|
||||
success: function() {
|
||||
request.object.set('file', file);
|
||||
response.success();
|
||||
},
|
||||
error: response.error
|
||||
});
|
||||
},
|
||||
error: response.error
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
As we can see the current way, with backbone style callbacks is quite tough to read and maintain.
|
||||
It's also not really trivial to handle errors, as you need to pass the response.error to each error handlers.
|
||||
|
||||
Now it can be modernized with promises:
|
||||
|
||||
```js
|
||||
// after (with promises)
|
||||
Parse.Cloud.beforeSave('MyClassName', (request) => {
|
||||
return Parse.Cloud.httpRequest({
|
||||
url: request.object.get('fileURL')
|
||||
}).then((contents) => {
|
||||
const file = new Parse.File('image.png', { base64: contents.buffer.toString('base64') });
|
||||
request.object.set('file', file);
|
||||
return file.save();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
And you can even make it better with async/await.
|
||||
|
||||
```js
|
||||
// after with async/await
|
||||
Parse.Cloud.beforeSave('MyClassName', async (request) => {
|
||||
const contents = await Parse.Cloud.httpRequest({
|
||||
url: request.object.get('fileURL')
|
||||
});
|
||||
|
||||
const file = new Parse.File('image.png', { base64: contents.buffer.toString('base64') });
|
||||
request.object.set('file', file);
|
||||
await file.save();
|
||||
});
|
||||
```
|
||||
|
||||
## Aborting hooks and functions
|
||||
|
||||
In order to abort a `beforeSave` or any hook, you now need to throw an error:
|
||||
|
||||
```js
|
||||
// after with async/await
|
||||
Parse.Cloud.beforeSave('MyClassName', async (request) => {
|
||||
// valid, will result in a Parse.Error(SCRIPT_FAILED, 'Something went wrong')
|
||||
throw 'Something went wrong'
|
||||
// also valid, will fail with Parse.Error(SCRIPT_FAILED, 'Something else went wrong')
|
||||
throw new Error('Something else went wrong')
|
||||
// using a Parse.Error is also valid
|
||||
throw new Parse.Error(1001, 'My error')
|
||||
});
|
||||
```
|
||||
|
||||
## Migrating Functions
|
||||
|
||||
Cloud Functions can be migrated the same way as cloud code hooks.
|
||||
In functions, the response object is not passed anymore, as it is in cloud code hooks
|
||||
|
||||
Continuing with the image downloader example:
|
||||
|
||||
```js
|
||||
// before
|
||||
Parse.Cloud.define('downloadImage', function(request, response) {
|
||||
Parse.Cloud.httpRequest({
|
||||
url: request.params.url,
|
||||
success: function(contents) {
|
||||
const file = new Parse.File(request.params.name, { base64: contents.buffer.toString('base64') });
|
||||
file.save({
|
||||
success: function() {
|
||||
response.success(file);
|
||||
},
|
||||
error: response.error
|
||||
});
|
||||
},
|
||||
error: response.error
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
You would call this method with:
|
||||
|
||||
```js
|
||||
Parse.Cloud.run('downloadImage',{
|
||||
url: 'https://example.com/file',
|
||||
name: 'my-file'
|
||||
});
|
||||
```
|
||||
|
||||
To migrate this function you would follow the same practices as the ones before, and we'll jump right away to the async/await implementation:
|
||||
|
||||
```js
|
||||
// after with async/await
|
||||
Parse.Cloud.define('downloadImage', async (request) => {
|
||||
const {
|
||||
url, name
|
||||
} = request.params;
|
||||
const response = await Parse.Cloud.httpRequest({ url });
|
||||
|
||||
const file = new Parse.File(name, { base64: response.buffer.toString('base64') });
|
||||
await file.save();
|
||||
return file;
|
||||
});
|
||||
```
|
||||
|
||||
## Migrating jobs
|
||||
|
||||
As with hooks and functions, jobs don't have a status object anymore.
|
||||
The message method has been moved to the request object.
|
||||
|
||||
```js
|
||||
// before
|
||||
Parse.Cloud.job('downloadImageJob', function(request, status) {
|
||||
var query = new Parse.Query('ImagesToDownload');
|
||||
query.find({
|
||||
success: function(images) {
|
||||
var done = 0;
|
||||
for (var i = 0; i<images.length; i++) {
|
||||
var image = images[i];
|
||||
status.message('Doing '+image.get('url'));
|
||||
Parse.Cloud.httpRequest({
|
||||
url: image.get('url'),
|
||||
success: function(contents) {
|
||||
status.message('Got '+image.get('url'));
|
||||
const file = new Parse.File(image.get('name'), { base64: contents.buffer.toString('base64') });
|
||||
file.save({
|
||||
success: function() {
|
||||
status.message('Saved '+image.get('url'));
|
||||
done++;
|
||||
if (done == images.length) {
|
||||
status.success();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
response.message('Error '+image.get('url'));
|
||||
done++;
|
||||
if (done == images.length) {
|
||||
status.error();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
error: status.error
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
Parse.Cloud.job('downloadImageJob', async function(request) {
|
||||
const query = new Parse.Query('ImagesToDownload');
|
||||
const images = await query.find();
|
||||
const promises = images.map(async (image) => {
|
||||
request.message('Doing ' + image.get('url'));
|
||||
const contents = await Parse.Cloud.httpRequest({
|
||||
url: image.get('url')
|
||||
});
|
||||
request.message('Got ' + image.get('url'));
|
||||
const file = new Parse.File(image.get('name'), { base64: contents.buffer.toString('base64') });
|
||||
await file.save();
|
||||
request.message('Saved ' + image.get('url'));
|
||||
});
|
||||
await Promise.all(promises);
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
As you can see the new implementation is more concise, easier to read and maintain.
|
||||
|
||||
If you encounter a problem or discover an issue with this guide, or with any Parse Community project, feel free to [reach out on github](https://github.com/parse-community/parse-server/issues/new/choose)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Parse Server 6 Migration Guide <!-- omit in toc -->
|
||||
|
||||
This document only highlights specific changes that require a longer explanation. For a full list of changes in Parse Server 6 please refer to the [changelog](https://github.com/parse-community/parse-server/blob/alpha/CHANGELOG.md).
|
||||
|
||||
---
|
||||
|
||||
- [Incompatible git protocol with Node 14](#incompatible-git-protocol-with-node-14)
|
||||
- [Import Statement](#import-statement)
|
||||
- [Asynchronous Initialization](#asynchronous-initialization)
|
||||
|
||||
---
|
||||
|
||||
## Incompatible git protocol with Node 14
|
||||
|
||||
Parse Server 6 uses the Node Package Manger (npm) package lock file version 2. While version 2 is supposed to be backwards compatible with version 1, you may still encounter errors due to incompatible git protocols that cannot be interpreted correctly by npm bundled with Node 14.
|
||||
|
||||
If you are encountering issues installing Parse Server on Node 14 because of dependency references in the package lock file using the `ssh` protocol, configure git to use the `https` protocol instead:
|
||||
|
||||
```
|
||||
sudo git config --system url."https://github".insteadOf "ssh://git@github"
|
||||
```
|
||||
|
||||
Alternatively you could manually replace the dependency URLs in the package lock file.
|
||||
|
||||
⚠️ You could also delete the package lock file and recreate it with Node 14. Keep in mind that doing so you are not using an official version of Parse Server anymore. You may be using dependencies that have not been tested as part of the Parse Server release process.
|
||||
|
||||
## Import Statement
|
||||
|
||||
The import and initialization syntax has been simplified with more intuitive naming and structure.
|
||||
|
||||
*Parse Server 5:*
|
||||
```js
|
||||
// Returns a Parse Server instance
|
||||
const ParseServer = require('parse-server');
|
||||
|
||||
// Returns a Parse Server express middleware
|
||||
const { ParseServer } = require('parse-server');
|
||||
```
|
||||
|
||||
*Parse Server 6:*
|
||||
```js
|
||||
// Both return a Parse Server instance
|
||||
const ParseServer = require('parse-server');
|
||||
const { ParseServer } = require('parse-server');
|
||||
```
|
||||
|
||||
To get the express middleware in Parse Server 6, configure the Parse Server instance, start Parse Server and use its `app` property. See [Asynchronous Initialization](#asynchronous-initialization) for more details.
|
||||
|
||||
## Asynchronous Initialization
|
||||
|
||||
Previously, it was possible to mount Parse Server before it was fully started up and ready to receive requests. This could result in undefined behavior, such as Parse Objects could be saved before Cloud Code was registered. To prevent this, Parse Server 6 requires to be started asynchronously before being mounted.
|
||||
|
||||
*Parse Server 5:*
|
||||
```js
|
||||
// 1. Import Parse Server
|
||||
const { ParseServer } = require('parse-server');
|
||||
|
||||
// 2. Create a Parse Server instance as express middleware
|
||||
const server = new ParseServer(config);
|
||||
|
||||
// 3. Mount express middleware
|
||||
app.use("/parse", server);
|
||||
```
|
||||
|
||||
*Parse Server 6:*
|
||||
```js
|
||||
// 1. Import Parse Server
|
||||
const ParseServer = require('parse-server');
|
||||
|
||||
// 2. Create a Parse Server instance
|
||||
const server = new ParseServer(config);
|
||||
|
||||
// 3. Start up Parse Server asynchronously
|
||||
await server.start();
|
||||
|
||||
// 4. Mount express middleware
|
||||
app.use("/parse", server.app);
|
||||
```
|
||||
+148
-4
@@ -2,7 +2,15 @@
|
||||
|
||||
## Table of Contents <!-- omit in toc -->
|
||||
- [Contributing](#contributing)
|
||||
- [Issue vs. Pull Request](#issue-vs-pull-request)
|
||||
- [Scope](#scope)
|
||||
- [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,13 +28,19 @@
|
||||
- [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)
|
||||
- [Security Vulnerability](#security-vulnerability)
|
||||
- [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)
|
||||
|
||||
@@ -44,6 +58,30 @@ 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.
|
||||
|
||||
### Scope
|
||||
|
||||
An issue and pull request must limit its scope on a distinct issue. Pull requests can only contain changes that are required to address the scoped issue. While it may seem quick and easy to add unrelated changes to the pull request, it can cause singificant complications after merging. Some of the reasons are:
|
||||
|
||||
- A pull request corresponds to a single changelog entry. A changelog entry should not describe multiple unrelated changes in one entry for better readability.
|
||||
- A pull request creates a distinct commit; having an individual commit for each limited scope makes it easier for others to go back in the commit history and debug. Bugs are generally more difficult to identify and fix if there are various unrelated changes merged at once.
|
||||
- If a pull request needs to be reverted, unrelated changes will be reverted as well. That makes it more complex and time consuming to revert, having to consider its effects and possibly publishing a broken release or requiring a follow-up pull request with code manipulation.
|
||||
|
||||
### 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.
|
||||
@@ -65,6 +103,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
|
||||
@@ -301,6 +379,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:
|
||||
|
||||
```
|
||||
@@ -381,24 +461,88 @@ If the commit reverts a previous commit, use the prefix `revert:`, followed by t
|
||||
This reverts commit 1234567890abcdef.
|
||||
```
|
||||
|
||||
### Security Vulnerability
|
||||
|
||||
#### Local Testing
|
||||
|
||||
Fixes for securify vulnerabilities are developed in private forks with a closed audience, inaccessible to the public. A current GitHub limitation does not allow to run CI tests on pull requests in private forks. Whether a pull requests fully passes all CI tests can only be determined by publishing the fix as a public pull request and running the CI. This means the fix and implicitly information about the vulnerabilty are made accessible to the public. This increases the risk that a vulnerability fix is published, but then cannot be merged immediately due to a CI issue. To mitigate that risk, before publishing a vulnerability fix, the following tests needs to be run locally and pass:
|
||||
|
||||
- `npm run test` (MongoDB)
|
||||
- `npm run test` (Postgres)
|
||||
- `npm run madge:circular` (circular dependencies)
|
||||
- `npm run lint` (Lint)
|
||||
- `npm run definitions` (Parse Server options definitions)
|
||||
|
||||
#### Merging
|
||||
|
||||
A current GitHub limitation does not allow to customize the commit message when merging pull requests of a private fork that was created to fix a security vulnerabilty. Our release automation framework demands a specific commit message syntax which therefore cannot be met. This prohibits to follow the process that GitHub suggest, which is to merge a pull request from a private fork directly to a public branch. Instead, after [local testing](#local-testing), a public pull request needs to be created with the code fix copied over from the private pull request.
|
||||
|
||||
This creates a risk that a vulnerability is indirectly disclosed by publishing a pull request with the fix, but the fix cannot be merged due to a CI issue. To mitigate that risk, the pull request title and description should be kept marginal or generic, not hiting to a vulnerabilty or giving any details about the vulnerabilty, until the pull request has been successfully merged.
|
||||
|
||||
## 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-devependencies of a dependency and increase the chances of vulnerabilities. The file should be recreated once every release cycle which is usually monthly.
|
||||
- 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
|
||||
|
||||
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.
|
||||
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
|
||||
### 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
|
||||
|
||||
### 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.
|
||||
|
||||
+9
-4
@@ -4,10 +4,15 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
|
||||
|
||||
| ID | Change | Issue | Deprecation [ℹ️][i_deprecation] | Planned Removal [ℹ️][i_removal] | Status [ℹ️][i_status] | Notes |
|
||||
|--------|-------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------|
|
||||
| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
|
||||
| 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 | - |
|
||||
| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - |
|
||||
| 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) | removed | - |
|
||||
| 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) | removed | - |
|
||||
| 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) | removed | - |
|
||||
| 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 | - |
|
||||
| DEPPS8 | Login with expired 3rd party authentication token defaults to `false` | [#7079](https://github.com/parse-community/parse-server/pull/7079) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
|
||||
| DEPPS9 | Rename LiveQuery `fields` option to `keys` | [#8389](https://github.com/parse-community/parse-server/issues/8389) | 6.0.0 (2023) | 7.0.0 (2024) | deprecated | - |
|
||||
|
||||
[i_deprecation]: ## "The version and date of the deprecation."
|
||||
[i_removal]: ## "The version and date of the planned removal."
|
||||
|
||||
+15
-21
@@ -1,10 +1,9 @@
|
||||
############################################################
|
||||
# Build stage
|
||||
############################################################
|
||||
FROM node:lts-alpine as build
|
||||
FROM node:lts-alpine AS build
|
||||
|
||||
RUN apk update; \
|
||||
apk add git;
|
||||
RUN apk --no-cache add git
|
||||
WORKDIR /tmp
|
||||
|
||||
# Copy package.json first to benefit from layer caching
|
||||
@@ -13,37 +12,32 @@ COPY package*.json ./
|
||||
# Copy src to have config files for install
|
||||
COPY . .
|
||||
|
||||
# Clean npm cache; added to fix an issue with the install process
|
||||
RUN npm cache clean --force
|
||||
|
||||
# Install all dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Run build steps
|
||||
RUN npm run build
|
||||
# Install without scripts
|
||||
RUN npm ci --omit=dev --ignore-scripts \
|
||||
# Copy production node_modules aside for later
|
||||
&& cp -R node_modules prod_node_modules \
|
||||
# Install all dependencies
|
||||
&& npm ci \
|
||||
# Run build steps
|
||||
&& npm run build
|
||||
|
||||
############################################################
|
||||
# Release stage
|
||||
############################################################
|
||||
FROM node:lts-alpine as release
|
||||
|
||||
RUN apk update; \
|
||||
apk add git;
|
||||
FROM node:lts-alpine AS release
|
||||
|
||||
VOLUME /parse-server/cloud /parse-server/config
|
||||
|
||||
WORKDIR /parse-server
|
||||
|
||||
# Copy build stage folders
|
||||
COPY --from=build /tmp/prod_node_modules /parse-server/node_modules
|
||||
COPY --from=build /tmp/lib lib
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
# Clean npm cache; added to fix an issue with the install process
|
||||
RUN npm cache clean --force
|
||||
RUN npm ci --production --ignore-scripts
|
||||
|
||||
COPY bin bin
|
||||
COPY public_html public_html
|
||||
COPY views views
|
||||
COPY --from=build /tmp/lib lib
|
||||
RUN mkdir -p logs && chown -R node: logs
|
||||
|
||||
ENV PORT=1337
|
||||
|
||||
@@ -1,34 +1,176 @@
|
||||
BSD License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
For Parse Server software
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
Copyright (c) 2015-present, Parse, LLC. All rights reserved.
|
||||
1. Definitions.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
* Neither the name Parse nor the names of its contributors may be used to
|
||||
endorse or promote products derived from this software without specific
|
||||
prior written permission.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
-----
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
Parse Server
|
||||
|
||||
Copyright 2015-present Parse Platform
|
||||
|
||||
This product includes software developed at Parse Platform.
|
||||
www.parseplatform.org
|
||||
|
||||
---
|
||||
|
||||
As of April 5, 2017, Parse, LLC has transferred this code to the Parse Platform organization, and will no longer be contributing to or distributing this code.
|
||||
@@ -1,37 +0,0 @@
|
||||
Additional Grant of Patent Rights Version 2
|
||||
|
||||
"Software" means the Parse Server software distributed by Parse, LLC.
|
||||
|
||||
Parse, LLC. ("Parse") hereby grants to each recipient of the Software
|
||||
("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable
|
||||
(subject to the termination provision below) license under any Necessary
|
||||
Claims, to make, have made, use, sell, offer to sell, import, and otherwise
|
||||
transfer the Software. For avoidance of doubt, no license is granted under
|
||||
Parse’s rights in any patent claims that are infringed by (i) modifications
|
||||
to the Software made by you or any third party or (ii) the Software in
|
||||
combination with any software or other technology.
|
||||
|
||||
The license granted hereunder will terminate, automatically and without notice,
|
||||
if you (or any of your subsidiaries, corporate affiliates or agents) initiate
|
||||
directly or indirectly, or take a direct financial interest in, any Patent
|
||||
Assertion: (i) against Parse or any of its subsidiaries or corporate
|
||||
affiliates, (ii) against any party if such Patent Assertion arises in whole or
|
||||
in part from any software, technology, product or service of Parse or any of
|
||||
its subsidiaries or corporate affiliates, or (iii) against any party relating
|
||||
to the Software. Notwithstanding the foregoing, if Parse or any of its
|
||||
subsidiaries or corporate affiliates files a lawsuit alleging patent
|
||||
infringement against you in the first instance, and you respond by filing a
|
||||
patent infringement counterclaim in that lawsuit against that party that is
|
||||
unrelated to the Software, the license granted hereunder will not terminate
|
||||
under section (i) of this paragraph due to such counterclaim.
|
||||
|
||||
A "Necessary Claim" is a claim of a patent owned by Parse that is
|
||||
necessarily infringed by the Software standing alone.
|
||||
|
||||
A "Patent Assertion" is any lawsuit or other action alleging direct, indirect,
|
||||
or contributory infringement or inducement to infringe any patent, including a
|
||||
cross-claim or counterclaim.
|
||||
|
||||
-----
|
||||
|
||||
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
|
||||
@@ -1,15 +1,17 @@
|
||||

|
||||
|
||||
---
|
||||
|
||||
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Aalpha)
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Abeta)
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Arelease)
|
||||
[](https://snyk.io/test/github/parse-community/parse-server)
|
||||
[](https://codecov.io/github/parse-community/parse-server?branch=alpha)
|
||||
[](https://github.com/parse-community/parse-dashboard/releases)
|
||||
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
[](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)
|
||||
@@ -17,9 +19,9 @@
|
||||
|
||||
[][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)
|
||||
|
||||
---
|
||||
|
||||
@@ -31,16 +33,14 @@ The full documentation for Parse Server is available in the [wiki](https://githu
|
||||
|
||||
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/contribute/diamond-sponsor-10560)
|
||||
|
||||
#### 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)
|
||||
@@ -54,9 +54,12 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
|
||||
- [Running Parse Server elsewhere](#running-parse-server-elsewhere)
|
||||
- [Sample Application](#sample-application)
|
||||
- [Parse Server + Express](#parse-server--express)
|
||||
- [Parse Server Health](#parse-server-health)
|
||||
- [Status Values](#status-values)
|
||||
- [Configuration](#configuration)
|
||||
- [Basic Options](#basic-options)
|
||||
- [Client Key Options](#client-key-options)
|
||||
- [Access Scopes](#access-scopes)
|
||||
- [Email Verification and Password Reset](#email-verification-and-password-reset)
|
||||
- [Password and Account Policy](#password-and-account-policy)
|
||||
- [Custom Routes](#custom-routes)
|
||||
@@ -88,13 +91,24 @@ A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
|
||||
- [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)
|
||||
- [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-5.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 5.x will receive security updates until Parse Server 6.x is superseded by Parse Server 7.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-5.x.x` for the Parse Server 5.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-5.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.
|
||||
@@ -110,36 +124,42 @@ 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 16 | 16.13.0 | April 2024 | ✅ Yes |
|
||||
| Node.js 17 | 17.x | June 2022 | ❌ Not tested |
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|------------|----------------|-------------|------------|
|
||||
| Node.js 14 | 14.19.1 | April 2023 | ✅ Yes |
|
||||
| Node.js 16 | 16.14.2 | April 2024 | ✅ Yes |
|
||||
| Node.js 18 | 18.12.1 | April 2025 | ✅ Yes |
|
||||
| Node.js 19 | 19.3.0 | June 2023 | ✅ 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, 3.2 | November 2023 | April 2022 | ✅ Yes |
|
||||
| Postgres 12 | 3.2 | November 2024 | April 2023 | ✅ Yes |
|
||||
| Postgres 13 | 3.2 | November 2025 | April 2024 | ✅ Yes |
|
||||
| Postgres 14 | 3.2 | 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
|
||||
@@ -232,7 +252,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).
|
||||
@@ -265,11 +284,11 @@ We have provided a basic [Node.js application](https://github.com/parse-communit
|
||||
You can also create an instance of Parse Server, and mount it on a new or existing Express website:
|
||||
|
||||
```js
|
||||
var express = require('express');
|
||||
var ParseServer = require('parse-server').ParseServer;
|
||||
var app = express();
|
||||
const express = require('express');
|
||||
const ParseServer = require('parse-server').ParseServer;
|
||||
const app = express();
|
||||
|
||||
var api = new ParseServer({
|
||||
const server = new ParseServer({
|
||||
databaseURI: 'mongodb://localhost:27017/dev', // Connection string for your MongoDB database
|
||||
cloud: './cloud/main.js', // Path to your Cloud Code
|
||||
appId: 'myAppId',
|
||||
@@ -278,8 +297,11 @@ var api = new ParseServer({
|
||||
serverURL: 'http://localhost:1337/parse' // Don't forget to change to https if needed
|
||||
});
|
||||
|
||||
// Start server
|
||||
await server.start();
|
||||
|
||||
// Serve the Parse API on the /parse URL prefix
|
||||
app.use('/parse', api);
|
||||
app.use('/parse', server.app);
|
||||
|
||||
app.listen(1337, function() {
|
||||
console.log('parse-server-example running on port 1337.');
|
||||
@@ -288,6 +310,27 @@ app.listen(1337, function() {
|
||||
|
||||
For a full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations](http://parseplatform.org/parse-server/api/master/ParseServerOptions.html).
|
||||
|
||||
## Parse Server Health
|
||||
|
||||
Check the Parse Server health by sending a request to the `/parse/health` endpoint.
|
||||
|
||||
The response looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
### Status Values
|
||||
|
||||
| Value | Description |
|
||||
|---------------|-----------------------------------------------------------------------------|
|
||||
| `initialized` | The server has been created but the `start` method has not been called yet. |
|
||||
| `starting` | The server is starting up. |
|
||||
| `ok` | The server started and is running. |
|
||||
| `error` | There was a startup error, see the logs for details. |
|
||||
|
||||
# Configuration
|
||||
|
||||
Parse Server can be configured using the following options. You may pass these as parameters when running a standalone `parse-server`, or by loading a configuration file in JSON format using `parse-server path/to/configuration.json`. If you're using Parse Server on Express, you may also pass these to the `ParseServer` object as options.
|
||||
@@ -313,6 +356,15 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
||||
* `restAPIKey`
|
||||
* `dotNetKey`
|
||||
|
||||
## Access Scopes
|
||||
|
||||
| Scope | Internal data | Custom data | Restricted by CLP, ACL | Key |
|
||||
|----------------|---------------|-------------|------------------------|---------------------|
|
||||
| Internal | r/w | r/w | no | `maintenanceKey` |
|
||||
| Master | -/- | r/w | no | `masterKey` |
|
||||
| ReadOnlyMaster | -/- | r/- | no | `readOnlyMasterKey` |
|
||||
| Session | -/- | r/w | yes | `sessionToken` |
|
||||
|
||||
## Email Verification and Password Reset
|
||||
|
||||
Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) for more details and a full list of available options.
|
||||
@@ -338,8 +390,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)
|
||||
@@ -370,7 +424,7 @@ const server = ParseServer({
|
||||
},
|
||||
|
||||
// The password policy
|
||||
passwordPolicy: {
|
||||
passwordPolicy: {
|
||||
// Enforce a password of at least 8 characters which contain at least 1 lower case, 1 upper case and 1 digit
|
||||
validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/,
|
||||
// Do not allow the username as part of the password
|
||||
@@ -382,6 +436,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:
|
||||
@@ -410,10 +465,11 @@ const api = new ParseServer({
|
||||
|
||||
The above route can be invoked by sending a `GET` request to:
|
||||
`https://[parseServerPublicUrl]/[parseMount]/[pagesEndpoint]/[appId]/[customRoute]`
|
||||
|
||||
|
||||
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 |
|
||||
@@ -440,7 +496,7 @@ The following paths are already used by Parse Server's built-in features and are
|
||||
It’s possible to change the default pages of the app and redirect the user to another path or domain.
|
||||
|
||||
```js
|
||||
var server = ParseServer({
|
||||
const server = ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
customPages: {
|
||||
@@ -497,7 +553,7 @@ Parse Server allows developers to choose from several options when hosting files
|
||||
`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
|
||||
|
||||
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
This feature deduplicates identical requests that are received by Parse Server multiple times, typically due to network issues or network adapter access restrictions on mobile operating systems.
|
||||
@@ -509,6 +565,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: {
|
||||
@@ -517,6 +574,7 @@ let api = new ParseServer({
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters <!-- omit in toc -->
|
||||
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
@@ -549,6 +607,7 @@ Assuming the script above is named, `parse_idempotency_delete_expired_records.sh
|
||||
## 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:
|
||||
@@ -827,7 +886,7 @@ Then, create an `index.js` file with the following content:
|
||||
|
||||
```js
|
||||
const express = require('express');
|
||||
const { default: ParseServer, ParseGraphQLServer } = require('parse-server');
|
||||
const { ParseServer, ParseGraphQLServer } = require('parse-server');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -851,6 +910,7 @@ app.use('/parse', parseServer.app); // (Optional) Mounts the REST API
|
||||
parseGraphQLServer.applyGraphQL(app); // Mounts the GraphQL API
|
||||
parseGraphQLServer.applyPlayground(app); // (Optional) Mounts the GraphQL Playground - do NOT use in Production
|
||||
|
||||
await parseServer.start();
|
||||
app.listen(1337, function() {
|
||||
console.log('REST API running on http://localhost:1337/parse');
|
||||
console.log('GraphQL API running on http://localhost:1337/graphql');
|
||||
@@ -1092,37 +1152,9 @@ 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
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1171,10 +1203,7 @@ Support us with a monthly donation and help us continue our activities. [Become
|
||||
|
||||
<a href="https://opencollective.com/parse-server#backers" target="_blank"><img src="https://opencollective.com/parse-server/backers.svg?width=890" /></a>
|
||||
|
||||
-----
|
||||
|
||||
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
|
||||
|
||||
[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,675 @@
|
||||
# [6.1.0-alpha.8](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.7...6.1.0-alpha.8) (2023-05-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Allow multiple origins for header `Access-Control-Allow-Origin` ([#8517](https://github.com/parse-community/parse-server/issues/8517)) ([4f15539](https://github.com/parse-community/parse-server/commit/4f15539ac244aa2d393ac5177f7604b43f69e271))
|
||||
|
||||
# [6.1.0-alpha.7](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.6...6.1.0-alpha.7) (2023-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Rate limiting across multiple servers via Redis not working ([#8469](https://github.com/parse-community/parse-server/issues/8469)) ([d9e347d](https://github.com/parse-community/parse-server/commit/d9e347d7413f30f58ffbb8397fc8b5ae23be6ff0))
|
||||
|
||||
# [6.1.0-alpha.6](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.5...6.1.0-alpha.6) (2023-03-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add rate limiting across multiple servers via Redis ([#8394](https://github.com/parse-community/parse-server/issues/8394)) ([34833e4](https://github.com/parse-community/parse-server/commit/34833e42eec08b812b733be78df0535ab0e096b6))
|
||||
|
||||
# [6.1.0-alpha.5](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.4...6.1.0-alpha.5) (2023-03-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LiveQuery can return incorrectly formatted date ([#8456](https://github.com/parse-community/parse-server/issues/8456)) ([4ce135a](https://github.com/parse-community/parse-server/commit/4ce135a4fe930776044bc8fd786a4e17a0144e03))
|
||||
|
||||
# [6.1.0-alpha.4](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.3...6.1.0-alpha.4) (2023-03-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parameters missing in `afterFind` trigger of authentication adapters ([#8458](https://github.com/parse-community/parse-server/issues/8458)) ([ce34747](https://github.com/parse-community/parse-server/commit/ce34747e8af54cb0b6b975da38f779a5955d2d59))
|
||||
|
||||
# [6.1.0-alpha.3](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.2...6.1.0-alpha.3) (2023-03-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `afterFind` trigger to authentication adapters ([#8444](https://github.com/parse-community/parse-server/issues/8444)) ([c793bb8](https://github.com/parse-community/parse-server/commit/c793bb88e7485743c7ceb65fe419cde75833ff33))
|
||||
|
||||
# [6.1.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.1...6.1.0-alpha.2) (2023-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Nested date is incorrectly decoded as empty object `{}` when fetching a Parse Object ([#8446](https://github.com/parse-community/parse-server/issues/8446)) ([22d2446](https://github.com/parse-community/parse-server/commit/22d2446dfea2bc339affc20535d181097e152acf))
|
||||
|
||||
# [6.1.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.0.0...6.1.0-alpha.1) (2023-03-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade jsonwebtoken to 9.0.0 ([#8420](https://github.com/parse-community/parse-server/issues/8420)) ([f5bfe45](https://github.com/parse-community/parse-server/commit/f5bfe4571e82b2b7440d41f3cff0d49937398164))
|
||||
|
||||
### Features
|
||||
|
||||
* Add option `schemaCacheTtl` for schema cache pulling as alternative to `enableSchemaHooks` ([#8436](https://github.com/parse-community/parse-server/issues/8436)) ([b3b76de](https://github.com/parse-community/parse-server/commit/b3b76de71b1d4265689d052e7837c38ec1fa4323))
|
||||
* Add Parse Server option `resetPasswordSuccessOnInvalidEmail` to choose success or error response on password reset with invalid email ([#7551](https://github.com/parse-community/parse-server/issues/7551)) ([e5d610e](https://github.com/parse-community/parse-server/commit/e5d610e5e487ddab86409409ac3d7362aba8f59b))
|
||||
* Deprecate LiveQuery `fields` option in favor of `keys` for semantic consistency ([#8388](https://github.com/parse-community/parse-server/issues/8388)) ([a49e323](https://github.com/parse-community/parse-server/commit/a49e323d5ae640bff1c6603ec37fdaddb9328dd1))
|
||||
* Export `AuthAdapter` to make it available for extension with custom authentication adapters ([#8443](https://github.com/parse-community/parse-server/issues/8443)) ([40c1961](https://github.com/parse-community/parse-server/commit/40c196153b8efa12ae384c1c0092b2ed60a260d6))
|
||||
|
||||
# [6.0.0-alpha.35](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.34...6.0.0-alpha.35) (2023-02-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add option `schemaCacheTtl` for schema cache pulling as alternative to `enableSchemaHooks` ([#8436](https://github.com/parse-community/parse-server/issues/8436)) ([b3b76de](https://github.com/parse-community/parse-server/commit/b3b76de71b1d4265689d052e7837c38ec1fa4323))
|
||||
|
||||
# [6.0.0-alpha.34](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.33...6.0.0-alpha.34) (2023-02-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add Parse Server option `resetPasswordSuccessOnInvalidEmail` to choose success or error response on password reset with invalid email ([#7551](https://github.com/parse-community/parse-server/issues/7551)) ([e5d610e](https://github.com/parse-community/parse-server/commit/e5d610e5e487ddab86409409ac3d7362aba8f59b))
|
||||
|
||||
# [6.0.0-alpha.33](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.32...6.0.0-alpha.33) (2023-02-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deprecate LiveQuery `fields` option in favor of `keys` for semantic consistency ([#8388](https://github.com/parse-community/parse-server/issues/8388)) ([a49e323](https://github.com/parse-community/parse-server/commit/a49e323d5ae640bff1c6603ec37fdaddb9328dd1))
|
||||
|
||||
# [6.0.0-alpha.32](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.31...6.0.0-alpha.32) (2023-02-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade jsonwebtoken to 9.0.0 ([#8420](https://github.com/parse-community/parse-server/issues/8420)) ([f5bfe45](https://github.com/parse-community/parse-server/commit/f5bfe4571e82b2b7440d41f3cff0d49937398164))
|
||||
|
||||
# [6.0.0-alpha.31](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.30...6.0.0-alpha.31) (2023-01-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `requestKeywordDenylist` can be bypassed via Cloud Code Webhooks or Triggers; fixes security vulnerability [GHSA-xprv-wvh7-qqqx](https://github.com/parse-community/parse-server/security/advisories/GHSA-xprv-wvh7-qqqx) ([#8302](https://github.com/parse-community/parse-server/issues/8302)) ([6728da1](https://github.com/parse-community/parse-server/commit/6728da1e3591db1e27031d335d64d8f25546a06f))
|
||||
* Prototype pollution via Cloud Code Webhooks; fixes security vulnerability [GHSA-93vw-8fm5-p2jf](https://github.com/parse-community/parse-server/security/advisories/GHSA-93vw-8fm5-p2jf) ([#8305](https://github.com/parse-community/parse-server/issues/8305)) ([60c5a73](https://github.com/parse-community/parse-server/commit/60c5a73d257e0d536056b38bdafef8b7130524d8))
|
||||
* Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-prm5-8g2m-24gg](https://github.com/parse-community/parse-server/security/advisories/GHSA-prm5-8g2m-24gg) ([#8295](https://github.com/parse-community/parse-server/issues/8295)) ([50eed3c](https://github.com/parse-community/parse-server/commit/50eed3cffe80fadfb4bdac52b2783a18da2cfc4f))
|
||||
|
||||
# [6.0.0-alpha.30](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.29...6.0.0-alpha.30) (2023-01-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Schema without class level permissions may cause error ([#8409](https://github.com/parse-community/parse-server/issues/8409)) ([aa2cd51](https://github.com/parse-community/parse-server/commit/aa2cd51b703388d925e4572e5c2b2d883c68e49c))
|
||||
|
||||
# [6.0.0-alpha.29](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.28...6.0.0-alpha.29) (2023-01-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to Parse JavaScript SDK 4 ([#8332](https://github.com/parse-community/parse-server/issues/8332)) ([9092874](https://github.com/parse-community/parse-server/commit/9092874a9a482a24dfdce1dce56615702999d6b8))
|
||||
|
||||
# [6.0.0-alpha.28](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.27...6.0.0-alpha.28) (2023-01-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Rate limiter may reject requests that contain a session token ([#8399](https://github.com/parse-community/parse-server/issues/8399)) ([c114dc8](https://github.com/parse-community/parse-server/commit/c114dc8831055d74187b9dfb4c9eeb558520237c))
|
||||
|
||||
# [6.0.0-alpha.27](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.26...6.0.0-alpha.27) (2023-01-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `ParseServer.verifyServerUrl` may fail if server response headers are missing; remove unnecessary logging ([#8391](https://github.com/parse-community/parse-server/issues/8391)) ([1c37a7c](https://github.com/parse-community/parse-server/commit/1c37a7cd0715949a70b220a629071c7dab7d5e7b))
|
||||
|
||||
# [6.0.0-alpha.26](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.25...6.0.0-alpha.26) (2023-01-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ES6 modules do not await the import of Cloud Code files ([#8368](https://github.com/parse-community/parse-server/issues/8368)) ([a7bd180](https://github.com/parse-community/parse-server/commit/a7bd180cddd784c8735622f22e012c342ad535fb))
|
||||
|
||||
# [6.0.0-alpha.25](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.24...6.0.0-alpha.25) (2023-01-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `ParseQuery.watch` to trigger LiveQuery only on update of specific fields ([#8028](https://github.com/parse-community/parse-server/issues/8028)) ([fc92faa](https://github.com/parse-community/parse-server/commit/fc92faac75107b3392eeddd916c4c5b45e3c5e0c))
|
||||
|
||||
# [6.0.0-alpha.24](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.23...6.0.0-alpha.24) (2023-01-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Reduce Docker image size by improving stages ([#8359](https://github.com/parse-community/parse-server/issues/8359)) ([40810b4](https://github.com/parse-community/parse-server/commit/40810b48ebde8b1f21d2448a3a4de0585b1b5e34))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Docker image does not contain the git dependency anymore; if you have been using git as a transitive dependency it now needs to be explicitly installed in your Docker file, for example with `RUN apk --no-cache add git` (#8359) ([40810b4](40810b4))
|
||||
|
||||
# [6.0.0-alpha.23](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.22...6.0.0-alpha.23) (2023-01-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Access the internal scope of Parse Server using the new `maintenanceKey`; the internal scope contains unofficial and undocumented fields (prefixed with underscore `_`) which are used internally by Parse Server; you may want to manipulate these fields for out-of-band changes such as data migration or correction tasks; changes within the internal scope of Parse Server may happen at any time without notice or changelog entry, it is therefore recommended to look at the source code of Parse Server to understand the effects of manipulating internal fields before using the key; it is discouraged to use the `maintenanceKey` for routine operations in a production environment; see [access scopes](https://github.com/parse-community/parse-server#access-scopes) ([#8212](https://github.com/parse-community/parse-server/issues/8212)) ([f3bcc93](https://github.com/parse-community/parse-server/commit/f3bcc9365cd6f08b0a32c132e8e5ff6d1b650863))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Fields in the internal scope of Parse Server (prefixed with underscore `_`) are only returned using the new `maintenanceKey`; previously the `masterKey` allowed reading of internal fields; see [access scopes](https://github.com/parse-community/parse-server#access-scopes) for a comparison of the keys' access permissions (#8212) ([f3bcc93](f3bcc93))
|
||||
|
||||
# [6.0.0-alpha.22](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.21...6.0.0-alpha.22) (2023-01-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Adapt `verifyServerUrl` for new asynchronous Parse Server start-up states ([#8366](https://github.com/parse-community/parse-server/issues/8366)) ([ffa4974](https://github.com/parse-community/parse-server/commit/ffa4974158615fbff4a2692b9db41dcb50d3f77b))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The method `ParseServer.verifyServerUrl` now returns a promise instead of a callback. ([ffa4974](ffa4974))
|
||||
|
||||
# [6.0.0-alpha.21](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.20...6.0.0-alpha.21) (2023-01-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add request rate limiter based on IP address ([#8174](https://github.com/parse-community/parse-server/issues/8174)) ([6c79f6a](https://github.com/parse-community/parse-server/commit/6c79f6a69e25e47846e3b0685d6bdfd6b91086b1))
|
||||
|
||||
# [6.0.0-alpha.20](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.19...6.0.0-alpha.20) (2023-01-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add Node 19 support ([#8363](https://github.com/parse-community/parse-server/issues/8363)) ([a4990dc](https://github.com/parse-community/parse-server/commit/a4990dcd29abcb4442f3c424aff482a0a116160f))
|
||||
|
||||
# [6.0.0-alpha.19](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.18...6.0.0-alpha.19) (2023-01-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Remove deprecation `DEPPS1`: Native MongoDB syntax in aggregation pipeline ([#8362](https://github.com/parse-community/parse-server/issues/8362)) ([d0d30c4](https://github.com/parse-community/parse-server/commit/d0d30c4f1394f563724644a8fc81734be538a2c0))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The MongoDB aggregation pipeline requires native MongoDB syntax instead of the custom Parse Server syntax; for example pipeline stage names require a leading dollar sign like `$match` and the MongoDB document ID is referenced using `_id` instead of `objectId` (#8362) ([d0d30c4](d0d30c4))
|
||||
|
||||
# [6.0.0-alpha.18](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.17...6.0.0-alpha.18) (2023-01-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* The client IP address may be determined incorrectly in some cases; this fixes a security vulnerability in which the Parse Server option `masterKeyIps` may be circumvented, see [GHSA-vm5r-c87r-pf6x](https://github.com/parse-community/parse-server/security/advisories/GHSA-vm5r-c87r-pf6x) ([#8372](https://github.com/parse-community/parse-server/issues/8372)) ([892040d](https://github.com/parse-community/parse-server/commit/892040dc2f82a3e2abe2824e4b553521b6f894de))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The mechanism to determine the client IP address has been rewritten; to correctly determine the IP address it is now required to set the Parse Server option `trustProxy` accordingly if Parse Server runs behind a proxy server, see the express framework's [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting (#8372) ([892040d](892040d))
|
||||
|
||||
# [6.0.0-alpha.17](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.16...6.0.0-alpha.17) (2022-12-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade Node Package Manager lock file `package-lock.json` to version 2 ([#8285](https://github.com/parse-community/parse-server/issues/8285)) ([ee72467](https://github.com/parse-community/parse-server/commit/ee7246733d63e4bda20401f7b00262ff03299f20))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Node Package Manager lock file `package-lock.json` is upgraded to version 2; while it is backwards with version 1 for the npm installer, consider this if you run any non-npm analysis tools that use the lock file (#8285) ([ee72467](ee72467))
|
||||
|
||||
# [6.0.0-alpha.16](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.15...6.0.0-alpha.16) (2022-12-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Asynchronous initialization of Parse Server ([#8232](https://github.com/parse-community/parse-server/issues/8232)) ([99fcf45](https://github.com/parse-community/parse-server/commit/99fcf45e55c368de2345b0c4d780e70e0adf0e15))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This release introduces the asynchronous initialization of Parse Server to prevent mounting Parse Server before being ready to receive request; it changes how Parse Server is imported, initialized and started; it also removes the callback `serverStartComplete`; see the [Parse Server 6 migration guide](https://github.com/parse-community/parse-server/blob/alpha/6.0.0.md) for more details (#8232) ([99fcf45](99fcf45))
|
||||
|
||||
# [6.0.0-alpha.15](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.14...6.0.0-alpha.15) (2022-12-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Nested objects are encoded incorrectly for MongoDB ([#8209](https://github.com/parse-community/parse-server/issues/8209)) ([1412666](https://github.com/parse-community/parse-server/commit/1412666f75829612de6fb9d7ccae35761c9b75cb))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Nested objects are now properly stored in the database using JSON serialization; previously, due to a bug only top-level objects were serialized, but nested objects were saved as raw JSON; for example, a nested `Date` object was saved as a JSON object like `{ "__type": "Date", "iso": "2020-01-01T00:00:00.000Z" }` instead of its serialized representation `2020-01-01T00:00:00.000Z` (#8209) ([1412666](1412666))
|
||||
|
||||
# [6.0.0-alpha.14](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.13...6.0.0-alpha.14) (2022-12-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Write log entry when request with master key is rejected as outside of `masterKeyIps` ([#8350](https://github.com/parse-community/parse-server/issues/8350)) ([e22b73d](https://github.com/parse-community/parse-server/commit/e22b73d4b700c8ff745aa81726c6680082294b45))
|
||||
|
||||
# [6.0.0-alpha.13](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.12...6.0.0-alpha.13) (2022-12-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add option to change the log level of the logs emitted by triggers ([#8328](https://github.com/parse-community/parse-server/issues/8328)) ([8f3b694](https://github.com/parse-community/parse-server/commit/8f3b694e39d4a966567e50dbea4d62e954fa5c06))
|
||||
|
||||
# [6.0.0-alpha.12](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.11...6.0.0-alpha.12) (2022-11-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade Redis 3 to 4 for LiveQuery ([#8333](https://github.com/parse-community/parse-server/issues/8333)) ([b2761fb](https://github.com/parse-community/parse-server/commit/b2761fb3786b519d9bbcf35be54309d2d35da1a9))
|
||||
|
||||
# [6.0.0-alpha.11](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.10...6.0.0-alpha.11) (2022-11-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `masterKeyIps` does not include localhost by default for IPv6 ([#8322](https://github.com/parse-community/parse-server/issues/8322)) ([ab82635](https://github.com/parse-community/parse-server/commit/ab82635b0d4cf323a07ddee51fee587b43dce95c))
|
||||
|
||||
# [6.0.0-alpha.10](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.9...6.0.0-alpha.10) (2022-11-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Cloud Code trigger `beforeSave` does not work with `Parse.Role` ([#8320](https://github.com/parse-community/parse-server/issues/8320)) ([f29d972](https://github.com/parse-community/parse-server/commit/f29d9720e9b37918fd885c97a31e34c42750e724))
|
||||
|
||||
# [6.0.0-alpha.9](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.8...6.0.0-alpha.9) (2022-11-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Remove deprecation `DEPPS3`: Config option `enforcePrivateUsers` defaults to `true` ([#8283](https://github.com/parse-community/parse-server/issues/8283)) ([ed499e3](https://github.com/parse-community/parse-server/commit/ed499e32a21bab9a874a9e5367dc71248ce836c4))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Parse Server option `enforcePrivateUsers` is set to `true` by default; in previous releases this option defaults to `false`; this change improves the default security configuration of Parse Server (#8283) ([ed499e3](ed499e3))
|
||||
|
||||
# [6.0.0-alpha.8](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.7...6.0.0-alpha.8) (2022-11-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Restrict use of `masterKey` to localhost by default ([#8281](https://github.com/parse-community/parse-server/issues/8281)) ([6c16021](https://github.com/parse-community/parse-server/commit/6c16021a1f03a70a6d9e68cb64df362d07f3b693))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This release restricts the use of `masterKey` to localhost by default; if you are using Parse Dashboard on a different server to connect to Parse Server you need to add the IP address of the server that hosts Parse Dashboard to this option (#8281) ([6c16021](6c16021))
|
||||
|
||||
# [6.0.0-alpha.7](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.6...6.0.0-alpha.7) (2022-11-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade Redis 3 to 4 ([#8293](https://github.com/parse-community/parse-server/issues/8293)) ([7d622f0](https://github.com/parse-community/parse-server/commit/7d622f06a4347e0ad2cba9a4ec07d8d4fb0f67bc))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This release upgrades to Redis 4; if you are using the Redis cache adapter with Parse Server then this is a breaking change as the Redis client options have changed; see the [Redis migration guide](https://github.com/redis/node-redis/blob/redis%404.0.0/docs/v3-to-v4.md) for more details (#8293) ([7d622f0](7d622f0))
|
||||
|
||||
# [6.0.0-alpha.6](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.5...6.0.0-alpha.6) (2022-11-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Remove support for MongoDB 4.0 ([#8292](https://github.com/parse-community/parse-server/issues/8292)) ([37245f6](https://github.com/parse-community/parse-server/commit/37245f62ce83516b6b95a54b850f0274ef680478))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This release removes support for MongoDB 4.0; the new minimum supported MongoDB version is 4.2. which also removes support for the deprecated MongoDB MMAPv1 storage engine ([37245f6](37245f6))
|
||||
|
||||
# [6.0.0-alpha.5](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.4...6.0.0-alpha.5) (2022-11-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Throwing error in Cloud Code Triggers `afterLogin`, `afterLogout` crashes server ([#8280](https://github.com/parse-community/parse-server/issues/8280)) ([130d290](https://github.com/parse-community/parse-server/commit/130d29074e3f763460e5685d0b9059e5a333caff))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Throwing an error in Cloud Code Triggers `afterLogin`, `afterLogout` returns a rejected promise; in previous releases it crashed the server if you did not handle the error on the Node.js process level; consider adapting your code if your app currently handles these errors on the Node.js process level with `process.on('unhandledRejection', ...)` ([130d290](130d290))
|
||||
|
||||
# [6.0.0-alpha.4](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.3...6.0.0-alpha.4) (2022-11-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Remove deprecation `DEPPS2`: Config option `directAccess` defaults to true ([#8284](https://github.com/parse-community/parse-server/issues/8284)) ([f535ee6](https://github.com/parse-community/parse-server/commit/f535ee6ec2abba63f702127258ca49fa5b4e08c9))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Config option `directAccess` defaults to true; set this to `false` 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`. ([f535ee6](f535ee6))
|
||||
|
||||
# [6.0.0-alpha.3](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.2...6.0.0-alpha.3) (2022-11-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Remove deprecation `DEPPS4`: Remove convenience method for http request `Parse.Cloud.httpRequest` ([#8287](https://github.com/parse-community/parse-server/issues/8287)) ([2d79c08](https://github.com/parse-community/parse-server/commit/2d79c0835b6a9acaf20d5c943d9b4619bb96831c))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The convenience method for HTTP requests `Parse.Cloud.httpRequest` is removed; use your preferred 3rd party library for making HTTP requests ([2d79c08](2d79c08))
|
||||
|
||||
# [6.0.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.0.0-alpha.1...6.0.0-alpha.2) (2022-11-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Improve authentication adapter interface to support multi-factor authentication (MFA), authentication challenges, and provide a more powerful interface for writing custom authentication adapters ([#8156](https://github.com/parse-community/parse-server/issues/8156)) ([5bbf9ca](https://github.com/parse-community/parse-server/commit/5bbf9cade9a527787fd1002072d4013ab5d8db2b))
|
||||
|
||||
# [6.0.0-alpha.1](https://github.com/parse-community/parse-server/compare/5.4.0-alpha.1...6.0.0-alpha.1) (2022-11-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove Node 12 and Node 17 support ([#8279](https://github.com/parse-community/parse-server/issues/8279)) ([2546cc8](https://github.com/parse-community/parse-server/commit/2546cc8572bea6610cb9b3c7401d9afac0e3c1d6))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This release removes Node 12 and Node 17 support ([2546cc8](2546cc8))
|
||||
|
||||
# [5.4.0-alpha.1](https://github.com/parse-community/parse-server/compare/5.3.0...5.4.0-alpha.1) (2022-10-31)
|
||||
|
||||
|
||||
### 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-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)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,155 @@
|
||||
# [6.1.0-beta.2](https://github.com/parse-community/parse-server/compare/6.1.0-beta.1...6.1.0-beta.2) (2023-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LiveQuery can return incorrectly formatted date ([#8456](https://github.com/parse-community/parse-server/issues/8456)) ([4ce135a](https://github.com/parse-community/parse-server/commit/4ce135a4fe930776044bc8fd786a4e17a0144e03))
|
||||
* Nested date is incorrectly decoded as empty object `{}` when fetching a Parse Object ([#8446](https://github.com/parse-community/parse-server/issues/8446)) ([22d2446](https://github.com/parse-community/parse-server/commit/22d2446dfea2bc339affc20535d181097e152acf))
|
||||
* Parameters missing in `afterFind` trigger of authentication adapters ([#8458](https://github.com/parse-community/parse-server/issues/8458)) ([ce34747](https://github.com/parse-community/parse-server/commit/ce34747e8af54cb0b6b975da38f779a5955d2d59))
|
||||
* Rate limiting across multiple servers via Redis not working ([#8469](https://github.com/parse-community/parse-server/issues/8469)) ([d9e347d](https://github.com/parse-community/parse-server/commit/d9e347d7413f30f58ffbb8397fc8b5ae23be6ff0))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `afterFind` trigger to authentication adapters ([#8444](https://github.com/parse-community/parse-server/issues/8444)) ([c793bb8](https://github.com/parse-community/parse-server/commit/c793bb88e7485743c7ceb65fe419cde75833ff33))
|
||||
* Add rate limiting across multiple servers via Redis ([#8394](https://github.com/parse-community/parse-server/issues/8394)) ([34833e4](https://github.com/parse-community/parse-server/commit/34833e42eec08b812b733be78df0535ab0e096b6))
|
||||
* Allow multiple origins for header `Access-Control-Allow-Origin` ([#8517](https://github.com/parse-community/parse-server/issues/8517)) ([4f15539](https://github.com/parse-community/parse-server/commit/4f15539ac244aa2d393ac5177f7604b43f69e271))
|
||||
* Export `AuthAdapter` to make it available for extension with custom authentication adapters ([#8443](https://github.com/parse-community/parse-server/issues/8443)) ([40c1961](https://github.com/parse-community/parse-server/commit/40c196153b8efa12ae384c1c0092b2ed60a260d6))
|
||||
|
||||
# [6.1.0-beta.1](https://github.com/parse-community/parse-server/compare/6.0.0...6.1.0-beta.1) (2023-03-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade jsonwebtoken to 9.0.0 ([#8420](https://github.com/parse-community/parse-server/issues/8420)) ([f5bfe45](https://github.com/parse-community/parse-server/commit/f5bfe4571e82b2b7440d41f3cff0d49937398164))
|
||||
|
||||
### Features
|
||||
|
||||
* Add option `schemaCacheTtl` for schema cache pulling as alternative to `enableSchemaHooks` ([#8436](https://github.com/parse-community/parse-server/issues/8436)) ([b3b76de](https://github.com/parse-community/parse-server/commit/b3b76de71b1d4265689d052e7837c38ec1fa4323))
|
||||
* Add Parse Server option `resetPasswordSuccessOnInvalidEmail` to choose success or error response on password reset with invalid email ([#7551](https://github.com/parse-community/parse-server/issues/7551)) ([e5d610e](https://github.com/parse-community/parse-server/commit/e5d610e5e487ddab86409409ac3d7362aba8f59b))
|
||||
* Deprecate LiveQuery `fields` option in favor of `keys` for semantic consistency ([#8388](https://github.com/parse-community/parse-server/issues/8388)) ([a49e323](https://github.com/parse-community/parse-server/commit/a49e323d5ae640bff1c6603ec37fdaddb9328dd1))
|
||||
|
||||
# [6.0.0-beta.1](https://github.com/parse-community/parse-server/compare/5.4.0...6.0.0-beta.1) (2023-01-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `ParseServer.verifyServerUrl` may fail if server response headers are missing; remove unnecessary logging ([#8391](https://github.com/parse-community/parse-server/issues/8391)) ([1c37a7c](https://github.com/parse-community/parse-server/commit/1c37a7cd0715949a70b220a629071c7dab7d5e7b))
|
||||
* Cloud Code trigger `beforeSave` does not work with `Parse.Role` ([#8320](https://github.com/parse-community/parse-server/issues/8320)) ([f29d972](https://github.com/parse-community/parse-server/commit/f29d9720e9b37918fd885c97a31e34c42750e724))
|
||||
* ES6 modules do not await the import of Cloud Code files ([#8368](https://github.com/parse-community/parse-server/issues/8368)) ([a7bd180](https://github.com/parse-community/parse-server/commit/a7bd180cddd784c8735622f22e012c342ad535fb))
|
||||
* Nested objects are encoded incorrectly for MongoDB ([#8209](https://github.com/parse-community/parse-server/issues/8209)) ([1412666](https://github.com/parse-community/parse-server/commit/1412666f75829612de6fb9d7ccae35761c9b75cb))
|
||||
* Parse Server option `masterKeyIps` does not include localhost by default for IPv6 ([#8322](https://github.com/parse-community/parse-server/issues/8322)) ([ab82635](https://github.com/parse-community/parse-server/commit/ab82635b0d4cf323a07ddee51fee587b43dce95c))
|
||||
* Rate limiter may reject requests that contain a session token ([#8399](https://github.com/parse-community/parse-server/issues/8399)) ([c114dc8](https://github.com/parse-community/parse-server/commit/c114dc8831055d74187b9dfb4c9eeb558520237c))
|
||||
* Remove Node 12 and Node 17 support ([#8279](https://github.com/parse-community/parse-server/issues/8279)) ([2546cc8](https://github.com/parse-community/parse-server/commit/2546cc8572bea6610cb9b3c7401d9afac0e3c1d6))
|
||||
* Schema without class level permissions may cause error ([#8409](https://github.com/parse-community/parse-server/issues/8409)) ([aa2cd51](https://github.com/parse-community/parse-server/commit/aa2cd51b703388d925e4572e5c2b2d883c68e49c))
|
||||
* The client IP address may be determined incorrectly in some cases; this fixes a security vulnerability in which the Parse Server option `masterKeyIps` may be circumvented, see [GHSA-vm5r-c87r-pf6x](https://github.com/parse-community/parse-server/security/advisories/GHSA-vm5r-c87r-pf6x) ([#8372](https://github.com/parse-community/parse-server/issues/8372)) ([892040d](https://github.com/parse-community/parse-server/commit/892040dc2f82a3e2abe2824e4b553521b6f894de))
|
||||
* Throwing error in Cloud Code Triggers `afterLogin`, `afterLogout` crashes server ([#8280](https://github.com/parse-community/parse-server/issues/8280)) ([130d290](https://github.com/parse-community/parse-server/commit/130d29074e3f763460e5685d0b9059e5a333caff))
|
||||
|
||||
### Features
|
||||
|
||||
* Access the internal scope of Parse Server using the new `maintenanceKey`; the internal scope contains unofficial and undocumented fields (prefixed with underscore `_`) which are used internally by Parse Server; you may want to manipulate these fields for out-of-band changes such as data migration or correction tasks; changes within the internal scope of Parse Server may happen at any time without notice or changelog entry, it is therefore recommended to look at the source code of Parse Server to understand the effects of manipulating internal fields before using the key; it is discouraged to use the `maintenanceKey` for routine operations in a production environment; see [access scopes](https://github.com/parse-community/parse-server#access-scopes) ([#8212](https://github.com/parse-community/parse-server/issues/8212)) ([f3bcc93](https://github.com/parse-community/parse-server/commit/f3bcc9365cd6f08b0a32c132e8e5ff6d1b650863))
|
||||
* Adapt `verifyServerUrl` for new asynchronous Parse Server start-up states ([#8366](https://github.com/parse-community/parse-server/issues/8366)) ([ffa4974](https://github.com/parse-community/parse-server/commit/ffa4974158615fbff4a2692b9db41dcb50d3f77b))
|
||||
* Add `ParseQuery.watch` to trigger LiveQuery only on update of specific fields ([#8028](https://github.com/parse-community/parse-server/issues/8028)) ([fc92faa](https://github.com/parse-community/parse-server/commit/fc92faac75107b3392eeddd916c4c5b45e3c5e0c))
|
||||
* Add Node 19 support ([#8363](https://github.com/parse-community/parse-server/issues/8363)) ([a4990dc](https://github.com/parse-community/parse-server/commit/a4990dcd29abcb4442f3c424aff482a0a116160f))
|
||||
* Add option to change the log level of the logs emitted by triggers ([#8328](https://github.com/parse-community/parse-server/issues/8328)) ([8f3b694](https://github.com/parse-community/parse-server/commit/8f3b694e39d4a966567e50dbea4d62e954fa5c06))
|
||||
* Add request rate limiter based on IP address ([#8174](https://github.com/parse-community/parse-server/issues/8174)) ([6c79f6a](https://github.com/parse-community/parse-server/commit/6c79f6a69e25e47846e3b0685d6bdfd6b91086b1))
|
||||
* Asynchronous initialization of Parse Server ([#8232](https://github.com/parse-community/parse-server/issues/8232)) ([99fcf45](https://github.com/parse-community/parse-server/commit/99fcf45e55c368de2345b0c4d780e70e0adf0e15))
|
||||
* Improve authentication adapter interface to support multi-factor authentication (MFA), authentication challenges, and provide a more powerful interface for writing custom authentication adapters ([#8156](https://github.com/parse-community/parse-server/issues/8156)) ([5bbf9ca](https://github.com/parse-community/parse-server/commit/5bbf9cade9a527787fd1002072d4013ab5d8db2b))
|
||||
* Reduce Docker image size by improving stages ([#8359](https://github.com/parse-community/parse-server/issues/8359)) ([40810b4](https://github.com/parse-community/parse-server/commit/40810b48ebde8b1f21d2448a3a4de0585b1b5e34))
|
||||
* Remove deprecation `DEPPS1`: Native MongoDB syntax in aggregation pipeline ([#8362](https://github.com/parse-community/parse-server/issues/8362)) ([d0d30c4](https://github.com/parse-community/parse-server/commit/d0d30c4f1394f563724644a8fc81734be538a2c0))
|
||||
* Remove deprecation `DEPPS2`: Config option `directAccess` defaults to true ([#8284](https://github.com/parse-community/parse-server/issues/8284)) ([f535ee6](https://github.com/parse-community/parse-server/commit/f535ee6ec2abba63f702127258ca49fa5b4e08c9))
|
||||
* Remove deprecation `DEPPS3`: Config option `enforcePrivateUsers` defaults to `true` ([#8283](https://github.com/parse-community/parse-server/issues/8283)) ([ed499e3](https://github.com/parse-community/parse-server/commit/ed499e32a21bab9a874a9e5367dc71248ce836c4))
|
||||
* Remove deprecation `DEPPS4`: Remove convenience method for http request `Parse.Cloud.httpRequest` ([#8287](https://github.com/parse-community/parse-server/issues/8287)) ([2d79c08](https://github.com/parse-community/parse-server/commit/2d79c0835b6a9acaf20d5c943d9b4619bb96831c))
|
||||
* Remove support for MongoDB 4.0 ([#8292](https://github.com/parse-community/parse-server/issues/8292)) ([37245f6](https://github.com/parse-community/parse-server/commit/37245f62ce83516b6b95a54b850f0274ef680478))
|
||||
* Restrict use of `masterKey` to localhost by default ([#8281](https://github.com/parse-community/parse-server/issues/8281)) ([6c16021](https://github.com/parse-community/parse-server/commit/6c16021a1f03a70a6d9e68cb64df362d07f3b693))
|
||||
* Upgrade Node Package Manager lock file `package-lock.json` to version 2 ([#8285](https://github.com/parse-community/parse-server/issues/8285)) ([ee72467](https://github.com/parse-community/parse-server/commit/ee7246733d63e4bda20401f7b00262ff03299f20))
|
||||
* Upgrade Redis 3 to 4 ([#8293](https://github.com/parse-community/parse-server/issues/8293)) ([7d622f0](https://github.com/parse-community/parse-server/commit/7d622f06a4347e0ad2cba9a4ec07d8d4fb0f67bc))
|
||||
* Upgrade Redis 3 to 4 for LiveQuery ([#8333](https://github.com/parse-community/parse-server/issues/8333)) ([b2761fb](https://github.com/parse-community/parse-server/commit/b2761fb3786b519d9bbcf35be54309d2d35da1a9))
|
||||
* Upgrade to Parse JavaScript SDK 4 ([#8332](https://github.com/parse-community/parse-server/issues/8332)) ([9092874](https://github.com/parse-community/parse-server/commit/9092874a9a482a24dfdce1dce56615702999d6b8))
|
||||
* Write log entry when request with master key is rejected as outside of `masterKeyIps` ([#8350](https://github.com/parse-community/parse-server/issues/8350)) ([e22b73d](https://github.com/parse-community/parse-server/commit/e22b73d4b700c8ff745aa81726c6680082294b45))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Docker image does not contain the git dependency anymore; if you have been using git as a transitive dependency it now needs to be explicitly installed in your Docker file, for example with `RUN apk --no-cache add git` (#8359) ([40810b4](40810b4))
|
||||
* Fields in the internal scope of Parse Server (prefixed with underscore `_`) are only returned using the new `maintenanceKey`; previously the `masterKey` allowed reading of internal fields; see [access scopes](https://github.com/parse-community/parse-server#access-scopes) for a comparison of the keys' access permissions (#8212) ([f3bcc93](f3bcc93))
|
||||
* The method `ParseServer.verifyServerUrl` now returns a promise instead of a callback. ([ffa4974](ffa4974))
|
||||
* The MongoDB aggregation pipeline requires native MongoDB syntax instead of the custom Parse Server syntax; for example pipeline stage names require a leading dollar sign like `$match` and the MongoDB document ID is referenced using `_id` instead of `objectId` (#8362) ([d0d30c4](d0d30c4))
|
||||
* The mechanism to determine the client IP address has been rewritten; to correctly determine the IP address it is now required to set the Parse Server option `trustProxy` accordingly if Parse Server runs behind a proxy server, see the express framework's [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting (#8372) ([892040d](892040d))
|
||||
* The Node Package Manager lock file `package-lock.json` is upgraded to version 2; while it is backwards with version 1 for the npm installer, consider this if you run any non-npm analysis tools that use the lock file (#8285) ([ee72467](ee72467))
|
||||
* This release introduces the asynchronous initialization of Parse Server to prevent mounting Parse Server before being ready to receive request; it changes how Parse Server is imported, initialized and started; it also removes the callback `serverStartComplete`; see the [Parse Server 6 migration guide](https://github.com/parse-community/parse-server/blob/alpha/6.0.0.md) for more details (#8232) ([99fcf45](99fcf45))
|
||||
* Nested objects are now properly stored in the database using JSON serialization; previously, due to a bug only top-level objects were serialized, but nested objects were saved as raw JSON; for example, a nested `Date` object was saved as a JSON object like `{ "__type": "Date", "iso": "2020-01-01T00:00:00.000Z" }` instead of its serialized representation `2020-01-01T00:00:00.000Z` (#8209) ([1412666](1412666))
|
||||
* The Parse Server option `enforcePrivateUsers` is set to `true` by default; in previous releases this option defaults to `false`; this change improves the default security configuration of Parse Server (#8283) ([ed499e3](ed499e3))
|
||||
* This release restricts the use of `masterKey` to localhost by default; if you are using Parse Dashboard on a different server to connect to Parse Server you need to add the IP address of the server that hosts Parse Dashboard to this option (#8281) ([6c16021](6c16021))
|
||||
* This release upgrades to Redis 4; if you are using the Redis cache adapter with Parse Server then this is a breaking change as the Redis client options have changed; see the [Redis migration guide](https://github.com/redis/node-redis/blob/redis%404.0.0/docs/v3-to-v4.md) for more details (#8293) ([7d622f0](7d622f0))
|
||||
* This release removes support for MongoDB 4.0; the new minimum supported MongoDB version is 4.2. which also removes support for the deprecated MongoDB MMAPv1 storage engine ([37245f6](37245f6))
|
||||
* Throwing an error in Cloud Code Triggers `afterLogin`, `afterLogout` returns a rejected promise; in previous releases it crashed the server if you did not handle the error on the Node.js process level; consider adapting your code if your app currently handles these errors on the Node.js process level with `process.on('unhandledRejection', ...)` ([130d290](130d290))
|
||||
* Config option `directAccess` defaults to true; set this to `false` 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`. ([f535ee6](f535ee6))
|
||||
* The convenience method for HTTP requests `Parse.Cloud.httpRequest` is removed; use your preferred 3rd party library for making HTTP requests ([2d79c08](2d79c08))
|
||||
* This release removes Node 12 and Node 17 support ([2546cc8](2546cc8))
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,195 @@
|
||||
## [6.2.2](https://github.com/parse-community/parse-server/compare/6.2.1...6.2.2) (2023-09-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Pointer allows to access internal Parse Server classes and circumvent `beforeFind` query trigger; fixes security vulnerability [GHSA-fcv6-fg5r-jm9q](https://github.com/parse-community/parse-server/security/advisories/GHSA-fcv6-fg5r-jm9q) ([be4c7e2](https://github.com/parse-community/parse-server/commit/be4c7e23c63a2fb690685665cebed0de26be05c5))
|
||||
|
||||
## [6.2.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.2.1) (2023-06-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-462x-c3jw-7vr6](https://github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6) ([#8674](https://github.com/parse-community/parse-server/issues/8674)) ([3dd99dd](https://github.com/parse-community/parse-server/commit/3dd99dd80e27e5e1d99b42844180546d90c7aa90))
|
||||
|
||||
# [6.2.0](https://github.com/parse-community/parse-server/compare/6.1.0...6.2.0) (2023-05-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new Parse Server option `fileUpload.fileExtensions` to restrict file upload by file extension; this fixes a security vulnerability in which a phishing attack could be performed using an uploaded HTML file; by default the new option only allows file extensions matching the regex pattern `^[^hH][^tT][^mM][^lL]?$`, which excludes HTML files; if your app currently depends on uploading files with HTML file extensions then this may be a breaking change and you could allow HTML file upload by setting the option to `['.*']` ([#8538](https://github.com/parse-community/parse-server/issues/8538)) ([a318e7b](https://github.com/parse-community/parse-server/commit/a318e7bbafcf7a3425b0a1b3c2dd30f526b4b6f9))
|
||||
|
||||
# [6.1.0](https://github.com/parse-community/parse-server/compare/6.0.0...6.1.0) (2023-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LiveQuery can return incorrectly formatted date ([#8456](https://github.com/parse-community/parse-server/issues/8456)) ([4ce135a](https://github.com/parse-community/parse-server/commit/4ce135a4fe930776044bc8fd786a4e17a0144e03))
|
||||
* Nested date is incorrectly decoded as empty object `{}` when fetching a Parse Object ([#8446](https://github.com/parse-community/parse-server/issues/8446)) ([22d2446](https://github.com/parse-community/parse-server/commit/22d2446dfea2bc339affc20535d181097e152acf))
|
||||
* Parameters missing in `afterFind` trigger of authentication adapters ([#8458](https://github.com/parse-community/parse-server/issues/8458)) ([ce34747](https://github.com/parse-community/parse-server/commit/ce34747e8af54cb0b6b975da38f779a5955d2d59))
|
||||
* Rate limiting across multiple servers via Redis not working ([#8469](https://github.com/parse-community/parse-server/issues/8469)) ([d9e347d](https://github.com/parse-community/parse-server/commit/d9e347d7413f30f58ffbb8397fc8b5ae23be6ff0))
|
||||
* Security upgrade jsonwebtoken to 9.0.0 ([#8420](https://github.com/parse-community/parse-server/issues/8420)) ([f5bfe45](https://github.com/parse-community/parse-server/commit/f5bfe4571e82b2b7440d41f3cff0d49937398164))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `afterFind` trigger to authentication adapters ([#8444](https://github.com/parse-community/parse-server/issues/8444)) ([c793bb8](https://github.com/parse-community/parse-server/commit/c793bb88e7485743c7ceb65fe419cde75833ff33))
|
||||
* Add option `schemaCacheTtl` for schema cache pulling as alternative to `enableSchemaHooks` ([#8436](https://github.com/parse-community/parse-server/issues/8436)) ([b3b76de](https://github.com/parse-community/parse-server/commit/b3b76de71b1d4265689d052e7837c38ec1fa4323))
|
||||
* Add Parse Server option `resetPasswordSuccessOnInvalidEmail` to choose success or error response on password reset with invalid email ([#7551](https://github.com/parse-community/parse-server/issues/7551)) ([e5d610e](https://github.com/parse-community/parse-server/commit/e5d610e5e487ddab86409409ac3d7362aba8f59b))
|
||||
* Add rate limiting across multiple servers via Redis ([#8394](https://github.com/parse-community/parse-server/issues/8394)) ([34833e4](https://github.com/parse-community/parse-server/commit/34833e42eec08b812b733be78df0535ab0e096b6))
|
||||
* Allow multiple origins for header `Access-Control-Allow-Origin` ([#8517](https://github.com/parse-community/parse-server/issues/8517)) ([4f15539](https://github.com/parse-community/parse-server/commit/4f15539ac244aa2d393ac5177f7604b43f69e271))
|
||||
* Deprecate LiveQuery `fields` option in favor of `keys` for semantic consistency ([#8388](https://github.com/parse-community/parse-server/issues/8388)) ([a49e323](https://github.com/parse-community/parse-server/commit/a49e323d5ae640bff1c6603ec37fdaddb9328dd1))
|
||||
* Export `AuthAdapter` to make it available for extension with custom authentication adapters ([#8443](https://github.com/parse-community/parse-server/issues/8443)) ([40c1961](https://github.com/parse-community/parse-server/commit/40c196153b8efa12ae384c1c0092b2ed60a260d6))
|
||||
|
||||
# [6.0.0](https://github.com/parse-community/parse-server/compare/5.4.0...6.0.0) (2023-01-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `ParseServer.verifyServerUrl` may fail if server response headers are missing; remove unnecessary logging ([#8391](https://github.com/parse-community/parse-server/issues/8391)) ([1c37a7c](https://github.com/parse-community/parse-server/commit/1c37a7cd0715949a70b220a629071c7dab7d5e7b))
|
||||
* Cloud Code trigger `beforeSave` does not work with `Parse.Role` ([#8320](https://github.com/parse-community/parse-server/issues/8320)) ([f29d972](https://github.com/parse-community/parse-server/commit/f29d9720e9b37918fd885c97a31e34c42750e724))
|
||||
* ES6 modules do not await the import of Cloud Code files ([#8368](https://github.com/parse-community/parse-server/issues/8368)) ([a7bd180](https://github.com/parse-community/parse-server/commit/a7bd180cddd784c8735622f22e012c342ad535fb))
|
||||
* Nested objects are encoded incorrectly for MongoDB ([#8209](https://github.com/parse-community/parse-server/issues/8209)) ([1412666](https://github.com/parse-community/parse-server/commit/1412666f75829612de6fb9d7ccae35761c9b75cb))
|
||||
* Parse Server option `masterKeyIps` does not include localhost by default for IPv6 ([#8322](https://github.com/parse-community/parse-server/issues/8322)) ([ab82635](https://github.com/parse-community/parse-server/commit/ab82635b0d4cf323a07ddee51fee587b43dce95c))
|
||||
* Rate limiter may reject requests that contain a session token ([#8399](https://github.com/parse-community/parse-server/issues/8399)) ([c114dc8](https://github.com/parse-community/parse-server/commit/c114dc8831055d74187b9dfb4c9eeb558520237c))
|
||||
* Remove Node 12 and Node 17 support ([#8279](https://github.com/parse-community/parse-server/issues/8279)) ([2546cc8](https://github.com/parse-community/parse-server/commit/2546cc8572bea6610cb9b3c7401d9afac0e3c1d6))
|
||||
* Schema without class level permissions may cause error ([#8409](https://github.com/parse-community/parse-server/issues/8409)) ([aa2cd51](https://github.com/parse-community/parse-server/commit/aa2cd51b703388d925e4572e5c2b2d883c68e49c))
|
||||
* The client IP address may be determined incorrectly in some cases; this fixes a security vulnerability in which the Parse Server option `masterKeyIps` may be circumvented, see [GHSA-vm5r-c87r-pf6x](https://github.com/parse-community/parse-server/security/advisories/GHSA-vm5r-c87r-pf6x) ([#8372](https://github.com/parse-community/parse-server/issues/8372)) ([892040d](https://github.com/parse-community/parse-server/commit/892040dc2f82a3e2abe2824e4b553521b6f894de))
|
||||
* Throwing error in Cloud Code Triggers `afterLogin`, `afterLogout` crashes server ([#8280](https://github.com/parse-community/parse-server/issues/8280)) ([130d290](https://github.com/parse-community/parse-server/commit/130d29074e3f763460e5685d0b9059e5a333caff))
|
||||
|
||||
### Features
|
||||
|
||||
* Access the internal scope of Parse Server using the new `maintenanceKey`; the internal scope contains unofficial and undocumented fields (prefixed with underscore `_`) which are used internally by Parse Server; you may want to manipulate these fields for out-of-band changes such as data migration or correction tasks; changes within the internal scope of Parse Server may happen at any time without notice or changelog entry, it is therefore recommended to look at the source code of Parse Server to understand the effects of manipulating internal fields before using the key; it is discouraged to use the `maintenanceKey` for routine operations in a production environment; see [access scopes](https://github.com/parse-community/parse-server#access-scopes) ([#8212](https://github.com/parse-community/parse-server/issues/8212)) ([f3bcc93](https://github.com/parse-community/parse-server/commit/f3bcc9365cd6f08b0a32c132e8e5ff6d1b650863))
|
||||
* Adapt `verifyServerUrl` for new asynchronous Parse Server start-up states ([#8366](https://github.com/parse-community/parse-server/issues/8366)) ([ffa4974](https://github.com/parse-community/parse-server/commit/ffa4974158615fbff4a2692b9db41dcb50d3f77b))
|
||||
* Add `ParseQuery.watch` to trigger LiveQuery only on update of specific fields ([#8028](https://github.com/parse-community/parse-server/issues/8028)) ([fc92faa](https://github.com/parse-community/parse-server/commit/fc92faac75107b3392eeddd916c4c5b45e3c5e0c))
|
||||
* Add Node 19 support ([#8363](https://github.com/parse-community/parse-server/issues/8363)) ([a4990dc](https://github.com/parse-community/parse-server/commit/a4990dcd29abcb4442f3c424aff482a0a116160f))
|
||||
* Add option to change the log level of the logs emitted by triggers ([#8328](https://github.com/parse-community/parse-server/issues/8328)) ([8f3b694](https://github.com/parse-community/parse-server/commit/8f3b694e39d4a966567e50dbea4d62e954fa5c06))
|
||||
* Add request rate limiter based on IP address ([#8174](https://github.com/parse-community/parse-server/issues/8174)) ([6c79f6a](https://github.com/parse-community/parse-server/commit/6c79f6a69e25e47846e3b0685d6bdfd6b91086b1))
|
||||
* Asynchronous initialization of Parse Server ([#8232](https://github.com/parse-community/parse-server/issues/8232)) ([99fcf45](https://github.com/parse-community/parse-server/commit/99fcf45e55c368de2345b0c4d780e70e0adf0e15))
|
||||
* Improve authentication adapter interface to support multi-factor authentication (MFA), authentication challenges, and provide a more powerful interface for writing custom authentication adapters ([#8156](https://github.com/parse-community/parse-server/issues/8156)) ([5bbf9ca](https://github.com/parse-community/parse-server/commit/5bbf9cade9a527787fd1002072d4013ab5d8db2b))
|
||||
* Reduce Docker image size by improving stages ([#8359](https://github.com/parse-community/parse-server/issues/8359)) ([40810b4](https://github.com/parse-community/parse-server/commit/40810b48ebde8b1f21d2448a3a4de0585b1b5e34))
|
||||
* Remove deprecation `DEPPS1`: Native MongoDB syntax in aggregation pipeline ([#8362](https://github.com/parse-community/parse-server/issues/8362)) ([d0d30c4](https://github.com/parse-community/parse-server/commit/d0d30c4f1394f563724644a8fc81734be538a2c0))
|
||||
* Remove deprecation `DEPPS2`: Config option `directAccess` defaults to true ([#8284](https://github.com/parse-community/parse-server/issues/8284)) ([f535ee6](https://github.com/parse-community/parse-server/commit/f535ee6ec2abba63f702127258ca49fa5b4e08c9))
|
||||
* Remove deprecation `DEPPS3`: Config option `enforcePrivateUsers` defaults to `true` ([#8283](https://github.com/parse-community/parse-server/issues/8283)) ([ed499e3](https://github.com/parse-community/parse-server/commit/ed499e32a21bab9a874a9e5367dc71248ce836c4))
|
||||
* Remove deprecation `DEPPS4`: Remove convenience method for http request `Parse.Cloud.httpRequest` ([#8287](https://github.com/parse-community/parse-server/issues/8287)) ([2d79c08](https://github.com/parse-community/parse-server/commit/2d79c0835b6a9acaf20d5c943d9b4619bb96831c))
|
||||
* Remove support for MongoDB 4.0 ([#8292](https://github.com/parse-community/parse-server/issues/8292)) ([37245f6](https://github.com/parse-community/parse-server/commit/37245f62ce83516b6b95a54b850f0274ef680478))
|
||||
* Restrict use of `masterKey` to localhost by default ([#8281](https://github.com/parse-community/parse-server/issues/8281)) ([6c16021](https://github.com/parse-community/parse-server/commit/6c16021a1f03a70a6d9e68cb64df362d07f3b693))
|
||||
* Upgrade Node Package Manager lock file `package-lock.json` to version 2 ([#8285](https://github.com/parse-community/parse-server/issues/8285)) ([ee72467](https://github.com/parse-community/parse-server/commit/ee7246733d63e4bda20401f7b00262ff03299f20))
|
||||
* Upgrade Redis 3 to 4 ([#8293](https://github.com/parse-community/parse-server/issues/8293)) ([7d622f0](https://github.com/parse-community/parse-server/commit/7d622f06a4347e0ad2cba9a4ec07d8d4fb0f67bc))
|
||||
* Upgrade Redis 3 to 4 for LiveQuery ([#8333](https://github.com/parse-community/parse-server/issues/8333)) ([b2761fb](https://github.com/parse-community/parse-server/commit/b2761fb3786b519d9bbcf35be54309d2d35da1a9))
|
||||
* Upgrade to Parse JavaScript SDK 4 ([#8332](https://github.com/parse-community/parse-server/issues/8332)) ([9092874](https://github.com/parse-community/parse-server/commit/9092874a9a482a24dfdce1dce56615702999d6b8))
|
||||
* Write log entry when request with master key is rejected as outside of `masterKeyIps` ([#8350](https://github.com/parse-community/parse-server/issues/8350)) ([e22b73d](https://github.com/parse-community/parse-server/commit/e22b73d4b700c8ff745aa81726c6680082294b45))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Docker image does not contain the git dependency anymore; if you have been using git as a transitive dependency it now needs to be explicitly installed in your Docker file, for example with `RUN apk --no-cache add git` (#8359) ([40810b4](40810b4))
|
||||
* Fields in the internal scope of Parse Server (prefixed with underscore `_`) are only returned using the new `maintenanceKey`; previously the `masterKey` allowed reading of internal fields; see [access scopes](https://github.com/parse-community/parse-server#access-scopes) for a comparison of the keys' access permissions (#8212) ([f3bcc93](f3bcc93))
|
||||
* The method `ParseServer.verifyServerUrl` now returns a promise instead of a callback. ([ffa4974](ffa4974))
|
||||
* The MongoDB aggregation pipeline requires native MongoDB syntax instead of the custom Parse Server syntax; for example pipeline stage names require a leading dollar sign like `$match` and the MongoDB document ID is referenced using `_id` instead of `objectId` (#8362) ([d0d30c4](d0d30c4))
|
||||
* The mechanism to determine the client IP address has been rewritten; to correctly determine the IP address it is now required to set the Parse Server option `trustProxy` accordingly if Parse Server runs behind a proxy server, see the express framework's [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting (#8372) ([892040d](892040d))
|
||||
* The Node Package Manager lock file `package-lock.json` is upgraded to version 2; while it is backwards with version 1 for the npm installer, consider this if you run any non-npm analysis tools that use the lock file (#8285) ([ee72467](ee72467))
|
||||
* This release introduces the asynchronous initialization of Parse Server to prevent mounting Parse Server before being ready to receive request; it changes how Parse Server is imported, initialized and started; it also removes the callback `serverStartComplete`; see the [Parse Server 6 migration guide](https://github.com/parse-community/parse-server/blob/alpha/6.0.0.md) for more details (#8232) ([99fcf45](99fcf45))
|
||||
* Nested objects are now properly stored in the database using JSON serialization; previously, due to a bug only top-level objects were serialized, but nested objects were saved as raw JSON; for example, a nested `Date` object was saved as a JSON object like `{ "__type": "Date", "iso": "2020-01-01T00:00:00.000Z" }` instead of its serialized representation `2020-01-01T00:00:00.000Z` (#8209) ([1412666](1412666))
|
||||
* The Parse Server option `enforcePrivateUsers` is set to `true` by default; in previous releases this option defaults to `false`; this change improves the default security configuration of Parse Server (#8283) ([ed499e3](ed499e3))
|
||||
* This release restricts the use of `masterKey` to localhost by default; if you are using Parse Dashboard on a different server to connect to Parse Server you need to add the IP address of the server that hosts Parse Dashboard to this option (#8281) ([6c16021](6c16021))
|
||||
* This release upgrades to Redis 4; if you are using the Redis cache adapter with Parse Server then this is a breaking change as the Redis client options have changed; see the [Redis migration guide](https://github.com/redis/node-redis/blob/redis%404.0.0/docs/v3-to-v4.md) for more details (#8293) ([7d622f0](7d622f0))
|
||||
* This release removes support for MongoDB 4.0; the new minimum supported MongoDB version is 4.2. which also removes support for the deprecated MongoDB MMAPv1 storage engine ([37245f6](37245f6))
|
||||
* Throwing an error in Cloud Code Triggers `afterLogin`, `afterLogout` returns a rejected promise; in previous releases it crashed the server if you did not handle the error on the Node.js process level; consider adapting your code if your app currently handles these errors on the Node.js process level with `process.on('unhandledRejection', ...)` ([130d290](130d290))
|
||||
* Config option `directAccess` defaults to true; set this to `false` 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`. ([f535ee6](f535ee6))
|
||||
* The convenience method for HTTP requests `Parse.Cloud.httpRequest` is removed; use your preferred 3rd party library for making HTTP requests ([2d79c08](2d79c08))
|
||||
* This release removes Node 12 and Node 17 support ([2546cc8](2546cc8))
|
||||
|
||||
# [5.4.0](https://github.com/parse-community/parse-server/compare/5.3.3...5.4.0) (2022-11-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 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))
|
||||
* 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))
|
||||
* 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))
|
||||
* 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.3](https://github.com/parse-community/parse-server/compare/5.3.2...5.3.3) (2022-11-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Prototype pollution via Cloud Code Webhooks; fixes security vulnerability [GHSA-93vw-8fm5-p2jf](https://github.com/parse-community/parse-server/security/advisories/GHSA-93vw-8fm5-p2jf) ([#8305](https://github.com/parse-community/parse-server/issues/8305)) ([60c5a73](https://github.com/parse-community/parse-server/commit/60c5a73d257e0d536056b38bdafef8b7130524d8))
|
||||
|
||||
## [5.3.2](https://github.com/parse-community/parse-server/compare/5.3.1...5.3.2) (2022-11-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `requestKeywordDenylist` can be bypassed via Cloud Code Webhooks or Triggers; fixes security vulnerability [GHSA-xprv-wvh7-qqqx](https://github.com/parse-community/parse-server/security/advisories/GHSA-xprv-wvh7-qqqx) ([#8302](https://github.com/parse-community/parse-server/issues/8302)) ([6728da1](https://github.com/parse-community/parse-server/commit/6728da1e3591db1e27031d335d64d8f25546a06f))
|
||||
|
||||
## [5.3.1](https://github.com/parse-community/parse-server/compare/5.3.0...5.3.1) (2022-11-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-prm5-8g2m-24gg](https://github.com/parse-community/parse-server/security/advisories/GHSA-prm5-8g2m-24gg) ([#8295](https://github.com/parse-community/parse-server/issues/8295)) ([50eed3c](https://github.com/parse-community/parse-server/commit/50eed3cffe80fadfb4bdac52b2783a18da2cfc4f))
|
||||
|
||||
# [5.3.0](https://github.com/parse-community/parse-server/compare/5.2.8...5.3.0) (2022-10-29)
|
||||
|
||||
|
||||
### 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.8](https://github.com/parse-community/parse-server/compare/5.2.7...5.2.8) (2022-10-14)
|
||||
|
||||
|
||||
### 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)) ([#8235](https://github.com/parse-community/parse-server/issues/8235)) ([066f296](https://github.com/parse-community/parse-server/commit/066f29673ab4030b6b5b90c0c0326f7d3fe7612a))
|
||||
|
||||
## [5.2.7](https://github.com/parse-community/parse-server/compare/5.2.6...5.2.7) (2022-09-20)
|
||||
|
||||
|
||||
### 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)) ([#8185](https://github.com/parse-community/parse-server/issues/8185)) ([ecf0814](https://github.com/parse-community/parse-server/commit/ecf0814499bde31ab6082b6e42854aa65ad2e03e))
|
||||
|
||||
## [5.2.6](https://github.com/parse-community/parse-server/compare/5.2.5...5.2.6) (2022-09-20)
|
||||
|
||||
|
||||
|
||||
+2
-4
@@ -30,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();
|
||||
}
|
||||
@@ -61,7 +59,7 @@ async function checkNodeVersions() {
|
||||
'<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
|
||||
'>=15.0.0 <16.0.0', // These versions have reached their end-of-life support date
|
||||
'>=17.0.0', // These versions are not officially supported yet
|
||||
'>=19.0.0', // These versions are not officially supported yet
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const fs = require('fs').promises;
|
||||
const { exec } = require('child_process');
|
||||
const core = require('@actions/core');
|
||||
const { nextTick } = require('process');
|
||||
const { AbortController } = require("node-abort-controller");
|
||||
(async () => {
|
||||
const [currentDefinitions, currentDocs] = await Promise.all([
|
||||
fs.readFile('./src/Options/Definitions.js', 'utf8'),
|
||||
fs.readFile('./src/Options/docs.js', 'utf8'),
|
||||
]);
|
||||
exec('npm run definitions');
|
||||
const ac = new AbortController();
|
||||
const { signal } = ac;
|
||||
const watcher = fs.watch('./src/Options/docs.js', {signal});
|
||||
let i = 0;
|
||||
// eslint-disable-next-line
|
||||
for await (const _ of watcher) {
|
||||
i++;
|
||||
if (i === 3) {
|
||||
ac.abort();
|
||||
break;
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => nextTick(resolve));
|
||||
const [newDefinitions, newDocs] = await Promise.all([
|
||||
fs.readFile('./src/Options/Definitions.js', 'utf8'),
|
||||
fs.readFile('./src/Options/docs.js', 'utf8'),
|
||||
]);
|
||||
if (currentDefinitions !== newDefinitions || currentDocs !== newDocs) {
|
||||
console.error(
|
||||
'\x1b[31m%s\x1b[0m',
|
||||
'Definitions files cannot be updated manually. Please update src/Options/index.js then run `npm run definitions` to generate definitions.'
|
||||
);
|
||||
core.error('Definitions files cannot be updated manually. Please update src/Options/index.js then run `npm run definitions` to generate definitions.');
|
||||
process.exit(1);
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
})();
|
||||
+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
|
||||
|
||||
+22
-5
@@ -1,10 +1,16 @@
|
||||
{
|
||||
"plugins": ["node_modules/jsdoc-babel", "plugins/markdown"],
|
||||
"babel": {
|
||||
"plugins": ["@babel/plugin-transform-flow-strip-types"]
|
||||
"plugins": ["@babel/plugin-transform-flow-strip-types"]
|
||||
},
|
||||
"source": {
|
||||
"include": ["./README.md", "./src/cloud-code", "./src/Options/docs.js", "./src/ParseServer.js", "./src/Adapters"],
|
||||
"include": [
|
||||
"README.md",
|
||||
"./src/cloud-code",
|
||||
"./src/Options/docs.js",
|
||||
"./src/ParseServer.js",
|
||||
"./src/Adapters"
|
||||
],
|
||||
"excludePattern": "(^|\\/|\\\\)_"
|
||||
},
|
||||
"templates": {
|
||||
@@ -17,7 +23,18 @@
|
||||
"monospaceLinks": false
|
||||
},
|
||||
"opts": {
|
||||
"template": "node_modules/@parse/minami",
|
||||
"recurse": true
|
||||
"encoding": "utf8",
|
||||
"readme": "./README.md",
|
||||
"recurse": true,
|
||||
"template": "./node_modules/clean-jsdoc-theme",
|
||||
"theme_opts": {
|
||||
"default_theme": "dark",
|
||||
"title": "<img src='../.github/parse-server-logo.png' class='logo'/>",
|
||||
"create_style": "header, .sidebar-section-title, .sidebar-title { color: #139cee !important } .logo { margin-left : 40px; margin-right: 40px }"
|
||||
}
|
||||
},
|
||||
"markdown": {
|
||||
"hardwrap": false,
|
||||
"idInHeadings": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+25530
-6421
File diff suppressed because it is too large
Load Diff
+57
-53
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "5.2.6",
|
||||
"version": "6.2.2",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -13,62 +13,64 @@
|
||||
"public_html/",
|
||||
"views/",
|
||||
"LICENSE",
|
||||
"PATENTS",
|
||||
"NOTICE",
|
||||
"postinstall.js",
|
||||
"README.md"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@apollographql/graphql-playground-html": "1.6.29",
|
||||
"@graphql-tools/links": "8.2.2",
|
||||
"@apollo/client": "3.5.8",
|
||||
"@graphql-tools/stitch": "6.2.4",
|
||||
"@graphql-tools/utils": "6.2.4",
|
||||
"@parse/fs-files-adapter": "1.2.1",
|
||||
"@parse/push-adapter": "4.1.0",
|
||||
"apollo-server-express": "2.25.2",
|
||||
"@babel/eslint-parser": "7.19.1",
|
||||
"@graphql-tools/merge": "8.3.6",
|
||||
"@graphql-tools/schema": "9.0.4",
|
||||
"@graphql-tools/utils": "8.12.0",
|
||||
"@graphql-yoga/node": "2.6.0",
|
||||
"@parse/fs-files-adapter": "1.2.2",
|
||||
"@parse/push-adapter": "4.1.2",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.19.1",
|
||||
"body-parser": "1.20.1",
|
||||
"commander": "5.1.0",
|
||||
"cors": "2.8.5",
|
||||
"deepcopy": "2.1.0",
|
||||
"express": "4.17.2",
|
||||
"follow-redirects": "1.14.8",
|
||||
"graphql": "15.7.1",
|
||||
"express": "4.18.2",
|
||||
"express-rate-limit": "6.6.0",
|
||||
"follow-redirects": "1.15.2",
|
||||
"graphql": "16.6.0",
|
||||
"graphql-list-fields": "2.0.2",
|
||||
"graphql-relay": "0.7.0",
|
||||
"graphql-relay": "0.10.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"graphql-upload": "11.0.0",
|
||||
"intersect": "1.0.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"jwks-rsa": "2.0.5",
|
||||
"ldapjs": "2.3.1",
|
||||
"ip-range-check": "0.2.0",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
"jwks-rsa": "2.1.5",
|
||||
"ldapjs": "2.3.3",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "6.0.0",
|
||||
"lru-cache": "7.12.0",
|
||||
"mime": "3.0.0",
|
||||
"mongodb": "4.3.1",
|
||||
"mongodb": "4.10.0",
|
||||
"mustache": "4.2.0",
|
||||
"parse": "3.4.1",
|
||||
"pg-monitor": "1.4.1",
|
||||
"pg-promise": "10.11.1",
|
||||
"parse": "4.0.1",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"pg-monitor": "2.0.0",
|
||||
"pg-promise": "11.3.0",
|
||||
"pluralize": "8.0.0",
|
||||
"redis": "3.1.2",
|
||||
"semver": "7.3.5",
|
||||
"rate-limit-redis": "3.0.1",
|
||||
"redis": "4.0.6",
|
||||
"semver": "7.3.8",
|
||||
"subscriptions-transport-ws": "0.11.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "8.3.2",
|
||||
"winston": "3.5.1",
|
||||
"winston-daily-rotate-file": "4.6.0",
|
||||
"ws": "8.2.3"
|
||||
"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",
|
||||
"@actions/core": "1.9.1",
|
||||
"@apollo/client": "3.6.1",
|
||||
"@babel/cli": "7.10.0",
|
||||
"@babel/core": "7.10.0",
|
||||
"@babel/core": "7.20.2",
|
||||
"@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",
|
||||
@@ -76,18 +78,18 @@
|
||||
"@semantic-release/github": "7.2.3",
|
||||
"@semantic-release/npm": "7.1.3",
|
||||
"@semantic-release/release-notes-generator": "9.0.3",
|
||||
"all-node-versions": "8.0.0",
|
||||
"apollo-upload-client": "16.0.0",
|
||||
"apollo-utilities": "1.3.4",
|
||||
"babel-eslint": "10.1.0",
|
||||
"all-node-versions": "11.3.0",
|
||||
"apollo-upload-client": "17.0.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"clean-jsdoc-theme": "4.2.7",
|
||||
"cross-env": "7.0.2",
|
||||
"deep-diff": "1.0.2",
|
||||
"eslint": "6.8.0",
|
||||
"eslint-plugin-flowtype": "5.1.3",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-plugin-flowtype": "8.0.3",
|
||||
"flow-bin": "0.119.1",
|
||||
"form-data": "3.0.0",
|
||||
"husky": "4.2.5",
|
||||
"graphql-tag": "2.12.6",
|
||||
"husky": "4.3.8",
|
||||
"jasmine": "3.5.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"jsdoc": "3.6.3",
|
||||
@@ -98,7 +100,8 @@
|
||||
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
|
||||
"mongodb-runner": "4.8.1",
|
||||
"mongodb-version-list": "1.0.0",
|
||||
"node-fetch": "3.1.1",
|
||||
"node-abort-controller": "3.0.1",
|
||||
"node-fetch": "3.2.10",
|
||||
"nyc": "15.1.0",
|
||||
"prettier": "2.0.5",
|
||||
"semantic-release": "17.4.6",
|
||||
@@ -107,25 +110,26 @@
|
||||
"scripts": {
|
||||
"ci:check": "node ./ci/ciCheck.js",
|
||||
"ci:checkNodeEngine": "node ./ci/nodeEngineCheck.js",
|
||||
"ci:definitionsCheck": "node ./ci/definitionsCheck.js",
|
||||
"definitions": "node ./resources/buildConfigDefinitions.js && prettier --write 'src/Options/*.js'",
|
||||
"docs": "jsdoc -c ./jsdoc-conf.json",
|
||||
"lint": "flow && eslint --cache ./",
|
||||
"lint-fix": "eslint --fix --cache ./",
|
||||
"build": "babel src/ -d lib/ --copy-files",
|
||||
"watch": "babel --watch src/ -d lib/ --copy-files",
|
||||
"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:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start",
|
||||
"test:mongodb:testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} 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:5.0.5": "npm run test:mongodb --dbversion=5.0.5",
|
||||
"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:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} 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-runner start",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
|
||||
"test": "npm run testonly",
|
||||
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} 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-runner stop",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
|
||||
"start": "node ./bin/parse-server",
|
||||
"prettier": "prettier --write {src,spec}/{**/*,*}.js",
|
||||
"prepare": "npm run build",
|
||||
@@ -133,7 +137,7 @@
|
||||
"madge:circular": "node_modules/.bin/madge ./src --circular"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.10 <17"
|
||||
"node": ">=14.21.0 <17 || >=18 <19"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "bin/parse-server"
|
||||
|
||||
+4
-6
@@ -24,7 +24,7 @@ const templates = {
|
||||
async function config() {
|
||||
|
||||
// Get branch
|
||||
const branch = ref.split('/').pop();
|
||||
const branch = ref.split('/').pop().split('-')[0];
|
||||
console.log(`Running on branch: ${branch}`);
|
||||
|
||||
// Set changelog file
|
||||
@@ -40,11 +40,8 @@ async function config() {
|
||||
{ name: 'alpha', prerelease: true },
|
||||
{ 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' },
|
||||
// Long-Term-Support branches; defined as GLOB pattern
|
||||
'release-+([0-9]).x.x',
|
||||
],
|
||||
dryRun: false,
|
||||
debug: true,
|
||||
@@ -88,6 +85,7 @@ async function config() {
|
||||
labels: ['type:ci'],
|
||||
releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
|
||||
}],
|
||||
// Back-merge module runs last because if it fails it should not impede the release process
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
|
||||
+145
-114
@@ -23,34 +23,39 @@ const nestedOptionTypes = [
|
||||
'PagesRoute',
|
||||
'PasswordPolicyOptions',
|
||||
'SecurityOptions',
|
||||
'SchemaOptions',
|
||||
'LogLevels',
|
||||
];
|
||||
|
||||
/** The prefix of environment variables for nested options. */
|
||||
const nestedOptionEnvPrefix = {
|
||||
'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_',
|
||||
'PagesRoute': 'PARSE_SERVER_PAGES_ROUTE_',
|
||||
'ParseServerOptions' : 'PARSE_SERVER_',
|
||||
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_',
|
||||
'SecurityOptions': 'PARSE_SERVER_SECURITY_',
|
||||
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_',
|
||||
PagesRoute: 'PARSE_SERVER_PAGES_ROUTE_',
|
||||
ParseServerOptions: 'PARSE_SERVER_',
|
||||
PasswordPolicyOptions: 'PARSE_SERVER_PASSWORD_POLICY_',
|
||||
SecurityOptions: 'PARSE_SERVER_SECURITY_',
|
||||
SchemaOptions: 'PARSE_SERVER_SCHEMA_',
|
||||
LogLevels: 'PARSE_SERVER_LOG_LEVELS_',
|
||||
RateLimitOptions: 'PARSE_SERVER_RATE_LIMIT_',
|
||||
};
|
||||
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
|
||||
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
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) {
|
||||
@@ -66,13 +71,15 @@ function toENV(key) {
|
||||
}
|
||||
|
||||
function getCommentValue(comment) {
|
||||
if (!comment) { return }
|
||||
if (!comment) {
|
||||
return;
|
||||
}
|
||||
return comment.value.trim();
|
||||
}
|
||||
|
||||
function getENVPrefix(iface) {
|
||||
if (nestedOptionEnvPrefix[iface.id.name]) {
|
||||
return nestedOptionEnvPrefix[iface.id.name]
|
||||
return nestedOptionEnvPrefix[iface.id.name];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,11 +91,11 @@ function processProperty(property, iface) {
|
||||
if (!firstComment) {
|
||||
return;
|
||||
}
|
||||
const lines = firstComment.split('\n').map((line) => line.trim());
|
||||
const lines = firstComment.split('\n').map(line => line.trim());
|
||||
let help = '';
|
||||
let envLine;
|
||||
let defaultLine;
|
||||
lines.forEach((line) => {
|
||||
lines.forEach(line => {
|
||||
if (line.indexOf(':ENV:') === 0) {
|
||||
envLine = line;
|
||||
} else if (line.indexOf(':DEFAULT:') === 0) {
|
||||
@@ -101,11 +108,13 @@ function processProperty(property, iface) {
|
||||
if (envLine) {
|
||||
env = envLine.split(' ')[1];
|
||||
} else {
|
||||
env = (prefix + toENV(name));
|
||||
env = prefix + toENV(name);
|
||||
}
|
||||
let defaultValue;
|
||||
if (defaultLine) {
|
||||
defaultValue = defaultLine.split(' ')[1];
|
||||
const defaultArray = defaultLine.split(' ');
|
||||
defaultArray.shift();
|
||||
defaultValue = defaultArray.join(' ');
|
||||
}
|
||||
let type = property.value.type;
|
||||
let isRequired = true;
|
||||
@@ -121,21 +130,20 @@ function processProperty(property, iface) {
|
||||
defaultValue,
|
||||
types: property.value.types,
|
||||
typeAnnotation: property.value.typeAnnotation,
|
||||
required: isRequired
|
||||
required: isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function doInterface(iface) {
|
||||
return iface.body.properties
|
||||
.sort((a, b) => a.key.name.localeCompare(b.key.name))
|
||||
.map((prop) => processProperty(prop, iface))
|
||||
.filter((e) => e !== undefined);
|
||||
.map(prop => processProperty(prop, iface))
|
||||
.filter(e => e !== undefined);
|
||||
}
|
||||
|
||||
function mapperFor(elt, t) {
|
||||
const p = t.identifier('parsers');
|
||||
const wrap = (identifier) => t.memberExpression(p, identifier);
|
||||
const wrap = identifier => t.memberExpression(p, identifier);
|
||||
|
||||
if (t.isNumberTypeAnnotation(elt)) {
|
||||
return t.callExpression(wrap(t.identifier('numberParser')), [t.stringLiteral(elt.name)]);
|
||||
@@ -153,6 +161,9 @@ function mapperFor(elt, t) {
|
||||
if (type == 'NumberOrBoolean') {
|
||||
return wrap(t.identifier('numberOrBooleanParser'));
|
||||
}
|
||||
if (type === 'StringOrStringArray') {
|
||||
return wrap(t.identifier('arrayParser'));
|
||||
}
|
||||
return wrap(t.identifier('objectParser'));
|
||||
}
|
||||
}
|
||||
@@ -169,27 +180,29 @@ function parseDefaultValue(elt, value, t) {
|
||||
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
|
||||
} else if (t.isArrayTypeAnnotation(elt)) {
|
||||
const array = parsers.objectParser(value);
|
||||
literalValue = t.arrayExpression(array.map((value) => {
|
||||
if (typeof value == 'string') {
|
||||
return t.stringLiteral(value);
|
||||
} else if (typeof value == 'number') {
|
||||
return t.numericLiteral(value);
|
||||
} else if (typeof value == 'object') {
|
||||
const object = parsers.objectParser(value);
|
||||
const props = Object.entries(object).map(([k, v]) => {
|
||||
if (typeof v == 'string') {
|
||||
return t.objectProperty(t.identifier(k), t.stringLiteral(v));
|
||||
} else if (typeof v == 'number') {
|
||||
return t.objectProperty(t.identifier(k), t.numericLiteral(v));
|
||||
} else if (typeof v == 'boolean') {
|
||||
return t.objectProperty(t.identifier(k), t.booleanLiteral(v));
|
||||
}
|
||||
});
|
||||
return t.objectExpression(props);
|
||||
} else {
|
||||
throw new Error('Unable to parse array');
|
||||
}
|
||||
}));
|
||||
literalValue = t.arrayExpression(
|
||||
array.map(value => {
|
||||
if (typeof value == 'string') {
|
||||
return t.stringLiteral(value);
|
||||
} else if (typeof value == 'number') {
|
||||
return t.numericLiteral(value);
|
||||
} else if (typeof value == 'object') {
|
||||
const object = parsers.objectParser(value);
|
||||
const props = Object.entries(object).map(([k, v]) => {
|
||||
if (typeof v == 'string') {
|
||||
return t.objectProperty(t.identifier(k), t.stringLiteral(v));
|
||||
} else if (typeof v == 'number') {
|
||||
return t.objectProperty(t.identifier(k), t.numericLiteral(v));
|
||||
} else if (typeof v == 'boolean') {
|
||||
return t.objectProperty(t.identifier(k), t.booleanLiteral(v));
|
||||
}
|
||||
});
|
||||
return t.objectExpression(props);
|
||||
} else {
|
||||
throw new Error('Unable to parse array');
|
||||
}
|
||||
})
|
||||
);
|
||||
} else if (t.isAnyTypeAnnotation(elt)) {
|
||||
literalValue = t.arrayExpression([]);
|
||||
} else if (t.isBooleanTypeAnnotation(elt)) {
|
||||
@@ -202,15 +215,16 @@ function parseDefaultValue(elt, value, t) {
|
||||
|
||||
if (nestedOptionTypes.includes(type)) {
|
||||
const object = parsers.objectParser(value);
|
||||
const props = Object.keys(object).map((key) => {
|
||||
const props = Object.keys(object).map(key => {
|
||||
return t.objectProperty(key, object[value]);
|
||||
});
|
||||
literalValue = t.objectExpression(props);
|
||||
}
|
||||
if (type == 'ProtectedFields') {
|
||||
const prop = t.objectProperty(
|
||||
t.stringLiteral('_User'), t.objectPattern([
|
||||
t.objectProperty(t.stringLiteral('*'), t.arrayExpression([t.stringLiteral('email')]))
|
||||
t.stringLiteral('_User'),
|
||||
t.objectPattern([
|
||||
t.objectProperty(t.stringLiteral('*'), t.arrayExpression([t.stringLiteral('email')])),
|
||||
])
|
||||
);
|
||||
literalValue = t.objectExpression([prop]);
|
||||
@@ -221,98 +235,115 @@ function parseDefaultValue(elt, value, t) {
|
||||
|
||||
function inject(t, list) {
|
||||
let comments = '';
|
||||
const results = list.map((elt) => {
|
||||
if (!elt.name) {
|
||||
return;
|
||||
}
|
||||
const props = ['env', 'help'].map((key) => {
|
||||
if (elt[key]) {
|
||||
return t.objectProperty(t.stringLiteral(key), t.stringLiteral(elt[key]));
|
||||
const results = list
|
||||
.map(elt => {
|
||||
if (!elt.name) {
|
||||
return;
|
||||
}
|
||||
}).filter((e) => e !== undefined);
|
||||
if (elt.required) {
|
||||
props.push(t.objectProperty(t.stringLiteral('required'), t.booleanLiteral(true)))
|
||||
}
|
||||
const action = mapperFor(elt, t);
|
||||
if (action) {
|
||||
props.push(t.objectProperty(t.stringLiteral('action'), action))
|
||||
}
|
||||
if (elt.defaultValue) {
|
||||
const parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
if (parsedValue) {
|
||||
props.push(t.objectProperty(t.stringLiteral('default'), parsedValue));
|
||||
} else {
|
||||
throw new Error(`Unable to parse value for ${elt.name} `);
|
||||
const props = ['env', 'help']
|
||||
.map(key => {
|
||||
if (elt[key]) {
|
||||
return t.objectProperty(t.stringLiteral(key), t.stringLiteral(elt[key]));
|
||||
}
|
||||
})
|
||||
.filter(e => e !== undefined);
|
||||
if (elt.required) {
|
||||
props.push(t.objectProperty(t.stringLiteral('required'), t.booleanLiteral(true)));
|
||||
}
|
||||
}
|
||||
let type = elt.type.replace('TypeAnnotation', '');
|
||||
if (type === 'Generic') {
|
||||
type = elt.typeAnnotation.id.name;
|
||||
}
|
||||
if (type === 'Array') {
|
||||
type = elt.typeAnnotation.elementType.id
|
||||
? `${elt.typeAnnotation.elementType.id.name}[]`
|
||||
: `${elt.typeAnnotation.elementType.type.replace('TypeAnnotation', '')}[]`;
|
||||
}
|
||||
if (type === 'NumberOrBoolean') {
|
||||
type = 'Number|Boolean';
|
||||
}
|
||||
if (type === 'NumberOrString') {
|
||||
type = 'Number|String';
|
||||
}
|
||||
if (type === 'Adapter') {
|
||||
const adapterType = elt.typeAnnotation.typeParameters.params[0].id.name;
|
||||
type = `Adapter<${adapterType}>`;
|
||||
}
|
||||
comments += ` * @property {${type}} ${elt.name} ${elt.help}\n`;
|
||||
const obj = t.objectExpression(props);
|
||||
return t.objectProperty(t.stringLiteral(elt.name), obj);
|
||||
}).filter((elt) => {
|
||||
return elt != undefined;
|
||||
});
|
||||
const action = mapperFor(elt, t);
|
||||
if (action) {
|
||||
props.push(t.objectProperty(t.stringLiteral('action'), action));
|
||||
}
|
||||
if (elt.defaultValue) {
|
||||
const parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
if (parsedValue) {
|
||||
props.push(t.objectProperty(t.stringLiteral('default'), parsedValue));
|
||||
} else {
|
||||
throw new Error(`Unable to parse value for ${elt.name} `);
|
||||
}
|
||||
}
|
||||
let type = elt.type.replace('TypeAnnotation', '');
|
||||
if (type === 'Generic') {
|
||||
type = elt.typeAnnotation.id.name;
|
||||
}
|
||||
if (type === 'Array') {
|
||||
type = elt.typeAnnotation.elementType.id
|
||||
? `${elt.typeAnnotation.elementType.id.name}[]`
|
||||
: `${elt.typeAnnotation.elementType.type.replace('TypeAnnotation', '')}[]`;
|
||||
}
|
||||
if (type === 'NumberOrBoolean') {
|
||||
type = 'Number|Boolean';
|
||||
}
|
||||
if (type === 'NumberOrString') {
|
||||
type = 'Number|String';
|
||||
}
|
||||
if (type === 'Adapter') {
|
||||
const adapterType = elt.typeAnnotation.typeParameters.params[0].id.name;
|
||||
type = `Adapter<${adapterType}>`;
|
||||
}
|
||||
if (type === 'StringOrStringArray') {
|
||||
type = 'String|String[]';
|
||||
}
|
||||
comments += ` * @property {${type}} ${elt.name} ${elt.help}\n`;
|
||||
const obj = t.objectExpression(props);
|
||||
return t.objectProperty(t.stringLiteral(elt.name), obj);
|
||||
})
|
||||
.filter(elt => {
|
||||
return elt != undefined;
|
||||
});
|
||||
return { results, comments };
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
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 = ``;
|
||||
const plugin = function (babel) {
|
||||
const t = babel.types;
|
||||
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') {
|
||||
if (
|
||||
path.node &&
|
||||
path.node.declaration &&
|
||||
path.node.declaration.type == 'InterfaceDeclaration'
|
||||
) {
|
||||
const { results, comments } = inject(t, doInterface(path.node.declaration));
|
||||
const id = path.node.declaration.id.name;
|
||||
const exports = t.memberExpression(moduleExports, t.identifier(id));
|
||||
docs += `/**\n * @interface ${id}\n${comments} */\n\n`;
|
||||
path.replaceWith(
|
||||
t.assignmentExpression('=', exports, t.objectExpression(results))
|
||||
)
|
||||
path.replaceWith(t.assignmentExpression('=', exports, t.objectExpression(results)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const auxiliaryCommentBefore = `
|
||||
**** GENERATED CODE ****
|
||||
This code has been generated by resources/buildConfigDefinitions.js
|
||||
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 babel = require('@babel/core');
|
||||
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
-3
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"plugins": [
|
||||
"transform-object-rest-spread"
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
],
|
||||
"presets": [
|
||||
["env", {
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": "8"
|
||||
"node": "14"
|
||||
}
|
||||
}]
|
||||
],
|
||||
|
||||
+5
-2
@@ -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,8 +34,8 @@
|
||||
"jequal": true,
|
||||
"create": true,
|
||||
"arrayContains": true,
|
||||
"expectAsync": true,
|
||||
"databaseAdapter": true
|
||||
"databaseAdapter": true,
|
||||
"databaseURI": true
|
||||
},
|
||||
"rules": {
|
||||
"no-console": [0],
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
const AggregateRouter = require('../lib/Routers/AggregateRouter').AggregateRouter;
|
||||
|
||||
describe('AggregateRouter', () => {
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Array', () => {
|
||||
const body = [
|
||||
{
|
||||
group: { objectId: {} },
|
||||
$group: { _id: {} },
|
||||
},
|
||||
];
|
||||
const expected = [{ $group: { _id: {} } }];
|
||||
@@ -13,22 +12,20 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Object', () => {
|
||||
const body = {
|
||||
group: { objectId: {} },
|
||||
$group: { _id: {} },
|
||||
};
|
||||
const expected = [{ $group: { _id: {} } }];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Pipeline Operator (Array)', () => {
|
||||
const body = {
|
||||
pipeline: [
|
||||
{
|
||||
group: { objectId: {} },
|
||||
$group: { _id: {} },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -37,11 +34,10 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Pipeline Operator (Object)', () => {
|
||||
const body = {
|
||||
pipeline: {
|
||||
group: { objectId: {} },
|
||||
$group: { _id: {} },
|
||||
},
|
||||
};
|
||||
const expected = [{ $group: { _id: {} } }];
|
||||
@@ -49,43 +45,42 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline fails multiple keys in Array stage ', () => {
|
||||
const body = [
|
||||
{
|
||||
group: { objectId: {} },
|
||||
match: { name: 'Test' },
|
||||
$group: { _id: {} },
|
||||
$match: { name: 'Test' },
|
||||
},
|
||||
];
|
||||
try {
|
||||
AggregateRouter.getPipeline(body);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Pipeline stages should only have one key found group, match');
|
||||
}
|
||||
expect(() => AggregateRouter.getPipeline(body)).toThrow(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
'Pipeline stages should only have one key but found $group, $match.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline fails multiple keys in Pipeline Operator Array stage ', () => {
|
||||
const body = {
|
||||
pipeline: [
|
||||
{
|
||||
group: { objectId: {} },
|
||||
match: { name: 'Test' },
|
||||
$group: { _id: {} },
|
||||
$match: { name: 'Test' },
|
||||
},
|
||||
],
|
||||
};
|
||||
try {
|
||||
AggregateRouter.getPipeline(body);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Pipeline stages should only have one key found group, match');
|
||||
}
|
||||
expect(() => AggregateRouter.getPipeline(body)).toThrow(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
'Pipeline stages should only have one key but found $group, $match.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get search pipeline from Pipeline Operator (Array)', () => {
|
||||
const body = {
|
||||
pipeline: {
|
||||
search: {},
|
||||
$search: {},
|
||||
},
|
||||
};
|
||||
const expected = [{ $search: {} }];
|
||||
@@ -105,7 +100,7 @@ describe('AggregateRouter', () => {
|
||||
it('support nested stage names starting with `$`', () => {
|
||||
const body = [
|
||||
{
|
||||
lookup: {
|
||||
$lookup: {
|
||||
from: 'ACollection',
|
||||
let: { id: '_id' },
|
||||
as: 'results',
|
||||
@@ -145,11 +140,11 @@ describe('AggregateRouter', () => {
|
||||
|
||||
it('support the use of `_id` in stages', () => {
|
||||
const body = [
|
||||
{ match: { _id: 'randomId' } },
|
||||
{ sort: { _id: -1 } },
|
||||
{ addFields: { _id: 1 } },
|
||||
{ group: { _id: {} } },
|
||||
{ project: { _id: 0 } },
|
||||
{ $match: { _id: 'randomId' } },
|
||||
{ $sort: { _id: -1 } },
|
||||
{ $addFields: { _id: 1 } },
|
||||
{ $group: { _id: {} } },
|
||||
{ $project: { _id: 0 } },
|
||||
];
|
||||
const expected = [
|
||||
{ $match: { _id: 'randomId' } },
|
||||
@@ -161,4 +156,19 @@ describe('AggregateRouter', () => {
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should throw with invalid stage', () => {
|
||||
expect(() => AggregateRouter.getPipeline([{ foo: 'bar' }])).toThrow(
|
||||
new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid aggregate stage 'foo'.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with invalid group', () => {
|
||||
expect(() => AggregateRouter.getPipeline([{ $group: { objectId: 'bar' } }])).toThrow(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Cannot use 'objectId' in aggregation stage $group.`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -256,6 +256,49 @@ describe('AuthenticationProviders', function () {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should support loginWith with session token and with/without mutated authData', async () => {
|
||||
const fakeAuthProvider = {
|
||||
validateAppId: () => Promise.resolve(),
|
||||
validateAuthData: () => Promise.resolve(),
|
||||
};
|
||||
const payload = { authData: { id: 'user1', token: 'fakeToken' } };
|
||||
const payload2 = { authData: { id: 'user1', token: 'fakeToken2' } };
|
||||
await reconfigureServer({ auth: { fakeAuthProvider } });
|
||||
const user = await Parse.User.logInWith('fakeAuthProvider', payload);
|
||||
const user2 = await Parse.User.logInWith('fakeAuthProvider', payload, {
|
||||
sessionToken: user.getSessionToken(),
|
||||
});
|
||||
const user3 = await Parse.User.logInWith('fakeAuthProvider', payload2, {
|
||||
sessionToken: user2.getSessionToken(),
|
||||
});
|
||||
expect(user.id).toEqual(user2.id);
|
||||
expect(user.id).toEqual(user3.id);
|
||||
});
|
||||
|
||||
it('should support sync/async validateAppId', async () => {
|
||||
const syncProvider = {
|
||||
validateAppId: () => true,
|
||||
appIds: 'test',
|
||||
validateAuthData: () => Promise.resolve(),
|
||||
};
|
||||
const asyncProvider = {
|
||||
appIds: 'test',
|
||||
validateAppId: () => Promise.resolve(true),
|
||||
validateAuthData: () => Promise.resolve(),
|
||||
};
|
||||
const payload = { authData: { id: 'user1', token: 'fakeToken' } };
|
||||
const syncSpy = spyOn(syncProvider, 'validateAppId');
|
||||
const asyncSpy = spyOn(asyncProvider, 'validateAppId');
|
||||
|
||||
await reconfigureServer({ auth: { asyncProvider, syncProvider } });
|
||||
const user = await Parse.User.logInWith('asyncProvider', payload);
|
||||
const user2 = await Parse.User.logInWith('syncProvider', payload);
|
||||
expect(user.getSessionToken()).toBeDefined();
|
||||
expect(user2.getSessionToken()).toBeDefined();
|
||||
expect(syncSpy).toHaveBeenCalledTimes(1);
|
||||
expect(asyncSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('unlink and link with custom provider', async () => {
|
||||
const provider = getMockMyOauthProvider();
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
@@ -339,10 +382,10 @@ describe('AuthenticationProviders', function () {
|
||||
});
|
||||
|
||||
validateAuthenticationHandler(authenticationHandler);
|
||||
const validator = authenticationHandler.getValidatorForProvider('customAuthentication');
|
||||
const { validator } = authenticationHandler.getValidatorForProvider('customAuthentication');
|
||||
validateValidator(validator);
|
||||
|
||||
validator(validAuthData).then(
|
||||
validator(validAuthData, {}, {}).then(
|
||||
() => {
|
||||
expect(authDataSpy).toHaveBeenCalled();
|
||||
// AppIds are not provided in the adapter, should not be called
|
||||
@@ -362,12 +405,15 @@ describe('AuthenticationProviders', function () {
|
||||
});
|
||||
|
||||
validateAuthenticationHandler(authenticationHandler);
|
||||
const validator = authenticationHandler.getValidatorForProvider('customAuthentication');
|
||||
const { validator } = authenticationHandler.getValidatorForProvider('customAuthentication');
|
||||
validateValidator(validator);
|
||||
|
||||
validator({
|
||||
token: 'my-token',
|
||||
}).then(
|
||||
validator(
|
||||
{
|
||||
token: 'my-token',
|
||||
},
|
||||
{},
|
||||
{}
|
||||
).then(
|
||||
() => {
|
||||
done();
|
||||
},
|
||||
@@ -387,12 +433,16 @@ describe('AuthenticationProviders', function () {
|
||||
});
|
||||
|
||||
validateAuthenticationHandler(authenticationHandler);
|
||||
const validator = authenticationHandler.getValidatorForProvider('customAuthentication');
|
||||
const { validator } = authenticationHandler.getValidatorForProvider('customAuthentication');
|
||||
validateValidator(validator);
|
||||
|
||||
validator({
|
||||
token: 'valid-token',
|
||||
}).then(
|
||||
validator(
|
||||
{
|
||||
token: 'valid-token',
|
||||
},
|
||||
{},
|
||||
{}
|
||||
).then(
|
||||
() => {
|
||||
done();
|
||||
},
|
||||
@@ -419,6 +469,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 +514,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 +573,36 @@ 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 deprecate', async () => {
|
||||
await reconfigureServer();
|
||||
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: 'Using the authentication adapter "myoauth" without explicitly enabling it',
|
||||
solution:
|
||||
'Enable the authentication adapter by setting the Parse Server option "auth.myoauth.enabled: true".',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('instagram auth adapter', () => {
|
||||
@@ -522,6 +648,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 +671,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 +690,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 +706,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 +730,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 +752,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 +1455,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 +1490,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 +1512,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 +1529,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 +1566,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 +1588,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 +1610,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 +1630,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 +1658,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 +1687,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 +1759,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' };
|
||||
};
|
||||
@@ -1654,6 +1782,8 @@ 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: {
|
||||
@@ -1671,6 +1801,7 @@ describe('Apple Game Center Auth adapter', () => {
|
||||
providerOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('validateAuthData should validate', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
@@ -1702,6 +1833,8 @@ describe('Apple Game Center Auth adapter', () => {
|
||||
});
|
||||
|
||||
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',
|
||||
{}
|
||||
@@ -1813,7 +1946,7 @@ describe('Apple Game Center Auth adapter', () => {
|
||||
);
|
||||
|
||||
const duration = new Date().getTime() - previous.getTime();
|
||||
expect(duration).toEqual(0);
|
||||
expect(duration <= 1).toBe(true);
|
||||
});
|
||||
|
||||
it('adapter should throw', async () => {
|
||||
@@ -1893,6 +2026,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 () => {
|
||||
@@ -1931,7 +2065,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' },
|
||||
@@ -1955,7 +2089,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 {
|
||||
@@ -1977,7 +2111,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',
|
||||
@@ -2019,7 +2153,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',
|
||||
@@ -2046,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',
|
||||
@@ -2073,7 +2207,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',
|
||||
@@ -2098,7 +2232,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',
|
||||
@@ -2131,7 +2265,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',
|
||||
@@ -2165,7 +2299,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',
|
||||
@@ -2251,7 +2385,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',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+45
-36
@@ -219,17 +219,14 @@ describe('execution', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('shoud start Parse Server', done => {
|
||||
childProcess = spawn(binPath, [
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
'mongodb://localhost/test',
|
||||
'--port',
|
||||
'1339',
|
||||
]);
|
||||
it('should start Parse Server', done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
|
||||
childProcess = spawn(
|
||||
binPath,
|
||||
['--appId', 'test', '--masterKey', 'test', '--databaseURI', databaseURI, '--port', '1339'],
|
||||
{ env }
|
||||
);
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
if (data.includes('parse-server running on')) {
|
||||
@@ -241,18 +238,24 @@ describe('execution', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shoud start Parse Server with GraphQL', done => {
|
||||
childProcess = spawn(binPath, [
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
'mongodb://localhost/test',
|
||||
'--port',
|
||||
'1340',
|
||||
'--mountGraphQL',
|
||||
]);
|
||||
it('should start Parse Server with GraphQL', async done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
|
||||
childProcess = spawn(
|
||||
binPath,
|
||||
[
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
databaseURI,
|
||||
'--port',
|
||||
'1340',
|
||||
'--mountGraphQL',
|
||||
],
|
||||
{ env }
|
||||
);
|
||||
let output = '';
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
@@ -267,19 +270,25 @@ describe('execution', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shoud start Parse Server with GraphQL and Playground', done => {
|
||||
childProcess = spawn(binPath, [
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
'mongodb://localhost/test',
|
||||
'--port',
|
||||
'1341',
|
||||
'--mountGraphQL',
|
||||
'--mountPlayground',
|
||||
]);
|
||||
it('should start Parse Server with GraphQL and Playground', async done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
|
||||
childProcess = spawn(
|
||||
binPath,
|
||||
[
|
||||
'--appId',
|
||||
'test',
|
||||
'--masterKey',
|
||||
'test',
|
||||
'--databaseURI',
|
||||
databaseURI,
|
||||
'--port',
|
||||
'1341',
|
||||
'--mountGraphQL',
|
||||
'--mountPlayground',
|
||||
],
|
||||
{ env }
|
||||
);
|
||||
let output = '';
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
|
||||
@@ -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();
|
||||
|
||||
+360
-43
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
const Config = require('../lib/Config');
|
||||
const Parse = require('parse/node');
|
||||
const ParseServer = require('../lib/index').ParseServer;
|
||||
const request = require('../lib/request');
|
||||
const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter')
|
||||
.InMemoryCacheAdapter;
|
||||
@@ -39,6 +40,47 @@ describe('Cloud Code', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can load cloud code as a module', async () => {
|
||||
process.env.npm_package_type = 'module';
|
||||
await reconfigureServer({ appId: 'test1', cloud: './spec/cloud/cloudCodeModuleFile.js' });
|
||||
const result = await Parse.Cloud.run('cloudCodeInFile');
|
||||
expect(result).toEqual('It is possible to define cloud code in a file.');
|
||||
delete process.env.npm_package_type;
|
||||
});
|
||||
|
||||
it('cloud code must be valid type', async () => {
|
||||
await expectAsync(reconfigureServer({ cloud: true })).toBeRejectedWith(
|
||||
"argument 'cloud' must either be a string or a function"
|
||||
);
|
||||
});
|
||||
|
||||
it('should wait for cloud code to load', async () => {
|
||||
await reconfigureServer({ appId: 'test3' });
|
||||
const initiated = new Date();
|
||||
const parseServer = await new ParseServer({
|
||||
...defaultConfiguration,
|
||||
appId: 'test3',
|
||||
masterKey: 'test',
|
||||
serverURL: 'http://localhost:12668/parse',
|
||||
async cloud() {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
Parse.Cloud.beforeSave('Test', () => {
|
||||
throw 'Cannot save.';
|
||||
});
|
||||
},
|
||||
}).start();
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
app.use('/parse', parseServer.app);
|
||||
const server = app.listen(12668);
|
||||
const now = new Date();
|
||||
expect(now.getTime() - initiated.getTime() > 1000).toBeTrue();
|
||||
await expectAsync(new Parse.Object('Test').save()).toBeRejectedWith(
|
||||
new Parse.Error(141, 'Cannot save.')
|
||||
);
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
});
|
||||
|
||||
it('can create functions', done => {
|
||||
Parse.Cloud.define('hello', () => {
|
||||
return 'Hello world!';
|
||||
@@ -50,6 +92,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 +1546,152 @@ 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('create role with name and ACL and a beforeSave', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.Role, ({ object }) => {
|
||||
return object;
|
||||
});
|
||||
|
||||
const obj = new Parse.Role('TestRole', new Parse.ACL({ '*': { read: true, write: true } }));
|
||||
await obj.save();
|
||||
|
||||
expect(obj.getACL()).toEqual(new Parse.ACL({ '*': { read: true, write: true } }));
|
||||
expect(obj.get('name')).toEqual('TestRole');
|
||||
await obj.fetch();
|
||||
|
||||
expect(obj.getACL()).toEqual(new Parse.ACL({ '*': { read: true, write: true } }));
|
||||
expect(obj.get('name')).toEqual('TestRole');
|
||||
});
|
||||
|
||||
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();
|
||||
@@ -1546,25 +1744,6 @@ describe('Cloud Code', () => {
|
||||
obj.save().then(done, done.fail);
|
||||
});
|
||||
|
||||
it('can deprecate Parse.Cloud.httpRequest', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'warn').and.callFake(() => {});
|
||||
Parse.Cloud.define('hello', () => {
|
||||
return 'Hello world!';
|
||||
});
|
||||
await Parse.Cloud.httpRequest({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/hello',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
});
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
'DeprecationWarning: Parse.Cloud.httpRequest is deprecated and will be removed in a future version. Use a http request library instead.'
|
||||
);
|
||||
});
|
||||
|
||||
describe('cloud jobs', () => {
|
||||
it('should define a job', done => {
|
||||
expect(() => {
|
||||
@@ -1860,6 +2039,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', () => {
|
||||
@@ -2172,6 +2381,35 @@ describe('beforeFind hooks', () => {
|
||||
})
|
||||
.then(() => done());
|
||||
});
|
||||
|
||||
it('should run beforeFind on pointers and array of pointers from an object', async () => {
|
||||
const obj1 = new Parse.Object('TestObject');
|
||||
const obj2 = new Parse.Object('TestObject2');
|
||||
const obj3 = new Parse.Object('TestObject');
|
||||
obj2.set('aField', 'aFieldValue');
|
||||
await obj2.save();
|
||||
obj1.set('pointerField', obj2);
|
||||
obj3.set('pointerFieldArray', [obj2]);
|
||||
await obj1.save();
|
||||
await obj3.save();
|
||||
const spy = jasmine.createSpy('beforeFindSpy');
|
||||
Parse.Cloud.beforeFind('TestObject2', spy);
|
||||
const query = new Parse.Query('TestObject');
|
||||
await query.get(obj1.id);
|
||||
// Pointer not included in query so we don't expect beforeFind to be called
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
const query2 = new Parse.Query('TestObject');
|
||||
query2.include('pointerField');
|
||||
const res = await query2.get(obj1.id);
|
||||
expect(res.get('pointerField').get('aField')).toBe('aFieldValue');
|
||||
// Pointer included in query so we expect beforeFind to be called
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const query3 = new Parse.Query('TestObject');
|
||||
query3.include('pointerFieldArray');
|
||||
const res2 = await query3.get(obj3.id);
|
||||
expect(res2.get('pointerFieldArray')[0].get('aField')).toBe('aFieldValue');
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterFind hooks', () => {
|
||||
@@ -2565,7 +2803,7 @@ describe('afterFind hooks', () => {
|
||||
const obj = new Parse.Object('MyObject');
|
||||
const pipeline = [
|
||||
{
|
||||
group: { objectId: {} },
|
||||
$group: { _id: {} },
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -2952,6 +3190,36 @@ describe('beforeLogin hook', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('does not crash server when throwing in afterLogin hook', async () => {
|
||||
const error = new Parse.Error(2000, 'afterLogin error');
|
||||
const trigger = {
|
||||
afterLogin() {
|
||||
throw error;
|
||||
},
|
||||
};
|
||||
const spy = spyOn(trigger, 'afterLogin').and.callThrough();
|
||||
Parse.Cloud.afterLogin(trigger.afterLogin);
|
||||
await Parse.User.signUp('user', 'pass');
|
||||
const response = await Parse.User.logIn('user', 'pass').catch(e => e);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(response).toEqual(error);
|
||||
});
|
||||
|
||||
it('does not crash server when throwing in afterLogout hook', async () => {
|
||||
const error = new Parse.Error(2000, 'afterLogout error');
|
||||
const trigger = {
|
||||
afterLogout() {
|
||||
throw error;
|
||||
},
|
||||
};
|
||||
const spy = spyOn(trigger, 'afterLogout').and.callThrough();
|
||||
Parse.Cloud.afterLogout(trigger.afterLogout);
|
||||
await Parse.User.signUp('user', 'pass');
|
||||
const response = await Parse.User.logOut().catch(e => e);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(response).toEqual(error);
|
||||
});
|
||||
|
||||
it('should have expected data in request', async done => {
|
||||
Parse.Cloud.beforeLogin(req => {
|
||||
expect(req.object).toBeDefined();
|
||||
@@ -3245,7 +3513,7 @@ 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;
|
||||
@@ -3260,7 +3528,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');
|
||||
@@ -3274,8 +3542,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');
|
||||
@@ -3303,8 +3571,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' });
|
||||
@@ -3337,8 +3605,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');
|
||||
@@ -3366,7 +3634,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 });
|
||||
@@ -3380,13 +3648,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');
|
||||
@@ -3399,7 +3667,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';
|
||||
@@ -3413,11 +3681,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' });
|
||||
@@ -3429,13 +3697,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();
|
||||
@@ -3446,7 +3714,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');
|
||||
@@ -3458,7 +3726,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');
|
||||
@@ -3472,12 +3740,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');
|
||||
@@ -3489,7 +3757,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');
|
||||
@@ -3498,7 +3766,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 {
|
||||
@@ -3512,7 +3780,7 @@ describe('saveFile hooks', () => {
|
||||
});
|
||||
|
||||
it('throw empty error from beforeSaveFile', async done => {
|
||||
Parse.Cloud.beforeSaveFile(() => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw null;
|
||||
});
|
||||
try {
|
||||
@@ -3524,6 +3792,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', () => {
|
||||
|
||||
@@ -182,6 +182,41 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should log cloud function triggers using the custom log level', async () => {
|
||||
Parse.Cloud.beforeSave('TestClass', () => {});
|
||||
Parse.Cloud.afterSave('TestClass', () => {});
|
||||
|
||||
const execTest = async (logLevel, triggerBeforeSuccess, triggerAfter) => {
|
||||
await reconfigureServer({
|
||||
silent: true,
|
||||
logLevel,
|
||||
logLevels: {
|
||||
triggerAfter,
|
||||
triggerBeforeSuccess,
|
||||
},
|
||||
});
|
||||
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
const obj = new Parse.Object('TestClass');
|
||||
await obj.save();
|
||||
|
||||
return {
|
||||
beforeSave: spy.calls
|
||||
.allArgs()
|
||||
.find(log => log[1].startsWith('beforeSave triggered for TestClass for user '))?.[0],
|
||||
afterSave: spy.calls
|
||||
.allArgs()
|
||||
.find(log => log[1].startsWith('afterSave triggered for TestClass for user '))?.[0],
|
||||
};
|
||||
};
|
||||
|
||||
let calls = await execTest('silly', 'silly', 'debug');
|
||||
expect(calls).toEqual({ beforeSave: 'silly', afterSave: 'debug' });
|
||||
|
||||
calls = await execTest('info', 'warn', 'debug');
|
||||
expect(calls).toEqual({ beforeSave: 'warn', afterSave: undefined });
|
||||
});
|
||||
|
||||
it('should log cloud function failure', done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
throw 'it failed!';
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -595,7 +631,7 @@ describe('DefinedSchemas', () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(DefinedSchemas.prototype, 'wait').and.resolveTo();
|
||||
spyOn(logger, 'error').and.callThrough();
|
||||
spyOn(Parse.Schema, 'all').and.callFake(() => {
|
||||
spyOn(DefinedSchemas.prototype, 'createDeleteSession').and.callFake(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
@@ -641,4 +677,33 @@ describe('DefinedSchemas', () => {
|
||||
expect(testSchema.classLevelPermissions.create).toEqual({ requiresAuthentication: true });
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should not affect cacheAdapter', async () => {
|
||||
const server = await reconfigureServer();
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callThrough();
|
||||
const migrationOptions = {
|
||||
definitions: [
|
||||
{
|
||||
className: 'Test',
|
||||
fields: { aField: { type: 'String' } },
|
||||
indexes: { aField: { aField: 1 } },
|
||||
classLevelPermissions: {
|
||||
create: { requiresAuthentication: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const cacheAdapter = {
|
||||
get: () => Promise.resolve(null),
|
||||
put: () => {},
|
||||
del: () => {},
|
||||
clear: () => {},
|
||||
connect: jasmine.createSpy('clear'),
|
||||
};
|
||||
server.config.cacheAdapter = cacheAdapter;
|
||||
await new DefinedSchemas(migrationOptions, server.config).execute();
|
||||
expect(cacheAdapter.connect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const Auth = require('../lib/Auth');
|
||||
const Config = require('../lib/Config');
|
||||
const request = require('../lib/request');
|
||||
|
||||
@@ -262,9 +263,14 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
})
|
||||
.then(() => {
|
||||
const config = Config.get('test');
|
||||
return config.database.find('_User', {
|
||||
username: 'sets_email_verify_token_expires_at',
|
||||
});
|
||||
return config.database.find(
|
||||
'_User',
|
||||
{
|
||||
username: 'sets_email_verify_token_expires_at',
|
||||
},
|
||||
{},
|
||||
Auth.maintenance(config)
|
||||
);
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(1);
|
||||
@@ -499,7 +505,12 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
.then(() => {
|
||||
const config = Config.get('test');
|
||||
return config.database
|
||||
.find('_User', { username: 'newEmailVerifyTokenOnEmailReset' })
|
||||
.find(
|
||||
'_User',
|
||||
{ username: 'newEmailVerifyTokenOnEmailReset' },
|
||||
{},
|
||||
Auth.maintenance(config)
|
||||
)
|
||||
.then(results => {
|
||||
return results[0];
|
||||
});
|
||||
@@ -582,7 +593,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
// query for this user again
|
||||
const config = Config.get('test');
|
||||
return config.database
|
||||
.find('_User', { username: 'resends_verification_token' })
|
||||
.find('_User', { username: 'resends_verification_token' }, {}, Auth.maintenance(config))
|
||||
.then(results => {
|
||||
return results[0];
|
||||
});
|
||||
@@ -599,6 +610,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
jfail(error);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -43,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());
|
||||
|
||||
|
||||
@@ -438,7 +438,7 @@ describe_only_db('mongo')('GridFSBucket', () => {
|
||||
await db.admin().serverStatus();
|
||||
expect(false).toBe(true);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('MongoClient must be connected to perform this operation');
|
||||
expect(e.message).toEqual('Client must be connected before running operations');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const httpRequest = require('../lib/cloud-code/httpRequest'),
|
||||
HTTPResponse = require('../lib/cloud-code/HTTPResponse').default,
|
||||
const httpRequest = require('../lib/request'),
|
||||
HTTPResponse = require('../lib/request').HTTPResponse,
|
||||
bodyParser = require('body-parser'),
|
||||
express = require('express');
|
||||
|
||||
|
||||
+73
-130
@@ -3,7 +3,6 @@ const AppCache = require('../lib/cache').AppCache;
|
||||
|
||||
describe('middlewares', () => {
|
||||
let fakeReq, fakeRes;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeReq = {
|
||||
originalUrl: 'http://example.com/parse/',
|
||||
@@ -117,10 +116,12 @@ describe('middlewares', () => {
|
||||
const otherKeys = BodyKeys.filter(
|
||||
otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey'
|
||||
);
|
||||
|
||||
it(`it should pull ${bodyKey} into req.info`, done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
});
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
fakeReq.body[bodyKey] = keyValue;
|
||||
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeReq.body[bodyKey]).toEqual(undefined);
|
||||
expect(fakeReq.info[infoKey]).toEqual(keyValue);
|
||||
@@ -134,161 +135,74 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not succeed if the ip does not belong to masterKeyIps list', () => {
|
||||
it('should not succeed and log if the ip does not belong to masterKeyIps list', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
});
|
||||
fakeReq.ip = 'ip3';
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaster).toBe(false);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Request using master key rejected as the request IP address '127.0.0.1' is not set in Parse Server option 'masterKeyIps'.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should succeed if the ip does belong to masterKeyIps list', done => {
|
||||
it('should not succeed if the ip does not belong to masterKeyIps list', async () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
});
|
||||
fakeReq.ip = 'ip1';
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaster).toBe(false);
|
||||
});
|
||||
|
||||
it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', () => {
|
||||
it('should not succeed if the ip does not belong to maintenanceKeyIps list', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
maintenanceKey: 'masterKey',
|
||||
maintenanceKeyIps: ['10.0.0.0', '10.0.0.1'],
|
||||
});
|
||||
fakeReq.connection = { remoteAddress: 'ip3' };
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
fakeReq.ip = '10.0.0.2';
|
||||
fakeReq.headers['x-parse-maintenance-key'] = 'masterKey';
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaintenance).toBe(false);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Request using maintenance key rejected as the request IP address '10.0.0.2' is not set in Parse Server option 'maintenanceKeyIps'.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', done => {
|
||||
it('should succeed if the ip does belong to masterKeyIps list', async () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
});
|
||||
fakeReq.connection = { remoteAddress: 'ip1' };
|
||||
fakeReq.ip = '10.0.0.1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaster).toBe(true);
|
||||
});
|
||||
|
||||
it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', () => {
|
||||
it('should allow any ip to use masterKey if masterKeyIps is empty', async () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['ip1', 'ip2'],
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
});
|
||||
fakeReq.socket = { remoteAddress: 'ip3' };
|
||||
fakeReq.ip = '10.0.0.1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaster).toBe(true);
|
||||
});
|
||||
|
||||
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',
|
||||
masterKeyIps: [],
|
||||
});
|
||||
fakeReq.ip = 'ip1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
@@ -373,6 +287,35 @@ describe('middlewares', () => {
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('https://parseplatform.org/');
|
||||
});
|
||||
|
||||
it('should support multiple origins if several are defined in allowOrigin as an array', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
allowOrigin: ['https://a.com', 'https://b.com', 'https://c.com'],
|
||||
});
|
||||
const headers = {};
|
||||
const res = {
|
||||
header: (key, value) => {
|
||||
headers[key] = value;
|
||||
},
|
||||
};
|
||||
const allowCrossDomain = middlewares.allowCrossDomain(fakeReq.body._ApplicationId);
|
||||
// Test with the first domain
|
||||
fakeReq.headers.origin = 'https://a.com';
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('https://a.com');
|
||||
// Test with the second domain
|
||||
fakeReq.headers.origin = 'https://b.com';
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('https://b.com');
|
||||
// Test with the third domain
|
||||
fakeReq.headers.origin = 'https://c.com';
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('https://c.com');
|
||||
// Test with an unauthorized domain
|
||||
fakeReq.headers.origin = 'https://unauthorized.com';
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('https://a.com');
|
||||
});
|
||||
|
||||
it('should use user provided on field userFromJWT', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
|
||||
@@ -6,7 +6,6 @@ const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDataba
|
||||
const request = require('../lib/request');
|
||||
const Config = require('../lib/Config');
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
const semver = require('semver');
|
||||
|
||||
const fakeClient = {
|
||||
s: { options: { dbName: null } },
|
||||
@@ -213,6 +212,48 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles nested dates', async () => {
|
||||
await new Parse.Object('MyClass', {
|
||||
foo: {
|
||||
test: {
|
||||
date: new Date(),
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
date: new Date(),
|
||||
},
|
||||
date: new Date(),
|
||||
}).save();
|
||||
const adapter = Config.get(Parse.applicationId).database.adapter;
|
||||
const [object] = await adapter._rawFind('MyClass', {});
|
||||
expect(object.date instanceof Date).toBeTrue();
|
||||
expect(object.bar.date instanceof Date).toBeTrue();
|
||||
expect(object.foo.test.date instanceof Date).toBeTrue();
|
||||
});
|
||||
|
||||
it('handles nested dates in array ', async () => {
|
||||
await new Parse.Object('MyClass', {
|
||||
foo: {
|
||||
test: {
|
||||
date: [new Date()],
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
date: [new Date()],
|
||||
},
|
||||
date: [new Date()],
|
||||
}).save();
|
||||
const adapter = Config.get(Parse.applicationId).database.adapter;
|
||||
const [object] = await adapter._rawFind('MyClass', {});
|
||||
expect(object.date[0] instanceof Date).toBeTrue();
|
||||
expect(object.bar.date[0] instanceof Date).toBeTrue();
|
||||
expect(object.foo.test.date[0] instanceof Date).toBeTrue();
|
||||
const obj = await new Parse.Query('MyClass').first({ useMasterKey: true });
|
||||
expect(obj.get('date')[0] instanceof Date).toBeTrue();
|
||||
expect(obj.get('bar').date[0] instanceof Date).toBeTrue();
|
||||
expect(obj.get('foo').test.date[0] instanceof Date).toBeTrue();
|
||||
});
|
||||
|
||||
it('handles updating a single object with array, object date', done => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
|
||||
@@ -283,7 +324,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
await adapter.database.admin().serverStatus();
|
||||
expect(false).toBe(true);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('MongoClient must be connected to perform this operation');
|
||||
expect(e.message).toEqual('Client must be connected before running operations');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -308,7 +349,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 +383,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');
|
||||
@@ -367,11 +442,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
expect(schemaAfterDeletion.fields.test).toBeUndefined();
|
||||
});
|
||||
|
||||
if (
|
||||
semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') &&
|
||||
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
|
||||
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger'
|
||||
) {
|
||||
if (process.env.MONGODB_TOPOLOGY === 'replicaset') {
|
||||
describe('transactions', () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
+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 => {
|
||||
|
||||
+360
-35
@@ -37,8 +37,14 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('works with _ContentType', done => {
|
||||
request({
|
||||
it('works with _ContentType', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['*'],
|
||||
},
|
||||
});
|
||||
let response = await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
@@ -47,21 +53,18 @@ describe('Parse.File testing', () => {
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
}).then(response => {
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/_file.html/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
|
||||
request({ url: b.url }).then(response => {
|
||||
const body = response.text;
|
||||
try {
|
||||
expect(response.headers['content-type']).toMatch('^text/html');
|
||||
expect(body).toEqual('<html></html>\n');
|
||||
} catch (e) {
|
||||
jfail(e);
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/_file.html/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
|
||||
response = await request({ url: b.url });
|
||||
const body = response.text;
|
||||
try {
|
||||
expect(response.headers['content-type']).toMatch('^text/html');
|
||||
expect(body).toEqual('<html></html>\n');
|
||||
} catch (e) {
|
||||
jfail(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('works without Content-Type', done => {
|
||||
@@ -351,25 +354,28 @@ describe('Parse.File testing', () => {
|
||||
ok(object.toJSON().file.url);
|
||||
});
|
||||
|
||||
it('content-type used with no extension', done => {
|
||||
it('content-type used with no extension', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['*'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'Content-Type': 'text/html',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
request({
|
||||
let response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: 'fee fi fo',
|
||||
}).then(response => {
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/\.html$/);
|
||||
request({ url: b.url }).then(response => {
|
||||
expect(response.headers['content-type']).toMatch(/^text\/html/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/\.html$/);
|
||||
response = await request({ url: b.url });
|
||||
expect(response.headers['content-type']).toMatch(/^text\/html/);
|
||||
});
|
||||
|
||||
it('filename is url encoded', done => {
|
||||
@@ -692,7 +698,198 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
xdescribe('Gridstore Range tests', () => {
|
||||
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',
|
||||
@@ -781,7 +978,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',
|
||||
@@ -879,8 +1076,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',
|
||||
@@ -888,12 +1085,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.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1109,6 +1304,136 @@ describe('Parse.File testing', () => {
|
||||
await expectAsync(reconfigureServer({ fileUpload: { [key]: value } })).toBeResolved();
|
||||
}
|
||||
}
|
||||
await expectAsync(
|
||||
reconfigureServer({
|
||||
fileUpload: {
|
||||
fileExtensions: 1,
|
||||
},
|
||||
})
|
||||
).toBeRejectedWith('fileUpload.fileExtensions must be an array.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fileExtensions', () => {
|
||||
it('works with _ContentType', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['png'],
|
||||
},
|
||||
});
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('works without Content-Type', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.html',
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('works with array', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['jpg'],
|
||||
},
|
||||
});
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('works with array without Content-Type', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['jpg'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/files/file.html',
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
|
||||
);
|
||||
});
|
||||
|
||||
it('works with array with correct file type', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['html'],
|
||||
},
|
||||
});
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'text/html',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
});
|
||||
const b = response.data;
|
||||
expect(b.name).toMatch(/_file.html$/);
|
||||
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('Parse.GeoPoint testing', () => {
|
||||
obj.set('location', point);
|
||||
obj.set('name', 'Zhoul');
|
||||
await obj.save();
|
||||
Parse.Cloud.httpRequest({
|
||||
request({
|
||||
url: 'http://localhost:8378/1/classes/TestObject/' + obj.id,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
|
||||
@@ -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);
|
||||
|
||||
+464
-73
@@ -8,10 +8,11 @@ require('./helper');
|
||||
const { updateCLP } = require('./support/dev');
|
||||
|
||||
const pluralize = require('pluralize');
|
||||
const { getMainDefinition } = require('apollo-utilities');
|
||||
const { getMainDefinition } = require('@apollo/client/utilities');
|
||||
const { createUploadLink } = require('apollo-upload-client');
|
||||
const { SubscriptionClient } = require('subscriptions-transport-ws');
|
||||
const { WebSocketLink } = require('@apollo/client/link/ws');
|
||||
const { mergeSchemas } = require('@graphql-tools/schema');
|
||||
const {
|
||||
ApolloClient,
|
||||
InMemoryCache,
|
||||
@@ -98,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(),
|
||||
@@ -106,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 () => {
|
||||
@@ -269,30 +292,37 @@ describe('ParseGraphQLServer', () => {
|
||||
let objects = [];
|
||||
|
||||
async function prepareData() {
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user1 = new Parse.User();
|
||||
user1.setUsername('user1');
|
||||
user1.setPassword('user1');
|
||||
user1.setEmail('user1@user1.user1');
|
||||
user1.setACL(acl);
|
||||
await user1.signUp();
|
||||
|
||||
user2 = new Parse.User();
|
||||
user2.setUsername('user2');
|
||||
user2.setPassword('user2');
|
||||
user2.setACL(acl);
|
||||
await user2.signUp();
|
||||
|
||||
user3 = new Parse.User();
|
||||
user3.setUsername('user3');
|
||||
user3.setPassword('user3');
|
||||
user3.setACL(acl);
|
||||
await user3.signUp();
|
||||
|
||||
user4 = new Parse.User();
|
||||
user4.setUsername('user4');
|
||||
user4.setPassword('user4');
|
||||
user4.setACL(acl);
|
||||
await user4.signUp();
|
||||
|
||||
user5 = new Parse.User();
|
||||
user5.setUsername('user5');
|
||||
user5.setPassword('user5');
|
||||
user5.setACL(acl);
|
||||
await user5.signUp();
|
||||
|
||||
const roleACL = new Parse.ACL();
|
||||
@@ -402,7 +432,7 @@ describe('ParseGraphQLServer', () => {
|
||||
const expressApp = express();
|
||||
httpServer = http.createServer(expressApp);
|
||||
expressApp.use('/parse', parseServer.app);
|
||||
parseLiveQueryServer = ParseServer.createLiveQueryServer(httpServer, {
|
||||
parseLiveQueryServer = await ParseServer.createLiveQueryServer(httpServer, {
|
||||
port: 1338,
|
||||
});
|
||||
parseGraphQLServer.applyGraphQL(expressApp);
|
||||
@@ -467,7 +497,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) => {
|
||||
@@ -476,7 +506,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;
|
||||
});
|
||||
@@ -486,7 +516,7 @@ describe('ParseGraphQLServer', () => {
|
||||
fetch,
|
||||
headers: {
|
||||
...headers,
|
||||
Origin: 'http://someorigin.com',
|
||||
Origin: 'http://example.com',
|
||||
},
|
||||
})
|
||||
),
|
||||
@@ -504,14 +534,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({
|
||||
@@ -523,7 +564,7 @@ describe('ParseGraphQLServer', () => {
|
||||
})
|
||||
).data.health;
|
||||
expect(health).toBeTruthy();
|
||||
expect(checked).toBeTruthy();
|
||||
expect(contextSpy).toHaveBeenCalledTimes(1);
|
||||
parseGraphQLServer._getGraphQLOptions = originalGetGraphQLOptions;
|
||||
});
|
||||
});
|
||||
@@ -908,8 +949,7 @@ describe('ParseGraphQLServer', () => {
|
||||
).data['__type'].inputFields
|
||||
.map(field => field.name)
|
||||
.sort();
|
||||
|
||||
expect(inputFields).toEqual(['clientMutationId', 'password', 'username']);
|
||||
expect(inputFields).toEqual(['authData', 'clientMutationId', 'password', 'username']);
|
||||
});
|
||||
|
||||
it('should have clientMutationId in log in mutation payload', async () => {
|
||||
@@ -5235,7 +5275,6 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
it('should only count', async () => {
|
||||
await prepareData();
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const where = {
|
||||
@@ -6786,14 +6825,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({
|
||||
@@ -6993,7 +7032,66 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
|
||||
describe('Users Mutations', () => {
|
||||
const challengeAdapter = {
|
||||
validateAuthData: () => Promise.resolve({ response: { someData: true } }),
|
||||
validateAppId: () => Promise.resolve(),
|
||||
challenge: () => Promise.resolve({ someData: true }),
|
||||
options: { anOption: true },
|
||||
};
|
||||
|
||||
it('should create user and return authData response', async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
auth: {
|
||||
challengeAdapter,
|
||||
},
|
||||
});
|
||||
const clientMutationId = uuidv4();
|
||||
|
||||
const result = await apolloClient.mutate({
|
||||
mutation: gql`
|
||||
mutation createUser($input: CreateUserInput!) {
|
||||
createUser(input: $input) {
|
||||
clientMutationId
|
||||
user {
|
||||
id
|
||||
authDataResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
clientMutationId,
|
||||
fields: {
|
||||
authData: {
|
||||
challengeAdapter: {
|
||||
id: 'challengeAdapter',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
context: {
|
||||
headers: {
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.data.createUser.clientMutationId).toEqual(clientMutationId);
|
||||
expect(result.data.createUser.user.authDataResponse).toEqual({
|
||||
challengeAdapter: { someData: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should sign user up', async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
auth: {
|
||||
challengeAdapter,
|
||||
},
|
||||
});
|
||||
const clientMutationId = uuidv4();
|
||||
const userSchema = new Parse.Schema('_User');
|
||||
userSchema.addString('someField');
|
||||
@@ -7010,6 +7108,7 @@ describe('ParseGraphQLServer', () => {
|
||||
sessionToken
|
||||
user {
|
||||
someField
|
||||
authDataResponse
|
||||
aPointer {
|
||||
id
|
||||
username
|
||||
@@ -7025,11 +7124,17 @@ describe('ParseGraphQLServer', () => {
|
||||
fields: {
|
||||
username: 'user1',
|
||||
password: 'user1',
|
||||
authData: {
|
||||
challengeAdapter: {
|
||||
id: 'challengeAdapter',
|
||||
},
|
||||
},
|
||||
aPointer: {
|
||||
createAndLink: {
|
||||
username: 'user2',
|
||||
password: 'user2',
|
||||
someField: 'someValue2',
|
||||
ACL: { public: { read: true, write: true } },
|
||||
},
|
||||
},
|
||||
someField: 'someValue',
|
||||
@@ -7044,6 +7149,9 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result.data.signUp.viewer.user.aPointer.id).toBeDefined();
|
||||
expect(result.data.signUp.viewer.user.aPointer.username).toEqual('user2');
|
||||
expect(typeof result.data.signUp.viewer.sessionToken).toBe('string');
|
||||
expect(result.data.signUp.viewer.user.authDataResponse).toEqual({
|
||||
challengeAdapter: { someData: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should login with user', async () => {
|
||||
@@ -7052,6 +7160,7 @@ describe('ParseGraphQLServer', () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
auth: {
|
||||
challengeAdapter,
|
||||
myAuth: {
|
||||
module: global.mockCustomAuthenticator('parse', 'graphql'),
|
||||
},
|
||||
@@ -7071,6 +7180,7 @@ describe('ParseGraphQLServer', () => {
|
||||
sessionToken
|
||||
user {
|
||||
someField
|
||||
authDataResponse
|
||||
aPointer {
|
||||
id
|
||||
username
|
||||
@@ -7084,6 +7194,7 @@ describe('ParseGraphQLServer', () => {
|
||||
input: {
|
||||
clientMutationId,
|
||||
authData: {
|
||||
challengeAdapter: { id: 'challengeAdapter' },
|
||||
myAuth: {
|
||||
id: 'parse',
|
||||
password: 'graphql',
|
||||
@@ -7096,6 +7207,7 @@ describe('ParseGraphQLServer', () => {
|
||||
username: 'user2',
|
||||
password: 'user2',
|
||||
someField: 'someValue2',
|
||||
ACL: { public: { read: true, write: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -7109,9 +7221,92 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(typeof result.data.logInWith.viewer.sessionToken).toBe('string');
|
||||
expect(result.data.logInWith.viewer.user.aPointer.id).toBeDefined();
|
||||
expect(result.data.logInWith.viewer.user.aPointer.username).toEqual('user2');
|
||||
expect(result.data.logInWith.viewer.user.authDataResponse).toEqual({
|
||||
challengeAdapter: { someData: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle challenge', async () => {
|
||||
const clientMutationId = uuidv4();
|
||||
|
||||
spyOn(challengeAdapter, 'challenge').and.callThrough();
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
auth: {
|
||||
challengeAdapter,
|
||||
},
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
await user.save({ username: 'username', password: 'password' });
|
||||
|
||||
const result = await apolloClient.mutate({
|
||||
mutation: gql`
|
||||
mutation Challenge($input: ChallengeInput!) {
|
||||
challenge(input: $input) {
|
||||
clientMutationId
|
||||
challengeData
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
clientMutationId,
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
challengeData: {
|
||||
challengeAdapter: { someChallengeData: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const challengeCall = challengeAdapter.challenge.calls.argsFor(0);
|
||||
expect(challengeAdapter.challenge).toHaveBeenCalledTimes(1);
|
||||
expect(challengeCall[0]).toEqual({ someChallengeData: true });
|
||||
expect(challengeCall[1]).toEqual(undefined);
|
||||
expect(challengeCall[2]).toEqual(challengeAdapter);
|
||||
expect(challengeCall[3].object instanceof Parse.User).toBeTruthy();
|
||||
expect(challengeCall[3].original instanceof Parse.User).toBeTruthy();
|
||||
expect(challengeCall[3].isChallenge).toBeTruthy();
|
||||
expect(challengeCall[3].object.id).toEqual(user.id);
|
||||
expect(challengeCall[3].original.id).toEqual(user.id);
|
||||
expect(result.data.challenge.clientMutationId).toEqual(clientMutationId);
|
||||
expect(result.data.challenge.challengeData).toEqual({
|
||||
challengeAdapter: { someData: true },
|
||||
});
|
||||
|
||||
await expectAsync(
|
||||
apolloClient.mutate({
|
||||
mutation: gql`
|
||||
mutation Challenge($input: ChallengeInput!) {
|
||||
challenge(input: $input) {
|
||||
clientMutationId
|
||||
challengeData
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
clientMutationId,
|
||||
username: 'username',
|
||||
password: 'wrongPassword',
|
||||
challengeData: {
|
||||
challengeAdapter: { someChallengeData: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).toBeRejected();
|
||||
});
|
||||
|
||||
it('should log the user in', async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
auth: {
|
||||
challengeAdapter,
|
||||
},
|
||||
});
|
||||
const clientMutationId = uuidv4();
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user1');
|
||||
@@ -7128,6 +7323,7 @@ describe('ParseGraphQLServer', () => {
|
||||
viewer {
|
||||
sessionToken
|
||||
user {
|
||||
authDataResponse
|
||||
someField
|
||||
}
|
||||
}
|
||||
@@ -7139,6 +7335,7 @@ describe('ParseGraphQLServer', () => {
|
||||
clientMutationId,
|
||||
username: 'user1',
|
||||
password: 'user1',
|
||||
authData: { challengeAdapter: { token: true } },
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -7147,6 +7344,9 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result.data.logIn.viewer.sessionToken).toBeDefined();
|
||||
expect(result.data.logIn.viewer.user.someField).toEqual('someValue');
|
||||
expect(typeof result.data.logIn.viewer.sessionToken).toBe('string');
|
||||
expect(result.data.logIn.viewer.user.authDataResponse).toEqual({
|
||||
challengeAdapter: { someData: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should log the user out', async () => {
|
||||
@@ -8121,19 +8321,21 @@ describe('ParseGraphQLServer', () => {
|
||||
const someClass = new Parse.Object('SomeClass');
|
||||
await someClass.save();
|
||||
|
||||
const roleACL = new Parse.ACL();
|
||||
roleACL.setPublicReadAccess(true);
|
||||
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'username');
|
||||
user.set('password', 'password');
|
||||
user.setACL(roleACL);
|
||||
await user.signUp();
|
||||
|
||||
const user2 = new Parse.User();
|
||||
user2.set('username', 'username2');
|
||||
user2.set('password', 'password2');
|
||||
user2.setACL(roleACL);
|
||||
await user2.signUp();
|
||||
|
||||
const roleACL = new Parse.ACL();
|
||||
roleACL.setPublicReadAccess(true);
|
||||
|
||||
const role = new Parse.Role('aRole', roleACL);
|
||||
await role.save();
|
||||
|
||||
@@ -9096,7 +9298,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',
|
||||
@@ -9107,15 +9309,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,
|
||||
@@ -9176,46 +9378,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 },
|
||||
@@ -9344,6 +9546,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 = {
|
||||
@@ -9437,6 +9684,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 = {
|
||||
@@ -10241,6 +10612,9 @@ describe('ParseGraphQLServer', () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user1');
|
||||
user.setPassword('user1');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
await user.signUp();
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
@@ -10381,7 +10755,7 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
|
||||
describe('Custom API', () => {
|
||||
describe('GraphQL Schema Based', () => {
|
||||
describe('SDL based', () => {
|
||||
let httpServer;
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -10504,7 +10878,7 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('SDL Based', () => {
|
||||
describe('GraphQL Schema Based', () => {
|
||||
let httpServer;
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -10567,6 +10941,12 @@ describe('ParseGraphQLServer', () => {
|
||||
},
|
||||
resolve: (p, { message }) => message,
|
||||
},
|
||||
errorQuery: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
resolve: () => {
|
||||
throw new Error('A test error');
|
||||
},
|
||||
},
|
||||
customQueryWithAutoTypeReturn: {
|
||||
type: SomeClassType,
|
||||
args: {
|
||||
@@ -10655,6 +11035,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' });
|
||||
@@ -10801,8 +11193,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
|
||||
|
||||
+201
-109
@@ -1,4 +1,5 @@
|
||||
'use strict';
|
||||
const Auth = require('../lib/Auth');
|
||||
const UserController = require('../lib/Controllers/UserController').UserController;
|
||||
const Config = require('../lib/Config');
|
||||
const validatorFail = () => {
|
||||
@@ -6,6 +7,15 @@ const validatorFail = () => {
|
||||
};
|
||||
|
||||
describe('ParseLiveQuery', function () {
|
||||
beforeEach(() => {
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
|
||||
});
|
||||
afterEach(async () => {
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
client.close();
|
||||
// Wait for live query client to disconnect
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
});
|
||||
it('access user on onLiveQueryEvent disconnect', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
@@ -15,7 +25,6 @@ describe('ParseLiveQuery', function () {
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
|
||||
const requestedUser = new Parse.User();
|
||||
requestedUser.setUsername('username');
|
||||
requestedUser.setPassword('password');
|
||||
@@ -393,6 +402,49 @@ describe('ParseLiveQuery', function () {
|
||||
await object.save();
|
||||
});
|
||||
|
||||
xit('can handle live query with fields - enable upon JS SDK support', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['Test'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
});
|
||||
const query = new Parse.Query('Test');
|
||||
query.watch('yolo');
|
||||
const subscription = await query.subscribe();
|
||||
const spy = {
|
||||
create(obj) {
|
||||
if (!obj.get('yolo')) {
|
||||
fail('create should not have been called');
|
||||
}
|
||||
},
|
||||
update(object, original) {
|
||||
if (object.get('yolo') === original.get('yolo')) {
|
||||
fail('create should not have been called');
|
||||
}
|
||||
},
|
||||
};
|
||||
const createSpy = spyOn(spy, 'create').and.callThrough();
|
||||
const updateSpy = spyOn(spy, 'update').and.callThrough();
|
||||
subscription.on('create', spy.create);
|
||||
subscription.on('update', spy.update);
|
||||
const obj = new Parse.Object('Test');
|
||||
obj.set('foo', 'bar');
|
||||
await obj.save();
|
||||
obj.set('foo', 'xyz');
|
||||
obj.set('yolo', 'xyz');
|
||||
await obj.save();
|
||||
const obj2 = new Parse.Object('Test');
|
||||
obj2.set('foo', 'bar');
|
||||
obj2.set('yolo', 'bar');
|
||||
await obj2.save();
|
||||
obj2.set('foo', 'bart');
|
||||
await obj2.save();
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
expect(createSpy).toHaveBeenCalledTimes(1);
|
||||
expect(updateSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('can handle afterEvent set pointers', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
@@ -469,76 +521,67 @@ describe('ParseLiveQuery', function () {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
Parse.Cloud.beforeSubscribe('TestObject', req => {
|
||||
expect(req.op).toBe('subscribe');
|
||||
expect(req.requestId).toBe(1);
|
||||
expect(req.query).toBeDefined();
|
||||
expect(req.user).toBeUndefined();
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeConnect(req => {
|
||||
expect(req.event).toBe('connect');
|
||||
expect(req.clients).toBe(0);
|
||||
expect(req.subscriptions).toBe(0);
|
||||
expect(req.useMasterKey).toBe(false);
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.user).toBeUndefined();
|
||||
expect(req.client).toBeDefined();
|
||||
});
|
||||
const hooks = {
|
||||
beforeSubscribe(req) {
|
||||
expect(req.op).toBe('subscribe');
|
||||
expect(req.requestId).toBe(1);
|
||||
expect(req.query).toBeDefined();
|
||||
expect(req.user).toBeUndefined();
|
||||
},
|
||||
beforeConnect(req) {
|
||||
expect(req.event).toBe('connect');
|
||||
expect(req.clients).toBe(0);
|
||||
expect(req.subscriptions).toBe(0);
|
||||
expect(req.useMasterKey).toBe(false);
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.user).toBeUndefined();
|
||||
expect(req.client).toBeDefined();
|
||||
},
|
||||
};
|
||||
spyOn(hooks, 'beforeSubscribe').and.callThrough();
|
||||
spyOn(hooks, 'beforeConnect').and.callThrough();
|
||||
Parse.Cloud.beforeSubscribe('TestObject', hooks.beforeSubscribe);
|
||||
Parse.Cloud.beforeConnect(hooks.beforeConnect);
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('update', object => {
|
||||
expect(object.get('foo')).toBe('bar');
|
||||
expect(hooks.beforeConnect).toHaveBeenCalled();
|
||||
expect(hooks.beforeSubscribe).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('can handle beforeConnect validation function', async done => {
|
||||
it('can handle beforeConnect validation function', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
Parse.Cloud.beforeConnect(() => {}, validatorFail);
|
||||
let complete = false;
|
||||
Parse.LiveQuery.on('error', error => {
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
if (complete) {
|
||||
return;
|
||||
}
|
||||
complete = true;
|
||||
expect(error).toBe('you are not authorized');
|
||||
done();
|
||||
});
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
await query.subscribe();
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.VALIDATION_ERROR, 'you are not authorized')
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle beforeSubscribe validation function', async done => {
|
||||
it('can handle beforeSubscribe validation function', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
@@ -546,11 +589,9 @@ describe('ParseLiveQuery', function () {
|
||||
Parse.Cloud.beforeSubscribe(TestObject, () => {}, validatorFail);
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('error', error => {
|
||||
expect(error).toBe('you are not authorized');
|
||||
done();
|
||||
});
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.VALIDATION_ERROR, 'you are not authorized')
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle afterEvent validation function', async done => {
|
||||
@@ -576,14 +617,12 @@ describe('ParseLiveQuery', function () {
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('can handle beforeConnect error', async done => {
|
||||
it('can handle beforeConnect error', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
@@ -591,14 +630,9 @@ describe('ParseLiveQuery', function () {
|
||||
Parse.Cloud.beforeConnect(() => {
|
||||
throw new Error('You shall not pass!');
|
||||
});
|
||||
Parse.LiveQuery.on('error', error => {
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
expect(error).toBe('You shall not pass!');
|
||||
done();
|
||||
});
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
await query.subscribe();
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(new Error('You shall not pass!'));
|
||||
});
|
||||
|
||||
it('can log on beforeConnect throw', async () => {
|
||||
@@ -607,8 +641,6 @@ describe('ParseLiveQuery', function () {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const logger = require('../lib/logger').logger;
|
||||
@@ -620,22 +652,20 @@ describe('ParseLiveQuery', function () {
|
||||
foo.bar();
|
||||
/* eslint-enable no-undef */
|
||||
});
|
||||
new Parse.Query(TestObject).subscribe();
|
||||
await new Promise(resolve => Parse.LiveQuery.on('error', resolve));
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
await expectAsync(new Parse.Query(TestObject).subscribe()).toBeRejectedWith(
|
||||
new Error('foo is not defined')
|
||||
);
|
||||
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 => {
|
||||
it('can handle beforeSubscribe error', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
@@ -643,17 +673,9 @@ describe('ParseLiveQuery', function () {
|
||||
Parse.Cloud.beforeSubscribe(TestObject, () => {
|
||||
throw new Error('You shall not subscribe!');
|
||||
});
|
||||
Parse.LiveQuery.on('error', error => {
|
||||
expect(error).toBe('You shall not subscribe!');
|
||||
});
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('error', error => {
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
expect(error).toBe('You shall not subscribe!');
|
||||
done();
|
||||
});
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(new Error('You shall not subscribe!'));
|
||||
});
|
||||
|
||||
it('can log on beforeSubscribe error', async () => {
|
||||
@@ -662,8 +684,6 @@ describe('ParseLiveQuery', function () {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const logger = require('../lib/logger').logger;
|
||||
@@ -676,8 +696,7 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
const subscription = await query.subscribe();
|
||||
await new Promise(resolve => subscription.on('error', resolve));
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(new Error('foo is not defined'));
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Failed running beforeSubscribe on TestObject for session undefined with:\n Error: {"message":"foo is not defined","code":141}`
|
||||
@@ -690,29 +709,35 @@ describe('ParseLiveQuery', function () {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.beforeSubscribe(TestObject, request => {
|
||||
const query = request.query;
|
||||
query.equalTo('yolo', 'abc');
|
||||
});
|
||||
|
||||
const hook = {
|
||||
beforeSubscribe(request) {
|
||||
request.query.equalTo('yolo', 'abc');
|
||||
},
|
||||
};
|
||||
spyOn(hook, 'beforeSubscribe').and.callThrough();
|
||||
Parse.Cloud.beforeSubscribe('TestObject', hook.beforeSubscribe);
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.equalTo('objectId', object.id);
|
||||
const subscription = await query.subscribe();
|
||||
|
||||
subscription.on('update', () => {
|
||||
fail();
|
||||
fail('beforeSubscribe should restrict subscription');
|
||||
});
|
||||
object.set({ foo: 'bar' });
|
||||
subscription.on('enter', object => {
|
||||
if (object.get('yolo') === 'abc') {
|
||||
done();
|
||||
} else {
|
||||
fail('beforeSubscribe should restrict queries');
|
||||
}
|
||||
});
|
||||
object.set({ yolo: 'bar' });
|
||||
await object.save();
|
||||
setTimeout(async () => {
|
||||
done();
|
||||
}, 1000);
|
||||
object.set({ yolo: 'abc' });
|
||||
await object.save();
|
||||
expect(hook.beforeSubscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can return a new beforeSubscribe query', async done => {
|
||||
@@ -836,6 +861,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] },
|
||||
@@ -866,26 +935,15 @@ describe('ParseLiveQuery', function () {
|
||||
await Parse.User.logIn('username', 'password');
|
||||
});
|
||||
|
||||
it('prevent liveQuery on Session class when not logged in', async done => {
|
||||
it('prevent liveQuery on Session class when not logged in', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: [Parse.Session],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
Parse.LiveQuery.on('error', error => {
|
||||
expect(error).toBe('Invalid session token');
|
||||
});
|
||||
const query = new Parse.Query(Parse.Session);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('error', error => {
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
expect(error).toBe('Invalid session token');
|
||||
done();
|
||||
});
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(new Error('Invalid session token'));
|
||||
});
|
||||
|
||||
it('handle invalid websocket payload length', async done => {
|
||||
@@ -933,6 +991,7 @@ describe('ParseLiveQuery', function () {
|
||||
};
|
||||
|
||||
await reconfigureServer({
|
||||
maintenanceKey: 'test2',
|
||||
liveQuery: {
|
||||
classNames: [Parse.User],
|
||||
},
|
||||
@@ -954,9 +1013,14 @@ describe('ParseLiveQuery', function () {
|
||||
.signUp()
|
||||
.then(() => {
|
||||
const config = Config.get('test');
|
||||
return config.database.find('_User', {
|
||||
username: 'zxcv',
|
||||
});
|
||||
return config.database.find(
|
||||
'_User',
|
||||
{
|
||||
username: 'zxcv',
|
||||
},
|
||||
{},
|
||||
Auth.maintenance(config)
|
||||
);
|
||||
})
|
||||
.then(async results => {
|
||||
const foundUser = results[0];
|
||||
@@ -1032,6 +1096,9 @@ describe('ParseLiveQuery', function () {
|
||||
user.setUsername('username');
|
||||
user.setPassword('password');
|
||||
user.set('foo', 'bar');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
|
||||
const query = new Parse.Query(Parse.User);
|
||||
query.equalTo('foo', 'bar');
|
||||
@@ -1112,12 +1179,37 @@ describe('ParseLiveQuery', function () {
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(async function (done) {
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
client.close();
|
||||
// Wait for live query client to disconnect
|
||||
setTimeout(() => {
|
||||
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();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
const secondPoint = new Parse.GeoPoint({ latitude: 40.0, longitude: -30.0 });
|
||||
object.set({ location: secondPoint });
|
||||
await object.save();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
|
||||
describe('ParseLiveQuery redis', () => {
|
||||
afterEach(async () => {
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
client.close();
|
||||
});
|
||||
it('can connect', async () => {
|
||||
await reconfigureServer({
|
||||
appId: 'redis_live_query',
|
||||
startLiveQueryServer: true,
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
redisURL: 'redis://localhost:6379',
|
||||
},
|
||||
liveQueryServerOptions: {
|
||||
redisURL: 'redis://localhost:6379',
|
||||
},
|
||||
});
|
||||
const subscription = await new Parse.Query('TestObject').subscribe();
|
||||
const [object] = await Promise.all([
|
||||
new Parse.Object('TestObject').save(),
|
||||
new Promise(resolve =>
|
||||
subscription.on('create', () => {
|
||||
resolve();
|
||||
})
|
||||
),
|
||||
]);
|
||||
await Promise.all([
|
||||
new Promise(resolve =>
|
||||
subscription.on('delete', () => {
|
||||
resolve();
|
||||
})
|
||||
),
|
||||
object.destroy(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('can call connect twice', async () => {
|
||||
const server = await reconfigureServer({
|
||||
appId: 'redis_live_query',
|
||||
startLiveQueryServer: true,
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
redisURL: 'redis://localhost:6379',
|
||||
},
|
||||
liveQueryServerOptions: {
|
||||
redisURL: 'redis://localhost:6379',
|
||||
},
|
||||
});
|
||||
expect(server.config.liveQueryController.liveQueryPublisher.parsePublisher.isOpen).toBeTrue();
|
||||
await server.config.liveQueryController.connect();
|
||||
expect(server.config.liveQueryController.liveQueryPublisher.parsePublisher.isOpen).toBeTrue();
|
||||
expect(server.liveQueryServer.subscriber.isOpen).toBe(true);
|
||||
await server.liveQueryServer.connect();
|
||||
expect(server.liveQueryServer.subscriber.isOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -94,29 +94,30 @@ describe('ParseLiveQueryServer', function () {
|
||||
expect(parseLiveQueryServer.subscriptions.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can be initialized from ParseServer', function () {
|
||||
it('can be initialized from ParseServer', async () => {
|
||||
const httpServer = {};
|
||||
const parseLiveQueryServer = ParseServer.createLiveQueryServer(httpServer, {});
|
||||
const parseLiveQueryServer = await ParseServer.createLiveQueryServer(httpServer, {});
|
||||
|
||||
expect(parseLiveQueryServer.clientId).toBeUndefined();
|
||||
expect(parseLiveQueryServer.clients.size).toBe(0);
|
||||
expect(parseLiveQueryServer.subscriptions.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can be initialized from ParseServer without httpServer', function (done) {
|
||||
const parseLiveQueryServer = ParseServer.createLiveQueryServer(undefined, {
|
||||
it('can be initialized from ParseServer without httpServer', async () => {
|
||||
const parseLiveQueryServer = await ParseServer.createLiveQueryServer(undefined, {
|
||||
port: 22345,
|
||||
});
|
||||
|
||||
expect(parseLiveQueryServer.clientId).toBeUndefined();
|
||||
expect(parseLiveQueryServer.clients.size).toBe(0);
|
||||
expect(parseLiveQueryServer.subscriptions.size).toBe(0);
|
||||
parseLiveQueryServer.server.close(done);
|
||||
await new Promise(resolve => parseLiveQueryServer.server.close(resolve));
|
||||
});
|
||||
|
||||
describe_only_db('mongo')('initialization', () => {
|
||||
it('can be initialized through ParseServer without liveQueryServerOptions', function (done) {
|
||||
const parseServer = ParseServer.start({
|
||||
beforeEach(() => reconfigureServer({ appId: 'mongo_init_test' }));
|
||||
it('can be initialized through ParseServer without liveQueryServerOptions', async () => {
|
||||
const parseServer = await ParseServer.startApp({
|
||||
appId: 'hello',
|
||||
masterKey: 'world',
|
||||
port: 22345,
|
||||
@@ -126,19 +127,14 @@ describe('ParseLiveQueryServer', function () {
|
||||
classNames: ['Yolo'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
serverStartComplete: () => {
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).toBe(parseServer.server);
|
||||
parseServer.server.close(async () => {
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
},
|
||||
});
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).toBe(parseServer.server);
|
||||
await new Promise(resolve => parseServer.server.close(resolve));
|
||||
});
|
||||
|
||||
it('can be initialized through ParseServer with liveQueryServerOptions', function (done) {
|
||||
const parseServer = ParseServer.start({
|
||||
it('can be initialized through ParseServer with liveQueryServerOptions', async () => {
|
||||
const parseServer = await ParseServer.startApp({
|
||||
appId: 'hello',
|
||||
masterKey: 'world',
|
||||
port: 22346,
|
||||
@@ -150,17 +146,10 @@ describe('ParseLiveQueryServer', function () {
|
||||
liveQueryServerOptions: {
|
||||
port: 22347,
|
||||
},
|
||||
serverStartComplete: () => {
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
|
||||
parseServer.liveQueryServer.server.close(
|
||||
parseServer.server.close.bind(parseServer.server, async () => {
|
||||
await reconfigureServer();
|
||||
done();
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
|
||||
await new Promise(resolve => parseServer.server.close(resolve));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -306,7 +295,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
fields: ['test'],
|
||||
keys: ['test'],
|
||||
};
|
||||
const requestId = 2;
|
||||
const request = {
|
||||
@@ -343,7 +332,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
fields: ['test'],
|
||||
keys: ['test'],
|
||||
};
|
||||
const requestId = 2;
|
||||
const request = {
|
||||
@@ -390,7 +379,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
fields: ['test'],
|
||||
keys: ['test'],
|
||||
};
|
||||
await addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
|
||||
// Add subscription for mock client 2
|
||||
@@ -402,7 +391,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
fields: ['testAgain'],
|
||||
keys: ['testAgain'],
|
||||
};
|
||||
const requestIdAgain = 1;
|
||||
await addMockSubscription(
|
||||
@@ -766,6 +755,49 @@ describe('ParseLiveQueryServer', function () {
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
});
|
||||
|
||||
it('sends correct object for dates', async () => {
|
||||
jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'matchesQuery');
|
||||
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
|
||||
const date = new Date();
|
||||
const message = {
|
||||
currentParseObject: {
|
||||
date: { __type: 'Date', iso: date.toISOString() },
|
||||
__type: 'Object',
|
||||
key: 'value',
|
||||
className: testClassName,
|
||||
},
|
||||
};
|
||||
// Add mock client
|
||||
const clientId = 1;
|
||||
const client = addMockClient(parseLiveQueryServer, clientId);
|
||||
|
||||
const requestId2 = 2;
|
||||
|
||||
await addMockSubscription(parseLiveQueryServer, clientId, requestId2);
|
||||
|
||||
parseLiveQueryServer._matchesACL = function () {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
parseLiveQueryServer._inflateParseObject(message);
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send leave and enter command to client
|
||||
await timeout();
|
||||
|
||||
expect(client.pushCreate).toHaveBeenCalledWith(
|
||||
requestId2,
|
||||
{
|
||||
className: 'TestObject',
|
||||
key: 'value',
|
||||
date: { __type: 'Date', iso: date.toISOString() },
|
||||
},
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle object save command which does not match any subscription', async done => {
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
// Make mock request message
|
||||
@@ -1072,7 +1104,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
fields: ['test'],
|
||||
keys: ['test'],
|
||||
};
|
||||
await addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
|
||||
// Mock _matchesSubscription to return matching
|
||||
@@ -1099,6 +1131,116 @@ describe('ParseLiveQueryServer', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can deprecate fields', async () => {
|
||||
const Deprecator = require('../lib/Deprecator/Deprecator');
|
||||
const spy = spyOn(Deprecator, 'logRuntimeDeprecation').and.callFake(() => {});
|
||||
jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client');
|
||||
const Client = require('../lib/LiveQuery/Client').Client;
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
// Make mock request message
|
||||
const message = generateMockMessage();
|
||||
|
||||
const clientId = 1;
|
||||
const parseWebSocket = {
|
||||
clientId,
|
||||
send: jasmine.createSpy('send'),
|
||||
};
|
||||
const client = new Client(clientId, parseWebSocket);
|
||||
spyOn(client, 'pushCreate').and.callThrough();
|
||||
parseLiveQueryServer.clients.set(clientId, client);
|
||||
|
||||
// Add mock subscription
|
||||
const requestId = 2;
|
||||
const query = {
|
||||
className: testClassName,
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
fields: ['test'],
|
||||
};
|
||||
await addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
|
||||
// Mock _matchesSubscription to return matching
|
||||
parseLiveQueryServer._matchesSubscription = function (parseObject) {
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function () {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send create command to client
|
||||
await timeout();
|
||||
|
||||
expect(client.pushCreate).toHaveBeenCalled();
|
||||
const args = parseWebSocket.send.calls.mostRecent().args;
|
||||
const toSend = JSON.parse(args[0]);
|
||||
expect(toSend.object).toBeDefined();
|
||||
expect(toSend.original).toBeUndefined();
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
usage: 'Subscribing using fields parameter',
|
||||
solution: `Subscribe using "keys" instead.`,
|
||||
});
|
||||
});
|
||||
|
||||
it('can handle create command with watch', async () => {
|
||||
jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client');
|
||||
const Client = require('../lib/LiveQuery/Client').Client;
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
// Make mock request message
|
||||
const message = generateMockMessage();
|
||||
|
||||
const clientId = 1;
|
||||
const parseWebSocket = {
|
||||
clientId,
|
||||
send: jasmine.createSpy('send'),
|
||||
};
|
||||
const client = new Client(clientId, parseWebSocket);
|
||||
spyOn(client, 'pushCreate').and.callThrough();
|
||||
parseLiveQueryServer.clients.set(clientId, client);
|
||||
|
||||
// Add mock subscription
|
||||
const requestId = 2;
|
||||
const query = {
|
||||
className: testClassName,
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
watch: ['yolo'],
|
||||
};
|
||||
await addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
|
||||
// Mock _matchesSubscription to return matching
|
||||
parseLiveQueryServer._matchesSubscription = function (parseObject) {
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function () {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send create command to client
|
||||
await timeout();
|
||||
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
|
||||
message.currentParseObject.set('yolo', 'test');
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
await timeout();
|
||||
|
||||
const args = parseWebSocket.send.calls.mostRecent().args;
|
||||
const toSend = JSON.parse(args[0]);
|
||||
expect(toSend.object).toBeDefined();
|
||||
expect(toSend.original).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can match subscription for null or undefined parse object', function () {
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
// Make mock subscription
|
||||
@@ -1822,7 +1964,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
where: {
|
||||
key: 'value',
|
||||
},
|
||||
fields: ['test'],
|
||||
keys: ['test'],
|
||||
};
|
||||
}
|
||||
const request = {
|
||||
@@ -1846,6 +1988,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
} else {
|
||||
subscription.clientRequestIds = new Map([[clientId, [requestId]]]);
|
||||
}
|
||||
subscription.query = query.where;
|
||||
return subscription;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,28 +23,28 @@ const loadTestData = () => {
|
||||
const data1 = {
|
||||
score: 10,
|
||||
name: 'foo',
|
||||
sender: { group: 'A' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
sender: { group: 'A' },
|
||||
views: 900,
|
||||
size: ['S', 'M'],
|
||||
};
|
||||
const data2 = {
|
||||
score: 10,
|
||||
name: 'foo',
|
||||
sender: { group: 'A' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
sender: { group: 'A' },
|
||||
views: 800,
|
||||
size: ['M', 'L'],
|
||||
};
|
||||
const data3 = {
|
||||
score: 10,
|
||||
name: 'bar',
|
||||
sender: { group: 'B' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
sender: { group: 'B' },
|
||||
views: 700,
|
||||
size: ['S'],
|
||||
};
|
||||
const data4 = {
|
||||
score: 20,
|
||||
name: 'dpl',
|
||||
sender: { group: 'B' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
sender: { group: 'B' },
|
||||
views: 700,
|
||||
size: ['S'],
|
||||
};
|
||||
@@ -86,7 +86,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('invalid query group _id required', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
group: {}, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
$group: {},
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options).catch(error => {
|
||||
@@ -98,7 +98,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group by field', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
group: { objectId: '$name' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
$group: { _id: '$name' },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -119,7 +119,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
pipeline: {
|
||||
group: { objectId: '$name' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
$group: { _id: '$name' },
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -137,7 +137,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
group: { objectId: {} }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
$group: { _id: {} },
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -156,7 +156,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
group: { objectId: '' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
$group: { _id: '' },
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -175,7 +175,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
group: { objectId: [] }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
$group: { _id: [] },
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -196,9 +196,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: {
|
||||
$group: {
|
||||
_id: {
|
||||
score: '$score',
|
||||
views: '$views',
|
||||
},
|
||||
@@ -280,7 +279,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group by number', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
group: { objectId: '$score' }, // TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
$group: { _id: '$score' },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -302,9 +301,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
|
||||
const pipeline = [
|
||||
{
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: null,
|
||||
$group: {
|
||||
_id: null,
|
||||
total: { $sum: { $multiply: ['$quantity', '$price'] } },
|
||||
},
|
||||
},
|
||||
@@ -326,10 +324,10 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
|
||||
const pipeline = [
|
||||
{
|
||||
match: { quantity: { $exists: true } },
|
||||
$match: { quantity: { $exists: true } },
|
||||
},
|
||||
{
|
||||
project: {
|
||||
$project: {
|
||||
name: 1,
|
||||
total: { $multiply: ['$quantity', '$price'] },
|
||||
},
|
||||
@@ -358,16 +356,16 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
|
||||
const pipeline = [
|
||||
{
|
||||
match: { quantity: { $exists: true } },
|
||||
$match: { quantity: { $exists: true } },
|
||||
},
|
||||
{
|
||||
project: {
|
||||
objectId: 0, // TODO: change to `_id`. See [#7339](https://bit.ly/3incnWx)
|
||||
$project: {
|
||||
_id: 0,
|
||||
total: { $multiply: ['$quantity', '$price'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
sort: { total: 1 },
|
||||
$sort: { total: 1 },
|
||||
},
|
||||
];
|
||||
Parse.Object.saveAll([obj1, obj2])
|
||||
@@ -388,7 +386,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it_exclude_dbs(['postgres'])('project updatedAt only transform', done => {
|
||||
const pipeline = [
|
||||
{
|
||||
project: { objectId: 0, updatedAt: 1 },
|
||||
$project: { _id: 0, updatedAt: 1 },
|
||||
},
|
||||
];
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -411,13 +409,13 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject({ dateField2019: new Date(1990, 11, 1) });
|
||||
const pipeline = [
|
||||
{
|
||||
match: {
|
||||
$match: {
|
||||
dateField2019: { $exists: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
group: {
|
||||
objectId: {
|
||||
$group: {
|
||||
_id: {
|
||||
day: { $dayOfMonth: '$dateField2019' },
|
||||
month: { $month: '$dateField2019' },
|
||||
year: { $year: '$dateField2019' },
|
||||
@@ -449,9 +447,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject({ dateField2019: new Date(1990, 11, 1) });
|
||||
const pipeline = [
|
||||
{
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: {
|
||||
$group: {
|
||||
_id: {
|
||||
day: { $dayOfMonth: '$dateField2019' },
|
||||
month: { $month: '$dateField2019' },
|
||||
year: { $year: '$dateField2019' },
|
||||
@@ -481,7 +478,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj1 = new TestObject({ pointer: pointer1 });
|
||||
const obj2 = new TestObject({ pointer: pointer2 });
|
||||
const obj3 = new TestObject({ pointer: pointer1 });
|
||||
const pipeline = [{ group: { objectId: '$pointer' } }];
|
||||
const pipeline = [{ $group: { _id: '$pointer' } }];
|
||||
Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3])
|
||||
.then(() => {
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -499,8 +496,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group sum query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: '$score' } },
|
||||
$group: { _id: null, total: { $sum: '$score' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -516,8 +512,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group count query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: 1 } },
|
||||
$group: { _id: null, total: { $sum: 1 } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -533,8 +528,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group min query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, minScore: { $min: '$score' } },
|
||||
$group: { _id: null, minScore: { $min: '$score' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -550,8 +544,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group max query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, maxScore: { $max: '$score' } },
|
||||
$group: { _id: null, maxScore: { $max: '$score' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -567,8 +560,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group avg query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, avgScore: { $avg: '$score' } },
|
||||
$group: { _id: null, avgScore: { $avg: '$score' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -584,7 +576,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('limit query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
limit: 2,
|
||||
$limit: 2,
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -598,7 +590,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('sort ascending query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
sort: { name: 1 },
|
||||
$sort: { name: 1 },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -616,7 +608,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('sort decending query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
sort: { name: -1 },
|
||||
$sort: { name: -1 },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -634,7 +626,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('skip query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
skip: 2,
|
||||
$skip: 2,
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -654,7 +646,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj1 = new TestObject({ dateField: yesterday });
|
||||
const obj2 = new TestObject({ dateField: today });
|
||||
const obj3 = new TestObject({ dateField: tomorrow });
|
||||
const pipeline = [{ match: { dateField: { $lt: tomorrow } } }];
|
||||
const pipeline = [{ $match: { dateField: { $lt: tomorrow } } }];
|
||||
Parse.Object.saveAll([obj1, obj2, obj3])
|
||||
.then(() => {
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -666,10 +658,27 @@ 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: {
|
||||
match: { score: { $gt: 15 } },
|
||||
$match: { score: { $gt: 15 } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -684,7 +693,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('match multiple comparison query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
match: { score: { $gt: 5, $lt: 15 } },
|
||||
$match: { score: { $gt: 5, $lt: 15 } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -701,7 +710,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('match complex comparison query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
match: { score: { $gt: 5, $lt: 15 }, views: { $gt: 850, $lt: 1000 } },
|
||||
$match: { score: { $gt: 5, $lt: 15 }, views: { $gt: 850, $lt: 1000 } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -717,7 +726,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('match comparison and equality query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
match: { score: { $gt: 5, $lt: 15 }, views: 900 },
|
||||
$match: { score: { $gt: 5, $lt: 15 }, views: 900 },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -733,7 +742,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('match $or query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
match: {
|
||||
$match: {
|
||||
$or: [{ score: { $gt: 15, $lt: 25 } }, { views: { $gt: 750, $lt: 850 } }],
|
||||
},
|
||||
},
|
||||
@@ -758,7 +767,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj2 = new TestObject();
|
||||
Parse.Object.saveAll([obj1, obj2])
|
||||
.then(() => {
|
||||
const pipeline = [{ match: { objectId: obj1.id } }];
|
||||
const pipeline = [{ $match: { _id: obj1.id } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -774,7 +783,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj2 = new TestObject({ name: 'TestObject2' });
|
||||
Parse.Object.saveAll([obj1, obj2])
|
||||
.then(() => {
|
||||
const pipeline = [{ match: { name: 'TestObject1' } }];
|
||||
const pipeline = [{ $match: { name: 'TestObject1' } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -794,7 +803,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
|
||||
Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3])
|
||||
.then(() => {
|
||||
const pipeline = [{ match: { pointer: pointer1.id } }];
|
||||
const pipeline = [{ $match: { pointer: pointer1.id } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -809,7 +818,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('match exists query', done => {
|
||||
const pipeline = [{ match: { score: { $exists: true } } }];
|
||||
const pipeline = [{ $match: { score: { $exists: true } } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.aggregate(pipeline).then(results => {
|
||||
expect(results.length).toEqual(4);
|
||||
@@ -825,7 +834,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.then(() => {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const pipeline = [{ match: { createdAt: { $gte: today } } }];
|
||||
const pipeline = [{ $match: { createdAt: { $gte: today } } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -844,7 +853,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.then(() => {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const pipeline = [{ match: { updatedAt: { $gte: today } } }];
|
||||
const pipeline = [{ $match: { updatedAt: { $gte: today } } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -863,7 +872,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.then(() => {
|
||||
const now = new Date();
|
||||
const future = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate());
|
||||
const pipeline = [{ match: { createdAt: future } }];
|
||||
const pipeline = [{ $match: { createdAt: future } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -882,7 +891,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
|
||||
Parse.Object.saveAll([pointer, obj1, obj2, obj3])
|
||||
.then(() => {
|
||||
const pipeline = [{ match: { pointer: { $exists: true } } }];
|
||||
const pipeline = [{ $match: { pointer: { $exists: true } } }];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
})
|
||||
@@ -921,7 +930,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
(
|
||||
await new Parse.Query('MyCollection').aggregate([
|
||||
{
|
||||
match: {
|
||||
$match: {
|
||||
language: { $in: [null, 'en'] },
|
||||
},
|
||||
},
|
||||
@@ -935,7 +944,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
(
|
||||
await new Parse.Query('MyCollection').aggregate([
|
||||
{
|
||||
match: {
|
||||
$match: {
|
||||
$or: [{ language: 'en' }, { language: null }],
|
||||
},
|
||||
},
|
||||
@@ -949,7 +958,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('project query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
project: { name: 1 },
|
||||
$project: { name: 1 },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -969,7 +978,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('multiple project query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
project: { name: 1, score: 1, sender: 1 },
|
||||
$project: { name: 1, score: 1, sender: 1 },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -994,8 +1003,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
.save()
|
||||
.then(() => {
|
||||
const pipeline = [
|
||||
{ match: { objectId: obj.id } },
|
||||
{ project: { pointer: 1, name: 1, createdAt: 1 } },
|
||||
{ $match: { _id: obj.id } },
|
||||
{ $project: { pointer: 1, name: 1, createdAt: 1 } },
|
||||
];
|
||||
const query = new Parse.Query(TestObject);
|
||||
return query.aggregate(pipeline);
|
||||
@@ -1012,9 +1021,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('project with group query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
project: { score: 1 },
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: '$score', score: { $sum: '$score' } },
|
||||
$project: { score: 1 },
|
||||
$group: { _id: '$score', score: { $sum: '$score' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -1041,8 +1049,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('class does not exist return empty', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: '$score' } },
|
||||
$group: { _id: null, total: { $sum: '$score' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/UnknownClass', options)
|
||||
@@ -1056,8 +1063,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('field does not exist return empty', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: '$unknownfield' } },
|
||||
$group: { _id: null, total: { $sum: '$unknownfield' } },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/UnknownClass', options)
|
||||
@@ -1086,7 +1092,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
distinct: 'score',
|
||||
where: {
|
||||
$where: {
|
||||
name: 'bar',
|
||||
},
|
||||
},
|
||||
@@ -1103,7 +1109,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
distinct: 'score',
|
||||
where: JSON.stringify({ name: 'bar' }),
|
||||
$where: JSON.stringify({ name: 'bar' }),
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -1253,7 +1259,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('does not return sensitive hidden properties', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
match: {
|
||||
$match: {
|
||||
score: {
|
||||
$gt: 5,
|
||||
},
|
||||
@@ -1300,7 +1306,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('aggregate allow multiple of same stage', async done => {
|
||||
await reconfigureServer();
|
||||
await reconfigureServer({ silent: false });
|
||||
const pointer1 = new TestObject({ value: 1 });
|
||||
const pointer2 = new TestObject({ value: 2 });
|
||||
const pointer3 = new TestObject({ value: 3 });
|
||||
@@ -1313,17 +1319,17 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
body: {
|
||||
pipeline: [
|
||||
{
|
||||
match: { name: 'Hello' },
|
||||
$match: { name: 'Hello' },
|
||||
},
|
||||
{
|
||||
// Transform className$objectId to objectId and store in new field tempPointer
|
||||
project: {
|
||||
$project: {
|
||||
tempPointer: { $substr: ['$_p_pointer', 11, -1] }, // Remove TestObject$
|
||||
},
|
||||
},
|
||||
{
|
||||
// Left Join, replace objectId stored in tempPointer with an actual object
|
||||
lookup: {
|
||||
$lookup: {
|
||||
from: 'test_TestObject',
|
||||
localField: 'tempPointer',
|
||||
foreignField: '_id',
|
||||
@@ -1332,12 +1338,12 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
},
|
||||
{
|
||||
// lookup returns an array, Deconstructs an array field to objects
|
||||
unwind: {
|
||||
$unwind: {
|
||||
path: '$tempPointer',
|
||||
},
|
||||
},
|
||||
{
|
||||
match: { 'tempPointer.value': 2 },
|
||||
$match: { 'tempPointer.value': 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1381,7 +1387,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
// Create query
|
||||
const pipeline = [
|
||||
{
|
||||
geoNear: {
|
||||
$geoNear: {
|
||||
near: {
|
||||
type: 'Point',
|
||||
coordinates: [1, 1],
|
||||
@@ -1434,7 +1440,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
// Create query
|
||||
const pipeline = [
|
||||
{
|
||||
geoNear: {
|
||||
$geoNear: {
|
||||
near: {
|
||||
type: 'Point',
|
||||
coordinates: [1, 1],
|
||||
@@ -1480,7 +1486,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
// Create query
|
||||
const pipeline = [
|
||||
{
|
||||
geoNear: {
|
||||
$geoNear: {
|
||||
near: [1, 1],
|
||||
key: 'location',
|
||||
spherical: true,
|
||||
|
||||
+224
-10
@@ -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();
|
||||
@@ -174,7 +322,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
@@ -186,7 +334,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
@@ -194,14 +342,14 @@ 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, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
@@ -215,7 +363,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
$group: JSON.stringify({ _id: '$foo' }),
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
@@ -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({ _id: '$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({ _id: '$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({ _id: '$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({ _id: '$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 });
|
||||
});
|
||||
});
|
||||
|
||||
+49
-21
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('Parse Role testing', () => {
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
const restExecute = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const restExecute = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
|
||||
let user, auth, getAllRolesSpy;
|
||||
createTestUser()
|
||||
|
||||
+10
-17
@@ -29,27 +29,20 @@ describe('Server Url Checks', () => {
|
||||
server.close(done);
|
||||
});
|
||||
|
||||
it('validate good server url', done => {
|
||||
it('validate good server url', async () => {
|
||||
Parse.serverURL = 'http://localhost:13376';
|
||||
ParseServer.verifyServerUrl(async result => {
|
||||
if (!result) {
|
||||
done.fail('Did not pass valid url');
|
||||
}
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
const response = await ParseServer.verifyServerUrl();
|
||||
expect(response).toBeTrue();
|
||||
});
|
||||
|
||||
it('mark bad server url', done => {
|
||||
it('mark bad server url', async () => {
|
||||
spyOn(console, 'warn').and.callFake(() => {});
|
||||
Parse.serverURL = 'notavalidurl';
|
||||
ParseServer.verifyServerUrl(async result => {
|
||||
if (result) {
|
||||
done.fail('Did not mark invalid url');
|
||||
}
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
const response = await ParseServer.verifyServerUrl();
|
||||
expect(response).not.toBeTrue();
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
`\nWARNING, Unable to connect to 'notavalidurl' as the URL is invalid. Cloud code and push notifications may be unavailable!\n`
|
||||
);
|
||||
});
|
||||
|
||||
xit('handleShutdown, close connection', done => {
|
||||
@@ -92,7 +85,7 @@ describe('Server Url Checks', () => {
|
||||
close = true;
|
||||
},
|
||||
});
|
||||
const parseServer = ParseServer.start(newConfiguration);
|
||||
const parseServer = ParseServer.startApp(newConfiguration);
|
||||
});
|
||||
|
||||
it('does not have unhandled promise rejection in the case of load error', done => {
|
||||
|
||||
@@ -2,7 +2,6 @@ const ParseServerRESTController = require('../lib/ParseServerRESTController')
|
||||
.ParseServerRESTController;
|
||||
const ParseServer = require('../lib/ParseServer').default;
|
||||
const Parse = require('parse/node').Parse;
|
||||
const semver = require('semver');
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
|
||||
let RESTController;
|
||||
@@ -130,19 +129,13 @@ describe('ParseServerRESTController', () => {
|
||||
});
|
||||
|
||||
if (
|
||||
(semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') &&
|
||||
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
|
||||
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger') ||
|
||||
process.env.MONGODB_TOPOLOGY === 'replicaset' ||
|
||||
process.env.PARSE_SERVER_TEST_DB === 'postgres'
|
||||
) {
|
||||
describe('transactions', () => {
|
||||
beforeEach(async () => {
|
||||
await TestUtils.destroyAllDataPermanently(true);
|
||||
if (
|
||||
semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') &&
|
||||
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
|
||||
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger'
|
||||
) {
|
||||
if (process.env.MONGODB_TOPOLOGY === 'replicaset') {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI:
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
//
|
||||
|
||||
'use strict';
|
||||
const request = require('../lib/request');
|
||||
|
||||
function setupTestUsers() {
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
const user1 = new Parse.User();
|
||||
const user2 = new Parse.User();
|
||||
const user3 = new Parse.User();
|
||||
@@ -17,6 +20,10 @@ function setupTestUsers() {
|
||||
user2.set('password', 'password');
|
||||
user3.set('password', 'password');
|
||||
|
||||
user1.setACL(acl);
|
||||
user2.setACL(acl);
|
||||
user3.setACL(acl);
|
||||
|
||||
return user1
|
||||
.signUp()
|
||||
.then(() => {
|
||||
@@ -137,7 +144,6 @@ describe('Parse.Session', () => {
|
||||
});
|
||||
|
||||
it('cannot edit session with known ID', async () => {
|
||||
const request = require('../lib/request');
|
||||
await setupTestUsers();
|
||||
const [first, second] = await new Parse.Query(Parse.Session).find({ useMasterKey: true });
|
||||
const headers = {
|
||||
|
||||
+246
-78
@@ -13,6 +13,63 @@ const passwordCrypto = require('../lib/password');
|
||||
const Config = require('../lib/Config');
|
||||
const cryptoUtils = require('../lib/cryptoUtils');
|
||||
|
||||
describe('allowExpiredAuthDataToken option', () => {
|
||||
it('should accept true value', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
await reconfigureServer({ allowExpiredAuthDataToken: true });
|
||||
expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(true);
|
||||
expect(
|
||||
logSpy.calls
|
||||
.all()
|
||||
.filter(
|
||||
log =>
|
||||
log.args[0] ===
|
||||
`DeprecationWarning: The Parse Server option 'allowExpiredAuthDataToken' default will change to 'false' in a future version.`
|
||||
).length
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it('should accept false value', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
await reconfigureServer({ allowExpiredAuthDataToken: false });
|
||||
expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(false);
|
||||
expect(
|
||||
logSpy.calls
|
||||
.all()
|
||||
.filter(
|
||||
log =>
|
||||
log.args[0] ===
|
||||
`DeprecationWarning: The Parse Server option 'allowExpiredAuthDataToken' default will change to 'false' in a future version.`
|
||||
).length
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it('should default true', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
await reconfigureServer({});
|
||||
expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(true);
|
||||
expect(
|
||||
logSpy.calls
|
||||
.all()
|
||||
.filter(
|
||||
log =>
|
||||
log.args[0] ===
|
||||
`DeprecationWarning: The Parse Server option 'allowExpiredAuthDataToken' default will change to 'false' in a future version.`
|
||||
).length
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it('should enforce boolean values', async () => {
|
||||
const options = [[], 'a', '', 0, 1, {}, 'true', 'false'];
|
||||
for (const option of options) {
|
||||
await expectAsync(reconfigureServer({ allowExpiredAuthDataToken: option })).toBeRejected();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parse.User testing', () => {
|
||||
it('user sign up class method', async done => {
|
||||
const user = await Parse.User.signUp('asdf', 'zxcv');
|
||||
@@ -139,14 +196,13 @@ describe('Parse.User testing', () => {
|
||||
const ACL = user.getACL();
|
||||
expect(ACL.getReadAccess(user)).toBe(true);
|
||||
expect(ACL.getWriteAccess(user)).toBe(true);
|
||||
expect(ACL.getPublicReadAccess()).toBe(true);
|
||||
expect(ACL.getPublicReadAccess()).toBe(false);
|
||||
expect(ACL.getPublicWriteAccess()).toBe(false);
|
||||
const perms = ACL.permissionsById;
|
||||
expect(Object.keys(perms).length).toBe(2);
|
||||
expect(Object.keys(perms).length).toBe(1);
|
||||
expect(perms[user.id].read).toBe(true);
|
||||
expect(perms[user.id].write).toBe(true);
|
||||
expect(perms['*'].read).toBe(true);
|
||||
expect(perms['*'].write).not.toBe(true);
|
||||
expect(perms['*']).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -818,8 +874,8 @@ describe('Parse.User testing', () => {
|
||||
kevin.set('password', 'mypass');
|
||||
await kevin.signUp();
|
||||
const query = new Parse.Query(Parse.User);
|
||||
const count = await query.count();
|
||||
equal(count, 2);
|
||||
const count = await query.find({ useMasterKey: true });
|
||||
equal(count.length, 2);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -1129,7 +1185,7 @@ describe('Parse.User testing', () => {
|
||||
this.synchronizedExpiration = authData.expiration_date;
|
||||
return true;
|
||||
},
|
||||
getAuthType: function () {
|
||||
getAuthType() {
|
||||
return 'facebook';
|
||||
},
|
||||
deauthenticate: function () {
|
||||
@@ -1158,7 +1214,7 @@ describe('Parse.User testing', () => {
|
||||
synchronizedAuthToken: null,
|
||||
synchronizedExpiration: null,
|
||||
|
||||
authenticate: function (options) {
|
||||
authenticate(options) {
|
||||
if (this.shouldError) {
|
||||
options.error(this, 'An error occurred');
|
||||
} else if (this.shouldCancel) {
|
||||
@@ -1167,7 +1223,7 @@ describe('Parse.User testing', () => {
|
||||
options.success(this, this.authData);
|
||||
}
|
||||
},
|
||||
restoreAuthentication: function (authData) {
|
||||
restoreAuthentication(authData) {
|
||||
if (!authData) {
|
||||
this.synchronizedUserId = null;
|
||||
this.synchronizedAuthToken = null;
|
||||
@@ -1179,10 +1235,10 @@ describe('Parse.User testing', () => {
|
||||
this.synchronizedExpiration = authData.expiration_date;
|
||||
return true;
|
||||
},
|
||||
getAuthType: function () {
|
||||
getAuthType() {
|
||||
return 'myoauth';
|
||||
},
|
||||
deauthenticate: function () {
|
||||
deauthenticate() {
|
||||
this.loggedOut = true;
|
||||
this.restoreAuthentication(null);
|
||||
},
|
||||
@@ -1792,7 +1848,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow login with old authData token', done => {
|
||||
it('should allow login with expired authData token by default', async () => {
|
||||
const provider = {
|
||||
authData: {
|
||||
id: '12345',
|
||||
@@ -1813,22 +1869,42 @@ describe('Parse.User testing', () => {
|
||||
};
|
||||
defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('token');
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
Parse.User._logInWith('shortLivedAuth', {})
|
||||
.then(() => {
|
||||
// Simulate a remotely expired token (like a short lived one)
|
||||
// In this case, we want success as it was valid once.
|
||||
// If the client needs an updated one, do lock the user out
|
||||
defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('otherToken');
|
||||
return Parse.User._logInWith('shortLivedAuth', {});
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
done();
|
||||
},
|
||||
err => {
|
||||
done.fail(err);
|
||||
}
|
||||
);
|
||||
await Parse.User._logInWith('shortLivedAuth', {});
|
||||
// Simulate a remotely expired token (like a short lived one)
|
||||
// In this case, we want success as it was valid once.
|
||||
// If the client needs an updated token, do lock the user out
|
||||
defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('otherToken');
|
||||
await Parse.User._logInWith('shortLivedAuth', {});
|
||||
});
|
||||
|
||||
it('should not allow login with expired authData token when allowExpiredAuthDataToken is set to false', async () => {
|
||||
await reconfigureServer({ allowExpiredAuthDataToken: false });
|
||||
const provider = {
|
||||
authData: {
|
||||
id: '12345',
|
||||
access_token: 'token',
|
||||
},
|
||||
restoreAuthentication() {
|
||||
return true;
|
||||
},
|
||||
deauthenticate() {
|
||||
provider.authData = {};
|
||||
},
|
||||
authenticate(options) {
|
||||
options.success(this, provider.authData);
|
||||
},
|
||||
getAuthType() {
|
||||
return 'shortLivedAuth';
|
||||
},
|
||||
};
|
||||
defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('token');
|
||||
Parse.User._registerAuthenticationProvider(provider);
|
||||
await Parse.User._logInWith('shortLivedAuth', {});
|
||||
// Simulate a remotely expired token (like a short lived one)
|
||||
// In this case, we want success as it was valid once.
|
||||
// If the client needs an updated token, do lock the user out
|
||||
defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('otherToken');
|
||||
expectAsync(Parse.User._logInWith('shortLivedAuth', {})).toBeRejected();
|
||||
});
|
||||
|
||||
it('should allow PUT request with stale auth Data', done => {
|
||||
@@ -2076,7 +2152,15 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
|
||||
it("querying for users doesn't get session tokens", done => {
|
||||
Parse.User.signUp('finn', 'human', { foo: 'bar' })
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'finn');
|
||||
user.set('password', 'human');
|
||||
user.set('foo', 'bar');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
user
|
||||
.signUp()
|
||||
.then(function () {
|
||||
return Parse.User.logOut();
|
||||
})
|
||||
@@ -2085,6 +2169,9 @@ describe('Parse.User testing', () => {
|
||||
user.set('username', 'jake');
|
||||
user.set('password', 'dog');
|
||||
user.set('foo', 'baz');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
return user.signUp();
|
||||
})
|
||||
.then(function () {
|
||||
@@ -2111,7 +2198,14 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
|
||||
it('querying for users only gets the expected fields', done => {
|
||||
Parse.User.signUp('finn', 'human', { foo: 'bar' }).then(() => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('finn');
|
||||
user.setPassword('human');
|
||||
user.set('foo', 'bar');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
user.signUp().then(() => {
|
||||
request({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -2260,37 +2354,14 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
|
||||
describe('anonymous users', () => {
|
||||
beforeEach(() => {
|
||||
const insensitiveCollisions = [
|
||||
'abcdefghijklmnop',
|
||||
'Abcdefghijklmnop',
|
||||
'ABcdefghijklmnop',
|
||||
'ABCdefghijklmnop',
|
||||
'ABCDefghijklmnop',
|
||||
'ABCDEfghijklmnop',
|
||||
'ABCDEFghijklmnop',
|
||||
'ABCDEFGhijklmnop',
|
||||
'ABCDEFGHijklmnop',
|
||||
'ABCDEFGHIjklmnop',
|
||||
'ABCDEFGHIJklmnop',
|
||||
'ABCDEFGHIJKlmnop',
|
||||
'ABCDEFGHIJKLmnop',
|
||||
'ABCDEFGHIJKLMnop',
|
||||
'ABCDEFGHIJKLMnop',
|
||||
'ABCDEFGHIJKLMNop',
|
||||
'ABCDEFGHIJKLMNOp',
|
||||
'ABCDEFGHIJKLMNOP',
|
||||
];
|
||||
|
||||
// need a bunch of spare random strings per api request
|
||||
spyOn(cryptoUtils, 'randomString').and.returnValues(...insensitiveCollisions);
|
||||
});
|
||||
|
||||
it('should not fail on case insensitive matches', async () => {
|
||||
const user1 = await Parse.AnonymousUtils.logIn();
|
||||
spyOn(cryptoUtils, 'randomString').and.returnValue('abcdefghijklmnop');
|
||||
const logIn = id => Parse.User.logInWith('anonymous', { authData: { id } });
|
||||
const user1 = await logIn('test1');
|
||||
const username1 = user1.get('username');
|
||||
|
||||
const user2 = await Parse.AnonymousUtils.logIn();
|
||||
cryptoUtils.randomString.and.returnValue('ABCDEFGHIJKLMNOp');
|
||||
const user2 = await logIn('test2');
|
||||
const username2 = user2.get('username');
|
||||
|
||||
expect(username1).not.toBeUndefined();
|
||||
@@ -2926,7 +2997,7 @@ describe('Parse.User testing', () => {
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve(),
|
||||
};
|
||||
reconfigureServer({
|
||||
await reconfigureServer({
|
||||
appName: 'unused',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
@@ -3372,6 +3443,9 @@ describe('Parse.User testing', () => {
|
||||
password: 'world',
|
||||
email: 'test@email.com',
|
||||
});
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
|
||||
reconfigureServer({
|
||||
appName: 'unused',
|
||||
@@ -3448,40 +3522,128 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow updates to hidden fields', done => {
|
||||
it('should not allow updates to hidden fields', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
const user = new Parse.User();
|
||||
user.set({
|
||||
username: 'hello',
|
||||
password: 'world',
|
||||
email: 'test@email.com',
|
||||
});
|
||||
|
||||
reconfigureServer({
|
||||
await reconfigureServer({
|
||||
appName: 'unused',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
})
|
||||
.then(() => {
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.User.current().set('_email_verify_token', 'bad').save();
|
||||
})
|
||||
.then(() => {
|
||||
fail('Should not be able to update email verification token');
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
expect(err).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
await user.signUp();
|
||||
user.set('_email_verify_token', 'bad', { ignoreValidation: true });
|
||||
await expectAsync(user.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid field name: _email_verify_token.')
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow updates to fields with maintenanceKey', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve(),
|
||||
};
|
||||
const user = new Parse.User();
|
||||
user.set({
|
||||
username: 'hello',
|
||||
password: 'world',
|
||||
email: 'test@example.com',
|
||||
});
|
||||
await reconfigureServer({
|
||||
appName: 'unused',
|
||||
maintenanceKey: 'test2',
|
||||
verifyUserEmails: true,
|
||||
emailVerifyTokenValidityDuration: 5,
|
||||
accountLockout: {
|
||||
duration: 1,
|
||||
threshold: 1,
|
||||
},
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
await user.signUp();
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await Parse.User.logIn(user.getEmail(), 'abc');
|
||||
} catch (e) {
|
||||
expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
|
||||
expect(
|
||||
e.message === 'Invalid username/password.' ||
|
||||
e.message ===
|
||||
'Your account is locked due to multiple failed login attempts. Please try again after 1 minute(s)'
|
||||
).toBeTrue();
|
||||
}
|
||||
}
|
||||
await Parse.User.requestPasswordReset(user.getEmail());
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'rest',
|
||||
'X-Parse-Maintenance-Key': 'test2',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const userMaster = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User`,
|
||||
json: true,
|
||||
headers,
|
||||
}).then(res => res.data.results[0]);
|
||||
expect(Object.keys(userMaster).sort()).toEqual(
|
||||
[
|
||||
'ACL',
|
||||
'_account_lockout_expires_at',
|
||||
'_email_verify_token',
|
||||
'_email_verify_token_expires_at',
|
||||
'_failed_login_count',
|
||||
'_perishable_token',
|
||||
'createdAt',
|
||||
'email',
|
||||
'emailVerified',
|
||||
'objectId',
|
||||
'updatedAt',
|
||||
'username',
|
||||
].sort()
|
||||
);
|
||||
const toSet = {
|
||||
_account_lockout_expires_at: new Date(),
|
||||
_email_verify_token: 'abc',
|
||||
_email_verify_token_expires_at: new Date(),
|
||||
_failed_login_count: 0,
|
||||
_perishable_token_expires_at: new Date(),
|
||||
_perishable_token: 'abc',
|
||||
};
|
||||
await request({
|
||||
method: 'PUT',
|
||||
headers,
|
||||
url: Parse.serverURL + '/users/' + userMaster.objectId,
|
||||
json: true,
|
||||
body: toSet,
|
||||
}).then(res => res.data);
|
||||
const update = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User`,
|
||||
json: true,
|
||||
headers,
|
||||
}).then(res => res.data.results[0]);
|
||||
for (const key in toSet) {
|
||||
const value = toSet[key];
|
||||
if (update[key] && update[key].iso) {
|
||||
expect(update[key].iso).toEqual(value.toISOString());
|
||||
} else if (value.toISOString) {
|
||||
expect(update[key]).toEqual(value.toISOString());
|
||||
} else {
|
||||
expect(update[key]).toEqual(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should revoke sessions when setting paswword with masterKey (#3289)', done => {
|
||||
@@ -4003,6 +4165,12 @@ describe('Parse.User testing', () => {
|
||||
silent: true,
|
||||
});
|
||||
|
||||
Parse.Cloud.beforeSave(Parse.User, ({ object }) => {
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
object.setACL(acl);
|
||||
});
|
||||
|
||||
const query = new Parse.Query(Parse.User);
|
||||
query.doesNotExist('foo');
|
||||
const subscription = await query.subscribe();
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -1677,12 +1677,19 @@ describe('Password Policy: ', () => {
|
||||
});
|
||||
|
||||
it('should not infinitely loop if maxPasswordHistory is 1 (#4918)', async () => {
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'test',
|
||||
'X-Parse-Maintenance-Key': 'test2',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const user = new Parse.User();
|
||||
const query = new Parse.Query(Parse.User);
|
||||
|
||||
await reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
verifyUserEmails: false,
|
||||
maintenanceKey: 'test2',
|
||||
passwordPolicy: {
|
||||
maxPasswordHistory: 1,
|
||||
},
|
||||
@@ -1696,15 +1703,28 @@ describe('Password Policy: ', () => {
|
||||
user.setPassword('user2');
|
||||
await user.save();
|
||||
|
||||
const result1 = await query.get(user.id, { useMasterKey: true });
|
||||
expect(result1.get('_password_history').length).toBe(1);
|
||||
const user1 = await query.get(user.id, { useMasterKey: true });
|
||||
expect(user1.get('_password_history')).toBeUndefined();
|
||||
|
||||
const result1 = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User/${user.id}`,
|
||||
json: true,
|
||||
headers,
|
||||
}).then(res => res.data);
|
||||
expect(result1._password_history.length).toBe(1);
|
||||
|
||||
user.setPassword('user3');
|
||||
await user.save();
|
||||
|
||||
const result2 = await query.get(user.id, { useMasterKey: true });
|
||||
expect(result2.get('_password_history').length).toBe(1);
|
||||
const result2 = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User/${user.id}`,
|
||||
json: true,
|
||||
headers,
|
||||
}).then(res => res.data);
|
||||
expect(result2._password_history.length).toBe(1);
|
||||
|
||||
expect(result1.get('_password_history')).not.toEqual(result2.get('_password_history'));
|
||||
expect(result1._password_history).not.toEqual(result2._password_history);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,13 +27,13 @@ const baseURI = 'postgres://username:password@localhost:5432/db-name';
|
||||
const testfile = fs.readFileSync('./Dockerfile').toString();
|
||||
const dbOptionsTest = {};
|
||||
dbOptionsTest[
|
||||
`${baseURI}?ssl=true&binary=true&application_name=app_name&fallback_application_name=f_app_name&poolSize=10`
|
||||
`${baseURI}?ssl=true&binary=true&application_name=app_name&fallback_application_name=f_app_name&poolSize=12`
|
||||
] = {
|
||||
ssl: true,
|
||||
binary: true,
|
||||
application_name: 'app_name',
|
||||
fallback_application_name: 'f_app_name',
|
||||
poolSize: 10,
|
||||
max: 12,
|
||||
};
|
||||
dbOptionsTest[`${baseURI}?ssl=&binary=aa`] = {
|
||||
binary: false,
|
||||
@@ -83,6 +83,20 @@ describe('PostgresConfigParser.getDatabaseOptionsFromURI', () => {
|
||||
it('sets the poolSize to 10 if the it is not a number', () => {
|
||||
const result = parser.getDatabaseOptionsFromURI(`${baseURI}?poolSize=sdf`);
|
||||
|
||||
expect(result.poolSize).toEqual(10);
|
||||
expect(result.max).toEqual(10);
|
||||
});
|
||||
|
||||
it('sets the max to 10 if the it is not a number', () => {
|
||||
const result = parser.getDatabaseOptionsFromURI(`${baseURI}?&max=sdf`);
|
||||
|
||||
expect(result.poolSize).toBeUndefined();
|
||||
expect(result.max).toEqual(10);
|
||||
});
|
||||
|
||||
it('max should take precedence over poolSize', () => {
|
||||
const result = parser.getDatabaseOptionsFromURI(`${baseURI}?poolSize=20&max=12`);
|
||||
|
||||
expect(result.poolSize).toBeUndefined();
|
||||
expect(result.max).toEqual(12);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,9 @@ describe('ProtectedFields', function () {
|
||||
user.setPassword('sekrit');
|
||||
user.set('email', 'alice@aol.com');
|
||||
user.set('favoriteColor', 'yellow');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
await user.save();
|
||||
|
||||
const fetched = await new Parse.Query(Parse.User).get(user.id);
|
||||
@@ -35,6 +38,9 @@ describe('ProtectedFields', function () {
|
||||
user.set('timeZone', 'America/Los_Angeles');
|
||||
user.set('favoriteColor', 'yellow');
|
||||
user.set('favoriteFood', 'pizza');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
await user.save();
|
||||
|
||||
const fetched = await new Parse.Query(Parse.User).get(user.id);
|
||||
@@ -57,6 +63,9 @@ describe('ProtectedFields', function () {
|
||||
user.set('timeZone', 'America/Los_Angeles');
|
||||
user.set('favoriteColor', 'yellow');
|
||||
user.set('favoriteFood', 'pizza');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
await user.save();
|
||||
|
||||
const fetched = await new Parse.Query(Parse.User).get(user.id);
|
||||
@@ -108,6 +117,9 @@ describe('ProtectedFields', function () {
|
||||
user.set('timeZone', 'America/Los_Angeles');
|
||||
user.set('favoriteColor', 'yellow');
|
||||
user.set('favoriteFood', 'pizza');
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
user.setACL(acl);
|
||||
await user.save();
|
||||
|
||||
const objA = await new Parse.Object('ClassA').set('foo', 'zzz').set('bar', 'yyy').save();
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default;
|
||||
describe('rate limit', () => {
|
||||
it('can limit cloud functions', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can limit cloud functions with user session token', async () => {
|
||||
await Parse.User.signUp('myUser', 'password');
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can add global limit', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: {
|
||||
requestPath: '*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
await expectAsync(new Parse.Object('Test').save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can limit cloud with validator', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc', {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can skip with masterKey', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test', null, { useMasterKey: true });
|
||||
expect(response1).toBe('Abc');
|
||||
const response2 = await Parse.Cloud.run('test', null, { useMasterKey: true });
|
||||
expect(response2).toBe('Abc');
|
||||
});
|
||||
|
||||
it('should run with masterKey', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
includeMasterKey: true,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test', null, { useMasterKey: true });
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can limit saving objects', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await expectAsync(obj.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set method to post', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
requestMethods: 'POST',
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await obj.save();
|
||||
const obj2 = new Parse.Object('Test');
|
||||
await expectAsync(obj2.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can use a validator for post', async () => {
|
||||
Parse.Cloud.beforeSave('Test', () => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await expectAsync(obj.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can use a validator for file', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const file = new Parse.File('yolo.txt', [1, 2, 3], 'text/plain');
|
||||
await file.save();
|
||||
const file2 = new Parse.File('yolo.txt', [1, 2, 3], 'text/plain');
|
||||
await expectAsync(file2.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set method to get', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/Test',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
requestMethods: 'GET',
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await obj.save();
|
||||
await new Parse.Query('Test').first();
|
||||
await expectAsync(new Parse.Query('Test').first()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can use a validator', async () => {
|
||||
await reconfigureServer({ silent: false });
|
||||
Parse.Cloud.beforeFind('TestObject', () => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const obj = new Parse.Object('TestObject');
|
||||
await obj.save();
|
||||
await obj.save();
|
||||
await new Parse.Query('TestObject').first();
|
||||
await expectAsync(new Parse.Query('TestObject').first()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
await expectAsync(new Parse.Query('TestObject').get('abc')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set method to delete', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/Test/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
requestMethods: 'DELETE',
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await obj.destroy();
|
||||
await expectAsync(obj.destroy()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set beforeDelete', async () => {
|
||||
const obj = new Parse.Object('TestDelete');
|
||||
await obj.save();
|
||||
Parse.Cloud.beforeDelete('TestDelete', () => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
await obj.destroy();
|
||||
await expectAsync(obj.destroy()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can set beforeLogin', async () => {
|
||||
Parse.Cloud.beforeLogin(() => {}, {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
await Parse.User.signUp('myUser', 'password');
|
||||
await Parse.User.logIn('myUser', 'password');
|
||||
await expectAsync(Parse.User.logIn('myUser', 'password')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
|
||||
it('can define limits via rateLimit and define', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 100,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
Parse.Cloud.define('test', () => 'Abc', {
|
||||
rateLimit: {
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
includeInternalRequests: true,
|
||||
},
|
||||
});
|
||||
const response1 = await Parse.Cloud.run('test');
|
||||
expect(response1).toBe('Abc');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests.')
|
||||
);
|
||||
});
|
||||
|
||||
it('does not limit internal calls', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
},
|
||||
],
|
||||
});
|
||||
Parse.Cloud.define('test1', () => 'Abc');
|
||||
Parse.Cloud.define('test2', async () => {
|
||||
await Parse.Cloud.run('test1');
|
||||
await Parse.Cloud.run('test1');
|
||||
});
|
||||
await Parse.Cloud.run('test2');
|
||||
});
|
||||
|
||||
it('can validate rateLimit', async () => {
|
||||
const Config = require('../lib/Config');
|
||||
const validateRateLimit = ({ rateLimit }) => Config.validateRateLimit(rateLimit);
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: 'a', requestTimeWindow: 1000, requestCount: 3 })
|
||||
).toThrow('rateLimit must be an array or object');
|
||||
expect(() => validateRateLimit({ rateLimit: ['a'] })).toThrow(
|
||||
'rateLimit must be an array of objects'
|
||||
);
|
||||
expect(() => validateRateLimit({ rateLimit: [{ requestPath: [] }] })).toThrow(
|
||||
'rateLimit.requestPath must be a string'
|
||||
);
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestTimeWindow: [], requestPath: 'a' }] })
|
||||
).toThrow('rateLimit.requestTimeWindow must be a number');
|
||||
expect(() =>
|
||||
validateRateLimit({
|
||||
rateLimit: [
|
||||
{
|
||||
includeInternalRequests: [],
|
||||
requestTimeWindow: 1000,
|
||||
requestCount: 3,
|
||||
requestPath: 'a',
|
||||
},
|
||||
],
|
||||
})
|
||||
).toThrow('rateLimit.includeInternalRequests must be a boolean');
|
||||
expect(() =>
|
||||
validateRateLimit({
|
||||
rateLimit: [{ requestCount: [], requestTimeWindow: 1000, requestPath: 'a' }],
|
||||
})
|
||||
).toThrow('rateLimit.requestCount must be a number');
|
||||
expect(() =>
|
||||
validateRateLimit({
|
||||
rateLimit: [
|
||||
{ errorResponseMessage: [], requestTimeWindow: 1000, requestCount: 3, requestPath: 'a' },
|
||||
],
|
||||
})
|
||||
).toThrow('rateLimit.errorResponseMessage must be a string');
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestCount: 3, requestPath: 'abc' }] })
|
||||
).toThrow('rateLimit.requestTimeWindow must be defined');
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestTimeWindow: 3, requestPath: 'abc' }] })
|
||||
).toThrow('rateLimit.requestCount must be defined');
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestTimeWindow: 3, requestCount: 'abc' }] })
|
||||
).toThrow('rateLimit.requestPath must be defined');
|
||||
await expectAsync(
|
||||
reconfigureServer({
|
||||
rateLimit: [{ requestTimeWindow: 3, requestCount: 1, path: 'abc', requestPath: 'a' }],
|
||||
})
|
||||
).toBeRejectedWith(`Invalid rate limit option "path"`);
|
||||
});
|
||||
describe_only(() => {
|
||||
return process.env.PARSE_SERVER_TEST_CACHE === 'redis';
|
||||
})('with RedisCache', function () {
|
||||
it('does work with cache', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: [
|
||||
{
|
||||
requestPath: '/classes/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
redisUrl: 'redis://localhost:6379',
|
||||
},
|
||||
],
|
||||
});
|
||||
const obj = new Parse.Object('Test');
|
||||
await obj.save();
|
||||
await expectAsync(obj.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
const cache = new RedisCacheAdapter();
|
||||
await cache.connect();
|
||||
const value = await cache.get('rl:127.0.0.1');
|
||||
expect(value).toEqual(2);
|
||||
const ttl = await cache.client.ttl('rl:127.0.0.1');
|
||||
expect(ttl).toEqual(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -625,7 +625,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
spyOn(Collection.prototype, 'aggregate').and.callThrough();
|
||||
// Query
|
||||
const query = new Parse.Query('MyObject');
|
||||
const results = await query.aggregate([{ match: { boolKey: false } }]);
|
||||
const results = await query.aggregate([{ $match: { boolKey: false } }]);
|
||||
// Validate
|
||||
expect(results.length).toBe(1);
|
||||
let readPreference = null;
|
||||
@@ -678,7 +678,7 @@ describe_only_db('mongo')('Read preference option', () => {
|
||||
// Query
|
||||
const query = new Parse.Query('MyObject');
|
||||
query.readPreference('SECONDARY');
|
||||
const results = await query.aggregate([{ match: { boolKey: false } }]);
|
||||
const results = await query.aggregate([{ $match: { boolKey: false } }]);
|
||||
// Validate
|
||||
expect(results.length).toBe(1);
|
||||
let readPreference = null;
|
||||
|
||||
@@ -19,23 +19,22 @@ describe_only(() => {
|
||||
|
||||
beforeEach(async () => {
|
||||
cache = new RedisCacheAdapter(null, 100);
|
||||
await cache.connect();
|
||||
await cache.clear();
|
||||
});
|
||||
|
||||
it('should get/set/clear', done => {
|
||||
it('should get/set/clear', async () => {
|
||||
const cacheNaN = new RedisCacheAdapter({
|
||||
ttl: NaN,
|
||||
});
|
||||
|
||||
cacheNaN
|
||||
.put(KEY, VALUE)
|
||||
.then(() => cacheNaN.get(KEY))
|
||||
.then(value => expect(value).toEqual(VALUE))
|
||||
.then(() => cacheNaN.clear())
|
||||
.then(() => cacheNaN.get(KEY))
|
||||
.then(value => expect(value).toEqual(null))
|
||||
.then(() => cacheNaN.clear())
|
||||
.then(done);
|
||||
await cacheNaN.connect();
|
||||
await cacheNaN.put(KEY, VALUE);
|
||||
let value = await cacheNaN.get(KEY);
|
||||
expect(value).toEqual(VALUE);
|
||||
await cacheNaN.clear();
|
||||
value = await cacheNaN.get(KEY);
|
||||
expect(value).toEqual(null);
|
||||
await cacheNaN.clear();
|
||||
});
|
||||
|
||||
it('should expire after ttl', done => {
|
||||
@@ -100,7 +99,7 @@ describe_only(() => {
|
||||
it('handleShutdown, close connection', async () => {
|
||||
await cache.handleShutdown();
|
||||
setTimeout(() => {
|
||||
expect(cache.client.connected).toBe(false);
|
||||
expect(cache.client.isOpen).toBe(false);
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
@@ -122,8 +121,9 @@ describe_only(() => {
|
||||
return Object.keys(cache.queue.queue).length;
|
||||
}
|
||||
|
||||
it('it should clear completed operations from queue', done => {
|
||||
it('it should clear completed operations from queue', async done => {
|
||||
const cache = new RedisCacheAdapter({ ttl: NaN });
|
||||
await cache.connect();
|
||||
|
||||
// execute a bunch of operations in sequence
|
||||
let promise = Promise.resolve();
|
||||
@@ -144,8 +144,9 @@ describe_only(() => {
|
||||
promise.then(() => expect(getQueueCount(cache)).toEqual(0)).then(done);
|
||||
});
|
||||
|
||||
it('it should count per key chained operations correctly', done => {
|
||||
it('it should count per key chained operations correctly', async done => {
|
||||
const cache = new RedisCacheAdapter({ ttl: NaN });
|
||||
await cache.connect();
|
||||
|
||||
let key1Promise = Promise.resolve();
|
||||
let key2Promise = Promise.resolve();
|
||||
|
||||
@@ -15,7 +15,8 @@ describe('RedisPubSub', function () {
|
||||
});
|
||||
|
||||
const redis = require('redis');
|
||||
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', {
|
||||
expect(redis.createClient).toHaveBeenCalledWith({
|
||||
url: 'redisAddress',
|
||||
socket_keepalive: true,
|
||||
no_ready_check: true,
|
||||
});
|
||||
@@ -28,7 +29,8 @@ describe('RedisPubSub', function () {
|
||||
});
|
||||
|
||||
const redis = require('redis');
|
||||
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', {
|
||||
expect(redis.createClient).toHaveBeenCalledWith({
|
||||
url: 'redisAddress',
|
||||
socket_keepalive: true,
|
||||
no_ready_check: true,
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ const publicServerURL = 'http://localhost:8378/1';
|
||||
describe('Regex Vulnerabilities', function () {
|
||||
beforeEach(async function () {
|
||||
await reconfigureServer({
|
||||
maintenanceKey: 'test2',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter,
|
||||
appName,
|
||||
@@ -98,11 +99,20 @@ describe('Regex Vulnerabilities', function () {
|
||||
|
||||
it('should work with plain token', async function () {
|
||||
expect(this.user.get('emailVerified')).toEqual(false);
|
||||
const current = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User/${this.user.id}`,
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'test',
|
||||
'X-Parse-Maintenance-Key': 'test2',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(res => res.data);
|
||||
// It should work
|
||||
await request({
|
||||
url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${this.user.get(
|
||||
'_email_verify_token'
|
||||
)}`,
|
||||
url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${current._email_verify_token}`,
|
||||
method: 'GET',
|
||||
});
|
||||
await this.user.fetch({ useMasterKey: true });
|
||||
@@ -164,8 +174,18 @@ describe('Regex Vulnerabilities', function () {
|
||||
email: 'someemail@somedomain.com',
|
||||
}),
|
||||
});
|
||||
await this.user.fetch({ useMasterKey: true });
|
||||
const token = this.user.get('_perishable_token');
|
||||
const current = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/_User/${this.user.id}`,
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'test',
|
||||
'X-Parse-Maintenance-Key': 'test2',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(res => res.data);
|
||||
const token = current._perishable_token;
|
||||
const passwordResetResponse = await request({
|
||||
url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`,
|
||||
method: 'GET',
|
||||
|
||||
+46
-20
@@ -96,6 +96,7 @@ describe('rest query', () => {
|
||||
let user = {
|
||||
username: 'aUsername',
|
||||
password: 'aPassword',
|
||||
ACL: { '*': { read: true } },
|
||||
};
|
||||
const activity = {
|
||||
type: 'comment',
|
||||
@@ -398,15 +399,16 @@ describe('RestQuery.each', () => {
|
||||
}
|
||||
const config = Config.get('test');
|
||||
await Parse.Object.saveAll(objects);
|
||||
const query = new RestQuery(
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config,
|
||||
auth.master(config),
|
||||
'Object',
|
||||
{ value: { $gt: 2 } },
|
||||
{ limit: 2 }
|
||||
);
|
||||
auth: auth.master(config),
|
||||
className: 'Object',
|
||||
restWhere: { value: { $gt: 2 } },
|
||||
restOptions: { limit: 2 },
|
||||
});
|
||||
const spy = spyOn(query, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
const results = [];
|
||||
await query.each(result => {
|
||||
expect(result.value).toBeGreaterThan(2);
|
||||
@@ -437,34 +439,37 @@ describe('RestQuery.each', () => {
|
||||
* Two queries needed since objectId are sorted and we can't know which one
|
||||
* going to be the first and then skip by the $gt added by each
|
||||
*/
|
||||
const queryOne = new RestQuery(
|
||||
const queryOne = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth.master(config),
|
||||
'Letter',
|
||||
{
|
||||
auth: auth.master(config),
|
||||
className: 'Letter',
|
||||
restWhere: {
|
||||
numbers: {
|
||||
__type: 'Pointer',
|
||||
className: 'Number',
|
||||
objectId: object1.id,
|
||||
},
|
||||
},
|
||||
{ limit: 1 }
|
||||
);
|
||||
const queryTwo = new RestQuery(
|
||||
restOptions: { limit: 1 },
|
||||
});
|
||||
|
||||
const queryTwo = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
config,
|
||||
auth.master(config),
|
||||
'Letter',
|
||||
{
|
||||
auth: auth.master(config),
|
||||
className: 'Letter',
|
||||
restWhere: {
|
||||
numbers: {
|
||||
__type: 'Pointer',
|
||||
className: 'Number',
|
||||
objectId: object2.id,
|
||||
},
|
||||
},
|
||||
{ limit: 1 }
|
||||
);
|
||||
restOptions: { limit: 1 },
|
||||
});
|
||||
|
||||
const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
|
||||
const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
|
||||
const resultsOne = [];
|
||||
const resultsTwo = [];
|
||||
await queryOne.each(result => {
|
||||
@@ -502,4 +507,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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -204,4 +204,58 @@ describe('Schema Performance', function () {
|
||||
);
|
||||
expect(getAllSpy.calls.count()).toBe(2);
|
||||
});
|
||||
|
||||
it('does reload with schemaCacheTtl', async () => {
|
||||
const databaseURI =
|
||||
process.env.PARSE_SERVER_TEST_DB === 'postgres'
|
||||
? process.env.PARSE_SERVER_TEST_DATABASE_URI
|
||||
: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI,
|
||||
silent: false,
|
||||
databaseOptions: { schemaCacheTtl: 1000 },
|
||||
});
|
||||
const SchemaController = require('../lib/Controllers/SchemaController').SchemaController;
|
||||
const spy = spyOn(SchemaController.prototype, 'reloadData').and.callThrough();
|
||||
Object.defineProperty(spy, 'reloadCalls', {
|
||||
get: () => spy.calls.all().filter(call => call.args[0].clearCache).length,
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
object.set('foo', 'bar');
|
||||
await object.save();
|
||||
|
||||
spy.calls.reset();
|
||||
|
||||
object.set('foo', 'bar');
|
||||
await object.save();
|
||||
|
||||
expect(spy.reloadCalls).toBe(0);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
|
||||
object.set('foo', 'bar');
|
||||
await object.save();
|
||||
|
||||
expect(spy.reloadCalls).toBe(1);
|
||||
});
|
||||
|
||||
it('cannot set invalid databaseOptions', async () => {
|
||||
const expectError = async (key, value, expected) =>
|
||||
expectAsync(
|
||||
reconfigureServer({ databaseAdapter: undefined, databaseOptions: { [key]: value } })
|
||||
).toBeRejectedWith(`databaseOptions.${key} must be a ${expected}`);
|
||||
for (const databaseOptions of [[], 0, 'string']) {
|
||||
await expectAsync(
|
||||
reconfigureServer({ databaseAdapter: undefined, databaseOptions })
|
||||
).toBeRejectedWith(`databaseOptions must be an object`);
|
||||
}
|
||||
for (const value of [null, 0, 'string', {}, []]) {
|
||||
await expectError('enableSchemaHooks', value, 'boolean');
|
||||
}
|
||||
for (const value of [null, false, 'string', {}, []]) {
|
||||
await expectError('schemaCacheTtl', value, 'number');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,9 @@ describe('Personally Identifiable Information', () => {
|
||||
await reconfigureServer();
|
||||
user = await Parse.User.signUp('tester', 'abc');
|
||||
user = await Parse.User.logIn(user.get('username'), 'abc');
|
||||
await user.set('email', EMAIL).set('zip', ZIP).set('ssn', SSN).save();
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
await user.set('email', EMAIL).set('zip', ZIP).set('ssn', SSN).setACL(acl).save();
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user'
|
||||
);
|
||||
user
|
||||
.fetch()
|
||||
.fetch({ useMasterKey: true })
|
||||
.then(
|
||||
() => {
|
||||
expect(user.get('emailVerified')).toEqual(true);
|
||||
@@ -1082,4 +1082,43 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw on an invalid reset password', async () => {
|
||||
await reconfigureServer({
|
||||
appName: 'coolapp',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
passwordPolicy: {
|
||||
resetPasswordSuccessOnInvalidEmail: false,
|
||||
},
|
||||
});
|
||||
|
||||
await expectAsync(Parse.User.requestPasswordReset('test@example.com')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'A user with that email does not exist.')
|
||||
);
|
||||
});
|
||||
|
||||
it('validate resetPasswordSuccessonInvalidEmail', async () => {
|
||||
const invalidValues = [[], {}, 1, 'string'];
|
||||
for (const value of invalidValues) {
|
||||
await expectAsync(
|
||||
reconfigureServer({
|
||||
appName: 'coolapp',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
passwordPolicy: {
|
||||
resetPasswordSuccessOnInvalidEmail: value,
|
||||
},
|
||||
})
|
||||
).toBeRejectedWith('resetPasswordSuccessOnInvalidEmail must be a boolean value');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+2
-9
@@ -1,6 +1,5 @@
|
||||
const batch = require('../lib/batch');
|
||||
const request = require('../lib/request');
|
||||
const semver = require('semver');
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
|
||||
const originalURL = '/parse/batch';
|
||||
@@ -206,19 +205,13 @@ describe('batch', () => {
|
||||
});
|
||||
|
||||
if (
|
||||
(semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') &&
|
||||
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
|
||||
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger') ||
|
||||
process.env.MONGODB_TOPOLOGY === 'replicaset' ||
|
||||
process.env.PARSE_SERVER_TEST_DB === 'postgres'
|
||||
) {
|
||||
describe('transactions', () => {
|
||||
beforeEach(async () => {
|
||||
await TestUtils.destroyAllDataPermanently(true);
|
||||
if (
|
||||
semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') &&
|
||||
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
|
||||
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger'
|
||||
) {
|
||||
if (process.env.MONGODB_TOPOLOGY === 'replicaset') {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Parse.Cloud.define('cloudCodeInFile', () => {
|
||||
return 'It is possible to define cloud code in a file.';
|
||||
});
|
||||
+80
-43
@@ -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());
|
||||
@@ -44,16 +50,19 @@ const { VolatileClassesSchemas } = require('../lib/Controllers/SchemaController'
|
||||
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
|
||||
let databaseAdapter;
|
||||
let databaseURI;
|
||||
// need to bind for mocking mocha
|
||||
|
||||
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
|
||||
databaseURI = process.env.PARSE_SERVER_TEST_DATABASE_URI || postgresURI;
|
||||
databaseAdapter = new PostgresStorageAdapter({
|
||||
uri: process.env.PARSE_SERVER_TEST_DATABASE_URI || postgresURI,
|
||||
uri: databaseURI,
|
||||
collectionPrefix: 'test_',
|
||||
});
|
||||
} else {
|
||||
databaseURI = mongoURI;
|
||||
databaseAdapter = new MongoStorageAdapter({
|
||||
uri: mongoURI,
|
||||
uri: databaseURI,
|
||||
collectionPrefix: 'test_',
|
||||
});
|
||||
}
|
||||
@@ -96,7 +105,7 @@ const defaultConfiguration = {
|
||||
masterKey: 'test',
|
||||
readOnlyMasterKey: 'read-only-test',
|
||||
fileKey: 'test',
|
||||
directAccess: false,
|
||||
directAccess: true,
|
||||
silent,
|
||||
logLevel,
|
||||
fileUpload: {
|
||||
@@ -142,47 +151,33 @@ let server;
|
||||
let didChangeConfiguration = false;
|
||||
|
||||
// Allows testing specific configurations of Parse Server
|
||||
const reconfigureServer = (changedConfiguration = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (server) {
|
||||
return server.close(() => {
|
||||
server = undefined;
|
||||
reconfigureServer(changedConfiguration).then(resolve, reject);
|
||||
});
|
||||
}
|
||||
try {
|
||||
let parseServer = undefined;
|
||||
didChangeConfiguration = Object.keys(changedConfiguration).length !== 0;
|
||||
const newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, {
|
||||
serverStartComplete: error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
Parse.CoreManager.setRESTController(RESTController);
|
||||
resolve(parseServer);
|
||||
}
|
||||
},
|
||||
mountPath: '/1',
|
||||
port,
|
||||
});
|
||||
cache.clear();
|
||||
parseServer = ParseServer.start(newConfiguration);
|
||||
parseServer.expressApp.use('/1', err => {
|
||||
console.error(err);
|
||||
fail('should not call next');
|
||||
});
|
||||
server = parseServer.server;
|
||||
server.on('connection', connection => {
|
||||
const key = `${connection.remoteAddress}:${connection.remotePort}`;
|
||||
openConnections[key] = connection;
|
||||
connection.on('close', () => {
|
||||
delete openConnections[key];
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
const reconfigureServer = async (changedConfiguration = {}) => {
|
||||
if (server) {
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
server = undefined;
|
||||
return reconfigureServer(changedConfiguration);
|
||||
}
|
||||
didChangeConfiguration = Object.keys(changedConfiguration).length !== 0;
|
||||
const newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, {
|
||||
mountPath: '/1',
|
||||
port,
|
||||
});
|
||||
cache.clear();
|
||||
const parseServer = await ParseServer.startApp(newConfiguration);
|
||||
server = parseServer.server;
|
||||
Parse.CoreManager.setRESTController(RESTController);
|
||||
parseServer.expressApp.use('/1', err => {
|
||||
console.error(err);
|
||||
fail('should not call next');
|
||||
});
|
||||
server.on('connection', connection => {
|
||||
const key = `${connection.remoteAddress}:${connection.remotePort}`;
|
||||
openConnections[key] = connection;
|
||||
connection.on('close', () => {
|
||||
delete openConnections[key];
|
||||
});
|
||||
});
|
||||
return parseServer;
|
||||
};
|
||||
|
||||
// Set up a Parse client to talk to our test API server
|
||||
@@ -203,6 +198,10 @@ beforeAll(async () => {
|
||||
Parse.serverURL = 'http://localhost:' + port + '/1';
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
const afterLogOut = async () => {
|
||||
if (Object.keys(openConnections).length > 0) {
|
||||
@@ -219,6 +218,7 @@ afterEach(function (done) {
|
||||
done();
|
||||
};
|
||||
Parse.Cloud._removeAllHooks();
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient();
|
||||
defaults.protectedFields = { _User: { '*': ['email'] } };
|
||||
databaseAdapter
|
||||
.getAllClasses()
|
||||
@@ -414,6 +414,7 @@ global.defaultConfiguration = defaultConfiguration;
|
||||
global.mockCustomAuthenticator = mockCustomAuthenticator;
|
||||
global.mockFacebookAuthenticator = mockFacebookAuthenticator;
|
||||
global.databaseAdapter = databaseAdapter;
|
||||
global.databaseURI = databaseURI;
|
||||
global.jfail = function (err) {
|
||||
fail(JSON.stringify(err));
|
||||
};
|
||||
@@ -455,6 +456,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 +474,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 +501,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 +519,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;
|
||||
|
||||
+147
-92
@@ -61,34 +61,19 @@ describe('server', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if database is unreachable', done => {
|
||||
reconfigureServer({
|
||||
it('fails if database is unreachable', async () => {
|
||||
const server = new ParseServer.default({
|
||||
...defaultConfiguration,
|
||||
databaseAdapter: new MongoStorageAdapter({
|
||||
uri: 'mongodb://fake:fake@localhost:43605/drew3',
|
||||
mongoOptions: {
|
||||
serverSelectionTimeoutMS: 2000,
|
||||
},
|
||||
}),
|
||||
}).catch(() => {
|
||||
const config = Config.get('test');
|
||||
config.schemaCache.clear();
|
||||
//Need to use rest api because saving via JS SDK results in fail() not getting called
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/NewClass',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
body: {},
|
||||
}).then(fail, response => {
|
||||
expect(response.status).toEqual(500);
|
||||
const body = response.data;
|
||||
expect(body.code).toEqual(1);
|
||||
expect(body.message).toEqual('Internal server error.');
|
||||
reconfigureServer().then(done, done);
|
||||
});
|
||||
});
|
||||
const error = await server.start().catch(e => e);
|
||||
expect(`${error}`.includes('MongoServerSelectionError')).toBeTrue();
|
||||
await reconfigureServer();
|
||||
});
|
||||
|
||||
describe('mail adapter', () => {
|
||||
@@ -295,91 +280,47 @@ describe('server', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can create a parse-server v1', done => {
|
||||
it('can create a parse-server v1', async () => {
|
||||
await reconfigureServer({ appId: 'aTestApp' });
|
||||
const parseServer = new ParseServer.default(
|
||||
Object.assign({}, defaultConfiguration, {
|
||||
appId: 'aTestApp',
|
||||
masterKey: 'aTestMasterKey',
|
||||
serverURL: 'http://localhost:12666/parse',
|
||||
serverStartComplete: () => {
|
||||
expect(Parse.applicationId).toEqual('aTestApp');
|
||||
const app = express();
|
||||
app.use('/parse', parseServer.app);
|
||||
|
||||
const server = app.listen(12666);
|
||||
const obj = new Parse.Object('AnObject');
|
||||
let objId;
|
||||
obj
|
||||
.save()
|
||||
.then(obj => {
|
||||
objId = obj.id;
|
||||
const q = new Parse.Query('AnObject');
|
||||
return q.first();
|
||||
})
|
||||
.then(obj => {
|
||||
expect(obj.id).toEqual(objId);
|
||||
server.close(async () => {
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
server.close(async () => {
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
await parseServer.start();
|
||||
expect(Parse.applicationId).toEqual('aTestApp');
|
||||
const app = express();
|
||||
app.use('/parse', parseServer.app);
|
||||
const server = app.listen(12666);
|
||||
const obj = new Parse.Object('AnObject');
|
||||
await obj.save();
|
||||
const query = await new Parse.Query('AnObject').first();
|
||||
expect(obj.id).toEqual(query.id);
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
});
|
||||
|
||||
it('can create a parse-server v2', done => {
|
||||
let objId;
|
||||
let server;
|
||||
it('can create a parse-server v2', async () => {
|
||||
await reconfigureServer({ appId: 'anOtherTestApp' });
|
||||
const parseServer = ParseServer.ParseServer(
|
||||
Object.assign({}, defaultConfiguration, {
|
||||
appId: 'anOtherTestApp',
|
||||
masterKey: 'anOtherTestMasterKey',
|
||||
serverURL: 'http://localhost:12667/parse',
|
||||
serverStartComplete: error => {
|
||||
const promise = error ? Promise.reject(error) : Promise.resolve();
|
||||
promise
|
||||
.then(() => {
|
||||
expect(Parse.applicationId).toEqual('anOtherTestApp');
|
||||
const app = express();
|
||||
app.use('/parse', parseServer);
|
||||
|
||||
server = app.listen(12667);
|
||||
const obj = new Parse.Object('AnObject');
|
||||
return obj.save();
|
||||
})
|
||||
.then(obj => {
|
||||
objId = obj.id;
|
||||
const q = new Parse.Query('AnObject');
|
||||
return q.first();
|
||||
})
|
||||
.then(obj => {
|
||||
expect(obj.id).toEqual(objId);
|
||||
server.close(async () => {
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
fail(JSON.stringify(error));
|
||||
if (server) {
|
||||
server.close(async () => {
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(Parse.applicationId).toEqual('anOtherTestApp');
|
||||
await parseServer.start();
|
||||
const app = express();
|
||||
app.use('/parse', parseServer.app);
|
||||
const server = app.listen(12667);
|
||||
const obj = new Parse.Object('AnObject');
|
||||
await obj.save();
|
||||
const q = await new Parse.Query('AnObject').first();
|
||||
expect(obj.id).toEqual(q.id);
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
});
|
||||
|
||||
it('has createLiveQueryServer', done => {
|
||||
@@ -462,6 +403,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.');
|
||||
@@ -475,7 +436,9 @@ describe('server', () => {
|
||||
|
||||
it('fails if you provides invalid ip in masterKeyIps', done => {
|
||||
reconfigureServer({ masterKeyIps: ['invalidIp', '1.2.3.4'] }).catch(error => {
|
||||
expect(error).toEqual('Invalid ip in masterKeyIps: invalidIp');
|
||||
expect(error).toEqual(
|
||||
'The Parse Server option "masterKeyIps" contains an invalid IP address "invalidIp".'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -486,6 +449,11 @@ describe('server', () => {
|
||||
}).then(done);
|
||||
});
|
||||
|
||||
it('should set default masterKeyIps for IPv4 and IPv6 localhost', () => {
|
||||
const definitions = require('../lib/Options/Definitions.js');
|
||||
expect(definitions.ParseServerOptions.masterKeyIps.default).toEqual(['127.0.0.1', '::1']);
|
||||
});
|
||||
|
||||
it('should load a middleware', done => {
|
||||
const obj = {
|
||||
middleware: function (req, res, next) {
|
||||
@@ -531,8 +499,95 @@ describe('server', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('can call start', async () => {
|
||||
await reconfigureServer({ appId: 'aTestApp' });
|
||||
const config = {
|
||||
...defaultConfiguration,
|
||||
appId: 'aTestApp',
|
||||
masterKey: 'aTestMasterKey',
|
||||
serverURL: 'http://localhost:12701/parse',
|
||||
};
|
||||
const parseServer = new ParseServer.ParseServer(config);
|
||||
await parseServer.start();
|
||||
expect(Parse.applicationId).toEqual('aTestApp');
|
||||
expect(Parse.serverURL).toEqual('http://localhost:12701/parse');
|
||||
const app = express();
|
||||
app.use('/parse', parseServer.app);
|
||||
const server = app.listen(12701);
|
||||
const testObject = new Parse.Object('TestObject');
|
||||
await expectAsync(testObject.save()).toBeResolved();
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
});
|
||||
|
||||
it('start is required to mount', async () => {
|
||||
await reconfigureServer({ appId: 'aTestApp' });
|
||||
const config = {
|
||||
...defaultConfiguration,
|
||||
appId: 'aTestApp',
|
||||
masterKey: 'aTestMasterKey',
|
||||
serverURL: 'http://localhost:12701/parse',
|
||||
};
|
||||
const parseServer = new ParseServer.ParseServer(config);
|
||||
expect(Parse.applicationId).toEqual('aTestApp');
|
||||
expect(Parse.serverURL).toEqual('http://localhost:12701/parse');
|
||||
const app = express();
|
||||
app.use('/parse', parseServer.app);
|
||||
const server = app.listen(12701);
|
||||
const response = await request({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'aTestApp',
|
||||
},
|
||||
method: 'POST',
|
||||
url: 'http://localhost:12701/parse/classes/TestObject',
|
||||
}).catch(e => new Parse.Error(e.data.code, e.data.error));
|
||||
expect(response).toEqual(
|
||||
new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid server state: initialized')
|
||||
);
|
||||
const health = await request({
|
||||
url: 'http://localhost:12701/parse/health',
|
||||
}).catch(e => e);
|
||||
spyOn(console, 'warn').and.callFake(() => {});
|
||||
const verify = await ParseServer.default.verifyServerUrl();
|
||||
expect(verify).not.toBeTrue();
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
`\nWARNING, Unable to connect to 'http://localhost:12701/parse'. Cloud code and push notifications may be unavailable!\n`
|
||||
);
|
||||
expect(health.data.status).toBe('initialized');
|
||||
expect(health.status).toBe(503);
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
});
|
||||
|
||||
it('can get starting state', async () => {
|
||||
await reconfigureServer({ appId: 'test2' });
|
||||
const parseServer = new ParseServer.ParseServer({
|
||||
...defaultConfiguration,
|
||||
appId: 'test2',
|
||||
masterKey: 'abc',
|
||||
serverURL: 'http://localhost:12668/parse',
|
||||
async cloud() {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
},
|
||||
});
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
app.use('/parse', parseServer.app);
|
||||
const server = app.listen(12668);
|
||||
const startingPromise = parseServer.start();
|
||||
const health = await request({
|
||||
url: 'http://localhost:12668/parse/health',
|
||||
}).catch(e => e);
|
||||
expect(health.data.status).toBe('starting');
|
||||
expect(health.status).toBe(503);
|
||||
expect(health.headers['retry-after']).toBe('1');
|
||||
const response = await ParseServer.default.verifyServerUrl();
|
||||
expect(response).toBeTrue();
|
||||
await startingPromise;
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
});
|
||||
|
||||
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 +600,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
|
||||
|
||||
@@ -660,6 +660,38 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot get object in volatileClasses if not masterKey through pointer', async () => {
|
||||
const masterKeyOnlyClassObject = new Parse.Object('_PushStatus');
|
||||
await masterKeyOnlyClassObject.save(null, { useMasterKey: true });
|
||||
const obj2 = new Parse.Object('TestObject');
|
||||
// Anyone is can basically create a pointer to any object
|
||||
// or some developers can use master key in some hook to link
|
||||
// private objects to standard objects
|
||||
obj2.set('pointer', masterKeyOnlyClassObject);
|
||||
await obj2.save();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.include('pointer');
|
||||
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
|
||||
"Clients aren't allowed to perform the get operation on the _PushStatus collection."
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot get object in _GlobalConfig if not masterKey through pointer', async () => {
|
||||
await Parse.Config.save({ privateData: 'secret' }, { privateData: true });
|
||||
const obj2 = new Parse.Object('TestObject');
|
||||
obj2.set('globalConfigPointer', {
|
||||
__type: 'Pointer',
|
||||
className: '_GlobalConfig',
|
||||
objectId: 1,
|
||||
});
|
||||
await obj2.save();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.include('globalConfigPointer');
|
||||
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
|
||||
"Clients aren't allowed to perform the get operation on the _GlobalConfig collection."
|
||||
);
|
||||
});
|
||||
|
||||
it('locks down session', done => {
|
||||
let currentUser;
|
||||
Parse.User.signUp('foo', 'bar')
|
||||
@@ -859,6 +891,15 @@ describe('read-only masterKey', () => {
|
||||
await reconfigureServer();
|
||||
});
|
||||
|
||||
it('should throw when masterKey and maintenanceKey are the same', async () => {
|
||||
await expectAsync(
|
||||
reconfigureServer({
|
||||
masterKey: 'yolo',
|
||||
maintenanceKey: 'yolo',
|
||||
})
|
||||
).toBeRejectedWith(new Error('masterKey and maintenanceKey should be different'));
|
||||
});
|
||||
|
||||
it('should throw when trying to create RestWrite', () => {
|
||||
const config = Config.get('test');
|
||||
expect(() => {
|
||||
|
||||
@@ -6,14 +6,20 @@ const ParseServer = require('../../lib/index').ParseServer;
|
||||
|
||||
const databaseURI = 'mongodb://doesnotexist:27017/parseServerMongoAdapterTestDatabase';
|
||||
|
||||
ParseServer.start({
|
||||
appId: 'test',
|
||||
masterKey: 'test',
|
||||
databaseAdapter: new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: {
|
||||
serverSelectionTimeoutMS: 2000,
|
||||
},
|
||||
}),
|
||||
filesAdapter: new GridFSBucketAdapter(databaseURI),
|
||||
});
|
||||
(async () => {
|
||||
try {
|
||||
await ParseServer.startApp({
|
||||
appId: 'test',
|
||||
masterKey: 'test',
|
||||
databaseAdapter: new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
mongoOptions: {
|
||||
serverSelectionTimeoutMS: 2000,
|
||||
},
|
||||
}),
|
||||
filesAdapter: new GridFSBucketAdapter(databaseURI),
|
||||
});
|
||||
} catch (e) {
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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,132 @@ 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 global config with polluted data', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
};
|
||||
const params = {
|
||||
method: 'PUT',
|
||||
url: 'http://localhost:8378/1/config',
|
||||
json: true,
|
||||
body: {
|
||||
params: {
|
||||
welcomeMesssage: 'Welcome to Parse',
|
||||
foo: { _bsontype: 'Code', code: 'shell' },
|
||||
},
|
||||
},
|
||||
headers,
|
||||
};
|
||||
const response = await request(params).catch(e => e);
|
||||
expect(response.status).toBe(400);
|
||||
const text = JSON.parse(response.text);
|
||||
expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
|
||||
expect(text.error).toBe(
|
||||
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
|
||||
);
|
||||
});
|
||||
|
||||
it('denies direct database write wih prohibited keys', async () => {
|
||||
const Config = require('../lib/Config');
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const user = {
|
||||
objectId: '1234567890',
|
||||
username: 'hello',
|
||||
password: 'pass',
|
||||
_session_token: 'abc',
|
||||
foo: { _bsontype: 'Code', code: 'shell' },
|
||||
};
|
||||
await expectAsync(config.database.create('_User', user)).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('denies direct database update wih prohibited keys', async () => {
|
||||
const Config = require('../lib/Config');
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const user = {
|
||||
objectId: '1234567890',
|
||||
username: 'hello',
|
||||
password: 'pass',
|
||||
_session_token: 'abc',
|
||||
foo: { _bsontype: 'Code', code: 'shell' },
|
||||
};
|
||||
await expectAsync(
|
||||
config.database.update('_User', { _id: user.objectId }, user)
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
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,6 +405,44 @@ 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', () => {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/*eslint no-unused-vars: "off"*/
|
||||
/**
|
||||
* @module Adapters
|
||||
*/
|
||||
/**
|
||||
* @interface AnalyticsAdapter
|
||||
* @module Adapters
|
||||
*/
|
||||
export class AnalyticsAdapter {
|
||||
/**
|
||||
|
||||
@@ -1,22 +1,116 @@
|
||||
/*eslint no-unused-vars: "off"*/
|
||||
|
||||
/**
|
||||
* @interface ParseAuthResponse
|
||||
* @property {Boolean} [doNotSave] If true, Parse Server will not save provided authData.
|
||||
* @property {Object} [response] If set, Parse Server will send the provided response to the client under authDataResponse
|
||||
* @property {Object} [save] If set, Parse Server will save the object provided into this key, instead of client provided authData
|
||||
*/
|
||||
|
||||
/**
|
||||
* AuthPolicy
|
||||
* default: can be combined with ONE additional auth provider if additional configured on user
|
||||
* additional: could be only used with a default policy auth provider
|
||||
* solo: Will ignore ALL additional providers if additional configured on user
|
||||
* @typedef {"default" | "additional" | "solo"} AuthPolicy
|
||||
*/
|
||||
|
||||
export class AuthAdapter {
|
||||
/*
|
||||
@param appIds: the specified app ids in the configuration
|
||||
@param authData: the client provided authData
|
||||
@param options: additional options
|
||||
@returns a promise that resolves if the applicationId is valid
|
||||
constructor() {
|
||||
/**
|
||||
* Usage policy
|
||||
* @type {AuthPolicy}
|
||||
*/
|
||||
this.policy = 'default';
|
||||
}
|
||||
/**
|
||||
* @param appIds The specified app IDs in the configuration
|
||||
* @param {Object} authData The client provided authData
|
||||
* @param {Object} options additional adapter options
|
||||
* @param {Parse.Cloud.TriggerRequest} request
|
||||
* @returns {(Promise<undefined|void>|void|undefined)} resolves or returns if the applicationId is valid
|
||||
*/
|
||||
validateAppId(appIds, authData, options) {
|
||||
validateAppId(appIds, authData, options, request) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
/*
|
||||
@param authData: the client provided authData
|
||||
@param options: additional options
|
||||
/**
|
||||
* Legacy usage, if provided it will be triggered when authData related to this provider is touched (signup/update/login)
|
||||
* otherwise you should implement validateSetup, validateLogin and validateUpdate
|
||||
* @param {Object} authData The client provided authData
|
||||
* @param {Parse.Cloud.TriggerRequest} request
|
||||
* @param {Object} options additional adapter options
|
||||
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
||||
*/
|
||||
validateAuthData(authData, options) {
|
||||
validateAuthData(authData, request, options) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when user provide for the first time this auth provider
|
||||
* could be a register or the user adding a new auth service
|
||||
* @param {Object} authData The client provided authData
|
||||
* @param {Parse.Cloud.TriggerRequest} request
|
||||
* @param {Object} options additional adapter options
|
||||
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
||||
*/
|
||||
validateSetUp(authData, req, options) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when user provide authData related to this provider
|
||||
* The user is not logged in and has already set this provider before
|
||||
* @param {Object} authData The client provided authData
|
||||
* @param {Parse.Cloud.TriggerRequest} request
|
||||
* @param {Object} options additional adapter options
|
||||
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
||||
*/
|
||||
validateLogin(authData, req, options) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when user provide authData related to this provider
|
||||
* the user is logged in and has already set this provider before
|
||||
* @param {Object} authData The client provided authData
|
||||
* @param {Object} options additional adapter options
|
||||
* @param {Parse.Cloud.TriggerRequest} request
|
||||
* @returns {Promise<ParseAuthResponse|void|undefined>}
|
||||
*/
|
||||
validateUpdate(authData, req, options) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered in pre authentication process if needed (like webauthn, SMS OTP)
|
||||
* @param {Object} challengeData Data provided by the client
|
||||
* @param {(Object|undefined)} authData Auth data provided by the client, can be used for validation
|
||||
* @param {Object} options additional adapter options
|
||||
* @param {Parse.Cloud.TriggerRequest} request
|
||||
* @returns {Promise<Object>} A promise that resolves, resolved value will be added to challenge response under challenge key
|
||||
*/
|
||||
challenge(challengeData, authData, options, request) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when auth data is fetched
|
||||
* @param {Object} authData authData
|
||||
* @param {Object} options additional adapter options
|
||||
* @returns {Promise<Object>} Any overrides required to authData
|
||||
*/
|
||||
afterFind(authData, options) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when the adapter is first attached to Parse Server
|
||||
* @param {Object} options Adapter Options
|
||||
*/
|
||||
validateOptions(options) {
|
||||
/* */
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthAdapter;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+136
-20
@@ -1,4 +1,6 @@
|
||||
import loadAdapter from '../AdapterLoader';
|
||||
import Parse from 'parse/node';
|
||||
import AuthAdapter from './AuthAdapter';
|
||||
|
||||
const apple = require('./apple');
|
||||
const gcenter = require('./gcenter');
|
||||
@@ -61,19 +63,83 @@ const providers = {
|
||||
ldap,
|
||||
};
|
||||
|
||||
function authDataValidator(adapter, appIds, options) {
|
||||
return function (authData) {
|
||||
return adapter.validateAuthData(authData, options).then(() => {
|
||||
if (appIds) {
|
||||
return adapter.validateAppId(appIds, authData, options);
|
||||
// Indexed auth policies
|
||||
const authAdapterPolicies = {
|
||||
default: true,
|
||||
solo: true,
|
||||
additional: true,
|
||||
};
|
||||
|
||||
function authDataValidator(provider, adapter, appIds, options) {
|
||||
return async function (authData, req, user, requestObject) {
|
||||
if (appIds && typeof adapter.validateAppId === 'function') {
|
||||
await Promise.resolve(adapter.validateAppId(appIds, authData, options, requestObject));
|
||||
}
|
||||
if (adapter.policy && !authAdapterPolicies[adapter.policy]) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OTHER_CAUSE,
|
||||
'AuthAdapter policy is not configured correctly. The value must be either "solo", "additional", "default" or undefined (will be handled as "default")'
|
||||
);
|
||||
}
|
||||
if (typeof adapter.validateAuthData === 'function') {
|
||||
return adapter.validateAuthData(authData, options, requestObject);
|
||||
}
|
||||
if (
|
||||
typeof adapter.validateSetUp !== 'function' ||
|
||||
typeof adapter.validateLogin !== 'function' ||
|
||||
typeof adapter.validateUpdate !== 'function'
|
||||
) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OTHER_CAUSE,
|
||||
'Adapter is not configured. Implement either validateAuthData or all of the following: validateSetUp, validateLogin and validateUpdate'
|
||||
);
|
||||
}
|
||||
// When masterKey is detected, we should trigger a logged in user
|
||||
const isLoggedIn =
|
||||
(req.auth.user && user && req.auth.user.id === user.id) || (user && req.auth.isMaster);
|
||||
let hasAuthDataConfigured = false;
|
||||
|
||||
if (user && user.get('authData') && user.get('authData')[provider]) {
|
||||
hasAuthDataConfigured = true;
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
// User is updating their authData
|
||||
if (hasAuthDataConfigured) {
|
||||
return {
|
||||
method: 'validateUpdate',
|
||||
validator: () => adapter.validateUpdate(authData, options, requestObject),
|
||||
};
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
// Set up if the user does not have the provider configured
|
||||
return {
|
||||
method: 'validateSetUp',
|
||||
validator: () => adapter.validateSetUp(authData, options, requestObject),
|
||||
};
|
||||
}
|
||||
|
||||
// Not logged in and authData is configured on the user
|
||||
if (hasAuthDataConfigured) {
|
||||
return {
|
||||
method: 'validateLogin',
|
||||
validator: () => adapter.validateLogin(authData, options, requestObject),
|
||||
};
|
||||
}
|
||||
|
||||
// User not logged in and the provider is not set up, for example when a new user
|
||||
// signs up or an existing user uses a new auth provider
|
||||
return {
|
||||
method: 'validateSetUp',
|
||||
validator: () => adapter.validateSetUp(authData, options, requestObject),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function loadAuthAdapter(provider, authOptions) {
|
||||
// providers are auth providers implemented by default
|
||||
let defaultAdapter = providers[provider];
|
||||
// authOptions can contain complete custom auth adapters or
|
||||
// a default auth adapter like Facebook
|
||||
const providerOptions = authOptions[provider];
|
||||
if (
|
||||
providerOptions &&
|
||||
@@ -83,31 +149,50 @@ function loadAuthAdapter(provider, authOptions) {
|
||||
defaultAdapter = oauth2;
|
||||
}
|
||||
|
||||
// Default provider not found and a custom auth provider was not provided
|
||||
if (!defaultAdapter && !providerOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const adapter = Object.assign({}, defaultAdapter);
|
||||
const adapter =
|
||||
defaultAdapter instanceof AuthAdapter ? defaultAdapter : Object.assign({}, defaultAdapter);
|
||||
const keys = [
|
||||
'validateAuthData',
|
||||
'validateAppId',
|
||||
'validateSetUp',
|
||||
'validateLogin',
|
||||
'validateUpdate',
|
||||
'challenge',
|
||||
'validateOptions',
|
||||
'policy',
|
||||
'afterFind',
|
||||
];
|
||||
const defaultAuthAdapter = new AuthAdapter();
|
||||
keys.forEach(key => {
|
||||
const existing = adapter?.[key];
|
||||
if (
|
||||
existing &&
|
||||
typeof existing === 'function' &&
|
||||
existing.toString() === defaultAuthAdapter[key].toString()
|
||||
) {
|
||||
adapter[key] = null;
|
||||
}
|
||||
});
|
||||
const appIds = providerOptions ? providerOptions.appIds : undefined;
|
||||
|
||||
// Try the configuration methods
|
||||
if (providerOptions) {
|
||||
const optionalAdapter = loadAdapter(providerOptions, undefined, providerOptions);
|
||||
if (optionalAdapter) {
|
||||
['validateAuthData', 'validateAppId'].forEach(key => {
|
||||
keys.forEach(key => {
|
||||
if (optionalAdapter[key]) {
|
||||
adapter[key] = optionalAdapter[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: create a new module from validateAdapter() in
|
||||
// src/Controllers/AdaptableController.js so we can use it here for adapter
|
||||
// validation based on the src/Adapters/Auth/AuthAdapter.js expected class
|
||||
// signature.
|
||||
if (!adapter.validateAuthData || !adapter.validateAppId) {
|
||||
return;
|
||||
if (adapter.validateOptions) {
|
||||
adapter.validateOptions(providerOptions);
|
||||
}
|
||||
|
||||
return { adapter, appIds, providerOptions };
|
||||
@@ -121,17 +206,48 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
|
||||
// To handle the test cases on configuration
|
||||
const getValidatorForProvider = function (provider) {
|
||||
if (provider === 'anonymous' && !_enableAnonymousUsers) {
|
||||
return { validator: undefined };
|
||||
}
|
||||
const authAdapter = loadAuthAdapter(provider, authOptions);
|
||||
if (!authAdapter) return;
|
||||
const { adapter, appIds, providerOptions } = authAdapter;
|
||||
return { validator: authDataValidator(provider, adapter, appIds, providerOptions), adapter };
|
||||
};
|
||||
|
||||
const runAfterFind = async (req, authData) => {
|
||||
if (!authData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { adapter, appIds, providerOptions } = loadAuthAdapter(provider, authOptions);
|
||||
|
||||
return authDataValidator(adapter, appIds, providerOptions);
|
||||
const adapters = Object.keys(authData);
|
||||
await Promise.all(
|
||||
adapters.map(async provider => {
|
||||
const authAdapter = getValidatorForProvider(provider);
|
||||
if (!authAdapter) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
adapter: { afterFind },
|
||||
providerOptions,
|
||||
} = authAdapter;
|
||||
if (afterFind && typeof afterFind === 'function') {
|
||||
const requestObject = {
|
||||
ip: req.config.ip,
|
||||
user: req.auth.user,
|
||||
master: req.auth.isMaster,
|
||||
};
|
||||
const result = afterFind(requestObject, authData[provider], providerOptions);
|
||||
if (result) {
|
||||
authData[provider] = result;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return Object.freeze({
|
||||
getValidatorForProvider,
|
||||
setEnableAnonymousUsers,
|
||||
runAfterFind,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
/*eslint no-unused-vars: "off"*/
|
||||
/**
|
||||
* @module Adapters
|
||||
*/
|
||||
/**
|
||||
* @interface CacheAdapter
|
||||
* @interface
|
||||
* @memberof module:Adapters
|
||||
*/
|
||||
export class CacheAdapter {
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ export class LRUCache {
|
||||
constructor({ ttl = defaults.cacheTTL, maxSize = defaults.cacheMaxSize }) {
|
||||
this.cache = new LRU({
|
||||
max: maxSize,
|
||||
maxAge: ttl,
|
||||
ttl,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ export class LRUCache {
|
||||
}
|
||||
|
||||
del(key) {
|
||||
this.cache.del(key);
|
||||
this.cache.delete(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache.reset();
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import redis from 'redis';
|
||||
import { createClient } from 'redis';
|
||||
import logger from '../../logger';
|
||||
import { KeyPromiseQueue } from '../../KeyPromiseQueue';
|
||||
|
||||
@@ -15,114 +15,76 @@ const isValidTTL = ttl => typeof ttl === 'number' && ttl > 0;
|
||||
export class RedisCacheAdapter {
|
||||
constructor(redisCtx, ttl = DEFAULT_REDIS_TTL) {
|
||||
this.ttl = isValidTTL(ttl) ? ttl : DEFAULT_REDIS_TTL;
|
||||
this.client = redis.createClient(redisCtx);
|
||||
this.client = createClient(redisCtx);
|
||||
this.queue = new KeyPromiseQueue();
|
||||
}
|
||||
|
||||
handleShutdown() {
|
||||
if (!this.client) {
|
||||
return Promise.resolve();
|
||||
async connect() {
|
||||
if (this.client.isOpen) {
|
||||
return;
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
this.client.quit(err => {
|
||||
if (err) {
|
||||
logger.error('RedisCacheAdapter error on shutdown', { error: err });
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return this.client.connect();
|
||||
}
|
||||
|
||||
get(key) {
|
||||
async handleShutdown() {
|
||||
if (!this.client) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.client.quit();
|
||||
} catch (err) {
|
||||
logger.error('RedisCacheAdapter error on shutdown', { error: err });
|
||||
}
|
||||
}
|
||||
|
||||
async get(key) {
|
||||
debug('get', { key });
|
||||
return this.queue.enqueue(
|
||||
key,
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
this.client.get(key, function (err, res) {
|
||||
debug('-> get', { key, res });
|
||||
if (!res) {
|
||||
return resolve(null);
|
||||
}
|
||||
resolve(JSON.parse(res));
|
||||
});
|
||||
})
|
||||
);
|
||||
try {
|
||||
await this.queue.enqueue(key);
|
||||
const res = await this.client.get(key);
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(res);
|
||||
} catch (err) {
|
||||
logger.error('RedisCacheAdapter error on get', { error: err });
|
||||
}
|
||||
}
|
||||
|
||||
put(key, value, ttl = this.ttl) {
|
||||
async put(key, value, ttl = this.ttl) {
|
||||
value = JSON.stringify(value);
|
||||
debug('put', { key, value, ttl });
|
||||
|
||||
await this.queue.enqueue(key);
|
||||
if (ttl === 0) {
|
||||
// ttl of zero is a logical no-op, but redis cannot set expire time of zero
|
||||
return this.queue.enqueue(key, () => Promise.resolve());
|
||||
return;
|
||||
}
|
||||
|
||||
if (ttl === Infinity) {
|
||||
return this.queue.enqueue(
|
||||
key,
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
this.client.set(key, value, function () {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
return this.client.set(key, value);
|
||||
}
|
||||
|
||||
if (!isValidTTL(ttl)) {
|
||||
ttl = this.ttl;
|
||||
}
|
||||
|
||||
return this.queue.enqueue(
|
||||
key,
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
this.client.psetex(key, ttl, value, function () {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
return this.client.set(key, value, { PX: ttl });
|
||||
}
|
||||
|
||||
del(key) {
|
||||
async del(key) {
|
||||
debug('del', { key });
|
||||
return this.queue.enqueue(
|
||||
key,
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
this.client.del(key, function () {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
await this.queue.enqueue(key);
|
||||
return this.client.del(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
async clear() {
|
||||
debug('clear');
|
||||
return this.queue.enqueue(
|
||||
FLUSH_DB_KEY,
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
this.client.flushdb(function () {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
await this.queue.enqueue(FLUSH_DB_KEY);
|
||||
return this.client.sendCommand(['FLUSHDB']);
|
||||
}
|
||||
|
||||
// Used for testing
|
||||
async getAllKeys() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.keys('*', (err, keys) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(keys);
|
||||
}
|
||||
});
|
||||
});
|
||||
getAllKeys() {
|
||||
return this.client.keys('*');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/*eslint no-unused-vars: "off"*/
|
||||
/**
|
||||
* @module Adapters
|
||||
*/
|
||||
/**
|
||||
* @interface MailAdapter
|
||||
* @interface
|
||||
* @memberof module:Adapters
|
||||
* Mail Adapter prototype
|
||||
* A MailAdapter should implement at least sendMail()
|
||||
*/
|
||||
|
||||
@@ -19,10 +19,8 @@
|
||||
import type { Config } from '../../Config';
|
||||
import Parse from 'parse/node';
|
||||
/**
|
||||
* @module Adapters
|
||||
*/
|
||||
/**
|
||||
* @interface FilesAdapter
|
||||
* @interface
|
||||
* @memberof module:Adapters
|
||||
*/
|
||||
export class FilesAdapter {
|
||||
/** Responsible for storing the file in order to be retrieved later by its filename
|
||||
|
||||
@@ -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();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user