mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
365
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1979de03da | ||
|
|
6f2fc443c8 | ||
|
|
c696e1e044 | ||
|
|
70146bbdce | ||
|
|
694da02ce7 | ||
|
|
e1d4d122b6 | ||
|
|
68922c89bc | ||
|
|
1e21b69376 | ||
|
|
3353998edd | ||
|
|
cf4c8807b9 | ||
|
|
892052cc44 | ||
|
|
a6e0a1d4a6 | ||
|
|
28c4330c89 | ||
|
|
c764203ce8 | ||
|
|
ef1634bf1f | ||
|
|
e7199e8d07 | ||
|
|
2ecc5a5bf5 | ||
|
|
cd61c937f4 | ||
|
|
dca187f91b | ||
|
|
6bdd87c21a | ||
|
|
284da09f45 | ||
|
|
4906644d67 | ||
|
|
0633f98562 | ||
|
|
31ad5d124e | ||
|
|
91dde99d3d | ||
|
|
b6452633a7 | ||
|
|
4116f571a7 | ||
|
|
39e529d458 | ||
|
|
2edf1e4c03 | ||
|
|
ae72cf08bf | ||
|
|
665b8d52d6 | ||
|
|
885aa12c8c | ||
|
|
c71deef865 | ||
|
|
cb73e2d94b | ||
|
|
5000c2ac3b | ||
|
|
73eec3dfb1 | ||
|
|
480282cf18 | ||
|
|
e6be5117d0 | ||
|
|
5379135c88 | ||
|
|
42ff468960 | ||
|
|
162c07b0a7 | ||
|
|
11d3e484df | ||
|
|
716925ec53 | ||
|
|
18f2002b84 | ||
|
|
61841859cc | ||
|
|
61653a3983 | ||
|
|
0a054e6b54 | ||
|
|
d8ebdb3632 | ||
|
|
ca20496f28 | ||
|
|
b4739509f7 | ||
|
|
9d0bd2badd | ||
|
|
eba9dfff7b | ||
|
|
242002432f | ||
|
|
5b0efb22ef | ||
|
|
6364948d81 | ||
|
|
e01c44a15b | ||
|
|
bceb621a32 | ||
|
|
2532acfce8 | ||
|
|
2c21aed890 | ||
|
|
8758e6abb9 | ||
|
|
f1469c6425 | ||
|
|
855b5df758 | ||
|
|
480fc57984 | ||
|
|
390bb92c15 | ||
|
|
45d8ff9802 | ||
|
|
9ac4fa70c9 | ||
|
|
18bdbf89c5 | ||
|
|
e31d230026 | ||
|
|
33c06b261e | ||
|
|
d58d159fa2 | ||
|
|
9f6da23511 | ||
|
|
a45d34691c | ||
|
|
3a087e7ad8 | ||
|
|
7959083760 | ||
|
|
06e54f1d37 | ||
|
|
4420835afa | ||
|
|
c277739623 | ||
|
|
cfcb943ba5 | ||
|
|
b07ec15382 | ||
|
|
79846f7c15 | ||
|
|
dc48be85f7 | ||
|
|
de54d1e9bc | ||
|
|
e5b5c444b0 | ||
|
|
1c0eced4bd | ||
|
|
854ac9a017 | ||
|
|
5f81efb429 | ||
|
|
7d84b244ab | ||
|
|
907ad4267c | ||
|
|
6fb19bd83a | ||
|
|
da368ea763 | ||
|
|
6d4663b47e | ||
|
|
1a2b513e8c | ||
|
|
c83de8c4ea | ||
|
|
4c74b2b926 | ||
|
|
60cf2dd054 | ||
|
|
0d5acf3d9b | ||
|
|
5c2c120021 | ||
|
|
63d4880ebf | ||
|
|
09310a7700 | ||
|
|
9f6e3429d3 | ||
|
|
901aaf8cd3 | ||
|
|
ad4aa83983 | ||
|
|
1c2bb7ca28 | ||
|
|
ba098970df | ||
|
|
1ffc48fd08 | ||
|
|
720d24e185 | ||
|
|
2065897dc6 | ||
|
|
37e3934a85 | ||
|
|
6f20e54cce | ||
|
|
a5050df7fe | ||
|
|
90e103528d | ||
|
|
c7b2068329 | ||
|
|
2760381183 | ||
|
|
3fbd926b80 | ||
|
|
41f009db4e | ||
|
|
fbf6d41a00 | ||
|
|
a56fb76433 | ||
|
|
98edbdbd4b | ||
|
|
3de9acc006 | ||
|
|
cf99570c75 | ||
|
|
21458ff502 | ||
|
|
7773d3b5ef | ||
|
|
d3f0886707 | ||
|
|
2988c450e1 | ||
|
|
042d69614e | ||
|
|
edd416b647 | ||
|
|
29624e0fae | ||
|
|
fe1e4d9775 | ||
|
|
1f62dd0f4e | ||
|
|
f9c39edcbc | ||
|
|
2170962a50 | ||
|
|
afcafdba1e | ||
|
|
a84ecc9fec | ||
|
|
9d3030f6a8 | ||
|
|
dfea561f44 | ||
|
|
0613174b34 | ||
|
|
dbe8b72d8a | ||
|
|
9293ce8ca5 | ||
|
|
105ae7c8a5 | ||
|
|
01c97f7ab7 | ||
|
|
a7b5b38418 | ||
|
|
5452c8f41f | ||
|
|
cbefe770a7 | ||
|
|
9c85e63354 | ||
|
|
ab58eee0de | ||
|
|
ec5a8ccbfd | ||
|
|
763738c92b | ||
|
|
6579e8254c | ||
|
|
f5d6fc98e7 | ||
|
|
68b745a604 | ||
|
|
94846cd4de | ||
|
|
1d80aa25e3 | ||
|
|
597a9c3163 | ||
|
|
1f6dadbb81 | ||
|
|
ac7399b226 | ||
|
|
9001df5063 | ||
|
|
a6d574ede6 | ||
|
|
70c280ca57 | ||
|
|
e73bc517a8 | ||
|
|
25796537f4 | ||
|
|
efc5897f0b | ||
|
|
5202212b0b | ||
|
|
c604657a27 | ||
|
|
514c2377e0 | ||
|
|
dfd1aee7dd | ||
|
|
af686f31da | ||
|
|
0cf58eb8d6 | ||
|
|
0e9b6d69ef | ||
|
|
e29845f8da | ||
|
|
bbda8d2ed6 | ||
|
|
38983e8e9b | ||
|
|
359b66f9d1 | ||
|
|
4e6a375b51 | ||
|
|
df3d0bc82d | ||
|
|
1aba6382c8 | ||
|
|
6f21195980 | ||
|
|
a2ac8d133c | ||
|
|
27ad6f476a | ||
|
|
ced487246e | ||
|
|
104990b52a | ||
|
|
99489b22e4 | ||
|
|
655e2c41f0 | ||
|
|
633a9d25e4 | ||
|
|
abdba68380 | ||
|
|
66e36039d8 | ||
|
|
038b7a978b | ||
|
|
1eb95aeb41 | ||
|
|
355baf9dfc | ||
|
|
e315c137bf | ||
|
|
3c07fcada6 | ||
|
|
0023ce448a | ||
|
|
4aba66c620 | ||
|
|
8adcbee112 | ||
|
|
0d58e3972c | ||
|
|
3b9f50ac90 | ||
|
|
972f630016 | ||
|
|
19fc5461d5 | ||
|
|
69c927c907 | ||
|
|
8e7a6b1480 | ||
|
|
f9dde4a9f8 | ||
|
|
a22f095a47 | ||
|
|
a22dbe16d5 | ||
|
|
2c2563ebd2 | ||
|
|
3de8494a22 | ||
|
|
d3087ed69f | ||
|
|
b87daba067 | ||
|
|
759731926f | ||
|
|
0762ba6b79 | ||
|
|
6ef1986c03 | ||
|
|
988ecdac54 | ||
|
|
87059ae1d6 | ||
|
|
42e2e12956 | ||
|
|
d58032d39d | ||
|
|
5b1bb598e4 | ||
|
|
2612a387ce | ||
|
|
c6355cda73 | ||
|
|
90aac622b0 | ||
|
|
4b3ce20300 | ||
|
|
09fbeebba8 | ||
|
|
80b987d00d | ||
|
|
9e0094980f | ||
|
|
f630a45aa5 | ||
|
|
ea57a7706d | ||
|
|
fe02d3e8aa | ||
|
|
b0c012e835 | ||
|
|
fd86278919 | ||
|
|
5dd3aa0d48 | ||
|
|
2d6b3d1849 | ||
|
|
93af48a8b4 | ||
|
|
5462834240 | ||
|
|
7d32d8934f | ||
|
|
8d3117e0bc | ||
|
|
a2a98b1684 | ||
|
|
77bbfb3f18 | ||
|
|
9b9c3a4214 | ||
|
|
3d6d50e0af | ||
|
|
b70c2d9027 | ||
|
|
71dfd8a7ec | ||
|
|
39a91d0d77 | ||
|
|
4945ab7520 | ||
|
|
a9c34ef1e2 | ||
|
|
05939858af | ||
|
|
f5e20f9121 | ||
|
|
9c6cdf4904 | ||
|
|
4baeae4f4f | ||
|
|
3602ecb169 | ||
|
|
7f89399300 | ||
|
|
88a9106956 | ||
|
|
391c7a02aa | ||
|
|
6ea65f255f | ||
|
|
45a3ed0fcf | ||
|
|
977edeaf28 | ||
|
|
739ffbed86 | ||
|
|
5954f0ffa0 | ||
|
|
d141b822ad | ||
|
|
be4c7e23c6 | ||
|
|
877eede075 | ||
|
|
2b3d4e5d3c | ||
|
|
42929e0e5e | ||
|
|
b9bdca4520 | ||
|
|
ae68f0c31b | ||
|
|
7a198f4eed | ||
|
|
95da5d64c1 | ||
|
|
6a4a00ca7a | ||
|
|
c9b59719ec | ||
|
|
02f40fd896 | ||
|
|
a9d376b61f | ||
|
|
a742656a90 | ||
|
|
4e2000bc56 | ||
|
|
446cbb1a31 | ||
|
|
3f03bd3c6f | ||
|
|
31805c96ec | ||
|
|
d6b17baa32 | ||
|
|
328918178f | ||
|
|
3dd99dd80e | ||
|
|
f8b5a99d54 | ||
|
|
cc079a40f6 | ||
|
|
3ec3e40dc8 | ||
|
|
068fb9e777 | ||
|
|
e212eb5195 | ||
|
|
1850be45b1 | ||
|
|
e6bd2baf71 | ||
|
|
44acd6d9ed | ||
|
|
3710da7379 | ||
|
|
9674d4a2c0 | ||
|
|
45301a67e8 | ||
|
|
4ad0800508 | ||
|
|
e2e51d94fa | ||
|
|
0c3382fbab | ||
|
|
24c0b03f5e | ||
|
|
fb54ac1f1e | ||
|
|
02a2ef2baa | ||
|
|
234fb2cbf3 | ||
|
|
c8910abdaa | ||
|
|
03fba97e05 | ||
|
|
e2a7218f74 | ||
|
|
967700bdbc | ||
|
|
3ea1ace631 | ||
|
|
656d673cf5 | ||
|
|
b01d4f0abb | ||
|
|
82da30842a | ||
|
|
0ce3692759 | ||
|
|
8c9a56e8ea | ||
|
|
3ec453d885 | ||
|
|
bf2a224e46 | ||
|
|
5ab370d58e | ||
|
|
c0d1bc4f72 | ||
|
|
15704a68f1 | ||
|
|
9f1edd09f7 | ||
|
|
fc3b7526a6 | ||
|
|
505dd6bcfe | ||
|
|
fc81b411e9 | ||
|
|
6722110f20 | ||
|
|
6c5f89a56b | ||
|
|
0823a02fbf | ||
|
|
5eb690c1c5 | ||
|
|
a37d1ee244 | ||
|
|
bd39ac54f4 | ||
|
|
d7ac0cd9f8 | ||
|
|
c2e4f8369b | ||
|
|
28aeda3f16 | ||
|
|
fd6a0077f2 | ||
|
|
3030c7652e | ||
|
|
f911f2cd3a | ||
|
|
b0b99e7451 | ||
|
|
a005874776 | ||
|
|
4c1093e2e4 | ||
|
|
00c362d739 | ||
|
|
cc57fafe38 | ||
|
|
7121829bbb | ||
|
|
a27482c57e | ||
|
|
150627328f | ||
|
|
a318e7bbaf | ||
|
|
db4c214eac | ||
|
|
ab301b6518 | ||
|
|
d4cda4b26c | ||
|
|
c78a5a6f10 | ||
|
|
6f885d36b9 | ||
|
|
559b1de828 | ||
|
|
afd0515e20 | ||
|
|
4187a973cc | ||
|
|
2caea310be | ||
|
|
1302853187 | ||
|
|
6207a02df5 | ||
|
|
832702dffd | ||
|
|
18b63d1da7 | ||
|
|
f59d46c9e9 | ||
|
|
acba5cf16d | ||
|
|
2e4119e23d | ||
|
|
2f53c80298 | ||
|
|
4f15539ac2 | ||
|
|
9e43bc2fa0 | ||
|
|
177891ea0e | ||
|
|
65e5879e42 | ||
|
|
cf1b59e97b | ||
|
|
d9e347d741 | ||
|
|
c4ce59cbd1 | ||
|
|
34833e42ee | ||
|
|
0f1979f814 | ||
|
|
4ce135a4fe | ||
|
|
6613872ce2 | ||
|
|
ce34747e8a | ||
|
|
d05cfcdb95 | ||
|
|
c793bb88e7 | ||
|
|
87cab09b6a |
+5
-1
@@ -24,6 +24,10 @@
|
||||
"prefer-const": "error",
|
||||
"space-infix-ops": "error",
|
||||
"no-useless-escape": "off",
|
||||
"require-atomic-updates": "off"
|
||||
"require-atomic-updates": "off",
|
||||
"object-curly-spacing": ["error", "always"]
|
||||
},
|
||||
"globals": {
|
||||
"Parse": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,10 @@ assignees: ''
|
||||
---
|
||||
|
||||
### New Issue Checklist
|
||||
<!--
|
||||
Check every following box [x] before submitting your issue.
|
||||
Click the "Preview" tab for better readability.
|
||||
Thanks for contributing to Parse Platform!
|
||||
-->
|
||||
|
||||
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
|
||||
- [ ] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
|
||||
- [ ] I have searched through [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
- [ ] I can reproduce the issue with the [latest version of Parse Server](https://github.com/parse-community/parse-server/releases). <!-- We don't investigate issues for outdated releases. -->
|
||||
- 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).
|
||||
- Before posting search [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
|
||||
### Issue Description
|
||||
<!-- What is the specific issue with Parse Server? -->
|
||||
@@ -30,6 +24,7 @@ assignees: ''
|
||||
|
||||
### Expected Outcome
|
||||
<!-- What outcome, for example query result, did you expect? -->
|
||||
|
||||
### Environment
|
||||
<!-- Be specific with versions, don't use "latest" or semver ranges like "~x.y.z" or "^x.y.z". -->
|
||||
|
||||
|
||||
@@ -8,15 +8,10 @@ assignees: ''
|
||||
---
|
||||
|
||||
### New Feature / Enhancement Checklist
|
||||
<!--
|
||||
Check every following box [x] before submitting your issue.
|
||||
Click the "Preview" tab for better readability.
|
||||
Thanks for contributing to Parse Platform!
|
||||
-->
|
||||
|
||||
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
|
||||
- [ ] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
|
||||
- [ ] I have searched through [existing issues](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).
|
||||
- Before posting search [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
|
||||
### Current Limitation
|
||||
<!-- Which current limitation is the feature or enhancement addressing? -->
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Dependabot dependency updates
|
||||
# Docs: https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
# Location of package-lock.json
|
||||
directory: "/"
|
||||
# Check daily for updates
|
||||
schedule:
|
||||
interval: "daily"
|
||||
commit-message:
|
||||
# Set commit message prefix
|
||||
prefix: "refactor"
|
||||
@@ -13,18 +13,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: CI Environments Check
|
||||
@@ -36,7 +30,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Compose branch name for PR
|
||||
id: branch
|
||||
run: echo "::set-output name=name::ci-bump-environment"
|
||||
|
||||
+125
-127
@@ -1,12 +1,14 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [ release*, alpha, beta ]
|
||||
branches: [release, alpha, beta, next-major, 'release-[0-9]+.x.x']
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
paths-ignore:
|
||||
- '**/**.md'
|
||||
env:
|
||||
NODE_VERSION: 19.3.0
|
||||
NODE_VERSION: 20.12.0
|
||||
PARSE_SERVER_TEST_TIMEOUT: 20000
|
||||
jobs:
|
||||
check-code-analysis:
|
||||
@@ -19,29 +21,45 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
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
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- 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-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install prod dependencies
|
||||
run: npm ci
|
||||
- name: Remove dev dependencies
|
||||
run: ./ci/uninstallDevDeps.sh @actions/core
|
||||
- name: CI Node Engine Check
|
||||
run: npm run ci:checkNodeEngine
|
||||
check-lint:
|
||||
name: Lint
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -49,40 +67,19 @@ jobs:
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: CI Node Engine Check
|
||||
run: npm run ci:checkNodeEngine
|
||||
check-lint:
|
||||
name: Lint
|
||||
timeout-minutes: 15
|
||||
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
|
||||
- run: npm run lint
|
||||
- run: npm run lint
|
||||
check-definitions:
|
||||
name: Check Definitions
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
name: Check Definitions
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -93,48 +90,48 @@ jobs:
|
||||
- name: CI Definitions Check
|
||||
run: npm run ci:definitionsCheck
|
||||
check-circular:
|
||||
name: Circular Dependencies
|
||||
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
|
||||
- run: npm run madge:circular
|
||||
name: Circular Dependencies
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v4
|
||||
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
|
||||
- run: npm run madge:circular
|
||||
check-docker:
|
||||
name: Docker Build
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Build docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64, linux/arm64/v8
|
||||
check-lock-file-version:
|
||||
name: NPM Lock File Version
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check NPM lock file version
|
||||
uses: mansona/npm-lockfile-version@v1
|
||||
with:
|
||||
@@ -144,38 +141,34 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- name: MongoDB 4.2, ReplicaSet
|
||||
MONGODB_VERSION: 4.2.19
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
NODE_VERSION: 19.3.0
|
||||
MONGODB_VERSION: 4.2.25
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: MongoDB 4.4, ReplicaSet
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
NODE_VERSION: 19.3.0
|
||||
MONGODB_VERSION: 4.4.29
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: MongoDB 5, ReplicaSet
|
||||
MONGODB_VERSION: 5.3.2
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
NODE_VERSION: 19.3.0
|
||||
MONGODB_VERSION: 5.0.26
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: MongoDB 6, ReplicaSet
|
||||
MONGODB_VERSION: 6.0.2
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
NODE_VERSION: 19.3.0
|
||||
MONGODB_VERSION: 6.0.14
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: MongoDB 7, ReplicaSet
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_TOPOLOGY: replset
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: Redis Cache
|
||||
PARSE_SERVER_TEST_CACHE: redis
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: Node 14
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 14.21.1
|
||||
- name: Node 16
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 16.18.1
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: Node 18
|
||||
MONGODB_VERSION: 4.4.13
|
||||
MONGODB_VERSION: 7.0.8
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
NODE_VERSION: 18.12.1
|
||||
NODE_VERSION: 18.20.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
@@ -184,7 +177,7 @@ jobs:
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
- 6379:6379
|
||||
env:
|
||||
MONGODB_VERSION: ${{ matrix.MONGODB_VERSION }}
|
||||
MONGODB_TOPOLOGY: ${{ matrix.MONGODB_TOPOLOGY }}
|
||||
@@ -194,16 +187,13 @@ jobs:
|
||||
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
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -215,35 +205,37 @@ jobs:
|
||||
- run: npm run coverage
|
||||
env:
|
||||
CI: true
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
# Set to `true` once codecov token bug is fixed; https://github.com/parse-community/parse-server/issues/9129
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
check-postgres:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: PostgreSQL 11, PostGIS 3.0
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.0
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 11, PostGIS 3.1
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.1
|
||||
NODE_VERSION: 19.3.0
|
||||
- name: PostgreSQL 11, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.2
|
||||
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.1
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.1
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: PostgreSQL 13, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.2
|
||||
NODE_VERSION: 20.12.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
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: PostgreSQL 13, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.4
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: PostgreSQL 14, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:14-3.4
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: PostgreSQL 15, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.4
|
||||
NODE_VERSION: 20.12.0
|
||||
- name: PostgreSQL 16, PostGIS 3.4
|
||||
POSTGRES_IMAGE: postgis/postgis:15-3.4
|
||||
NODE_VERSION: 20.12.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
@@ -269,13 +261,13 @@ jobs:
|
||||
PARSE_SERVER_TEST_DATABASE_URI: postgres://postgres:postgres@localhost:5432/parse_server_postgres_adapter_test_database
|
||||
NODE_VERSION: ${{ matrix.NODE_VERSION }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -289,7 +281,13 @@ jobs:
|
||||
- run: npm run coverage
|
||||
env:
|
||||
CI: true
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -12,15 +12,15 @@ jobs:
|
||||
- name: Determine trigger branch name
|
||||
id: branch
|
||||
run: echo "::set-output name=trigger_branch::${GITHUB_REF#refs/*/}"
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.1.0
|
||||
node-version: 20
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -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
|
||||
@@ -51,23 +51,23 @@ jobs:
|
||||
id: branch
|
||||
run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release.outputs.current_tag }}
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Log into Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
@@ -75,10 +75,10 @@ jobs:
|
||||
tags: |
|
||||
type=semver,pattern={{version}},value=${{ needs.release.outputs.current_tag }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64, linux/arm/v6, linux/arm/v7, linux/arm64/v8
|
||||
platforms: linux/amd64, linux/arm64/v8
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -86,16 +86,16 @@ 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
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.1.0
|
||||
node-version: 18.20.0
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -112,4 +112,4 @@ jobs:
|
||||
uses: peaceiris/actions-gh-pages@v3.7.3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
publish_dir: ./docs
|
||||
|
||||
@@ -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
|
||||
@@ -23,23 +23,23 @@ jobs:
|
||||
id: branch
|
||||
run: echo "::set-output name=branch_name::${GITHUB_REF#refs/*/}"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Log into Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
@@ -48,10 +48,10 @@ jobs:
|
||||
type=semver,enable=true,pattern={{version}},value=${{ github.event.inputs.ref }}
|
||||
type=raw,enable=${{ github.event.inputs.ref == '' }},value=latest
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64, linux/arm/v6, linux/arm/v7, linux/arm64/v8
|
||||
platforms: linux/amd64, linux/arm64/v8
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
* Semantic Release Config
|
||||
*/
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { readFile } = require('fs').promises;
|
||||
const { resolve } = require('path');
|
||||
|
||||
// For ES6 modules use:
|
||||
// import { readFile } from 'fs/promises';
|
||||
// import { resolve, dirname } from 'path';
|
||||
// import { fileURLToPath } from 'url';
|
||||
|
||||
// Get env vars
|
||||
const ref = process.env.GITHUB_REF;
|
||||
@@ -24,7 +29,7 @@ const templates = {
|
||||
async function config() {
|
||||
|
||||
// Get branch
|
||||
const branch = ref.split('/').pop().split('-')[0];
|
||||
const branch = ref?.split('/')?.pop()?.split('-')[0] || '(current branch could not be determined)';
|
||||
console.log(`Running on branch: ${branch}`);
|
||||
|
||||
// Set changelog file
|
||||
@@ -40,8 +45,8 @@ async function config() {
|
||||
{ name: 'alpha', prerelease: true },
|
||||
{ name: 'beta', prerelease: true },
|
||||
'next-major',
|
||||
// Long-Term-Support branches; defined as GLOB pattern
|
||||
'release-+([0-9]).x.x',
|
||||
// Long-Term-Support branch of previous major version
|
||||
'release-6.x.x',
|
||||
],
|
||||
dryRun: false,
|
||||
debug: true,
|
||||
@@ -89,7 +94,7 @@ async function config() {
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"branches": [
|
||||
"backmergeBranches": [
|
||||
{ from: "beta", to: "alpha" },
|
||||
{ from: "release", to: "beta" },
|
||||
]
|
||||
@@ -103,15 +108,17 @@ async function config() {
|
||||
|
||||
async function loadTemplates() {
|
||||
for (const template of Object.keys(templates)) {
|
||||
const text = await readFile(path.resolve(__dirname, resourcePath, templates[template].file));
|
||||
|
||||
// For ES6 modules use:
|
||||
// const fileUrl = import.meta.url;
|
||||
// const __dirname = dirname(fileURLToPath(fileUrl));
|
||||
|
||||
const filePath = resolve(__dirname, resourcePath, templates[template].file);
|
||||
const text = await readFile(filePath, 'utf-8');
|
||||
templates[template].text = text;
|
||||
}
|
||||
}
|
||||
|
||||
async function readFile(filePath) {
|
||||
return await fs.readFile(filePath, 'utf-8');
|
||||
}
|
||||
|
||||
function getReleaseComment() {
|
||||
const url = repositoryUrl + '/releases/tag/${nextRelease.gitTag}';
|
||||
const comment = '🎉 This change has been released in version [${nextRelease.version}](' + url + ')';
|
||||
+39
-5
@@ -11,6 +11,10 @@
|
||||
- [Review Feedback](#review-feedback)
|
||||
- [Merge Readiness](#merge-readiness)
|
||||
- [Review Validity](#review-validity)
|
||||
- [Code Ownership](#code-ownership)
|
||||
- [Access Permissions](#access-permissions)
|
||||
- [New Private Repository](#new-private-repository)
|
||||
- [New Public Repository](#new-public-repository)
|
||||
- [Environment Setup](#environment-setup)
|
||||
- [Recommended Tools](#recommended-tools)
|
||||
- [Setting up your local machine](#setting-up-your-local-machine)
|
||||
@@ -34,6 +38,8 @@
|
||||
- [Breaking Change](#breaking-change-1)
|
||||
- [Reverting](#reverting)
|
||||
- [Security Vulnerability](#security-vulnerability)
|
||||
- [Local Testing](#local-testing)
|
||||
- [Merging](#merging-1)
|
||||
- [Releasing](#releasing)
|
||||
- [General Considerations](#general-considerations)
|
||||
- [Major Release / Long-Term-Support](#major-release--long-term-support)
|
||||
@@ -143,6 +149,30 @@ It's contrary to an open, collaborative environment to expect others to be invol
|
||||
|
||||
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.
|
||||
|
||||
### Code Ownership
|
||||
|
||||
> *Can I open a new pull request based on another author's pull request?*
|
||||
|
||||
If your pull request contains work from someone else then you are required to get their permission to use their work in your pull request. Please make sure to observe the [license](LICENSE) for more details. In addition, as an appreciative gesture you should clearly mention that your pull request is based on another pull request with a link in the top-most comment of your pull request. To avoid this issue we encourage contributors to collaborate on a single pull request to preserve the commit history and clearly identify each author's contribution. To do so, you can review the other author's pull request and submit your code suggestions, or ask the original author to grant you write access to their repository to also be able to make commits directly to their pull request.
|
||||
|
||||
### Access Permissions
|
||||
|
||||
> *Can I get write access to the repository to make changes faster?*
|
||||
|
||||
Keeping our products safe and secure is one of your top priorities. Our security policy mandates that write access to repositories is only provided to as few people as necessary. All usual contributions can be made via public pull requests. If you think you need write access, contact the repository team and explain in detail what the constraint is that you are trying to overcome. We want to make contributing for you as easy as possible. If there are any bottlenecks that are slowing you down we are happy to receive your feedback to see where we can improve.
|
||||
|
||||
### New Private Repository
|
||||
|
||||
> *Can I get a new private repository within the Parse Platform organization to work on some stuff?*
|
||||
|
||||
Private repositories are not provided unless there is a significant constraint or requirement that makes it necessary. For example, when collaborating on fixing a security vulnerability we provide private repositories to allow collaborators to share sensitive information within a select group.
|
||||
|
||||
### New Public Repository
|
||||
|
||||
> *Can I get a new public repository within the Parse Platform organization to work on some stuff?*
|
||||
|
||||
First of all, we appreciate your contribution. In rare cases, where we consider it beneficial to the advancement of the repository, a new public repository for a specific purpose may be provided, for example for increased visibility or to provide the organization's GitHub ressources. In other cases, we encourage you to start your contribution in a personal repository of your own GitHub account, and later transfer it to the Parse Platform organization. We will be happy to assist you in the repository transfer.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
### Recommended Tools
|
||||
@@ -212,7 +242,11 @@ Once you have babel running in watch mode, you can start making changes to parse
|
||||
|
||||
If your pull request introduces a change that may affect the storage or retrieval of objects, you may want to make sure it plays nice with Postgres.
|
||||
|
||||
* Run the tests against the postgres database with `PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly`. You'll need to have postgres running on your machine and setup [appropriately](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) or use [`Docker`](#run-a-parse-postgres-with-docker).
|
||||
* You'll need to have postgres running on your machine and setup [appropriately](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) or use [`Docker`](#postgres-with-docker)
|
||||
* Run the tests against the postgres database with:
|
||||
```
|
||||
PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly
|
||||
```
|
||||
* The Postgres adapter has a special debugger that traces all the sql commands. You can enable it with setting the environment variable `PARSE_SERVER_LOG_LEVEL=debug`
|
||||
* If your feature is intended to only work with MongoDB, you should disable PostgreSQL-specific tests with:
|
||||
|
||||
@@ -234,10 +268,10 @@ If your pull request introduces a change that may affect the storage or retrieva
|
||||
|
||||
#### Postgres with Docker
|
||||
|
||||
[PostGIS images (select one with v2.2 or higher) on docker dashboard](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://registry.hub.docker.com/_/postgres/) image and will work out-of-the-box (as long as you create a user with the necessary extensions for each of your Parse databases; see below). To launch the compatible Postgres instance, copy and paste the following line into your shell:
|
||||
[PostGIS images (select one with v2.2 or higher) on docker hub](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://hub.docker.com/_/postgres) image and will work out-of-the-box (as long as you create a user with the necessary extensions for each of your Parse databases; see below). To launch the compatible Postgres instance, copy and paste the following line into your shell:
|
||||
|
||||
```
|
||||
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:13-3.2-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:16-3.4-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
```
|
||||
To stop the Postgres instance:
|
||||
|
||||
@@ -245,7 +279,7 @@ To stop the Postgres instance:
|
||||
docker stop parse-postgres
|
||||
```
|
||||
|
||||
You can also use the [postgis/postgis:13-3.2-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
You can also use the [postgis/postgis:16-3.4-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
|
||||
```
|
||||
#Install additional scripts. These are run in abc order during initial start
|
||||
@@ -496,7 +530,6 @@ The following changes are done in the `alpha` branch, before publishing the last
|
||||
- Make sure all [deprecations](https://github.com/parse-community/parse-server/blob/alpha/DEPRECATIONS.md) are reflected in code, old code is removed and the deprecations table is updated.
|
||||
- Add the future LTS branch `release-#.x.x` to the branch list in [release.config.js](https://github.com/parse-community/parse-server/blob/alpha/release.config.js) so that the branch will later be recognized for release automation.
|
||||
|
||||
|
||||
### Publishing Release (forward-merge):
|
||||
|
||||
1. Create new temporary branch `build` on branch `beta`.
|
||||
@@ -547,6 +580,7 @@ The following changes are done in the `alpha` branch, before publishing the last
|
||||
1. Create LTS branch `release-#.x.x` off the latest version tag on `release` branch.
|
||||
2. Create temporary branch `build-release` off branch `beta` and create a pull request with `release` as the base branch.
|
||||
3. Merge branch `build-release` into `release`. Given that there will be breaking changes, a new major release will be created. In the unlikely case that there have been no breaking changes between the previous major release and the upcoming release, a major version increment has to be triggered manually. See the docs of the release automation framework for how to do that.
|
||||
4. Add newly created LTS branch `release-#.x.x` from step 1 to [Snyk](https://snyk.io) so that Snyk opens pull requests for the LTS branch; remove previously existing LTS branch `release-#.x.x` from Snyk.
|
||||
|
||||
## Versioning
|
||||
|
||||
|
||||
+6
-5
@@ -8,11 +8,12 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
|
||||
| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | 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 | - |
|
||||
| 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) | removed | - |
|
||||
| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | removed | - |
|
||||
| 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) | removed | - |
|
||||
| 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) | removed | - |
|
||||
| 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) | removed | - |
|
||||
| DEPPS10 | Config option `encodeParseObjectInCloudFunction` defaults to `true` | [#8634](https://github.com/parse-community/parse-server/issues/8634) | 6.2.0 (2023) | 8.0.0 (2025) | deprecated | - |
|
||||
|
||||
[i_deprecation]: ## "The version and date of the deprecation."
|
||||
[i_removal]: ## "The version and date of the planned removal."
|
||||
|
||||
+7
-3
@@ -1,9 +1,13 @@
|
||||
############################################################
|
||||
# Build stage
|
||||
############################################################
|
||||
FROM node:lts-alpine AS build
|
||||
FROM node:20.14.0-alpine3.20 AS build
|
||||
|
||||
RUN apk --no-cache add \
|
||||
build-base \
|
||||
git \
|
||||
python3
|
||||
|
||||
RUN apk --no-cache add git
|
||||
WORKDIR /tmp
|
||||
|
||||
# Copy package.json first to benefit from layer caching
|
||||
@@ -24,7 +28,7 @@ RUN npm ci --omit=dev --ignore-scripts \
|
||||
############################################################
|
||||
# Release stage
|
||||
############################################################
|
||||
FROM node:lts-alpine AS release
|
||||
FROM node:20.14.0-alpine3.20 AS release
|
||||
|
||||
VOLUME /parse-server/cloud /parse-server/config
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
[](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://app.codecov.io/github/parse-community/parse-server/tree/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)
|
||||
@@ -129,22 +129,20 @@ Parse Server is continuously tested with the most recent releases of Node.js to
|
||||
|
||||
| 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 |
|
||||
| Node.js 18 | 18.20.0 | April 2025 | ✅ Yes |
|
||||
| Node.js 20 | 20.12.0 | April 2026 | ✅ 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 [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.
|
||||
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. MongoDB "rapid releases" are ignored as these are considered pre-releases of the next major version.
|
||||
|
||||
| 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 |
|
||||
| ----------- | -------------- | ------------- | ---------- |
|
||||
| MongoDB 4.2 | 4.2.25 | April 2023 | ✅ Yes |
|
||||
| MongoDB 4.4 | 4.4.29 | February 2024 | ✅ Yes |
|
||||
| MongoDB 5 | 5.0.26 | October 2024 | ✅ Yes |
|
||||
| MongoDB 6 | 6.0.14 | July 2025 | ✅ Yes |
|
||||
| MongoDB 7 | 7.0.8 | TDB | ✅ Yes |
|
||||
|
||||
#### PostgreSQL
|
||||
|
||||
@@ -152,11 +150,10 @@ Parse Server is continuously tested with the most recent releases of PostgreSQL
|
||||
|
||||
| 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 |
|
||||
| Postgres 13 | 3.1, 3.2, 3.3, 3.4 | November 2025 | <= 6.x (2023) | ✅ Yes |
|
||||
| Postgres 14 | 3.4 | November 2026 | <= 7.x (2024) | ✅ Yes |
|
||||
| Postgres 15 | 3.4 | November 2027 | <= 8.x (2025) | ✅ Yes |
|
||||
| Postgres 16 | 3.4 | November 2028 | <= 9.x (2026) | ✅ Yes |
|
||||
|
||||
### Locally
|
||||
|
||||
@@ -358,12 +355,14 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
||||
|
||||
## 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` |
|
||||
| Scope | Internal data | Read-only data <sub>(1)</sub> | Custom data | Restricted by CLP, ACL | Key |
|
||||
|----------------|---------------|-------------------------------|-------------|------------------------|---------------------|
|
||||
| Internal | r/w | r/w | r/w | no | `maintenanceKey` |
|
||||
| Master | -/- | r/- | r/w | no | `masterKey` |
|
||||
| ReadOnlyMaster | -/- | r/- | r/- | no | `readOnlyMasterKey` |
|
||||
| Session | -/- | r/- | r/w | yes | `sessionToken` |
|
||||
|
||||
<sub>(1) `Parse.Object.createdAt`, `Parse.Object.updatedAt`.</sub>
|
||||
|
||||
## Email Verification and Password Reset
|
||||
|
||||
|
||||
@@ -1,3 +1,691 @@
|
||||
# [7.1.0-alpha.16](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.15...7.1.0-alpha.16) (2024-07-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for dot notation on array fields of Parse Object ([#9115](https://github.com/parse-community/parse-server/issues/9115)) ([cf4c880](https://github.com/parse-community/parse-server/commit/cf4c8807b9da87a0a5f9c94e5bdfcf17cda80cf4))
|
||||
|
||||
# [7.1.0-alpha.15](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.14...7.1.0-alpha.15) (2024-07-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to @parse/push-adapter 6.4.0 ([#9182](https://github.com/parse-community/parse-server/issues/9182)) ([ef1634b](https://github.com/parse-community/parse-server/commit/ef1634bf1f360429108d29b08032fc7961ff96a1))
|
||||
|
||||
# [7.1.0-alpha.14](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.13...7.1.0-alpha.14) (2024-07-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to Parse JS SDK 5.3.0 ([#9180](https://github.com/parse-community/parse-server/issues/9180)) ([dca187f](https://github.com/parse-community/parse-server/commit/dca187f91b93cbb362b22a3fb9ee38451799ff13))
|
||||
|
||||
# [7.1.0-alpha.13](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.12...7.1.0-alpha.13) (2024-07-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Invalid push notification tokens are not cleaned up from database for FCM API v2 ([#9173](https://github.com/parse-community/parse-server/issues/9173)) ([284da09](https://github.com/parse-community/parse-server/commit/284da09f4546356b37511a589fb5f64a3efffe79))
|
||||
|
||||
# [7.1.0-alpha.12](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.11...7.1.0-alpha.12) (2024-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* SQL injection when using Parse Server with PostgreSQL; fixes security vulnerability [GHSA-c2hr-cqg6-8j6r](https://github.com/parse-community/parse-server/security/advisories/GHSA-c2hr-cqg6-8j6r) ([#9167](https://github.com/parse-community/parse-server/issues/9167)) ([2edf1e4](https://github.com/parse-community/parse-server/commit/2edf1e4c0363af01e97a7fbc97694f851b7d1ff3))
|
||||
|
||||
# [7.1.0-alpha.11](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.10...7.1.0-alpha.11) (2024-06-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to Parse JS SDK 5.2.0 ([#9128](https://github.com/parse-community/parse-server/issues/9128)) ([665b8d5](https://github.com/parse-community/parse-server/commit/665b8d52d6cf5275179a5e1fb132c934edb53ecc))
|
||||
|
||||
# [7.1.0-alpha.10](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.9...7.1.0-alpha.10) (2024-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Live query throws error when constraint `notEqualTo` is set to `null` ([#8835](https://github.com/parse-community/parse-server/issues/8835)) ([11d3e48](https://github.com/parse-community/parse-server/commit/11d3e484df862224c15d20f6171514948981ea90))
|
||||
|
||||
# [7.1.0-alpha.9](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.8...7.1.0-alpha.9) (2024-05-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `extendSessionOnUse` not working for session lengths < 24 hours ([#9113](https://github.com/parse-community/parse-server/issues/9113)) ([0a054e6](https://github.com/parse-community/parse-server/commit/0a054e6b541fd5ab470bf025665f5f7d2acedaa0))
|
||||
|
||||
# [7.1.0-alpha.8](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.7...7.1.0-alpha.8) (2024-05-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to @parse/push-adapter 6.2.0 ([#9127](https://github.com/parse-community/parse-server/issues/9127)) ([ca20496](https://github.com/parse-community/parse-server/commit/ca20496f28e5ec1294a7a23c8559df82b79b2a04))
|
||||
|
||||
# [7.1.0-alpha.7](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.6...7.1.0-alpha.7) (2024-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Facebook Limited Login not working due to incorrect domain in JWT validation ([#9122](https://github.com/parse-community/parse-server/issues/9122)) ([9d0bd2b](https://github.com/parse-community/parse-server/commit/9d0bd2badd6e5f7429d1af00b118225752e5d86a))
|
||||
|
||||
# [7.1.0-alpha.6](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.5...7.1.0-alpha.6) (2024-04-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Cloud.startJob` and `Parse.Push.send` not returning status ID when setting Parse Server option `directAccess: true` ([#8766](https://github.com/parse-community/parse-server/issues/8766)) ([5b0efb2](https://github.com/parse-community/parse-server/commit/5b0efb22efe94c47f243cf8b1e6407ed5c5a67d3))
|
||||
|
||||
# [7.1.0-alpha.5](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.4...7.1.0-alpha.5) (2024-04-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Prevent Parse Server start in case of unknown option in server configuration ([#8987](https://github.com/parse-community/parse-server/issues/8987)) ([8758e6a](https://github.com/parse-community/parse-server/commit/8758e6abb9dbb68757bddcbd332ad25100c24a0e))
|
||||
|
||||
# [7.1.0-alpha.4](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.3...7.1.0-alpha.4) (2024-03-31)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to @parse/push-adapter 6.0.0 ([#9066](https://github.com/parse-community/parse-server/issues/9066)) ([18bdbf8](https://github.com/parse-community/parse-server/commit/18bdbf89c53a57648891ef582614ba7c2941e587))
|
||||
|
||||
# [7.1.0-alpha.3](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.2...7.1.0-alpha.3) (2024-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Rate limiting can fail when using Parse Server option `rateLimit.redisUrl` with clusters ([#8632](https://github.com/parse-community/parse-server/issues/8632)) ([c277739](https://github.com/parse-community/parse-server/commit/c27773962399f8e27691e3b8087e7e1d59516efd))
|
||||
|
||||
# [7.1.0-alpha.2](https://github.com/parse-community/parse-server/compare/7.1.0-alpha.1...7.1.0-alpha.2) (2024-03-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add server security check status `security.enableCheck` to Features Router ([#8679](https://github.com/parse-community/parse-server/issues/8679)) ([b07ec15](https://github.com/parse-community/parse-server/commit/b07ec153825882e97cc48dc84072c7f549f3238b))
|
||||
|
||||
# [7.1.0-alpha.1](https://github.com/parse-community/parse-server/compare/7.0.0...7.1.0-alpha.1) (2024-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
|
||||
|
||||
# [7.0.0-alpha.31](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.30...7.0.0-alpha.31) (2024-03-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
|
||||
|
||||
# [7.0.0-alpha.30](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.29...7.0.0-alpha.30) (2024-03-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
|
||||
|
||||
# [7.0.0-alpha.29](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.28...7.0.0-alpha.29) (2024-03-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Server crashes on invalid Cloud Function or Cloud Job name; fixes security vulnerability [GHSA-6hh7-46r2-vf29](https://github.com/parse-community/parse-server/security/advisories/GHSA-6hh7-46r2-vf29) ([#9024](https://github.com/parse-community/parse-server/issues/9024)) ([9f6e342](https://github.com/parse-community/parse-server/commit/9f6e3429d3b326cf4e2994733c618d08032fac6e))
|
||||
|
||||
# [7.0.0-alpha.28](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.27...7.0.0-alpha.28) (2024-03-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Upgrade to Parse JS SDK 5 ([#9022](https://github.com/parse-community/parse-server/issues/9022)) ([ad4aa83](https://github.com/parse-community/parse-server/commit/ad4aa83983205a0e27639f6ee6a4a5963b67e4b8))
|
||||
|
||||
# [7.0.0-alpha.27](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.26...7.0.0-alpha.27) (2024-03-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* CacheAdapter does not connect when using a CacheAdapter with a JSON config ([#8633](https://github.com/parse-community/parse-server/issues/8633)) ([720d24e](https://github.com/parse-community/parse-server/commit/720d24e18540da35d50957f17be878316ec30318))
|
||||
|
||||
# [7.0.0-alpha.26](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.25...7.0.0-alpha.26) (2024-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `fileExtensions` default value rejects file extensions that are less than 3 or more than 4 characters long ([#8699](https://github.com/parse-community/parse-server/issues/8699)) ([2760381](https://github.com/parse-community/parse-server/commit/276038118377c2b22381bcd8d30337203822121b))
|
||||
|
||||
# [7.0.0-alpha.25](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.24...7.0.0-alpha.25) (2024-03-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deprecation DEPPS5: Config option `allowClientClassCreation` defaults to `false` ([#8849](https://github.com/parse-community/parse-server/issues/8849)) ([29624e0](https://github.com/parse-community/parse-server/commit/29624e0fae17161cd412ae58d35a195cfa286cad))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Parse Server option `allowClientClassCreation` defaults to `false`. ([29624e0](29624e0))
|
||||
|
||||
# [7.0.0-alpha.24](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.23...7.0.0-alpha.24) (2024-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Docker version releases by removing arm/v6 and arm/v7 support ([#8976](https://github.com/parse-community/parse-server/issues/8976)) ([1f62dd0](https://github.com/parse-community/parse-server/commit/1f62dd0f4e107b22a387692558a042ee26ce8703))
|
||||
|
||||
# [7.0.0-alpha.23](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.22...7.0.0-alpha.23) (2024-03-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for MongoDB query comment ([#8928](https://github.com/parse-community/parse-server/issues/8928)) ([2170962](https://github.com/parse-community/parse-server/commit/2170962a50fa353ed85eda3f11dce7ee3647b087))
|
||||
|
||||
# [7.0.0-alpha.22](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.21...7.0.0-alpha.22) (2024-03-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Switch GraphQL server from Yoga v2 to Apollo v4 ([#8959](https://github.com/parse-community/parse-server/issues/8959)) ([105ae7c](https://github.com/parse-community/parse-server/commit/105ae7c8a57d5a650b243174a80c26bf6db16e28))
|
||||
|
||||
# [7.0.0-alpha.21](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.20...7.0.0-alpha.21) (2024-03-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Deny request if master key is not set in Parse Server option `masterKeyIps` regardless of ACL and CLP ([#8957](https://github.com/parse-community/parse-server/issues/8957)) ([a7b5b38](https://github.com/parse-community/parse-server/commit/a7b5b38418cbed9be3f4a7665f25b97f592663e1))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* A request using the master key will now be rejected as unauthorized if the IP from which the request originates is not set in the Parse Server option `masterKeyIps`, even if the request does not require the master key permission, for example for a public object in a public class class. ([a7b5b38](a7b5b38))
|
||||
|
||||
# [7.0.0-alpha.20](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.19...7.0.0-alpha.20) (2024-03-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Improve PostgreSQL injection detection; fixes security vulnerability [GHSA-6927-3vr9-fxf2](https://github.com/parse-community/parse-server/security/advisories/GHSA-6927-3vr9-fxf2) which affects Parse Server deployments using a Postgres database ([#8961](https://github.com/parse-community/parse-server/issues/8961)) ([cbefe77](https://github.com/parse-community/parse-server/commit/cbefe770a7260b54748a058b8a7389937dc35833))
|
||||
|
||||
# [7.0.0-alpha.19](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.18...7.0.0-alpha.19) (2024-02-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Node process exits with error code 1 on uncaught exception to allow custom uncaught exception handling ([#8894](https://github.com/parse-community/parse-server/issues/8894)) ([70c280c](https://github.com/parse-community/parse-server/commit/70c280ca578ff28b5acf92f37fbe06d42a5b34ca))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Node process now exits with code 1 on uncaught exceptions, enabling custom handlers that were blocked by Parse Server's default behavior of re-throwing errors. This change may lead to automatic process restarts by the environment, unlike before. ([70c280c](70c280c))
|
||||
|
||||
# [7.0.0-alpha.18](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.17...7.0.0-alpha.18) (2024-02-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deprecation DEPPS6: Authentication adapters disabled by default ([#8858](https://github.com/parse-community/parse-server/issues/8858)) ([0cf58eb](https://github.com/parse-community/parse-server/commit/0cf58eb8d60c8e5f485764e154f3214c49eee430))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Authentication adapters are disabled by default; to use an authentication adapter it needs to be explicitly enabled in the Parse Server authentication adapter option `auth.<provider>.enabled: true` ([0cf58eb](0cf58eb))
|
||||
|
||||
# [7.0.0-alpha.17](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.16...7.0.0-alpha.17) (2024-02-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deprecation DEPPS8: Parse Server option `allowExpiredAuthDataToken` defaults to `false` ([#8860](https://github.com/parse-community/parse-server/issues/8860)) ([e29845f](https://github.com/parse-community/parse-server/commit/e29845f8dacac09ce3093d75c0d92330c24389e8))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Parse Server option `allowExpiredAuthDataToken` defaults to `false`; a 3rd party authentication token will be validated every time the user tries to log in and the login will fail if the token has expired; the effect of this change may differ for different authentication adapters, depending on the token lifetime and the token refresh logic of the adapter ([e29845f](e29845f))
|
||||
|
||||
# [7.0.0-alpha.16](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.15...7.0.0-alpha.16) (2024-02-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deprecation DEPPS9: LiveQuery `fields` option is renamed to `keys` ([#8852](https://github.com/parse-community/parse-server/issues/8852)) ([38983e8](https://github.com/parse-community/parse-server/commit/38983e8e9b5cdbd006f311a2338103624137d013))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* LiveQuery `fields` option is renamed to `keys` ([38983e8](38983e8))
|
||||
|
||||
# [7.0.0-alpha.15](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.14...7.0.0-alpha.15) (2024-02-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Deprecation DEPPS7: Remove deprecated Cloud Code file trigger syntax ([#8855](https://github.com/parse-community/parse-server/issues/8855)) ([4e6a375](https://github.com/parse-community/parse-server/commit/4e6a375b5184ae0f7aa256a921eca4021c609435))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Cloud Code file trigger syntax has been aligned with object trigger syntax, for example `Parse.Cloud.beforeDeleteFile'` has been changed to `Parse.Cloud.beforeDelete(Parse.File, (request) => {})'` ([4e6a375](4e6a375))
|
||||
|
||||
# [7.0.0-alpha.14](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.13...7.0.0-alpha.14) (2024-02-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* GraphQL file upload fails in case of use of pointer or relation ([#8721](https://github.com/parse-community/parse-server/issues/8721)) ([1aba638](https://github.com/parse-community/parse-server/commit/1aba6382c873fb489d4a898d301e6da9fb6aa61b))
|
||||
|
||||
# [7.0.0-alpha.13](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.12...7.0.0-alpha.13) (2024-02-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Docker image not published to Docker Hub on new release ([#8905](https://github.com/parse-community/parse-server/issues/8905)) ([a2ac8d1](https://github.com/parse-community/parse-server/commit/a2ac8d133c71cd7b61e5ef59c4be915cfea85db6))
|
||||
|
||||
# [7.0.0-alpha.12](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.11...7.0.0-alpha.12) (2024-02-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for Node 20, drop support for Node 14, 16 ([#8907](https://github.com/parse-community/parse-server/issues/8907)) ([ced4872](https://github.com/parse-community/parse-server/commit/ced487246ea0ef72a8aa014991f003209b34841e))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Removes support for Node 14 and 16 ([ced4872](ced4872))
|
||||
|
||||
# [7.0.0-alpha.11](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.10...7.0.0-alpha.11) (2024-01-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for Postgres 16 ([#8898](https://github.com/parse-community/parse-server/issues/8898)) ([99489b2](https://github.com/parse-community/parse-server/commit/99489b22e4f0982e6cb39992974b51aa8d3a31e4))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Removes support for Postgres 11 and 12 ([99489b2](99489b2))
|
||||
|
||||
# [7.0.0-alpha.10](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.9...7.0.0-alpha.10) (2024-01-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add password validation via POST request for user with unverified email using master key and option `ignoreEmailVerification` ([#8895](https://github.com/parse-community/parse-server/issues/8895)) ([633a9d2](https://github.com/parse-community/parse-server/commit/633a9d25e4253e2125bc93c02ee8a37e0f5f7b83))
|
||||
|
||||
# [7.0.0-alpha.9](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.8...7.0.0-alpha.9) (2024-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Server crashes when receiving an array of `Parse.Pointer` in the request body ([#8784](https://github.com/parse-community/parse-server/issues/8784)) ([66e3603](https://github.com/parse-community/parse-server/commit/66e36039d8af654cfa0284666c0ddd94975dcb52))
|
||||
|
||||
# [7.0.0-alpha.8](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.7...7.0.0-alpha.8) (2024-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Incomplete user object in `verifyEmail` function if both username and email are changed ([#8889](https://github.com/parse-community/parse-server/issues/8889)) ([1eb95ae](https://github.com/parse-community/parse-server/commit/1eb95aeb41a96250e582d79a703f6adcb403c08b))
|
||||
|
||||
# [7.0.0-alpha.7](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.6...7.0.0-alpha.7) (2024-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Username is `undefined` in email verification link on email change ([#8887](https://github.com/parse-community/parse-server/issues/8887)) ([e315c13](https://github.com/parse-community/parse-server/commit/e315c137bf41bedfa8f0df537f2c3f6ab45b7e60))
|
||||
|
||||
# [7.0.0-alpha.6](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.5...7.0.0-alpha.6) (2024-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `emailVerifyTokenReuseIfValid: true` generates new token on every email verification request ([#8885](https://github.com/parse-community/parse-server/issues/8885)) ([0023ce4](https://github.com/parse-community/parse-server/commit/0023ce448a5e9423337d0e1a25648bde1156bc95))
|
||||
|
||||
# [7.0.0-alpha.5](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.4...7.0.0-alpha.5) (2024-01-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `installationId`, `ip`, `resendRequest` to arguments passed to `verifyUserEmails` on verification email request ([#8873](https://github.com/parse-community/parse-server/issues/8873)) ([8adcbee](https://github.com/parse-community/parse-server/commit/8adcbee11283d3e95179ca2047e2615f52c18806))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The `Parse.User` passed as argument if `verifyUserEmails` is set to a function is renamed from `user` to `object` for consistency with invocations of `verifyUserEmails` on signup or login; the user object is not a plain JavaScript object anymore but an instance of `Parse.User` ([8adcbee](8adcbee))
|
||||
|
||||
# [7.0.0-alpha.4](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.3...7.0.0-alpha.4) (2023-12-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `Parse.User` as function parameter to Parse Server options `verifyUserEmails`, `preventLoginWithUnverifiedEmail` on login ([#8850](https://github.com/parse-community/parse-server/issues/8850)) ([972f630](https://github.com/parse-community/parse-server/commit/972f6300163b3cd7d95eeb95986e8322c95f821c))
|
||||
|
||||
# [7.0.0-alpha.3](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.2...7.0.0-alpha.3) (2023-12-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Conditional email verification not working in some cases if `verifyUserEmails`, `preventLoginWithUnverifiedEmail` set to functions ([#8838](https://github.com/parse-community/parse-server/issues/8838)) ([8e7a6b1](https://github.com/parse-community/parse-server/commit/8e7a6b1480c0117e6c73e7adc5a6619115a04e85))
|
||||
|
||||
### Features
|
||||
|
||||
* Allow `Parse.Session.current` on expired session token instead of throwing error ([#8722](https://github.com/parse-community/parse-server/issues/8722)) ([f9dde4a](https://github.com/parse-community/parse-server/commit/f9dde4a9f8a90c63f71172c9bc515b0f6c6d2e4a))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* `Parse.Session.current()` no longer throws an error if the session token is expired, but instead returns the session token with its expiration date to allow checking its validity ([f9dde4a](f9dde4a))
|
||||
|
||||
# [7.0.0-alpha.2](https://github.com/parse-community/parse-server/compare/7.0.0-alpha.1...7.0.0-alpha.2) (2023-12-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `installationId` to arguments for `verifyUserEmails`, `preventLoginWithUnverifiedEmail` ([#8836](https://github.com/parse-community/parse-server/issues/8836)) ([a22dbe1](https://github.com/parse-community/parse-server/commit/a22dbe16d5ac0090608f6caaf0ebd134925b7fd4))
|
||||
|
||||
# [7.0.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.5.0-alpha.2...7.0.0-alpha.1) (2023-12-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for MongoDB 7 ([#8761](https://github.com/parse-community/parse-server/issues/8761)) ([3de8494](https://github.com/parse-community/parse-server/commit/3de8494a221991dfd10a74e0a2dc89576265c9b7))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* `Parse.Query` no longer supports the BSON type `code`; although this feature was never officially documented, its removal is announced as a breaking change to protect deployments where it might be in use. ([3de8494](3de8494))
|
||||
|
||||
# [6.5.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.5.0-alpha.1...6.5.0-alpha.2) (2023-11-19)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improved IP validation performance for `masterKeyIPs`, `maintenanceKeyIPs` ([#8510](https://github.com/parse-community/parse-server/issues/8510)) ([b87daba](https://github.com/parse-community/parse-server/commit/b87daba0671a1b0b7b8d63bc671d665c91a04522))
|
||||
|
||||
# [6.5.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.4.0...6.5.0-alpha.1) (2023-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Context not passed to Cloud Code Trigger `beforeFind` when using `Parse.Query.include` ([#8765](https://github.com/parse-community/parse-server/issues/8765)) ([7d32d89](https://github.com/parse-community/parse-server/commit/7d32d8934f3ae7af7a7d8b9cc6a829c7d73973d3))
|
||||
* Parse Server option `fileUpload.fileExtensions` fails to determine file extension if filename contains multiple dots ([#8754](https://github.com/parse-community/parse-server/issues/8754)) ([3d6d50e](https://github.com/parse-community/parse-server/commit/3d6d50e0afff18b95fb906914e2cebd3839b517a))
|
||||
* Security bump @babel/traverse from 7.20.5 to 7.23.2 ([#8777](https://github.com/parse-community/parse-server/issues/8777)) ([2d6b3d1](https://github.com/parse-community/parse-server/commit/2d6b3d18499179e99be116f25c0850d3f449509c))
|
||||
* Security upgrade graphql from 16.6.0 to 16.8.1 ([#8758](https://github.com/parse-community/parse-server/issues/8758)) ([71dfd8a](https://github.com/parse-community/parse-server/commit/71dfd8a7ece8c0dd1a66d03bb9420cfd39f4f9b1))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `$setOnInsert` operator to `Parse.Server.database.update` ([#8791](https://github.com/parse-community/parse-server/issues/8791)) ([f630a45](https://github.com/parse-community/parse-server/commit/f630a45aa5e87bc73a81fded061400c199b71a29))
|
||||
* Add compatibility for MongoDB Atlas Serverless and AWS Amazon DocumentDB with collation options `enableCollationCaseComparison`, `transformEmailToLowercase`, `transformUsernameToLowercase` ([#8805](https://github.com/parse-community/parse-server/issues/8805)) ([09fbeeb](https://github.com/parse-community/parse-server/commit/09fbeebba8870e7cf371fb84371a254c7b368620))
|
||||
* Add context to Cloud Code Triggers `beforeLogin` and `afterLogin` ([#8724](https://github.com/parse-community/parse-server/issues/8724)) ([a9c34ef](https://github.com/parse-community/parse-server/commit/a9c34ef1e2c78a42fb8b5fa8d569b7677c74919d))
|
||||
* Allow setting `createdAt` and `updatedAt` during `Parse.Object` creation with maintenance key ([#8696](https://github.com/parse-community/parse-server/issues/8696)) ([77bbfb3](https://github.com/parse-community/parse-server/commit/77bbfb3f186f5651c33ba152f04cff95128eaf2d))
|
||||
* Upgrade Parse Server Push Adapter to 5.0.2 ([#8813](https://github.com/parse-community/parse-server/issues/8813)) ([6ef1986](https://github.com/parse-community/parse-server/commit/6ef1986c03a1d84b7e11c05851e5bf9688d88740))
|
||||
|
||||
# [6.4.0-alpha.8](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.7...6.4.0-alpha.8) (2023-11-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add compatibility for MongoDB Atlas Serverless and AWS Amazon DocumentDB with collation options `enableCollationCaseComparison`, `transformEmailToLowercase`, `transformUsernameToLowercase` ([#8805](https://github.com/parse-community/parse-server/issues/8805)) ([09fbeeb](https://github.com/parse-community/parse-server/commit/09fbeebba8870e7cf371fb84371a254c7b368620))
|
||||
|
||||
# [6.4.0-alpha.7](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.6...6.4.0-alpha.7) (2023-10-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `$setOnInsert` operator to `Parse.Server.database.update` ([#8791](https://github.com/parse-community/parse-server/issues/8791)) ([f630a45](https://github.com/parse-community/parse-server/commit/f630a45aa5e87bc73a81fded061400c199b71a29))
|
||||
|
||||
# [6.4.0-alpha.6](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.5...6.4.0-alpha.6) (2023-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security bump @babel/traverse from 7.20.5 to 7.23.2 ([#8777](https://github.com/parse-community/parse-server/issues/8777)) ([2d6b3d1](https://github.com/parse-community/parse-server/commit/2d6b3d18499179e99be116f25c0850d3f449509c))
|
||||
|
||||
# [6.4.0-alpha.5](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.4...6.4.0-alpha.5) (2023-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Context not passed to Cloud Code Trigger `beforeFind` when using `Parse.Query.include` ([#8765](https://github.com/parse-community/parse-server/issues/8765)) ([7d32d89](https://github.com/parse-community/parse-server/commit/7d32d8934f3ae7af7a7d8b9cc6a829c7d73973d3))
|
||||
|
||||
# [6.4.0-alpha.4](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.3...6.4.0-alpha.4) (2023-09-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Allow setting `createdAt` and `updatedAt` during `Parse.Object` creation with maintenance key ([#8696](https://github.com/parse-community/parse-server/issues/8696)) ([77bbfb3](https://github.com/parse-community/parse-server/commit/77bbfb3f186f5651c33ba152f04cff95128eaf2d))
|
||||
|
||||
# [6.4.0-alpha.3](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.2...6.4.0-alpha.3) (2023-09-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `fileUpload.fileExtensions` fails to determine file extension if filename contains multiple dots ([#8754](https://github.com/parse-community/parse-server/issues/8754)) ([3d6d50e](https://github.com/parse-community/parse-server/commit/3d6d50e0afff18b95fb906914e2cebd3839b517a))
|
||||
|
||||
# [6.4.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.4.0-alpha.1...6.4.0-alpha.2) (2023-09-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Security upgrade graphql from 16.6.0 to 16.8.1 ([#8758](https://github.com/parse-community/parse-server/issues/8758)) ([71dfd8a](https://github.com/parse-community/parse-server/commit/71dfd8a7ece8c0dd1a66d03bb9420cfd39f4f9b1))
|
||||
|
||||
# [6.4.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.3.0...6.4.0-alpha.1) (2023-09-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Add context to Cloud Code Triggers `beforeLogin` and `afterLogin` ([#8724](https://github.com/parse-community/parse-server/issues/8724)) ([a9c34ef](https://github.com/parse-community/parse-server/commit/a9c34ef1e2c78a42fb8b5fa8d569b7677c74919d))
|
||||
|
||||
# [6.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.8...6.3.0-alpha.9) (2023-09-13)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improve performance of recursive pointer iterations ([#8741](https://github.com/parse-community/parse-server/issues/8741)) ([45a3ed0](https://github.com/parse-community/parse-server/commit/45a3ed0fcf2c0170607505a1550fb15896e705fd))
|
||||
|
||||
# [6.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.7...6.3.0-alpha.8) (2023-08-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Redis 4 does not reconnect after unhandled error ([#8706](https://github.com/parse-community/parse-server/issues/8706)) ([2b3d4e5](https://github.com/parse-community/parse-server/commit/2b3d4e5d3c85cd142f85af68dec51a8523548d49))
|
||||
|
||||
# [6.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.6...6.3.0-alpha.7) (2023-08-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove config logging when launching Parse Server via CLI ([#8710](https://github.com/parse-community/parse-server/issues/8710)) ([ae68f0c](https://github.com/parse-community/parse-server/commit/ae68f0c31b741eeb83379c905c7ddfaa124436ec))
|
||||
|
||||
# [6.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.5...6.3.0-alpha.6) (2023-07-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions ([#8688](https://github.com/parse-community/parse-server/issues/8688)) ([6a4a00c](https://github.com/parse-community/parse-server/commit/6a4a00ca7af1163ea74b047b85cd6817366b824b))
|
||||
|
||||
# [6.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.4...6.3.0-alpha.5) (2023-07-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code ([#8670](https://github.com/parse-community/parse-server/issues/8670)) ([a9d376b](https://github.com/parse-community/parse-server/commit/a9d376b61f5b07806eafbda91c4e36c322f09298))
|
||||
|
||||
# [6.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.3...6.3.0-alpha.4) (2023-07-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Server does not start via CLI when `auth` option is set ([#8666](https://github.com/parse-community/parse-server/issues/8666)) ([4e2000b](https://github.com/parse-community/parse-server/commit/4e2000bc563324389584ace3c090a5c1a7796a64))
|
||||
|
||||
# [6.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.2...6.3.0-alpha.3) (2023-06-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add TOTP authentication adapter ([#8457](https://github.com/parse-community/parse-server/issues/8457)) ([cc079a4](https://github.com/parse-community/parse-server/commit/cc079a40f6849a0e9bc6fdc811e8649ecb67b589))
|
||||
|
||||
# [6.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/6.3.0-alpha.1...6.3.0-alpha.2) (2023-06-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add conditional email verification via dynamic Parse Server options `verifyUserEmails`, `sendUserEmailVerification` that now accept functions ([#8425](https://github.com/parse-community/parse-server/issues/8425)) ([44acd6d](https://github.com/parse-community/parse-server/commit/44acd6d9ed157ad4842200c9d01f9c77a05fec3a))
|
||||
|
||||
# [6.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.3.0-alpha.1) (2023-06-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Cloud Code Trigger `afterSave` executes even if not set ([#8520](https://github.com/parse-community/parse-server/issues/8520)) ([afd0515](https://github.com/parse-community/parse-server/commit/afd0515e207bd947840579d3f245980dffa6f804))
|
||||
* GridFS file storage doesn't work with certain `enableSchemaHooks` settings ([#8467](https://github.com/parse-community/parse-server/issues/8467)) ([d4cda4b](https://github.com/parse-community/parse-server/commit/d4cda4b26c9bde8c812549b8780bea1cfabdb394))
|
||||
* Inaccurate table total row count for PostgreSQL ([#8511](https://github.com/parse-community/parse-server/issues/8511)) ([0823a02](https://github.com/parse-community/parse-server/commit/0823a02fbf80bc88dc403bc47e9f5c6597ea78b4))
|
||||
* LiveQuery server is not shut down properly when `handleShutdown` is called ([#8491](https://github.com/parse-community/parse-server/issues/8491)) ([967700b](https://github.com/parse-community/parse-server/commit/967700bdbc94c74f75ba84d2b3f4b9f3fd2dca0b))
|
||||
* Rate limit feature is incompatible with Node 14 ([#8578](https://github.com/parse-community/parse-server/issues/8578)) ([f911f2c](https://github.com/parse-community/parse-server/commit/f911f2cd3a8c45cd326272dcd681532764a3761e))
|
||||
* Unnecessary log entries by `extendSessionOnUse` ([#8562](https://github.com/parse-community/parse-server/issues/8562)) ([fd6a007](https://github.com/parse-community/parse-server/commit/fd6a0077f2e5cf83d65e52172ae5a950ab0f1eae))
|
||||
|
||||
### Features
|
||||
|
||||
* `extendSessionOnUse` to automatically renew Parse Sessions ([#8505](https://github.com/parse-community/parse-server/issues/8505)) ([6f885d3](https://github.com/parse-community/parse-server/commit/6f885d36b94902fdfea873fc554dee83589e6029))
|
||||
* Add new Parse Server option `preventSignupWithUnverifiedEmail` to prevent returning a user without session token on sign-up with unverified email address ([#8451](https://github.com/parse-community/parse-server/issues/8451)) ([82da308](https://github.com/parse-community/parse-server/commit/82da30842a55980aa90cb7680fbf6db37ee16dab))
|
||||
* Add option to change the log level of logs emitted by Cloud Functions ([#8530](https://github.com/parse-community/parse-server/issues/8530)) ([2caea31](https://github.com/parse-community/parse-server/commit/2caea310be412d82b04a85716bc769ccc410316d))
|
||||
* Add support for `$eq` query constraint in LiveQuery ([#8614](https://github.com/parse-community/parse-server/issues/8614)) ([656d673](https://github.com/parse-community/parse-server/commit/656d673cf5dea354e4f2b3d4dc2b29a41d311b3e))
|
||||
* Add zones for rate limiting by `ip`, `user`, `session`, `global` ([#8508](https://github.com/parse-community/parse-server/issues/8508)) ([03fba97](https://github.com/parse-community/parse-server/commit/03fba97e0549bfcaeee9f2fa4c9905dbcc91840e))
|
||||
* Allow `Parse.Object` pointers in Cloud Code arguments ([#8490](https://github.com/parse-community/parse-server/issues/8490)) ([28aeda3](https://github.com/parse-community/parse-server/commit/28aeda3f160efcbbcf85a85484a8d26567fa9761))
|
||||
|
||||
### Reverts
|
||||
|
||||
* fix: Inaccurate table total row count for PostgreSQL ([6722110](https://github.com/parse-community/parse-server/commit/6722110f203bc5fdcaa68cdf091cf9e7b48d1cff))
|
||||
|
||||
# [6.1.0-alpha.20](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.19...6.1.0-alpha.20) (2023-06-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add zones for rate limiting by `ip`, `user`, `session`, `global` ([#8508](https://github.com/parse-community/parse-server/issues/8508)) ([03fba97](https://github.com/parse-community/parse-server/commit/03fba97e0549bfcaeee9f2fa4c9905dbcc91840e))
|
||||
|
||||
# [6.1.0-alpha.19](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.18...6.1.0-alpha.19) (2023-06-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LiveQuery server is not shut down properly when `handleShutdown` is called ([#8491](https://github.com/parse-community/parse-server/issues/8491)) ([967700b](https://github.com/parse-community/parse-server/commit/967700bdbc94c74f75ba84d2b3f4b9f3fd2dca0b))
|
||||
|
||||
# [6.1.0-alpha.18](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.17...6.1.0-alpha.18) (2023-06-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for `$eq` query constraint in LiveQuery ([#8614](https://github.com/parse-community/parse-server/issues/8614)) ([656d673](https://github.com/parse-community/parse-server/commit/656d673cf5dea354e4f2b3d4dc2b29a41d311b3e))
|
||||
|
||||
# [6.1.0-alpha.17](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.16...6.1.0-alpha.17) (2023-06-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new Parse Server option `preventSignupWithUnverifiedEmail` to prevent returning a user without session token on sign-up with unverified email address ([#8451](https://github.com/parse-community/parse-server/issues/8451)) ([82da308](https://github.com/parse-community/parse-server/commit/82da30842a55980aa90cb7680fbf6db37ee16dab))
|
||||
|
||||
# [6.1.0-alpha.16](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.15...6.1.0-alpha.16) (2023-05-28)
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* fix: Inaccurate table total row count for PostgreSQL ([6722110](https://github.com/parse-community/parse-server/commit/6722110f203bc5fdcaa68cdf091cf9e7b48d1cff))
|
||||
|
||||
# [6.1.0-alpha.15](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.14...6.1.0-alpha.15) (2023-05-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Inaccurate table total row count for PostgreSQL ([#8511](https://github.com/parse-community/parse-server/issues/8511)) ([0823a02](https://github.com/parse-community/parse-server/commit/0823a02fbf80bc88dc403bc47e9f5c6597ea78b4))
|
||||
|
||||
# [6.1.0-alpha.14](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.13...6.1.0-alpha.14) (2023-05-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Unnecessary log entries by `extendSessionOnUse` ([#8562](https://github.com/parse-community/parse-server/issues/8562)) ([fd6a007](https://github.com/parse-community/parse-server/commit/fd6a0077f2e5cf83d65e52172ae5a950ab0f1eae))
|
||||
|
||||
### Features
|
||||
|
||||
* Allow `Parse.Object` pointers in Cloud Code arguments ([#8490](https://github.com/parse-community/parse-server/issues/8490)) ([28aeda3](https://github.com/parse-community/parse-server/commit/28aeda3f160efcbbcf85a85484a8d26567fa9761))
|
||||
|
||||
# [6.1.0-alpha.13](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.12...6.1.0-alpha.13) (2023-05-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Rate limit feature is incompatible with Node 14 ([#8578](https://github.com/parse-community/parse-server/issues/8578)) ([f911f2c](https://github.com/parse-community/parse-server/commit/f911f2cd3a8c45cd326272dcd681532764a3761e))
|
||||
|
||||
# [6.1.0-alpha.12](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.11...6.1.0-alpha.12) (2023-05-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* GridFS file storage doesn't work with certain `enableSchemaHooks` settings ([#8467](https://github.com/parse-community/parse-server/issues/8467)) ([d4cda4b](https://github.com/parse-community/parse-server/commit/d4cda4b26c9bde8c812549b8780bea1cfabdb394))
|
||||
|
||||
# [6.1.0-alpha.11](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.10...6.1.0-alpha.11) (2023-05-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* `extendSessionOnUse` to automatically renew Parse Sessions ([#8505](https://github.com/parse-community/parse-server/issues/8505)) ([6f885d3](https://github.com/parse-community/parse-server/commit/6f885d36b94902fdfea873fc554dee83589e6029))
|
||||
|
||||
# [6.1.0-alpha.10](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.9...6.1.0-alpha.10) (2023-05-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Cloud Code Trigger `afterSave` executes even if not set ([#8520](https://github.com/parse-community/parse-server/issues/8520)) ([afd0515](https://github.com/parse-community/parse-server/commit/afd0515e207bd947840579d3f245980dffa6f804))
|
||||
|
||||
# [6.1.0-alpha.9](https://github.com/parse-community/parse-server/compare/6.1.0-alpha.8...6.1.0-alpha.9) (2023-05-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add option to change the log level of logs emitted by Cloud Functions ([#8530](https://github.com/parse-community/parse-server/issues/8530)) ([2caea31](https://github.com/parse-community/parse-server/commit/2caea310be412d82b04a85716bc769ccc410316d))
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,177 @@
|
||||
# [7.2.0-beta.1](https://github.com/parse-community/parse-server/compare/7.1.0...7.2.0-beta.1) (2024-07-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Invalid push notification tokens are not cleaned up from database for FCM API v2 ([#9173](https://github.com/parse-community/parse-server/issues/9173)) ([284da09](https://github.com/parse-community/parse-server/commit/284da09f4546356b37511a589fb5f64a3efffe79))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for dot notation on array fields of Parse Object ([#9115](https://github.com/parse-community/parse-server/issues/9115)) ([cf4c880](https://github.com/parse-community/parse-server/commit/cf4c8807b9da87a0a5f9c94e5bdfcf17cda80cf4))
|
||||
* Upgrade to @parse/push-adapter 6.4.0 ([#9182](https://github.com/parse-community/parse-server/issues/9182)) ([ef1634b](https://github.com/parse-community/parse-server/commit/ef1634bf1f360429108d29b08032fc7961ff96a1))
|
||||
* Upgrade to Parse JS SDK 5.3.0 ([#9180](https://github.com/parse-community/parse-server/issues/9180)) ([dca187f](https://github.com/parse-community/parse-server/commit/dca187f91b93cbb362b22a3fb9ee38451799ff13))
|
||||
|
||||
# [7.1.0-beta.1](https://github.com/parse-community/parse-server/compare/7.0.0...7.1.0-beta.1) (2024-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Cloud.startJob` and `Parse.Push.send` not returning status ID when setting Parse Server option `directAccess: true` ([#8766](https://github.com/parse-community/parse-server/issues/8766)) ([5b0efb2](https://github.com/parse-community/parse-server/commit/5b0efb22efe94c47f243cf8b1e6407ed5c5a67d3))
|
||||
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
|
||||
* Facebook Limited Login not working due to incorrect domain in JWT validation ([#9122](https://github.com/parse-community/parse-server/issues/9122)) ([9d0bd2b](https://github.com/parse-community/parse-server/commit/9d0bd2badd6e5f7429d1af00b118225752e5d86a))
|
||||
* Live query throws error when constraint `notEqualTo` is set to `null` ([#8835](https://github.com/parse-community/parse-server/issues/8835)) ([11d3e48](https://github.com/parse-community/parse-server/commit/11d3e484df862224c15d20f6171514948981ea90))
|
||||
* Parse Server option `extendSessionOnUse` not working for session lengths < 24 hours ([#9113](https://github.com/parse-community/parse-server/issues/9113)) ([0a054e6](https://github.com/parse-community/parse-server/commit/0a054e6b541fd5ab470bf025665f5f7d2acedaa0))
|
||||
* Rate limiting can fail when using Parse Server option `rateLimit.redisUrl` with clusters ([#8632](https://github.com/parse-community/parse-server/issues/8632)) ([c277739](https://github.com/parse-community/parse-server/commit/c27773962399f8e27691e3b8087e7e1d59516efd))
|
||||
* SQL injection when using Parse Server with PostgreSQL; fixes security vulnerability [GHSA-c2hr-cqg6-8j6r](https://github.com/parse-community/parse-server/security/advisories/GHSA-c2hr-cqg6-8j6r) ([#9167](https://github.com/parse-community/parse-server/issues/9167)) ([2edf1e4](https://github.com/parse-community/parse-server/commit/2edf1e4c0363af01e97a7fbc97694f851b7d1ff3))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
|
||||
* Add server security check status `security.enableCheck` to Features Router ([#8679](https://github.com/parse-community/parse-server/issues/8679)) ([b07ec15](https://github.com/parse-community/parse-server/commit/b07ec153825882e97cc48dc84072c7f549f3238b))
|
||||
* Prevent Parse Server start in case of unknown option in server configuration ([#8987](https://github.com/parse-community/parse-server/issues/8987)) ([8758e6a](https://github.com/parse-community/parse-server/commit/8758e6abb9dbb68757bddcbd332ad25100c24a0e))
|
||||
* Upgrade to @parse/push-adapter 6.0.0 ([#9066](https://github.com/parse-community/parse-server/issues/9066)) ([18bdbf8](https://github.com/parse-community/parse-server/commit/18bdbf89c53a57648891ef582614ba7c2941e587))
|
||||
* Upgrade to @parse/push-adapter 6.2.0 ([#9127](https://github.com/parse-community/parse-server/issues/9127)) ([ca20496](https://github.com/parse-community/parse-server/commit/ca20496f28e5ec1294a7a23c8559df82b79b2a04))
|
||||
* Upgrade to Parse JS SDK 5.2.0 ([#9128](https://github.com/parse-community/parse-server/issues/9128)) ([665b8d5](https://github.com/parse-community/parse-server/commit/665b8d52d6cf5275179a5e1fb132c934edb53ecc))
|
||||
|
||||
# [7.0.0-beta.1](https://github.com/parse-community/parse-server/compare/6.5.0-beta.1...7.0.0-beta.1) (2024-03-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* CacheAdapter does not connect when using a CacheAdapter with a JSON config ([#8633](https://github.com/parse-community/parse-server/issues/8633)) ([720d24e](https://github.com/parse-community/parse-server/commit/720d24e18540da35d50957f17be878316ec30318))
|
||||
* Conditional email verification not working in some cases if `verifyUserEmails`, `preventLoginWithUnverifiedEmail` set to functions ([#8838](https://github.com/parse-community/parse-server/issues/8838)) ([8e7a6b1](https://github.com/parse-community/parse-server/commit/8e7a6b1480c0117e6c73e7adc5a6619115a04e85))
|
||||
* Deny request if master key is not set in Parse Server option `masterKeyIps` regardless of ACL and CLP ([#8957](https://github.com/parse-community/parse-server/issues/8957)) ([a7b5b38](https://github.com/parse-community/parse-server/commit/a7b5b38418cbed9be3f4a7665f25b97f592663e1))
|
||||
* Docker image not published to Docker Hub on new release ([#8905](https://github.com/parse-community/parse-server/issues/8905)) ([a2ac8d1](https://github.com/parse-community/parse-server/commit/a2ac8d133c71cd7b61e5ef59c4be915cfea85db6))
|
||||
* Docker version releases by removing arm/v6 and arm/v7 support ([#8976](https://github.com/parse-community/parse-server/issues/8976)) ([1f62dd0](https://github.com/parse-community/parse-server/commit/1f62dd0f4e107b22a387692558a042ee26ce8703))
|
||||
* GraphQL file upload fails in case of use of pointer or relation ([#8721](https://github.com/parse-community/parse-server/issues/8721)) ([1aba638](https://github.com/parse-community/parse-server/commit/1aba6382c873fb489d4a898d301e6da9fb6aa61b))
|
||||
* Improve PostgreSQL injection detection; fixes security vulnerability [GHSA-6927-3vr9-fxf2](https://github.com/parse-community/parse-server/security/advisories/GHSA-6927-3vr9-fxf2) which affects Parse Server deployments using a Postgres database ([#8961](https://github.com/parse-community/parse-server/issues/8961)) ([cbefe77](https://github.com/parse-community/parse-server/commit/cbefe770a7260b54748a058b8a7389937dc35833))
|
||||
* Incomplete user object in `verifyEmail` function if both username and email are changed ([#8889](https://github.com/parse-community/parse-server/issues/8889)) ([1eb95ae](https://github.com/parse-community/parse-server/commit/1eb95aeb41a96250e582d79a703f6adcb403c08b))
|
||||
* Parse Server option `emailVerifyTokenReuseIfValid: true` generates new token on every email verification request ([#8885](https://github.com/parse-community/parse-server/issues/8885)) ([0023ce4](https://github.com/parse-community/parse-server/commit/0023ce448a5e9423337d0e1a25648bde1156bc95))
|
||||
* Parse Server option `fileExtensions` default value rejects file extensions that are less than 3 or more than 4 characters long ([#8699](https://github.com/parse-community/parse-server/issues/8699)) ([2760381](https://github.com/parse-community/parse-server/commit/276038118377c2b22381bcd8d30337203822121b))
|
||||
* Server crashes on invalid Cloud Function or Cloud Job name; fixes security vulnerability [GHSA-6hh7-46r2-vf29](https://github.com/parse-community/parse-server/security/advisories/GHSA-6hh7-46r2-vf29) ([#9024](https://github.com/parse-community/parse-server/issues/9024)) ([9f6e342](https://github.com/parse-community/parse-server/commit/9f6e3429d3b326cf4e2994733c618d08032fac6e))
|
||||
* Server crashes when receiving an array of `Parse.Pointer` in the request body ([#8784](https://github.com/parse-community/parse-server/issues/8784)) ([66e3603](https://github.com/parse-community/parse-server/commit/66e36039d8af654cfa0284666c0ddd94975dcb52))
|
||||
* Username is `undefined` in email verification link on email change ([#8887](https://github.com/parse-community/parse-server/issues/8887)) ([e315c13](https://github.com/parse-community/parse-server/commit/e315c137bf41bedfa8f0df537f2c3f6ab45b7e60))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `installationId` to arguments for `verifyUserEmails`, `preventLoginWithUnverifiedEmail` ([#8836](https://github.com/parse-community/parse-server/issues/8836)) ([a22dbe1](https://github.com/parse-community/parse-server/commit/a22dbe16d5ac0090608f6caaf0ebd134925b7fd4))
|
||||
* Add `installationId`, `ip`, `resendRequest` to arguments passed to `verifyUserEmails` on verification email request ([#8873](https://github.com/parse-community/parse-server/issues/8873)) ([8adcbee](https://github.com/parse-community/parse-server/commit/8adcbee11283d3e95179ca2047e2615f52c18806))
|
||||
* Add `Parse.User` as function parameter to Parse Server options `verifyUserEmails`, `preventLoginWithUnverifiedEmail` on login ([#8850](https://github.com/parse-community/parse-server/issues/8850)) ([972f630](https://github.com/parse-community/parse-server/commit/972f6300163b3cd7d95eeb95986e8322c95f821c))
|
||||
* Add password validation via POST request for user with unverified email using master key and option `ignoreEmailVerification` ([#8895](https://github.com/parse-community/parse-server/issues/8895)) ([633a9d2](https://github.com/parse-community/parse-server/commit/633a9d25e4253e2125bc93c02ee8a37e0f5f7b83))
|
||||
* Add support for MongoDB 7 ([#8761](https://github.com/parse-community/parse-server/issues/8761)) ([3de8494](https://github.com/parse-community/parse-server/commit/3de8494a221991dfd10a74e0a2dc89576265c9b7))
|
||||
* Add support for MongoDB query comment ([#8928](https://github.com/parse-community/parse-server/issues/8928)) ([2170962](https://github.com/parse-community/parse-server/commit/2170962a50fa353ed85eda3f11dce7ee3647b087))
|
||||
* Add support for Node 20, drop support for Node 14, 16 ([#8907](https://github.com/parse-community/parse-server/issues/8907)) ([ced4872](https://github.com/parse-community/parse-server/commit/ced487246ea0ef72a8aa014991f003209b34841e))
|
||||
* Add support for Postgres 16 ([#8898](https://github.com/parse-community/parse-server/issues/8898)) ([99489b2](https://github.com/parse-community/parse-server/commit/99489b22e4f0982e6cb39992974b51aa8d3a31e4))
|
||||
* Allow `Parse.Session.current` on expired session token instead of throwing error ([#8722](https://github.com/parse-community/parse-server/issues/8722)) ([f9dde4a](https://github.com/parse-community/parse-server/commit/f9dde4a9f8a90c63f71172c9bc515b0f6c6d2e4a))
|
||||
* Deprecation DEPPS5: Config option `allowClientClassCreation` defaults to `false` ([#8849](https://github.com/parse-community/parse-server/issues/8849)) ([29624e0](https://github.com/parse-community/parse-server/commit/29624e0fae17161cd412ae58d35a195cfa286cad))
|
||||
* Deprecation DEPPS6: Authentication adapters disabled by default ([#8858](https://github.com/parse-community/parse-server/issues/8858)) ([0cf58eb](https://github.com/parse-community/parse-server/commit/0cf58eb8d60c8e5f485764e154f3214c49eee430))
|
||||
* Deprecation DEPPS7: Remove deprecated Cloud Code file trigger syntax ([#8855](https://github.com/parse-community/parse-server/issues/8855)) ([4e6a375](https://github.com/parse-community/parse-server/commit/4e6a375b5184ae0f7aa256a921eca4021c609435))
|
||||
* Deprecation DEPPS8: Parse Server option `allowExpiredAuthDataToken` defaults to `false` ([#8860](https://github.com/parse-community/parse-server/issues/8860)) ([e29845f](https://github.com/parse-community/parse-server/commit/e29845f8dacac09ce3093d75c0d92330c24389e8))
|
||||
* Deprecation DEPPS9: LiveQuery `fields` option is renamed to `keys` ([#8852](https://github.com/parse-community/parse-server/issues/8852)) ([38983e8](https://github.com/parse-community/parse-server/commit/38983e8e9b5cdbd006f311a2338103624137d013))
|
||||
* Node process exits with error code 1 on uncaught exception to allow custom uncaught exception handling ([#8894](https://github.com/parse-community/parse-server/issues/8894)) ([70c280c](https://github.com/parse-community/parse-server/commit/70c280ca578ff28b5acf92f37fbe06d42a5b34ca))
|
||||
* Switch GraphQL server from Yoga v2 to Apollo v4 ([#8959](https://github.com/parse-community/parse-server/issues/8959)) ([105ae7c](https://github.com/parse-community/parse-server/commit/105ae7c8a57d5a650b243174a80c26bf6db16e28))
|
||||
* Upgrade Parse Server Push Adapter to 5.0.2 ([#8813](https://github.com/parse-community/parse-server/issues/8813)) ([6ef1986](https://github.com/parse-community/parse-server/commit/6ef1986c03a1d84b7e11c05851e5bf9688d88740))
|
||||
* Upgrade to Parse JS SDK 5 ([#9022](https://github.com/parse-community/parse-server/issues/9022)) ([ad4aa83](https://github.com/parse-community/parse-server/commit/ad4aa83983205a0e27639f6ee6a4a5963b67e4b8))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improved IP validation performance for `masterKeyIPs`, `maintenanceKeyIPs` ([#8510](https://github.com/parse-community/parse-server/issues/8510)) ([b87daba](https://github.com/parse-community/parse-server/commit/b87daba0671a1b0b7b8d63bc671d665c91a04522))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Parse Server option `allowClientClassCreation` defaults to `false`. ([29624e0](29624e0))
|
||||
* A request using the master key will now be rejected as unauthorized if the IP from which the request originates is not set in the Parse Server option `masterKeyIps`, even if the request does not require the master key permission, for example for a public object in a public class class. ([a7b5b38](a7b5b38))
|
||||
* Node process now exits with code 1 on uncaught exceptions, enabling custom handlers that were blocked by Parse Server's default behavior of re-throwing errors. This change may lead to automatic process restarts by the environment, unlike before. ([70c280c](70c280c))
|
||||
* Authentication adapters are disabled by default; to use an authentication adapter it needs to be explicitly enabled in the Parse Server authentication adapter option `auth.<provider>.enabled: true` ([0cf58eb](0cf58eb))
|
||||
* Parse Server option `allowExpiredAuthDataToken` defaults to `false`; a 3rd party authentication token will be validated every time the user tries to log in and the login will fail if the token has expired; the effect of this change may differ for different authentication adapters, depending on the token lifetime and the token refresh logic of the adapter ([e29845f](e29845f))
|
||||
* LiveQuery `fields` option is renamed to `keys` ([38983e8](38983e8))
|
||||
* Cloud Code file trigger syntax has been aligned with object trigger syntax, for example `Parse.Cloud.beforeDeleteFile'` has been changed to `Parse.Cloud.beforeDelete(Parse.File, (request) => {})'` ([4e6a375](4e6a375))
|
||||
* Removes support for Node 14 and 16 ([ced4872](ced4872))
|
||||
* Removes support for Postgres 11 and 12 ([99489b2](99489b2))
|
||||
* The `Parse.User` passed as argument if `verifyUserEmails` is set to a function is renamed from `user` to `object` for consistency with invocations of `verifyUserEmails` on signup or login; the user object is not a plain JavaScript object anymore but an instance of `Parse.User` ([8adcbee](8adcbee))
|
||||
* `Parse.Session.current()` no longer throws an error if the session token is expired, but instead returns the session token with its expiration date to allow checking its validity ([f9dde4a](f9dde4a))
|
||||
* `Parse.Query` no longer supports the BSON type `code`; although this feature was never officially documented, its removal is announced as a breaking change to protect deployments where it might be in use. ([3de8494](3de8494))
|
||||
|
||||
# [6.5.0-beta.1](https://github.com/parse-community/parse-server/compare/6.4.0...6.5.0-beta.1) (2023-11-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Context not passed to Cloud Code Trigger `beforeFind` when using `Parse.Query.include` ([#8765](https://github.com/parse-community/parse-server/issues/8765)) ([7d32d89](https://github.com/parse-community/parse-server/commit/7d32d8934f3ae7af7a7d8b9cc6a829c7d73973d3))
|
||||
* Parse Server option `fileUpload.fileExtensions` fails to determine file extension if filename contains multiple dots ([#8754](https://github.com/parse-community/parse-server/issues/8754)) ([3d6d50e](https://github.com/parse-community/parse-server/commit/3d6d50e0afff18b95fb906914e2cebd3839b517a))
|
||||
* Security bump @babel/traverse from 7.20.5 to 7.23.2 ([#8777](https://github.com/parse-community/parse-server/issues/8777)) ([2d6b3d1](https://github.com/parse-community/parse-server/commit/2d6b3d18499179e99be116f25c0850d3f449509c))
|
||||
* Security upgrade graphql from 16.6.0 to 16.8.1 ([#8758](https://github.com/parse-community/parse-server/issues/8758)) ([71dfd8a](https://github.com/parse-community/parse-server/commit/71dfd8a7ece8c0dd1a66d03bb9420cfd39f4f9b1))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `$setOnInsert` operator to `Parse.Server.database.update` ([#8791](https://github.com/parse-community/parse-server/issues/8791)) ([f630a45](https://github.com/parse-community/parse-server/commit/f630a45aa5e87bc73a81fded061400c199b71a29))
|
||||
* Add compatibility for MongoDB Atlas Serverless and AWS Amazon DocumentDB with collation options `enableCollationCaseComparison`, `transformEmailToLowercase`, `transformUsernameToLowercase` ([#8805](https://github.com/parse-community/parse-server/issues/8805)) ([09fbeeb](https://github.com/parse-community/parse-server/commit/09fbeebba8870e7cf371fb84371a254c7b368620))
|
||||
* Add context to Cloud Code Triggers `beforeLogin` and `afterLogin` ([#8724](https://github.com/parse-community/parse-server/issues/8724)) ([a9c34ef](https://github.com/parse-community/parse-server/commit/a9c34ef1e2c78a42fb8b5fa8d569b7677c74919d))
|
||||
* Allow setting `createdAt` and `updatedAt` during `Parse.Object` creation with maintenance key ([#8696](https://github.com/parse-community/parse-server/issues/8696)) ([77bbfb3](https://github.com/parse-community/parse-server/commit/77bbfb3f186f5651c33ba152f04cff95128eaf2d))
|
||||
|
||||
# [6.4.0-beta.1](https://github.com/parse-community/parse-server/compare/6.3.0...6.4.0-beta.1) (2023-09-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions ([#8688](https://github.com/parse-community/parse-server/issues/8688)) ([6a4a00c](https://github.com/parse-community/parse-server/commit/6a4a00ca7af1163ea74b047b85cd6817366b824b))
|
||||
* Redis 4 does not reconnect after unhandled error ([#8706](https://github.com/parse-community/parse-server/issues/8706)) ([2b3d4e5](https://github.com/parse-community/parse-server/commit/2b3d4e5d3c85cd142f85af68dec51a8523548d49))
|
||||
* Remove config logging when launching Parse Server via CLI ([#8710](https://github.com/parse-community/parse-server/issues/8710)) ([ae68f0c](https://github.com/parse-community/parse-server/commit/ae68f0c31b741eeb83379c905c7ddfaa124436ec))
|
||||
* Server does not start via CLI when `auth` option is set ([#8666](https://github.com/parse-community/parse-server/issues/8666)) ([4e2000b](https://github.com/parse-community/parse-server/commit/4e2000bc563324389584ace3c090a5c1a7796a64))
|
||||
|
||||
### Features
|
||||
|
||||
* Add conditional email verification via dynamic Parse Server options `verifyUserEmails`, `sendUserEmailVerification` that now accept functions ([#8425](https://github.com/parse-community/parse-server/issues/8425)) ([44acd6d](https://github.com/parse-community/parse-server/commit/44acd6d9ed157ad4842200c9d01f9c77a05fec3a))
|
||||
* Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code ([#8670](https://github.com/parse-community/parse-server/issues/8670)) ([a9d376b](https://github.com/parse-community/parse-server/commit/a9d376b61f5b07806eafbda91c4e36c322f09298))
|
||||
* Add TOTP authentication adapter ([#8457](https://github.com/parse-community/parse-server/issues/8457)) ([cc079a4](https://github.com/parse-community/parse-server/commit/cc079a40f6849a0e9bc6fdc811e8649ecb67b589))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improve performance of recursive pointer iterations ([#8741](https://github.com/parse-community/parse-server/issues/8741)) ([45a3ed0](https://github.com/parse-community/parse-server/commit/45a3ed0fcf2c0170607505a1550fb15896e705fd))
|
||||
|
||||
# [6.3.0-beta.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.3.0-beta.1) (2023-06-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Cloud Code Trigger `afterSave` executes even if not set ([#8520](https://github.com/parse-community/parse-server/issues/8520)) ([afd0515](https://github.com/parse-community/parse-server/commit/afd0515e207bd947840579d3f245980dffa6f804))
|
||||
* GridFS file storage doesn't work with certain `enableSchemaHooks` settings ([#8467](https://github.com/parse-community/parse-server/issues/8467)) ([d4cda4b](https://github.com/parse-community/parse-server/commit/d4cda4b26c9bde8c812549b8780bea1cfabdb394))
|
||||
* Inaccurate table total row count for PostgreSQL ([#8511](https://github.com/parse-community/parse-server/issues/8511)) ([0823a02](https://github.com/parse-community/parse-server/commit/0823a02fbf80bc88dc403bc47e9f5c6597ea78b4))
|
||||
* LiveQuery server is not shut down properly when `handleShutdown` is called ([#8491](https://github.com/parse-community/parse-server/issues/8491)) ([967700b](https://github.com/parse-community/parse-server/commit/967700bdbc94c74f75ba84d2b3f4b9f3fd2dca0b))
|
||||
* Rate limit feature is incompatible with Node 14 ([#8578](https://github.com/parse-community/parse-server/issues/8578)) ([f911f2c](https://github.com/parse-community/parse-server/commit/f911f2cd3a8c45cd326272dcd681532764a3761e))
|
||||
* Unnecessary log entries by `extendSessionOnUse` ([#8562](https://github.com/parse-community/parse-server/issues/8562)) ([fd6a007](https://github.com/parse-community/parse-server/commit/fd6a0077f2e5cf83d65e52172ae5a950ab0f1eae))
|
||||
|
||||
### Features
|
||||
|
||||
* `extendSessionOnUse` to automatically renew Parse Sessions ([#8505](https://github.com/parse-community/parse-server/issues/8505)) ([6f885d3](https://github.com/parse-community/parse-server/commit/6f885d36b94902fdfea873fc554dee83589e6029))
|
||||
* Add new Parse Server option `preventSignupWithUnverifiedEmail` to prevent returning a user without session token on sign-up with unverified email address ([#8451](https://github.com/parse-community/parse-server/issues/8451)) ([82da308](https://github.com/parse-community/parse-server/commit/82da30842a55980aa90cb7680fbf6db37ee16dab))
|
||||
* Add option to change the log level of logs emitted by Cloud Functions ([#8530](https://github.com/parse-community/parse-server/issues/8530)) ([2caea31](https://github.com/parse-community/parse-server/commit/2caea310be412d82b04a85716bc769ccc410316d))
|
||||
* Add support for `$eq` query constraint in LiveQuery ([#8614](https://github.com/parse-community/parse-server/issues/8614)) ([656d673](https://github.com/parse-community/parse-server/commit/656d673cf5dea354e4f2b3d4dc2b29a41d311b3e))
|
||||
* Add zones for rate limiting by `ip`, `user`, `session`, `global` ([#8508](https://github.com/parse-community/parse-server/issues/8508)) ([03fba97](https://github.com/parse-community/parse-server/commit/03fba97e0549bfcaeee9f2fa4c9905dbcc91840e))
|
||||
* Allow `Parse.Object` pointers in Cloud Code arguments ([#8490](https://github.com/parse-community/parse-server/issues/8490)) ([28aeda3](https://github.com/parse-community/parse-server/commit/28aeda3f160efcbbcf85a85484a8d26567fa9761))
|
||||
|
||||
### Reverts
|
||||
|
||||
* fix: Inaccurate table total row count for PostgreSQL ([6722110](https://github.com/parse-community/parse-server/commit/6722110f203bc5fdcaa68cdf091cf9e7b48d1cff))
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,200 @@
|
||||
# [7.2.0](https://github.com/parse-community/parse-server/compare/7.1.0...7.2.0) (2024-07-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Invalid push notification tokens are not cleaned up from database for FCM API v2 ([#9173](https://github.com/parse-community/parse-server/issues/9173)) ([284da09](https://github.com/parse-community/parse-server/commit/284da09f4546356b37511a589fb5f64a3efffe79))
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for dot notation on array fields of Parse Object ([#9115](https://github.com/parse-community/parse-server/issues/9115)) ([cf4c880](https://github.com/parse-community/parse-server/commit/cf4c8807b9da87a0a5f9c94e5bdfcf17cda80cf4))
|
||||
* Upgrade to @parse/push-adapter 6.4.0 ([#9182](https://github.com/parse-community/parse-server/issues/9182)) ([ef1634b](https://github.com/parse-community/parse-server/commit/ef1634bf1f360429108d29b08032fc7961ff96a1))
|
||||
* Upgrade to Parse JS SDK 5.3.0 ([#9180](https://github.com/parse-community/parse-server/issues/9180)) ([dca187f](https://github.com/parse-community/parse-server/commit/dca187f91b93cbb362b22a3fb9ee38451799ff13))
|
||||
|
||||
# [7.1.0](https://github.com/parse-community/parse-server/compare/7.0.0...7.1.0) (2024-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `Parse.Cloud.startJob` and `Parse.Push.send` not returning status ID when setting Parse Server option `directAccess: true` ([#8766](https://github.com/parse-community/parse-server/issues/8766)) ([5b0efb2](https://github.com/parse-community/parse-server/commit/5b0efb22efe94c47f243cf8b1e6407ed5c5a67d3))
|
||||
* `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations ([#8915](https://github.com/parse-community/parse-server/issues/8915)) ([907ad42](https://github.com/parse-community/parse-server/commit/907ad4267c228d26cfcefe7848b30ce85ba7ff8f))
|
||||
* Facebook Limited Login not working due to incorrect domain in JWT validation ([#9122](https://github.com/parse-community/parse-server/issues/9122)) ([9d0bd2b](https://github.com/parse-community/parse-server/commit/9d0bd2badd6e5f7429d1af00b118225752e5d86a))
|
||||
* Live query throws error when constraint `notEqualTo` is set to `null` ([#8835](https://github.com/parse-community/parse-server/issues/8835)) ([11d3e48](https://github.com/parse-community/parse-server/commit/11d3e484df862224c15d20f6171514948981ea90))
|
||||
* Parse Server option `extendSessionOnUse` not working for session lengths < 24 hours ([#9113](https://github.com/parse-community/parse-server/issues/9113)) ([0a054e6](https://github.com/parse-community/parse-server/commit/0a054e6b541fd5ab470bf025665f5f7d2acedaa0))
|
||||
* Rate limiting can fail when using Parse Server option `rateLimit.redisUrl` with clusters ([#8632](https://github.com/parse-community/parse-server/issues/8632)) ([c277739](https://github.com/parse-community/parse-server/commit/c27773962399f8e27691e3b8087e7e1d59516efd))
|
||||
* SQL injection when using Parse Server with PostgreSQL; fixes security vulnerability [GHSA-c2hr-cqg6-8j6r](https://github.com/parse-community/parse-server/security/advisories/GHSA-c2hr-cqg6-8j6r) ([#9167](https://github.com/parse-community/parse-server/issues/9167)) ([2edf1e4](https://github.com/parse-community/parse-server/commit/2edf1e4c0363af01e97a7fbc97694f851b7d1ff3))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `silent` log level for Cloud Code ([#8803](https://github.com/parse-community/parse-server/issues/8803)) ([5f81efb](https://github.com/parse-community/parse-server/commit/5f81efb42964c4c2fa8bcafee9446a0122e3ce21))
|
||||
* Add server security check status `security.enableCheck` to Features Router ([#8679](https://github.com/parse-community/parse-server/issues/8679)) ([b07ec15](https://github.com/parse-community/parse-server/commit/b07ec153825882e97cc48dc84072c7f549f3238b))
|
||||
* Prevent Parse Server start in case of unknown option in server configuration ([#8987](https://github.com/parse-community/parse-server/issues/8987)) ([8758e6a](https://github.com/parse-community/parse-server/commit/8758e6abb9dbb68757bddcbd332ad25100c24a0e))
|
||||
* Upgrade to @parse/push-adapter 6.0.0 ([#9066](https://github.com/parse-community/parse-server/issues/9066)) ([18bdbf8](https://github.com/parse-community/parse-server/commit/18bdbf89c53a57648891ef582614ba7c2941e587))
|
||||
* Upgrade to @parse/push-adapter 6.2.0 ([#9127](https://github.com/parse-community/parse-server/issues/9127)) ([ca20496](https://github.com/parse-community/parse-server/commit/ca20496f28e5ec1294a7a23c8559df82b79b2a04))
|
||||
* Upgrade to Parse JS SDK 5.2.0 ([#9128](https://github.com/parse-community/parse-server/issues/9128)) ([665b8d5](https://github.com/parse-community/parse-server/commit/665b8d52d6cf5275179a5e1fb132c934edb53ecc))
|
||||
|
||||
# [7.0.0](https://github.com/parse-community/parse-server/compare/6.4.0...7.0.0) (2024-03-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* CacheAdapter does not connect when using a CacheAdapter with a JSON config ([#8633](https://github.com/parse-community/parse-server/issues/8633)) ([720d24e](https://github.com/parse-community/parse-server/commit/720d24e18540da35d50957f17be878316ec30318))
|
||||
* Conditional email verification not working in some cases if `verifyUserEmails`, `preventLoginWithUnverifiedEmail` set to functions ([#8838](https://github.com/parse-community/parse-server/issues/8838)) ([8e7a6b1](https://github.com/parse-community/parse-server/commit/8e7a6b1480c0117e6c73e7adc5a6619115a04e85))
|
||||
* Context not passed to Cloud Code Trigger `beforeFind` when using `Parse.Query.include` ([#8765](https://github.com/parse-community/parse-server/issues/8765)) ([7d32d89](https://github.com/parse-community/parse-server/commit/7d32d8934f3ae7af7a7d8b9cc6a829c7d73973d3))
|
||||
* Deny request if master key is not set in Parse Server option `masterKeyIps` regardless of ACL and CLP ([#8957](https://github.com/parse-community/parse-server/issues/8957)) ([a7b5b38](https://github.com/parse-community/parse-server/commit/a7b5b38418cbed9be3f4a7665f25b97f592663e1))
|
||||
* Docker image not published to Docker Hub on new release ([#8905](https://github.com/parse-community/parse-server/issues/8905)) ([a2ac8d1](https://github.com/parse-community/parse-server/commit/a2ac8d133c71cd7b61e5ef59c4be915cfea85db6))
|
||||
* Docker version releases by removing arm/v6 and arm/v7 support ([#8976](https://github.com/parse-community/parse-server/issues/8976)) ([1f62dd0](https://github.com/parse-community/parse-server/commit/1f62dd0f4e107b22a387692558a042ee26ce8703))
|
||||
* GraphQL file upload fails in case of use of pointer or relation ([#8721](https://github.com/parse-community/parse-server/issues/8721)) ([1aba638](https://github.com/parse-community/parse-server/commit/1aba6382c873fb489d4a898d301e6da9fb6aa61b))
|
||||
* Improve PostgreSQL injection detection; fixes security vulnerability [GHSA-6927-3vr9-fxf2](https://github.com/parse-community/parse-server/security/advisories/GHSA-6927-3vr9-fxf2) which affects Parse Server deployments using a Postgres database ([#8961](https://github.com/parse-community/parse-server/issues/8961)) ([cbefe77](https://github.com/parse-community/parse-server/commit/cbefe770a7260b54748a058b8a7389937dc35833))
|
||||
* Incomplete user object in `verifyEmail` function if both username and email are changed ([#8889](https://github.com/parse-community/parse-server/issues/8889)) ([1eb95ae](https://github.com/parse-community/parse-server/commit/1eb95aeb41a96250e582d79a703f6adcb403c08b))
|
||||
* Parse Server option `emailVerifyTokenReuseIfValid: true` generates new token on every email verification request ([#8885](https://github.com/parse-community/parse-server/issues/8885)) ([0023ce4](https://github.com/parse-community/parse-server/commit/0023ce448a5e9423337d0e1a25648bde1156bc95))
|
||||
* Parse Server option `fileExtensions` default value rejects file extensions that are less than 3 or more than 4 characters long ([#8699](https://github.com/parse-community/parse-server/issues/8699)) ([2760381](https://github.com/parse-community/parse-server/commit/276038118377c2b22381bcd8d30337203822121b))
|
||||
* Parse Server option `fileUpload.fileExtensions` fails to determine file extension if filename contains multiple dots ([#8754](https://github.com/parse-community/parse-server/issues/8754)) ([3d6d50e](https://github.com/parse-community/parse-server/commit/3d6d50e0afff18b95fb906914e2cebd3839b517a))
|
||||
* Security bump @babel/traverse from 7.20.5 to 7.23.2 ([#8777](https://github.com/parse-community/parse-server/issues/8777)) ([2d6b3d1](https://github.com/parse-community/parse-server/commit/2d6b3d18499179e99be116f25c0850d3f449509c))
|
||||
* Security upgrade graphql from 16.6.0 to 16.8.1 ([#8758](https://github.com/parse-community/parse-server/issues/8758)) ([71dfd8a](https://github.com/parse-community/parse-server/commit/71dfd8a7ece8c0dd1a66d03bb9420cfd39f4f9b1))
|
||||
* Server crashes on invalid Cloud Function or Cloud Job name; fixes security vulnerability [GHSA-6hh7-46r2-vf29](https://github.com/parse-community/parse-server/security/advisories/GHSA-6hh7-46r2-vf29) ([#9024](https://github.com/parse-community/parse-server/issues/9024)) ([9f6e342](https://github.com/parse-community/parse-server/commit/9f6e3429d3b326cf4e2994733c618d08032fac6e))
|
||||
* Server crashes when receiving an array of `Parse.Pointer` in the request body ([#8784](https://github.com/parse-community/parse-server/issues/8784)) ([66e3603](https://github.com/parse-community/parse-server/commit/66e36039d8af654cfa0284666c0ddd94975dcb52))
|
||||
* Username is `undefined` in email verification link on email change ([#8887](https://github.com/parse-community/parse-server/issues/8887)) ([e315c13](https://github.com/parse-community/parse-server/commit/e315c137bf41bedfa8f0df537f2c3f6ab45b7e60))
|
||||
|
||||
### Features
|
||||
|
||||
* Add `$setOnInsert` operator to `Parse.Server.database.update` ([#8791](https://github.com/parse-community/parse-server/issues/8791)) ([f630a45](https://github.com/parse-community/parse-server/commit/f630a45aa5e87bc73a81fded061400c199b71a29))
|
||||
* Add `installationId` to arguments for `verifyUserEmails`, `preventLoginWithUnverifiedEmail` ([#8836](https://github.com/parse-community/parse-server/issues/8836)) ([a22dbe1](https://github.com/parse-community/parse-server/commit/a22dbe16d5ac0090608f6caaf0ebd134925b7fd4))
|
||||
* Add `installationId`, `ip`, `resendRequest` to arguments passed to `verifyUserEmails` on verification email request ([#8873](https://github.com/parse-community/parse-server/issues/8873)) ([8adcbee](https://github.com/parse-community/parse-server/commit/8adcbee11283d3e95179ca2047e2615f52c18806))
|
||||
* Add `Parse.User` as function parameter to Parse Server options `verifyUserEmails`, `preventLoginWithUnverifiedEmail` on login ([#8850](https://github.com/parse-community/parse-server/issues/8850)) ([972f630](https://github.com/parse-community/parse-server/commit/972f6300163b3cd7d95eeb95986e8322c95f821c))
|
||||
* Add compatibility for MongoDB Atlas Serverless and AWS Amazon DocumentDB with collation options `enableCollationCaseComparison`, `transformEmailToLowercase`, `transformUsernameToLowercase` ([#8805](https://github.com/parse-community/parse-server/issues/8805)) ([09fbeeb](https://github.com/parse-community/parse-server/commit/09fbeebba8870e7cf371fb84371a254c7b368620))
|
||||
* Add context to Cloud Code Triggers `beforeLogin` and `afterLogin` ([#8724](https://github.com/parse-community/parse-server/issues/8724)) ([a9c34ef](https://github.com/parse-community/parse-server/commit/a9c34ef1e2c78a42fb8b5fa8d569b7677c74919d))
|
||||
* Add password validation via POST request for user with unverified email using master key and option `ignoreEmailVerification` ([#8895](https://github.com/parse-community/parse-server/issues/8895)) ([633a9d2](https://github.com/parse-community/parse-server/commit/633a9d25e4253e2125bc93c02ee8a37e0f5f7b83))
|
||||
* Add support for MongoDB 7 ([#8761](https://github.com/parse-community/parse-server/issues/8761)) ([3de8494](https://github.com/parse-community/parse-server/commit/3de8494a221991dfd10a74e0a2dc89576265c9b7))
|
||||
* Add support for MongoDB query comment ([#8928](https://github.com/parse-community/parse-server/issues/8928)) ([2170962](https://github.com/parse-community/parse-server/commit/2170962a50fa353ed85eda3f11dce7ee3647b087))
|
||||
* Add support for Node 20, drop support for Node 14, 16 ([#8907](https://github.com/parse-community/parse-server/issues/8907)) ([ced4872](https://github.com/parse-community/parse-server/commit/ced487246ea0ef72a8aa014991f003209b34841e))
|
||||
* Add support for Postgres 16 ([#8898](https://github.com/parse-community/parse-server/issues/8898)) ([99489b2](https://github.com/parse-community/parse-server/commit/99489b22e4f0982e6cb39992974b51aa8d3a31e4))
|
||||
* Allow `Parse.Session.current` on expired session token instead of throwing error ([#8722](https://github.com/parse-community/parse-server/issues/8722)) ([f9dde4a](https://github.com/parse-community/parse-server/commit/f9dde4a9f8a90c63f71172c9bc515b0f6c6d2e4a))
|
||||
* Allow setting `createdAt` and `updatedAt` during `Parse.Object` creation with maintenance key ([#8696](https://github.com/parse-community/parse-server/issues/8696)) ([77bbfb3](https://github.com/parse-community/parse-server/commit/77bbfb3f186f5651c33ba152f04cff95128eaf2d))
|
||||
* Deprecation DEPPS5: Config option `allowClientClassCreation` defaults to `false` ([#8849](https://github.com/parse-community/parse-server/issues/8849)) ([29624e0](https://github.com/parse-community/parse-server/commit/29624e0fae17161cd412ae58d35a195cfa286cad))
|
||||
* Deprecation DEPPS6: Authentication adapters disabled by default ([#8858](https://github.com/parse-community/parse-server/issues/8858)) ([0cf58eb](https://github.com/parse-community/parse-server/commit/0cf58eb8d60c8e5f485764e154f3214c49eee430))
|
||||
* Deprecation DEPPS7: Remove deprecated Cloud Code file trigger syntax ([#8855](https://github.com/parse-community/parse-server/issues/8855)) ([4e6a375](https://github.com/parse-community/parse-server/commit/4e6a375b5184ae0f7aa256a921eca4021c609435))
|
||||
* Deprecation DEPPS8: Parse Server option `allowExpiredAuthDataToken` defaults to `false` ([#8860](https://github.com/parse-community/parse-server/issues/8860)) ([e29845f](https://github.com/parse-community/parse-server/commit/e29845f8dacac09ce3093d75c0d92330c24389e8))
|
||||
* Deprecation DEPPS9: LiveQuery `fields` option is renamed to `keys` ([#8852](https://github.com/parse-community/parse-server/issues/8852)) ([38983e8](https://github.com/parse-community/parse-server/commit/38983e8e9b5cdbd006f311a2338103624137d013))
|
||||
* Node process exits with error code 1 on uncaught exception to allow custom uncaught exception handling ([#8894](https://github.com/parse-community/parse-server/issues/8894)) ([70c280c](https://github.com/parse-community/parse-server/commit/70c280ca578ff28b5acf92f37fbe06d42a5b34ca))
|
||||
* Switch GraphQL server from Yoga v2 to Apollo v4 ([#8959](https://github.com/parse-community/parse-server/issues/8959)) ([105ae7c](https://github.com/parse-community/parse-server/commit/105ae7c8a57d5a650b243174a80c26bf6db16e28))
|
||||
* Upgrade Parse Server Push Adapter to 5.0.2 ([#8813](https://github.com/parse-community/parse-server/issues/8813)) ([6ef1986](https://github.com/parse-community/parse-server/commit/6ef1986c03a1d84b7e11c05851e5bf9688d88740))
|
||||
* Upgrade to Parse JS SDK 5 ([#9022](https://github.com/parse-community/parse-server/issues/9022)) ([ad4aa83](https://github.com/parse-community/parse-server/commit/ad4aa83983205a0e27639f6ee6a4a5963b67e4b8))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improved IP validation performance for `masterKeyIPs`, `maintenanceKeyIPs` ([#8510](https://github.com/parse-community/parse-server/issues/8510)) ([b87daba](https://github.com/parse-community/parse-server/commit/b87daba0671a1b0b7b8d63bc671d665c91a04522))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The Parse Server option `allowClientClassCreation` defaults to `false`. ([29624e0](29624e0))
|
||||
* A request using the master key will now be rejected as unauthorized if the IP from which the request originates is not set in the Parse Server option `masterKeyIps`, even if the request does not require the master key permission, for example for a public object in a public class class. ([a7b5b38](a7b5b38))
|
||||
* Node process now exits with code 1 on uncaught exceptions, enabling custom handlers that were blocked by Parse Server's default behavior of re-throwing errors. This change may lead to automatic process restarts by the environment, unlike before. ([70c280c](70c280c))
|
||||
* Authentication adapters are disabled by default; to use an authentication adapter it needs to be explicitly enabled in the Parse Server authentication adapter option `auth.<provider>.enabled: true` ([0cf58eb](0cf58eb))
|
||||
* Parse Server option `allowExpiredAuthDataToken` defaults to `false`; a 3rd party authentication token will be validated every time the user tries to log in and the login will fail if the token has expired; the effect of this change may differ for different authentication adapters, depending on the token lifetime and the token refresh logic of the adapter ([e29845f](e29845f))
|
||||
* LiveQuery `fields` option is renamed to `keys` ([38983e8](38983e8))
|
||||
* Cloud Code file trigger syntax has been aligned with object trigger syntax, for example `Parse.Cloud.beforeDeleteFile'` has been changed to `Parse.Cloud.beforeDelete(Parse.File, (request) => {})'` ([4e6a375](4e6a375))
|
||||
* Removes support for Node 14 and 16 ([ced4872](ced4872))
|
||||
* Removes support for Postgres 11 and 12 ([99489b2](99489b2))
|
||||
* The `Parse.User` passed as argument if `verifyUserEmails` is set to a function is renamed from `user` to `object` for consistency with invocations of `verifyUserEmails` on signup or login; the user object is not a plain JavaScript object anymore but an instance of `Parse.User` ([8adcbee](8adcbee))
|
||||
* `Parse.Session.current()` no longer throws an error if the session token is expired, but instead returns the session token with its expiration date to allow checking its validity ([f9dde4a](f9dde4a))
|
||||
* `Parse.Query` no longer supports the BSON type `code`; although this feature was never officially documented, its removal is announced as a breaking change to protect deployments where it might be in use. ([3de8494](3de8494))
|
||||
|
||||
# [6.4.0](https://github.com/parse-community/parse-server/compare/6.3.1...6.4.0) (2023-11-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `fileUpload.fileExtensions` does not work with an array of extensions ([#8688](https://github.com/parse-community/parse-server/issues/8688)) ([6a4a00c](https://github.com/parse-community/parse-server/commit/6a4a00ca7af1163ea74b047b85cd6817366b824b))
|
||||
* Redis 4 does not reconnect after unhandled error ([#8706](https://github.com/parse-community/parse-server/issues/8706)) ([2b3d4e5](https://github.com/parse-community/parse-server/commit/2b3d4e5d3c85cd142f85af68dec51a8523548d49))
|
||||
* Remove config logging when launching Parse Server via CLI ([#8710](https://github.com/parse-community/parse-server/issues/8710)) ([ae68f0c](https://github.com/parse-community/parse-server/commit/ae68f0c31b741eeb83379c905c7ddfaa124436ec))
|
||||
* Server does not start via CLI when `auth` option is set ([#8666](https://github.com/parse-community/parse-server/issues/8666)) ([4e2000b](https://github.com/parse-community/parse-server/commit/4e2000bc563324389584ace3c090a5c1a7796a64))
|
||||
|
||||
### Features
|
||||
|
||||
* Add conditional email verification via dynamic Parse Server options `verifyUserEmails`, `sendUserEmailVerification` that now accept functions ([#8425](https://github.com/parse-community/parse-server/issues/8425)) ([44acd6d](https://github.com/parse-community/parse-server/commit/44acd6d9ed157ad4842200c9d01f9c77a05fec3a))
|
||||
* Add property `Parse.Server.version` to determine current version of Parse Server in Cloud Code ([#8670](https://github.com/parse-community/parse-server/issues/8670)) ([a9d376b](https://github.com/parse-community/parse-server/commit/a9d376b61f5b07806eafbda91c4e36c322f09298))
|
||||
* Add TOTP authentication adapter ([#8457](https://github.com/parse-community/parse-server/issues/8457)) ([cc079a4](https://github.com/parse-community/parse-server/commit/cc079a40f6849a0e9bc6fdc811e8649ecb67b589))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Improve performance of recursive pointer iterations ([#8741](https://github.com/parse-community/parse-server/issues/8741)) ([45a3ed0](https://github.com/parse-community/parse-server/commit/45a3ed0fcf2c0170607505a1550fb15896e705fd))
|
||||
|
||||
## [6.3.1](https://github.com/parse-community/parse-server/compare/6.3.0...6.3.1) (2023-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Server crash when uploading file without extension; fixes security vulnerability [GHSA-792q-q67h-w579](https://github.com/parse-community/parse-server/security/advisories/GHSA-792q-q67h-w579) ([#8781](https://github.com/parse-community/parse-server/issues/8781)) ([fd86278](https://github.com/parse-community/parse-server/commit/fd86278919556d3682e7e2c856dfccd5beffbfc0))
|
||||
|
||||
# [6.3.0](https://github.com/parse-community/parse-server/compare/6.2.2...6.3.0) (2023-09-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Cloud Code Trigger `afterSave` executes even if not set ([#8520](https://github.com/parse-community/parse-server/issues/8520)) ([afd0515](https://github.com/parse-community/parse-server/commit/afd0515e207bd947840579d3f245980dffa6f804))
|
||||
* GridFS file storage doesn't work with certain `enableSchemaHooks` settings ([#8467](https://github.com/parse-community/parse-server/issues/8467)) ([d4cda4b](https://github.com/parse-community/parse-server/commit/d4cda4b26c9bde8c812549b8780bea1cfabdb394))
|
||||
* Inaccurate table total row count for PostgreSQL ([#8511](https://github.com/parse-community/parse-server/issues/8511)) ([0823a02](https://github.com/parse-community/parse-server/commit/0823a02fbf80bc88dc403bc47e9f5c6597ea78b4))
|
||||
* LiveQuery server is not shut down properly when `handleShutdown` is called ([#8491](https://github.com/parse-community/parse-server/issues/8491)) ([967700b](https://github.com/parse-community/parse-server/commit/967700bdbc94c74f75ba84d2b3f4b9f3fd2dca0b))
|
||||
* Rate limit feature is incompatible with Node 14 ([#8578](https://github.com/parse-community/parse-server/issues/8578)) ([f911f2c](https://github.com/parse-community/parse-server/commit/f911f2cd3a8c45cd326272dcd681532764a3761e))
|
||||
* Unnecessary log entries by `extendSessionOnUse` ([#8562](https://github.com/parse-community/parse-server/issues/8562)) ([fd6a007](https://github.com/parse-community/parse-server/commit/fd6a0077f2e5cf83d65e52172ae5a950ab0f1eae))
|
||||
|
||||
### Features
|
||||
|
||||
* `extendSessionOnUse` to automatically renew Parse Sessions ([#8505](https://github.com/parse-community/parse-server/issues/8505)) ([6f885d3](https://github.com/parse-community/parse-server/commit/6f885d36b94902fdfea873fc554dee83589e6029))
|
||||
* Add new Parse Server option `preventSignupWithUnverifiedEmail` to prevent returning a user without session token on sign-up with unverified email address ([#8451](https://github.com/parse-community/parse-server/issues/8451)) ([82da308](https://github.com/parse-community/parse-server/commit/82da30842a55980aa90cb7680fbf6db37ee16dab))
|
||||
* Add option to change the log level of logs emitted by Cloud Functions ([#8530](https://github.com/parse-community/parse-server/issues/8530)) ([2caea31](https://github.com/parse-community/parse-server/commit/2caea310be412d82b04a85716bc769ccc410316d))
|
||||
* Add support for `$eq` query constraint in LiveQuery ([#8614](https://github.com/parse-community/parse-server/issues/8614)) ([656d673](https://github.com/parse-community/parse-server/commit/656d673cf5dea354e4f2b3d4dc2b29a41d311b3e))
|
||||
* Add zones for rate limiting by `ip`, `user`, `session`, `global` ([#8508](https://github.com/parse-community/parse-server/issues/8508)) ([03fba97](https://github.com/parse-community/parse-server/commit/03fba97e0549bfcaeee9f2fa4c9905dbcc91840e))
|
||||
* Allow `Parse.Object` pointers in Cloud Code arguments ([#8490](https://github.com/parse-community/parse-server/issues/8490)) ([28aeda3](https://github.com/parse-community/parse-server/commit/28aeda3f160efcbbcf85a85484a8d26567fa9761))
|
||||
|
||||
### Reverts
|
||||
|
||||
* fix: Inaccurate table total row count for PostgreSQL ([6722110](https://github.com/parse-community/parse-server/commit/6722110f203bc5fdcaa68cdf091cf9e7b48d1cff))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
+20
-18
@@ -1,8 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const CiVersionCheck = require('./CiVersionCheck');
|
||||
const mongoVersionList = require('mongodb-version-list');
|
||||
const allNodeVersions = require('all-node-versions');
|
||||
const { exec } = require('child_process');
|
||||
|
||||
async function check() {
|
||||
// Run checks
|
||||
@@ -14,14 +13,16 @@ async function check() {
|
||||
* Check the MongoDB versions used in test environments.
|
||||
*/
|
||||
async function checkMongoDbVersions() {
|
||||
const releasedVersions = await new Promise((resolve, reject) => {
|
||||
mongoVersionList(function (error, versions) {
|
||||
let latestStableVersions = await new Promise((resolve, reject) => {
|
||||
exec('m ls', (error, stdout) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(versions);
|
||||
resolve(stdout.trim());
|
||||
});
|
||||
});
|
||||
latestStableVersions = latestStableVersions.split('\n').map(version => version.trim());
|
||||
|
||||
await new CiVersionCheck({
|
||||
packageName: 'MongoDB',
|
||||
@@ -29,13 +30,14 @@ async function checkMongoDbVersions() {
|
||||
yamlFilePath: './.github/workflows/ci.yml',
|
||||
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
|
||||
ciVersionKey: 'MONGODB_VERSION',
|
||||
releasedVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.minor,
|
||||
releasedVersions: latestStableVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.patch,
|
||||
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.2.0', // These versions have reached their end-of-life support date
|
||||
'>=4.3.0 <5.0.0', // Unsupported rapid release versions
|
||||
'>=5.1.0 <6.0.0', // Unsupported rapid release versions
|
||||
'>=6.1.0 <7.0.0', // Unsupported rapid release versions
|
||||
'>=7.1.0 <8.0.0', // Unsupported rapid release versions
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
@@ -44,8 +46,9 @@ async function checkMongoDbVersions() {
|
||||
* Check the Nodejs versions used in test environments.
|
||||
*/
|
||||
async function checkNodeVersions() {
|
||||
const allVersions = await allNodeVersions();
|
||||
const releasedVersions = allVersions.versions;
|
||||
const allVersions = (await import('all-node-versions')).default;
|
||||
const { versions } = await allVersions();
|
||||
const nodeVersions = versions.map(version => version.node);
|
||||
|
||||
await new CiVersionCheck({
|
||||
packageName: 'Node.js',
|
||||
@@ -53,13 +56,12 @@ async function checkNodeVersions() {
|
||||
yamlFilePath: './.github/workflows/ci.yml',
|
||||
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
|
||||
ciVersionKey: 'NODE_VERSION',
|
||||
releasedVersions,
|
||||
releasedVersions: nodeVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.minor,
|
||||
ignoreReleasedVersions: [
|
||||
'<12.0.0', // These versions have reached their end-of-life support date
|
||||
'>=13.0.0 <14.0.0', // These versions have reached their end-of-life support date
|
||||
'>=15.0.0 <16.0.0', // These versions have reached their end-of-life support date
|
||||
'>=19.0.0', // These versions are not officially supported yet
|
||||
'<18.0.0', // These versions have reached their end-of-life support date
|
||||
'>=19.0.0 <20.0.0', // These versions have reached their end-of-life support date
|
||||
'>=21.0.0', // These versions are not officially supported yet
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
|
||||
+3
-16
@@ -1,27 +1,14 @@
|
||||
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");
|
||||
const util = require('util');
|
||||
(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 execute = util.promisify(exec);
|
||||
await execute('npm run definitions');
|
||||
const [newDefinitions, newDocs] = await Promise.all([
|
||||
fs.readFile('./src/Options/Definitions.js', 'utf8'),
|
||||
fs.readFile('./src/Options/docs.js', 'utf8'),
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Read package exclusion list from arguments
|
||||
exclusionList=("$@")
|
||||
|
||||
# Convert exclusion list to grep pattern
|
||||
exclusionPattern=$(printf "|%s" "${exclusionList[@]}")
|
||||
exclusionPattern=${exclusionPattern:1}
|
||||
|
||||
# Get list of all dev dependencies
|
||||
devDeps=$(jq -r '.devDependencies | keys | .[]' package.json)
|
||||
|
||||
# Filter out exclusion list
|
||||
depsToUninstall=$(echo "$devDeps" | grep -Ev "$exclusionPattern")
|
||||
|
||||
# If there are dependencies to uninstall then uninstall them
|
||||
if [ -n "$depsToUninstall" ]; then
|
||||
echo "Uninstalling dev dependencies: $depsToUninstall"
|
||||
npm uninstall $depsToUninstall
|
||||
else
|
||||
echo "No dev dependencies to uninstall"
|
||||
fi
|
||||
+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='https://raw.githubusercontent.com/parse-community/parse-server/alpha/.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
+17140
-16053
File diff suppressed because it is too large
Load Diff
+63
-61
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "6.1.0-alpha.2",
|
||||
"version": "7.2.0",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -19,91 +19,92 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/eslint-parser": "7.19.1",
|
||||
"@graphql-tools/merge": "8.3.6",
|
||||
"@graphql-tools/schema": "9.0.4",
|
||||
"@apollo/server": "4.10.1",
|
||||
"@babel/eslint-parser": "7.21.8",
|
||||
"@graphql-tools/merge": "9.0.3",
|
||||
"@graphql-tools/schema": "10.0.3",
|
||||
"@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",
|
||||
"@parse/fs-files-adapter": "3.0.0",
|
||||
"@parse/push-adapter": "6.4.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.20.1",
|
||||
"commander": "5.1.0",
|
||||
"body-parser": "1.20.2",
|
||||
"commander": "12.0.0",
|
||||
"cors": "2.8.5",
|
||||
"deepcopy": "2.1.0",
|
||||
"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.10.0",
|
||||
"express": "4.19.2",
|
||||
"express-rate-limit": "6.11.2",
|
||||
"follow-redirects": "1.15.6",
|
||||
"graphql": "16.8.1",
|
||||
"graphql-list-fields": "2.0.4",
|
||||
"graphql-relay": "0.10.1",
|
||||
"graphql-tag": "2.12.6",
|
||||
"graphql-upload": "15.0.2",
|
||||
"intersect": "1.0.1",
|
||||
"ip-range-check": "0.2.0",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
"jwks-rsa": "2.1.5",
|
||||
"ldapjs": "2.3.3",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"jwks-rsa": "3.1.0",
|
||||
"ldapjs": "3.0.7",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "7.12.0",
|
||||
"lru-cache": "10.2.2",
|
||||
"mime": "3.0.0",
|
||||
"mongodb": "4.10.0",
|
||||
"mongodb": "5.9.0",
|
||||
"mustache": "4.2.0",
|
||||
"parse": "4.0.1",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"pg-monitor": "1.5.0",
|
||||
"pg-promise": "10.12.1",
|
||||
"otpauth": "9.2.2",
|
||||
"parse": "5.3.0",
|
||||
"path-to-regexp": "6.2.1",
|
||||
"pg-monitor": "2.0.0",
|
||||
"pg-promise": "11.7.8",
|
||||
"pluralize": "8.0.0",
|
||||
"redis": "4.0.6",
|
||||
"semver": "7.3.8",
|
||||
"rate-limit-redis": "4.2.0",
|
||||
"redis": "4.6.13",
|
||||
"semver": "7.6.0",
|
||||
"subscriptions-transport-ws": "0.11.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "9.0.0",
|
||||
"winston": "3.8.1",
|
||||
"winston-daily-rotate-file": "4.7.1",
|
||||
"ws": "8.9.0"
|
||||
"uuid": "9.0.1",
|
||||
"winston": "3.12.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "1.9.1",
|
||||
"@apollo/client": "3.6.1",
|
||||
"@babel/cli": "7.10.0",
|
||||
"@babel/core": "7.20.2",
|
||||
"@actions/core": "1.10.1",
|
||||
"@apollo/client": "3.9.11",
|
||||
"@babel/cli": "7.23.9",
|
||||
"@babel/core": "7.24.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.10.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.9.0",
|
||||
"@babel/preset-env": "7.10.0",
|
||||
"@parse/minami": "1.0.0",
|
||||
"@saithodev/semantic-release-backmerge": "2.1.2",
|
||||
"@semantic-release/changelog": "5.0.1",
|
||||
"@semantic-release/commit-analyzer": "8.0.1",
|
||||
"@semantic-release/git": "9.0.0",
|
||||
"@semantic-release/github": "7.2.3",
|
||||
"@semantic-release/npm": "7.1.3",
|
||||
"@semantic-release/release-notes-generator": "9.0.3",
|
||||
"all-node-versions": "11.3.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.23.3",
|
||||
"@babel/preset-env": "7.24.0",
|
||||
"@saithodev/semantic-release-backmerge": "4.0.1",
|
||||
"@semantic-release/changelog": "6.0.3",
|
||||
"@semantic-release/commit-analyzer": "13.0.0",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@semantic-release/github": "10.1.0",
|
||||
"@semantic-release/npm": "12.0.1",
|
||||
"@semantic-release/release-notes-generator": "14.0.1",
|
||||
"all-node-versions": "12.1.0",
|
||||
"apollo-upload-client": "17.0.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"clean-jsdoc-theme": "4.2.18",
|
||||
"cross-env": "7.0.2",
|
||||
"deep-diff": "1.0.2",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-plugin-flowtype": "8.0.3",
|
||||
"flow-bin": "0.119.1",
|
||||
"form-data": "3.0.0",
|
||||
"form-data": "4.0.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"husky": "4.3.8",
|
||||
"husky": "9.0.11",
|
||||
"jasmine": "3.5.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"jsdoc": "3.6.3",
|
||||
"jsdoc": "4.0.2",
|
||||
"jsdoc-babel": "0.5.0",
|
||||
"lint-staged": "10.2.3",
|
||||
"madge": "5.0.1",
|
||||
"m": "1.9.0",
|
||||
"madge": "6.1.0",
|
||||
"mock-files-adapter": "file:spec/dependencies/mock-files-adapter",
|
||||
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
|
||||
"mongodb-runner": "4.8.1",
|
||||
"mongodb-version-list": "1.0.0",
|
||||
"node-abort-controller": "3.0.1",
|
||||
"mongodb-runner": "5.5.4",
|
||||
"node-abort-controller": "3.1.1",
|
||||
"node-fetch": "3.2.10",
|
||||
"nyc": "15.1.0",
|
||||
"prettier": "2.0.5",
|
||||
"semantic-release": "17.4.6",
|
||||
"semantic-release": "24.0.0",
|
||||
"yaml": "1.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -116,18 +117,19 @@
|
||||
"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-runner start",
|
||||
"test:mongodb:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
|
||||
"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.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.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start",
|
||||
"test:mongodb:7.0.1": "npm run test:mongodb --dbversion=7.0.1",
|
||||
"test:postgres:testonly": "cross-env PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
|
||||
"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.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner stop",
|
||||
"posttest": "cross-env mongodb-runner stop --all",
|
||||
"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",
|
||||
@@ -136,13 +138,13 @@
|
||||
"madge:circular": "node_modules/.bin/madge ./src --circular"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.21.0 <17 || >=18 <19"
|
||||
"node": ">=18.0.0 <21"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "bin/parse-server"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@node-rs/bcrypt": "1.1.0"
|
||||
"@node-rs/bcrypt": "1.10.4"
|
||||
},
|
||||
"collective": {
|
||||
"type": "opencollective",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
const pkg = require('./package.json');
|
||||
|
||||
const version = parseFloat(process.version.substr(1));
|
||||
const version = parseFloat(process.version.substring(1));
|
||||
const minimum = parseFloat(pkg.engines.node.match(/\d+/g).join('.'));
|
||||
|
||||
module.exports = function () {
|
||||
|
||||
+21
-9
@@ -1,10 +1,17 @@
|
||||
#!/bin/sh -e
|
||||
set -x
|
||||
# GITHUB_ACTIONS=true SOURCE_TAG=test ./release_docs.sh
|
||||
|
||||
if [ "${GITHUB_ACTIONS}" = "" ];
|
||||
then
|
||||
echo "Cannot release docs without GITHUB_ACTIONS set"
|
||||
exit 0;
|
||||
fi
|
||||
if [ "${SOURCE_TAG}" = "" ];
|
||||
then
|
||||
echo "Cannot release docs without SOURCE_TAG set"
|
||||
exit 0;
|
||||
fi
|
||||
REPO="https://github.com/parse-community/parse-server"
|
||||
|
||||
rm -rf docs
|
||||
@@ -13,17 +20,22 @@ cd docs
|
||||
git pull origin gh-pages
|
||||
cd ..
|
||||
|
||||
DEST="master"
|
||||
RELEASE="release"
|
||||
VERSION="${SOURCE_TAG}"
|
||||
|
||||
if [ "${SOURCE_TAG}" != "" ];
|
||||
then
|
||||
DEST="${SOURCE_TAG}"
|
||||
# change the default page to the latest
|
||||
echo "<meta http-equiv='refresh' content='0; url=/parse-server/api/${DEST}'>" > "docs/api/index.html"
|
||||
fi
|
||||
# change the default page to the latest
|
||||
echo "<meta http-equiv='refresh' content='0; url=/parse-server/api/${VERSION}'>" > "docs/api/index.html"
|
||||
|
||||
npm run definitions
|
||||
npm run docs
|
||||
|
||||
mkdir -p "docs/api/${DEST}"
|
||||
cp -R out/* "docs/api/${DEST}"
|
||||
mkdir -p "docs/api/${RELEASE}"
|
||||
cp -R out/* "docs/api/${RELEASE}"
|
||||
|
||||
mkdir -p "docs/api/${VERSION}"
|
||||
cp -R out/* "docs/api/${VERSION}"
|
||||
|
||||
# Copy other resources
|
||||
RESOURCE_DIR=".github"
|
||||
mkdir -p "docs/${RESOURCE_DIR}"
|
||||
cp "./.github/parse-server-logo.png" "docs/${RESOURCE_DIR}/"
|
||||
|
||||
@@ -161,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'));
|
||||
}
|
||||
}
|
||||
@@ -251,8 +254,34 @@ function inject(t, list) {
|
||||
if (action) {
|
||||
props.push(t.objectProperty(t.stringLiteral('action'), action));
|
||||
}
|
||||
|
||||
if (t.isGenericTypeAnnotation(elt)) {
|
||||
if (elt.typeAnnotation.id.name in nestedOptionEnvPrefix) {
|
||||
props.push(
|
||||
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elt.typeAnnotation.id.name))
|
||||
);
|
||||
}
|
||||
} else if (t.isArrayTypeAnnotation(elt)) {
|
||||
const elementType = elt.typeAnnotation.elementType;
|
||||
if (t.isGenericTypeAnnotation(elementType)) {
|
||||
if (elementType.id.name in nestedOptionEnvPrefix) {
|
||||
props.push(
|
||||
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elementType.id.name + '[]'))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (elt.defaultValue) {
|
||||
const parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
let parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
if (!parsedValue) {
|
||||
for (const type of elt.typeAnnotation.types) {
|
||||
elt.type = type.type;
|
||||
parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
|
||||
if (parsedValue) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsedValue) {
|
||||
props.push(t.objectProperty(t.stringLiteral('default'), parsedValue));
|
||||
} else {
|
||||
@@ -278,6 +307,9 @@ function inject(t, list) {
|
||||
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);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"equal": true,
|
||||
"expectAsync": true,
|
||||
"notEqual": true,
|
||||
"it_id": true,
|
||||
"it_only_db": true,
|
||||
"it_only_mongodb_version": true,
|
||||
"it_only_postgres_version": true,
|
||||
@@ -27,6 +28,7 @@
|
||||
"fit_exclude_node_version": true,
|
||||
"it_exclude_dbs": true,
|
||||
"describe_only_db": true,
|
||||
"fdescribe_only_db": true,
|
||||
"describe_only": true,
|
||||
"on_db": true,
|
||||
"defaultConfiguration": true,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const loadAdapter = require('../lib/Adapters/AdapterLoader').loadAdapter;
|
||||
const { loadAdapter, loadModule } = require('../lib/Adapters/AdapterLoader');
|
||||
const FilesAdapter = require('@parse/fs-files-adapter').default;
|
||||
const ParsePushAdapter = require('@parse/push-adapter').default;
|
||||
const MockFilesAdapter = require('mock-files-adapter');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
@@ -103,19 +102,19 @@ describe('AdapterLoader', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load push adapter from options', done => {
|
||||
it('should load push adapter from options', async () => {
|
||||
const options = {
|
||||
android: {
|
||||
senderId: 'yolo',
|
||||
apiKey: 'yolo',
|
||||
},
|
||||
};
|
||||
const ParsePushAdapter = await loadModule('@parse/push-adapter');
|
||||
expect(() => {
|
||||
const adapter = loadAdapter(undefined, ParsePushAdapter, options);
|
||||
expect(adapter.constructor).toBe(ParsePushAdapter);
|
||||
expect(adapter).not.toBe(undefined);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load custom push adapter from string (#3544)', done => {
|
||||
@@ -142,6 +141,25 @@ describe('AdapterLoader', () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should load custom database adapter from config', done => {
|
||||
const adapterPath = require('path').resolve('./spec/support/MockDatabaseAdapter');
|
||||
const options = {
|
||||
databaseURI: 'oracledb://user:password@localhost:1521/freepdb1',
|
||||
collectionPrefix: '',
|
||||
};
|
||||
const databaseAdapterOptions = {
|
||||
adapter: adapterPath,
|
||||
options,
|
||||
};
|
||||
expect(() => {
|
||||
const databaseAdapter = loadAdapter(databaseAdapterOptions);
|
||||
expect(databaseAdapter).not.toBe(undefined);
|
||||
expect(databaseAdapter.options).toEqual(options);
|
||||
expect(databaseAdapter.getDatabaseURI()).toEqual(options.databaseURI);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load file adapter from direct passing', done => {
|
||||
spyOn(console, 'warn').and.callFake(() => {});
|
||||
const mockFilesAdapter = new MockFilesAdapter('key', 'secret', 'bucket');
|
||||
|
||||
@@ -339,11 +339,12 @@ describe('AudiencesRouter', () => {
|
||||
)
|
||||
.then(result => {
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
database
|
||||
.collection('test__Audience')
|
||||
.find({ _id: audience.objectId })
|
||||
.toArray((error, rows) => {
|
||||
expect(error).toEqual(undefined);
|
||||
.toArray()
|
||||
.then(rows => {
|
||||
expect(rows[0]['times_used']).toEqual(1);
|
||||
expect(rows[0]['_last_used']).toEqual(now);
|
||||
Parse._request(
|
||||
@@ -362,6 +363,9 @@ describe('AudiencesRouter', () => {
|
||||
.catch(error => {
|
||||
done.fail(error);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
done.fail(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+48
-9
@@ -81,17 +81,35 @@ describe('Auth', () => {
|
||||
.then(roles => expect(roles).toEqual([]))
|
||||
.then(() => done());
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly handle bcrypt upgrade', done => {
|
||||
const bcryptOriginal = require('bcrypt-nodejs');
|
||||
const bcryptNew = require('bcryptjs');
|
||||
bcryptOriginal.hash('my1Long:password', null, null, function (err, res) {
|
||||
bcryptNew.compare('my1Long:password', res, function (err, res) {
|
||||
expect(res).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can use extendSessionOnUse', async () => {
|
||||
await reconfigureServer({
|
||||
extendSessionOnUse: true,
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
await user.signUp({
|
||||
username: 'hello',
|
||||
password: 'password',
|
||||
});
|
||||
const session = await new Parse.Query(Parse.Session).first();
|
||||
const updatedAt = new Date('2010');
|
||||
const expiry = new Date();
|
||||
expiry.setHours(expiry.getHours() + 1);
|
||||
|
||||
await Parse.Server.database.update(
|
||||
'_Session',
|
||||
{ objectId: session.id },
|
||||
{
|
||||
expiresAt: { __type: 'Date', iso: expiry.toISOString() },
|
||||
updatedAt: updatedAt.toISOString(),
|
||||
}
|
||||
);
|
||||
await session.fetch();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await session.fetch();
|
||||
expect(session.get('expiresAt') > expiry).toBeTrue();
|
||||
});
|
||||
|
||||
it('should load auth without a config', async () => {
|
||||
@@ -242,3 +260,24 @@ describe('Auth', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('extendSessionOnUse', () => {
|
||||
it(`shouldUpdateSessionExpiry()`, async () => {
|
||||
const { shouldUpdateSessionExpiry } = require('../lib/Auth');
|
||||
let update = new Date(Date.now() - 86410 * 1000);
|
||||
|
||||
const res = shouldUpdateSessionExpiry(
|
||||
{ sessionLength: 86460 },
|
||||
{ updatedAt: update }
|
||||
);
|
||||
|
||||
update = new Date(Date.now() - 43210 * 1000);
|
||||
const res2 = shouldUpdateSessionExpiry(
|
||||
{ sessionLength: 86460 },
|
||||
{ updatedAt: update }
|
||||
);
|
||||
|
||||
expect(res).toBe(true);
|
||||
expect(res2).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+392
-150
@@ -589,20 +589,6 @@ describe('AuthenticationProviders', function () {
|
||||
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', () => {
|
||||
@@ -1454,7 +1440,6 @@ describe('oauth2 auth adapter', () => {
|
||||
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 () => {
|
||||
@@ -1512,12 +1497,10 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
|
||||
const result = await apple.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
@@ -1529,11 +1512,9 @@ describe('apple signin auth adapter', () => {
|
||||
|
||||
it('should not verify invalid id_token', async () => {
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
|
||||
try {
|
||||
await apple.validateAuthData(
|
||||
@@ -1566,11 +1547,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await apple.validateAuthData(
|
||||
@@ -1588,11 +1567,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await apple.validateAuthData(
|
||||
@@ -1610,11 +1587,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await apple.validateAuthData(
|
||||
@@ -1630,11 +1605,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -1658,11 +1631,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -1687,11 +1658,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -1759,11 +1728,9 @@ describe('apple signin auth adapter', () => {
|
||||
sub: 'a_different_user_id',
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2025,7 +1992,6 @@ describe('microsoft graph auth adapter', () => {
|
||||
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
|
||||
@@ -2081,23 +2047,16 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('should use algorithm from key header to verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
@@ -2108,17 +2067,10 @@ describe('facebook limited auth adapter', () => {
|
||||
});
|
||||
|
||||
it('should not verify invalid id_token', async () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
@@ -2145,22 +2097,15 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('(using client id as string) should verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
@@ -2172,22 +2117,15 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('(using client id as array) should verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
@@ -2199,22 +2137,15 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('(using client id as array with multiple items) should verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
@@ -2229,17 +2160,10 @@ describe('facebook limited auth adapter', () => {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2250,7 +2174,7 @@ describe('facebook limited auth adapter', () => {
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
|
||||
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -2262,17 +2186,10 @@ describe('facebook limited auth adapter', () => {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2286,7 +2203,7 @@ describe('facebook limited auth adapter', () => {
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
|
||||
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -2296,17 +2213,10 @@ describe('facebook limited auth adapter', () => {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2320,7 +2230,7 @@ describe('facebook limited auth adapter', () => {
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
|
||||
'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -2378,21 +2288,14 @@ describe('facebook limited auth adapter', () => {
|
||||
|
||||
it('should throw error with with invalid user id', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
iss: 'https://www.facebook.com',
|
||||
aud: 'invalid_client_id',
|
||||
sub: 'a_different_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
|
||||
const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
|
||||
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
@@ -2406,3 +2309,342 @@ describe('facebook limited auth adapter', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('OTP TOTP auth adatper', () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mfa: {
|
||||
enabled: true,
|
||||
options: ['TOTP'],
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can enroll', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
const response = user.get('authDataResponse');
|
||||
expect(response.mfa).toBeDefined();
|
||||
expect(response.mfa.recovery).toBeDefined();
|
||||
expect(response.mfa.recovery.split(',').length).toEqual(2);
|
||||
await user.fetch();
|
||||
expect(user.get('authData').mfa).toEqual({ status: 'enabled' });
|
||||
});
|
||||
|
||||
it('can login with valid token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
const response = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: totp.generate(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).then(res => res.data);
|
||||
expect(response.objectId).toEqual(user.id);
|
||||
expect(response.sessionToken).toBeDefined();
|
||||
expect(response.authData).toEqual({ mfa: { status: 'enabled' } });
|
||||
expect(Object.keys(response).sort()).toEqual(
|
||||
[
|
||||
'objectId',
|
||||
'username',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'authData',
|
||||
'ACL',
|
||||
'sessionToken',
|
||||
'authDataResponse',
|
||||
].sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('can change OTP with valid token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
|
||||
const new_secret = new OTPAuth.Secret();
|
||||
const new_totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret: new_secret,
|
||||
});
|
||||
const new_token = new_totp.generate();
|
||||
await user.save(
|
||||
{
|
||||
authData: { mfa: { secret: new_secret.base32, token: new_token, old: totp.generate() } },
|
||||
},
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
await user.fetch({ useMasterKey: true });
|
||||
expect(user.get('authData').mfa.secret).toEqual(new_secret.base32);
|
||||
});
|
||||
|
||||
it('cannot change OTP with invalid token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
|
||||
const new_secret = new OTPAuth.Secret();
|
||||
const new_totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret: new_secret,
|
||||
});
|
||||
const new_token = new_totp.generate();
|
||||
await expectAsync(
|
||||
user.save(
|
||||
{
|
||||
authData: { mfa: { secret: new_secret.base32, token: new_token, old: '123' } },
|
||||
},
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
)
|
||||
).toBeRejectedWith(new Parse.Error(Parse.Error.OTHER_CAUSE, 'Invalid MFA token'));
|
||||
await user.fetch({ useMasterKey: true });
|
||||
expect(user.get('authData').mfa.secret).toEqual(secret.base32);
|
||||
});
|
||||
|
||||
it('future logins require TOTP token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
await expectAsync(Parse.User.logIn('username', 'password')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing additional authData mfa')
|
||||
);
|
||||
});
|
||||
|
||||
it('future logins reject incorrect TOTP token', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const OTPAuth = require('otpauth');
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret,
|
||||
});
|
||||
const token = totp.generate();
|
||||
await user.save(
|
||||
{ authData: { mfa: { secret: secret.base32, token } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
await expectAsync(
|
||||
request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: 'abcd',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).catch(e => {
|
||||
throw e.data;
|
||||
})
|
||||
).toBeRejectedWith({ code: Parse.Error.SCRIPT_FAILED, error: 'Invalid MFA token' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('OTP SMS auth adatper', () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
let code;
|
||||
let mobile;
|
||||
const mfa = {
|
||||
enabled: true,
|
||||
options: ['SMS'],
|
||||
sendSMS(smsCode, number) {
|
||||
expect(smsCode).toBeDefined();
|
||||
expect(number).toBeDefined();
|
||||
expect(smsCode.length).toEqual(6);
|
||||
code = smsCode;
|
||||
mobile = number;
|
||||
},
|
||||
digits: 6,
|
||||
period: 30,
|
||||
};
|
||||
beforeEach(async () => {
|
||||
code = '';
|
||||
mobile = '';
|
||||
await reconfigureServer({
|
||||
auth: {
|
||||
mfa,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can enroll', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const sessionToken = user.getSessionToken();
|
||||
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
|
||||
await user.save({ authData: { mfa: { mobile: '+11111111111' } } }, { sessionToken });
|
||||
await user.fetch({ sessionToken });
|
||||
expect(user.get('authData')).toEqual({ mfa: { status: 'disabled' } });
|
||||
expect(spy).toHaveBeenCalledWith(code, '+11111111111');
|
||||
await user.fetch({ useMasterKey: true });
|
||||
const authData = user.get('authData').mfa?.pending;
|
||||
expect(authData).toBeDefined();
|
||||
expect(authData['+11111111111']).toBeDefined();
|
||||
expect(Object.keys(authData['+11111111111'])).toEqual(['token', 'expiry']);
|
||||
|
||||
await user.save({ authData: { mfa: { mobile, token: code } } }, { sessionToken });
|
||||
await user.fetch({ sessionToken });
|
||||
expect(user.get('authData')).toEqual({ mfa: { status: 'enabled' } });
|
||||
});
|
||||
|
||||
it('future logins require SMS code', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
|
||||
await user.save(
|
||||
{ authData: { mfa: { mobile: '+11111111111' } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
|
||||
await user.save(
|
||||
{ authData: { mfa: { mobile, token: code } } },
|
||||
{ sessionToken: user.getSessionToken() }
|
||||
);
|
||||
|
||||
spy.calls.reset();
|
||||
|
||||
await expectAsync(Parse.User.logIn('username', 'password')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OTHER_CAUSE, 'Missing additional authData mfa')
|
||||
);
|
||||
const res = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: 'request',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).catch(e => e.data);
|
||||
expect(res).toEqual({ code: Parse.Error.SCRIPT_FAILED, error: 'Please enter the token' });
|
||||
expect(spy).toHaveBeenCalledWith(code, '+11111111111');
|
||||
const response = await request({
|
||||
headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
authData: {
|
||||
mfa: {
|
||||
token: code,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}).then(res => res.data);
|
||||
expect(response.objectId).toEqual(user.id);
|
||||
expect(response.sessionToken).toBeDefined();
|
||||
expect(response.authData).toEqual({ mfa: { status: 'enabled' } });
|
||||
expect(Object.keys(response).sort()).toEqual(
|
||||
[
|
||||
'objectId',
|
||||
'username',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'authData',
|
||||
'ACL',
|
||||
'sessionToken',
|
||||
'authDataResponse',
|
||||
].sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('partially enrolled users can still login', async () => {
|
||||
const user = await Parse.User.signUp('username', 'password');
|
||||
await user.save({ authData: { mfa: { mobile: '+11111111111' } } });
|
||||
const spy = spyOn(mfa, 'sendSMS').and.callThrough();
|
||||
await Parse.User.logIn('username', 'password');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,6 +59,19 @@ describe('Auth Adapter features', () => {
|
||||
validateLogin: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
const modernAdapter3 = {
|
||||
validateAppId: () => Promise.resolve(),
|
||||
validateSetUp: () => Promise.resolve(),
|
||||
validateUpdate: () => Promise.resolve(),
|
||||
validateLogin: () => Promise.resolve(),
|
||||
validateOptions: () => Promise.resolve(),
|
||||
afterFind() {
|
||||
return {
|
||||
foo: 'bar',
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const wrongAdapter = {
|
||||
validateAppId: () => Promise.resolve(),
|
||||
};
|
||||
@@ -332,6 +345,30 @@ describe('Auth Adapter features', () => {
|
||||
expect(user.getSessionToken()).toBeDefined();
|
||||
});
|
||||
|
||||
it('should strip out authData if required', async () => {
|
||||
const spy = spyOn(modernAdapter3, 'validateOptions').and.callThrough();
|
||||
const afterSpy = spyOn(modernAdapter3, 'afterFind').and.callThrough();
|
||||
await reconfigureServer({ auth: { modernAdapter3 } });
|
||||
const user = new Parse.User();
|
||||
await user.save({ authData: { modernAdapter3: { id: 'modernAdapter3Data' } } });
|
||||
await user.fetch({ sessionToken: user.getSessionToken() });
|
||||
const authData = user.get('authData').modernAdapter3;
|
||||
expect(authData).toEqual({ foo: 'bar' });
|
||||
for (const call of afterSpy.calls.all()) {
|
||||
const args = call.args[0];
|
||||
if (args.user) {
|
||||
user._objCount = args.user._objCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(afterSpy).toHaveBeenCalledWith(
|
||||
{ ip: '127.0.0.1', user, master: false },
|
||||
{ id: 'modernAdapter3Data' },
|
||||
undefined
|
||||
);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw if no triggers found', async () => {
|
||||
await reconfigureServer({ auth: { wrongAdapter } });
|
||||
const user = new Parse.User();
|
||||
@@ -450,6 +487,33 @@ describe('Auth Adapter features', () => {
|
||||
expect(baseAdapter2.validateAuthData).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should not perform authData validation twice when data mutated', async () => {
|
||||
spyOn(baseAdapter, 'validateAuthData').and.resolveTo({});
|
||||
await reconfigureServer({
|
||||
auth: { baseAdapter },
|
||||
allowExpiredAuthDataToken: false,
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
|
||||
await user.save({
|
||||
authData: {
|
||||
baseAdapter: { id: 'baseAdapter', token: "sometoken1" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(baseAdapter.validateAuthData).toHaveBeenCalledTimes(1);
|
||||
|
||||
const user2 = new Parse.User();
|
||||
await user2.save({
|
||||
authData: {
|
||||
baseAdapter: { id: 'baseAdapter', token: "sometoken2" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(baseAdapter.validateAuthData).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should require additional provider if configured', async () => {
|
||||
await reconfigureServer({
|
||||
auth: { baseAdapter, additionalAdapter },
|
||||
|
||||
+21
-2
@@ -74,7 +74,7 @@ describe('commander additions', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should load properly use args over env', done => {
|
||||
it('should load properly use args over env', () => {
|
||||
commander.loadDefinitions(testDefinitions);
|
||||
commander.parse(['node', './CLI.spec.js', '--arg0', 'arg0Value', '--arg4', ''], {
|
||||
PROGRAM_ARG_0: 'arg0ENVValue',
|
||||
@@ -86,7 +86,6 @@ describe('commander additions', () => {
|
||||
expect(commander.arg1).toEqual('arg1ENVValue');
|
||||
expect(commander.arg2).toEqual(4);
|
||||
expect(commander.arg4).toEqual('');
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail in action as port is invalid', done => {
|
||||
@@ -303,4 +302,24 @@ describe('execution', () => {
|
||||
done.fail(data.toString());
|
||||
});
|
||||
});
|
||||
|
||||
it('can start Parse Server with auth via CLI', done => {
|
||||
const env = { ...process.env };
|
||||
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
|
||||
childProcess = spawn(
|
||||
binPath,
|
||||
['--databaseURI', databaseURI, './spec/configs/CLIConfigAuth.json'],
|
||||
{ env }
|
||||
);
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
if (data.includes('parse-server running on')) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
childProcess.stderr.on('data', data => {
|
||||
data = data.toString();
|
||||
done.fail(data.toString());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+174
-68
@@ -95,13 +95,22 @@ describe('Cloud Code', () => {
|
||||
it('can get config', () => {
|
||||
const config = Parse.Server;
|
||||
let currentConfig = Config.get('test');
|
||||
expect(Object.keys(config)).toEqual(Object.keys(currentConfig));
|
||||
const server = require('../lib/cloud-code/Parse.Server');
|
||||
expect(Object.keys(config)).toEqual(Object.keys({ ...currentConfig, ...server }));
|
||||
config.silent = false;
|
||||
Parse.Server = config;
|
||||
currentConfig = Config.get('test');
|
||||
expect(currentConfig.silent).toBeFalse();
|
||||
});
|
||||
|
||||
it('can get curent version', () => {
|
||||
const version = require('../package.json').version;
|
||||
const currentConfig = Config.get('test');
|
||||
expect(Parse.Server.version).toBeDefined();
|
||||
expect(currentConfig.version).toBeDefined();
|
||||
expect(Parse.Server.version).toEqual(version);
|
||||
});
|
||||
|
||||
it('show warning on duplicate cloud functions', done => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'warn').and.callFake(() => {});
|
||||
@@ -1352,6 +1361,48 @@ describe('Cloud Code', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not encode Parse Objects', async () => {
|
||||
await reconfigureServer({ encodeParseObjectInCloudFunction: false });
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username');
|
||||
user.setPassword('password');
|
||||
user.set('deleted', false);
|
||||
await user.signUp();
|
||||
Parse.Cloud.define(
|
||||
'deleteAccount',
|
||||
async req => {
|
||||
expect(req.params.object instanceof Parse.Object).not.toBeTrue();
|
||||
return 'Object deleted';
|
||||
},
|
||||
{
|
||||
requireMaster: true,
|
||||
}
|
||||
);
|
||||
await Parse.Cloud.run('deleteAccount', { object: user.toPointer() }, { useMasterKey: true });
|
||||
});
|
||||
|
||||
it('allow cloud to encode Parse Objects', async () => {
|
||||
await reconfigureServer({ encodeParseObjectInCloudFunction: true });
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username');
|
||||
user.setPassword('password');
|
||||
user.set('deleted', false);
|
||||
await user.signUp();
|
||||
Parse.Cloud.define(
|
||||
'deleteAccount',
|
||||
async req => {
|
||||
expect(req.params.object instanceof Parse.Object).toBeTrue();
|
||||
req.params.object.set('deleted', true);
|
||||
await req.params.object.save(null, { useMasterKey: true });
|
||||
return 'Object deleted';
|
||||
},
|
||||
{
|
||||
requireMaster: true,
|
||||
}
|
||||
);
|
||||
await Parse.Cloud.run('deleteAccount', { object: user.toPointer() }, { useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeSave should not affect fetched pointers', done => {
|
||||
Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => {});
|
||||
|
||||
@@ -2348,6 +2399,56 @@ describe('beforeFind hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sets correct beforeFind trigger isGet parameter for Parse.Object.fetch request', async () => {
|
||||
const hook = {
|
||||
method: req => {
|
||||
expect(req.isGet).toEqual(true);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
spyOn(hook, 'method').and.callThrough();
|
||||
Parse.Cloud.beforeFind('MyObject', hook.method);
|
||||
const obj = new Parse.Object('MyObject');
|
||||
await obj.save();
|
||||
const getObj = await obj.fetch();
|
||||
expect(getObj).toBeInstanceOf(Parse.Object);
|
||||
expect(hook.method).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('sets correct beforeFind trigger isGet parameter for Parse.Query.get request', async () => {
|
||||
const hook = {
|
||||
method: req => {
|
||||
expect(req.isGet).toEqual(false);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
spyOn(hook, 'method').and.callThrough();
|
||||
Parse.Cloud.beforeFind('MyObject', hook.method);
|
||||
const obj = new Parse.Object('MyObject');
|
||||
await obj.save();
|
||||
const query = new Parse.Query('MyObject');
|
||||
const getObj = await query.get(obj.id);
|
||||
expect(getObj).toBeInstanceOf(Parse.Object);
|
||||
expect(hook.method).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('sets correct beforeFind trigger isGet parameter for Parse.Query.find request', async () => {
|
||||
const hook = {
|
||||
method: req => {
|
||||
expect(req.isGet).toEqual(false);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
spyOn(hook, 'method').and.callThrough();
|
||||
Parse.Cloud.beforeFind('MyObject', hook.method);
|
||||
const obj = new Parse.Object('MyObject');
|
||||
await obj.save();
|
||||
const query = new Parse.Query('MyObject');
|
||||
const findObjs = await query.find();
|
||||
expect(findObjs?.[0]).toBeInstanceOf(Parse.Object);
|
||||
expect(hook.method).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should have request headers', done => {
|
||||
Parse.Cloud.beforeFind('MyObject', req => {
|
||||
expect(req.headers).toBeDefined();
|
||||
@@ -2381,6 +2482,60 @@ 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);
|
||||
});
|
||||
|
||||
it('should have access to context in include query in beforeFind hook', async () => {
|
||||
let beforeFindTestObjectCalled = false;
|
||||
let beforeFindTestObject2Called = false;
|
||||
const obj1 = new Parse.Object('TestObject');
|
||||
const obj2 = new Parse.Object('TestObject2');
|
||||
obj2.set('aField', 'aFieldValue');
|
||||
await obj2.save();
|
||||
obj1.set('pointerField', obj2);
|
||||
await obj1.save();
|
||||
Parse.Cloud.beforeFind('TestObject', req => {
|
||||
expect(req.context).toBeDefined();
|
||||
expect(req.context.a).toEqual('a');
|
||||
beforeFindTestObjectCalled = true;
|
||||
});
|
||||
Parse.Cloud.beforeFind('TestObject2', req => {
|
||||
expect(req.context).toBeDefined();
|
||||
expect(req.context.a).toEqual('a');
|
||||
beforeFindTestObject2Called = true;
|
||||
});
|
||||
const query = new Parse.Query('TestObject');
|
||||
await query.include('pointerField').find({ context: { a: 'a' } });
|
||||
expect(beforeFindTestObjectCalled).toBeTrue();
|
||||
expect(beforeFindTestObject2Called).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('afterFind hooks', () => {
|
||||
@@ -3198,7 +3353,7 @@ describe('beforeLogin hook', () => {
|
||||
expect(req.headers).toBeDefined();
|
||||
expect(req.ip).toBeDefined();
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.context).toBeUndefined();
|
||||
expect(req.context).toBeDefined();
|
||||
});
|
||||
|
||||
await Parse.User.signUp('tupac', 'shakur');
|
||||
@@ -3315,7 +3470,7 @@ describe('afterLogin hook', () => {
|
||||
expect(req.headers).toBeDefined();
|
||||
expect(req.ip).toBeDefined();
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.context).toBeUndefined();
|
||||
expect(req.context).toBeDefined();
|
||||
});
|
||||
|
||||
await Parse.User.signUp('testuser', 'p@ssword');
|
||||
@@ -3481,7 +3636,7 @@ describe('afterLogin hook', () => {
|
||||
});
|
||||
|
||||
describe('saveFile hooks', () => {
|
||||
it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => {
|
||||
it('beforeSave(Parse.File) 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.beforeSave(Parse.File, () => {
|
||||
@@ -3497,7 +3652,7 @@ describe('saveFile hooks', () => {
|
||||
expect(createFileSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('beforeSaveFile should throw error', async () => {
|
||||
it('beforeSave(Parse.File) should throw error', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw new Parse.Error(400, 'some-error-message');
|
||||
@@ -3510,7 +3665,7 @@ describe('saveFile hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('beforeSaveFile should change values of uploaded file by editing fileObject directly', async () => {
|
||||
it('beforeSave(Parse.File) should change values of uploaded file by editing fileObject directly', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
@@ -3539,7 +3694,7 @@ describe('saveFile hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('beforeSaveFile should change values by returning new fileObject', async () => {
|
||||
it('beforeSave(Parse.File) should change values by returning new fileObject', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
@@ -3573,7 +3728,7 @@ describe('saveFile hooks', () => {
|
||||
expect(file._name.indexOf(expectedFileName)).toBe(file._name.length - expectedFileName.length);
|
||||
});
|
||||
|
||||
it('beforeSaveFile should contain metadata and tags saved from client', async () => {
|
||||
it('beforeSave(Parse.File) should contain metadata and tags saved from client', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
@@ -3601,7 +3756,7 @@ describe('saveFile hooks', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('beforeSaveFile should return same file data with new file name', async () => {
|
||||
it('beforeSave(Parse.File) should return same file data with new file name', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
const config = Config.get('test');
|
||||
config.filesController.options.preserveFileName = true;
|
||||
@@ -3616,7 +3771,7 @@ describe('saveFile hooks', () => {
|
||||
expect(result.name()).toBe('2020-04-01.txt');
|
||||
});
|
||||
|
||||
it('afterSaveFile should set fileSize to null if beforeSave returns an already saved file', async () => {
|
||||
it('afterSave(Parse.File) 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.beforeSave(Parse.File, req => {
|
||||
@@ -3636,7 +3791,7 @@ describe('saveFile hooks', () => {
|
||||
expect(createFileSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('afterSaveFile should throw error', async () => {
|
||||
it('afterSave(Parse.File) should throw error', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.afterSave(Parse.File, async () => {
|
||||
throw new Parse.Error(400, 'some-error-message');
|
||||
@@ -3650,7 +3805,7 @@ describe('saveFile hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('afterSaveFile should call with fileObject', async done => {
|
||||
it('afterSave(Parse.File) should call with fileObject', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
req.file.setTags({ tagA: 'some-tag' });
|
||||
@@ -3666,7 +3821,7 @@ describe('saveFile hooks', () => {
|
||||
await file.save({ useMasterKey: true });
|
||||
});
|
||||
|
||||
it('afterSaveFile should change fileSize when file data changes', async done => {
|
||||
it('afterSave(Parse.File) should change fileSize when file data changes', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSave(Parse.File, async req => {
|
||||
expect(req.fileSize).toBe(3);
|
||||
@@ -3683,7 +3838,7 @@ describe('saveFile hooks', () => {
|
||||
await file.save({ useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeDeleteFile should call with fileObject', async () => {
|
||||
it('beforeDelete(Parse.File) should call with fileObject', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeDelete(Parse.File, req => {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
@@ -3695,7 +3850,7 @@ describe('saveFile hooks', () => {
|
||||
await file.destroy({ useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeDeleteFile should throw error', async done => {
|
||||
it('beforeDelete(Parse.File) should throw error', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeDelete(Parse.File, () => {
|
||||
throw new Error('some error message');
|
||||
@@ -3709,7 +3864,7 @@ describe('saveFile hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('afterDeleteFile should call with fileObject', async done => {
|
||||
it('afterDelete(Parse.File) should call with fileObject', async done => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeDelete(Parse.File, req => {
|
||||
expect(req.file).toBeInstanceOf(Parse.File);
|
||||
@@ -3726,7 +3881,7 @@ describe('saveFile hooks', () => {
|
||||
await file.destroy({ useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeSaveFile should not change file if nothing is returned', async () => {
|
||||
it('beforeSave(Parse.File) should not change file if nothing is returned', async () => {
|
||||
await reconfigureServer({ filesAdapter: mockAdapter });
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
return;
|
||||
@@ -3736,7 +3891,7 @@ describe('saveFile hooks', () => {
|
||||
expect(result).toBe(file);
|
||||
});
|
||||
|
||||
it('throw custom error from beforeSaveFile', async done => {
|
||||
it('throw custom error from beforeSave(Parse.File) ', async done => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail');
|
||||
});
|
||||
@@ -3750,7 +3905,7 @@ describe('saveFile hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('throw empty error from beforeSaveFile', async done => {
|
||||
it('throw empty error from beforeSave(Parse.File)', async done => {
|
||||
Parse.Cloud.beforeSave(Parse.File, () => {
|
||||
throw null;
|
||||
});
|
||||
@@ -3763,55 +3918,6 @@ 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', () => {
|
||||
|
||||
@@ -15,6 +15,13 @@ describe('Cloud Code Logger', () => {
|
||||
// useful to flip to false for fine tuning :).
|
||||
silent: true,
|
||||
logLevel: undefined,
|
||||
logLevels: {
|
||||
cloudFunctionError: 'error',
|
||||
cloudFunctionSuccess: 'info',
|
||||
triggerAfter: 'info',
|
||||
triggerBeforeError: 'error',
|
||||
triggerBeforeSuccess: 'info',
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.User.signUp('tester', 'abc')
|
||||
@@ -182,6 +189,42 @@ describe('Cloud Code Logger', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should log cloud function execution using the custom log level', async done => {
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
return 'it worked!';
|
||||
});
|
||||
|
||||
Parse.Cloud.define('bFunction', () => {
|
||||
throw new Error('Failed');
|
||||
});
|
||||
|
||||
await Parse.Cloud.run('aFunction', { foo: 'bar' }).then(() => {
|
||||
const log = spy.calls.allArgs().find(log => log[1].startsWith('Ran cloud function '))?.[0];
|
||||
expect(log).toEqual('info');
|
||||
});
|
||||
|
||||
await reconfigureServer({
|
||||
silent: true,
|
||||
logLevels: {
|
||||
cloudFunctionSuccess: 'warn',
|
||||
cloudFunctionError: 'info',
|
||||
},
|
||||
});
|
||||
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
|
||||
try {
|
||||
await Parse.Cloud.run('bFunction', { foo: 'bar' });
|
||||
throw new Error('bFunction should have failed');
|
||||
} catch {
|
||||
const log = spy.calls
|
||||
.allArgs()
|
||||
.find(log => log[1].startsWith('Failed running cloud function bFunction for '))?.[0];
|
||||
expect(log).toEqual('info');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('should log cloud function triggers using the custom log level', async () => {
|
||||
Parse.Cloud.beforeSave('TestClass', () => {});
|
||||
Parse.Cloud.afterSave('TestClass', () => {});
|
||||
@@ -298,4 +341,53 @@ describe('Cloud Code Logger', () => {
|
||||
expect(args[0]).toBe('Parse error: ');
|
||||
expect(args[1].message).toBe('Object not found.');
|
||||
});
|
||||
|
||||
it('should log cloud function execution using the silent log level', async () => {
|
||||
await reconfigureServer({
|
||||
logLevels: {
|
||||
cloudFunctionSuccess: 'silent',
|
||||
cloudFunctionError: 'silent',
|
||||
},
|
||||
});
|
||||
Parse.Cloud.define('aFunction', () => {
|
||||
return 'it worked!';
|
||||
});
|
||||
Parse.Cloud.define('bFunction', () => {
|
||||
throw new Error('Failed');
|
||||
});
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
|
||||
await Parse.Cloud.run('aFunction', { foo: 'bar' });
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
await expectAsync(Parse.Cloud.run('bFunction', { foo: 'bar' })).toBeRejected();
|
||||
// Not "Failed running cloud function message..."
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should log cloud function triggers using the silent log level', async () => {
|
||||
await reconfigureServer({
|
||||
logLevels: {
|
||||
triggerAfter: 'silent',
|
||||
triggerBeforeSuccess: 'silent',
|
||||
triggerBeforeError: 'silent',
|
||||
},
|
||||
});
|
||||
Parse.Cloud.beforeSave('TestClassError', () => {
|
||||
throw new Error('Failed');
|
||||
});
|
||||
Parse.Cloud.beforeSave('TestClass', () => {});
|
||||
Parse.Cloud.afterSave('TestClass', () => {});
|
||||
|
||||
spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough();
|
||||
|
||||
const obj = new Parse.Object('TestClass');
|
||||
await obj.save();
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
const objError = new Parse.Object('TestClassError');
|
||||
await expectAsync(objError.save()).toBeRejected();
|
||||
// Not "beforeSave failed for TestClassError for user ..."
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const Config = require('../lib/Config');
|
||||
const DatabaseController = require('../lib/Controllers/DatabaseController.js');
|
||||
const validateQuery = DatabaseController._validateQuery;
|
||||
|
||||
@@ -361,6 +362,259 @@ describe('DatabaseController', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('enableCollationCaseComparison', () => {
|
||||
const dummyStorageAdapter = {
|
||||
find: () => Promise.resolve([]),
|
||||
watch: () => Promise.resolve(),
|
||||
getAllClasses: () => Promise.resolve([]),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Config.get(Parse.applicationId).schemaCache.clear();
|
||||
});
|
||||
|
||||
it('should force caseInsensitive to false with enableCollationCaseComparison option', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
enableCollationCaseComparison: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'find');
|
||||
spy.and.callThrough();
|
||||
await databaseController.find('SomeClass', {}, { caseInsensitive: true });
|
||||
expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(false);
|
||||
});
|
||||
|
||||
it('should support caseInsensitive without enableCollationCaseComparison option', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'find');
|
||||
spy.and.callThrough();
|
||||
await databaseController.find('_User', {}, { caseInsensitive: true });
|
||||
expect(spy.calls.all()[0].args[3].caseInsensitive).toEqual(true);
|
||||
});
|
||||
|
||||
it_only_db('mongo')(
|
||||
'should create insensitive indexes without enableCollationCaseComparison',
|
||||
async () => {
|
||||
await reconfigureServer({
|
||||
databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonFalse',
|
||||
databaseAdapter: undefined,
|
||||
});
|
||||
const user = new Parse.User();
|
||||
await user.save({
|
||||
username: 'example',
|
||||
password: 'password',
|
||||
email: 'example@example.com',
|
||||
});
|
||||
const schemas = await Parse.Schema.all();
|
||||
const UserSchema = schemas.find(({ className }) => className === '_User');
|
||||
expect(UserSchema.indexes).toEqual({
|
||||
_id_: { _id: 1 },
|
||||
username_1: { username: 1 },
|
||||
case_insensitive_username: { username: 1 },
|
||||
case_insensitive_email: { email: 1 },
|
||||
email_1: { email: 1 },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it_only_db('mongo')(
|
||||
'should not create insensitive indexes with enableCollationCaseComparison',
|
||||
async () => {
|
||||
await reconfigureServer({
|
||||
enableCollationCaseComparison: true,
|
||||
databaseURI: 'mongodb://localhost:27017/enableCollationCaseComparisonTrue',
|
||||
databaseAdapter: undefined,
|
||||
});
|
||||
const user = new Parse.User();
|
||||
await user.save({
|
||||
username: 'example',
|
||||
password: 'password',
|
||||
email: 'example@example.com',
|
||||
});
|
||||
const schemas = await Parse.Schema.all();
|
||||
const UserSchema = schemas.find(({ className }) => className === '_User');
|
||||
expect(UserSchema.indexes).toEqual({
|
||||
_id_: { _id: 1 },
|
||||
username_1: { username: 1 },
|
||||
email_1: { email: 1 },
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('convertEmailToLowercase', () => {
|
||||
const dummyStorageAdapter = {
|
||||
createObject: () => Promise.resolve({ ops: [{}] }),
|
||||
findOneAndUpdate: () => Promise.resolve({}),
|
||||
watch: () => Promise.resolve(),
|
||||
getAllClasses: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
className: '_User',
|
||||
fields: { email: 'String' },
|
||||
indexes: {},
|
||||
classLevelPermissions: { protectedFields: {} },
|
||||
},
|
||||
]),
|
||||
};
|
||||
const dates = {
|
||||
createdAt: { iso: undefined, __type: 'Date' },
|
||||
updatedAt: { iso: undefined, __type: 'Date' },
|
||||
};
|
||||
|
||||
it('should not transform email to lower case without convertEmailToLowercase option on create', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'createObject');
|
||||
spy.and.callThrough();
|
||||
await databaseController.create('_User', {
|
||||
email: 'EXAMPLE@EXAMPLE.COM',
|
||||
});
|
||||
expect(spy.calls.all()[0].args[2]).toEqual({
|
||||
email: 'EXAMPLE@EXAMPLE.COM',
|
||||
...dates,
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform email to lower case with convertEmailToLowercase option on create', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
convertEmailToLowercase: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'createObject');
|
||||
spy.and.callThrough();
|
||||
await databaseController.create('_User', {
|
||||
email: 'EXAMPLE@EXAMPLE.COM',
|
||||
});
|
||||
expect(spy.calls.all()[0].args[2]).toEqual({
|
||||
email: 'example@example.com',
|
||||
...dates,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not transform email to lower case without convertEmailToLowercase option on update', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
|
||||
spy.and.callThrough();
|
||||
await databaseController.update('_User', { id: 'example' }, { email: 'EXAMPLE@EXAMPLE.COM' });
|
||||
expect(spy.calls.all()[0].args[3]).toEqual({
|
||||
email: 'EXAMPLE@EXAMPLE.COM',
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform email to lower case with convertEmailToLowercase option on update', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
convertEmailToLowercase: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
|
||||
spy.and.callThrough();
|
||||
await databaseController.update('_User', { id: 'example' }, { email: 'EXAMPLE@EXAMPLE.COM' });
|
||||
expect(spy.calls.all()[0].args[3]).toEqual({
|
||||
email: 'example@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not find a case insensitive user by email with convertEmailToLowercase', async () => {
|
||||
await reconfigureServer({ convertEmailToLowercase: true });
|
||||
const user = new Parse.User();
|
||||
await user.save({ username: 'EXAMPLE', email: 'EXAMPLE@EXAMPLE.COM', password: 'password' });
|
||||
|
||||
const query = new Parse.Query(Parse.User);
|
||||
query.equalTo('email', 'EXAMPLE@EXAMPLE.COM');
|
||||
const result = await query.find({ useMasterKey: true });
|
||||
expect(result.length).toEqual(0);
|
||||
|
||||
const query2 = new Parse.Query(Parse.User);
|
||||
query2.equalTo('email', 'example@example.com');
|
||||
const result2 = await query2.find({ useMasterKey: true });
|
||||
expect(result2.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertUsernameToLowercase', () => {
|
||||
const dummyStorageAdapter = {
|
||||
createObject: () => Promise.resolve({ ops: [{}] }),
|
||||
findOneAndUpdate: () => Promise.resolve({}),
|
||||
watch: () => Promise.resolve(),
|
||||
getAllClasses: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
className: '_User',
|
||||
fields: { username: 'String' },
|
||||
indexes: {},
|
||||
classLevelPermissions: { protectedFields: {} },
|
||||
},
|
||||
]),
|
||||
};
|
||||
const dates = {
|
||||
createdAt: { iso: undefined, __type: 'Date' },
|
||||
updatedAt: { iso: undefined, __type: 'Date' },
|
||||
};
|
||||
|
||||
it('should not transform username to lower case without convertUsernameToLowercase option on create', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'createObject');
|
||||
spy.and.callThrough();
|
||||
await databaseController.create('_User', {
|
||||
username: 'EXAMPLE',
|
||||
});
|
||||
expect(spy.calls.all()[0].args[2]).toEqual({
|
||||
username: 'EXAMPLE',
|
||||
...dates,
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform username to lower case with convertUsernameToLowercase option on create', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
convertUsernameToLowercase: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'createObject');
|
||||
spy.and.callThrough();
|
||||
await databaseController.create('_User', {
|
||||
username: 'EXAMPLE',
|
||||
});
|
||||
expect(spy.calls.all()[0].args[2]).toEqual({
|
||||
username: 'example',
|
||||
...dates,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not transform username to lower case without convertUsernameToLowercase option on update', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {});
|
||||
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
|
||||
spy.and.callThrough();
|
||||
await databaseController.update('_User', { id: 'example' }, { username: 'EXAMPLE' });
|
||||
expect(spy.calls.all()[0].args[3]).toEqual({
|
||||
username: 'EXAMPLE',
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform username to lower case with convertUsernameToLowercase option on update', async () => {
|
||||
const databaseController = new DatabaseController(dummyStorageAdapter, {
|
||||
convertUsernameToLowercase: true,
|
||||
});
|
||||
const spy = spyOn(dummyStorageAdapter, 'findOneAndUpdate');
|
||||
spy.and.callThrough();
|
||||
await databaseController.update('_User', { id: 'example' }, { username: 'EXAMPLE' });
|
||||
expect(spy.calls.all()[0].args[3]).toEqual({
|
||||
username: 'example',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not find a case insensitive user by username with convertUsernameToLowercase', async () => {
|
||||
await reconfigureServer({ convertUsernameToLowercase: true });
|
||||
const user = new Parse.User();
|
||||
await user.save({ username: 'EXAMPLE', password: 'password' });
|
||||
|
||||
const query = new Parse.Query(Parse.User);
|
||||
query.equalTo('username', 'EXAMPLE');
|
||||
const result = await query.find({ useMasterKey: true });
|
||||
expect(result.length).toEqual(0);
|
||||
|
||||
const query2 = new Parse.Query(Parse.User);
|
||||
query2.equalTo('username', 'example');
|
||||
const result2 = await query2.find({ useMasterKey: true });
|
||||
expect(result2.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildCLP(pointerNames) {
|
||||
|
||||
@@ -554,7 +554,7 @@ describe('DefinedSchemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not delete automatically classes', async () => {
|
||||
it('should not delete classes automatically', async () => {
|
||||
await reconfigureServer({
|
||||
schema: { definitions: [{ className: '_User' }, { className: 'Test' }] },
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const Auth = require('../lib/Auth');
|
||||
const Config = require('../lib/Config');
|
||||
const request = require('../lib/request');
|
||||
const MockEmailAdapterWithOptions = require('./support/MockEmailAdapterWithOptions');
|
||||
|
||||
describe('Email Verification Token Expiration: ', () => {
|
||||
it('show the invalid verification link page, if the user clicks on the verify email link after the email verify token expires', done => {
|
||||
@@ -126,6 +127,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -167,6 +169,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -214,6 +217,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -288,6 +292,223 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can conditionally send emails', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
const verifyUserEmails = {
|
||||
method(req) {
|
||||
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
|
||||
return false;
|
||||
},
|
||||
};
|
||||
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: verifyUserEmails.method,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
const beforeSave = {
|
||||
method(req) {
|
||||
req.object.set('emailVerified', true);
|
||||
},
|
||||
};
|
||||
const saveSpy = spyOn(beforeSave, 'method').and.callThrough();
|
||||
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
|
||||
Parse.Cloud.beforeSave(Parse.User, beforeSave.method);
|
||||
const user = new Parse.User();
|
||||
user.setUsername('sets_email_verify_token_expires_at');
|
||||
user.setPassword('expiringToken');
|
||||
user.set('email', 'user@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const config = Config.get('test');
|
||||
const results = await config.database.find(
|
||||
'_User',
|
||||
{
|
||||
username: 'sets_email_verify_token_expires_at',
|
||||
},
|
||||
{},
|
||||
Auth.maintenance(config)
|
||||
);
|
||||
|
||||
expect(results.length).toBe(1);
|
||||
const user_data = results[0];
|
||||
expect(typeof user_data).toBe('object');
|
||||
expect(user_data.emailVerified).toEqual(true);
|
||||
expect(user_data._email_verify_token).toBeUndefined();
|
||||
expect(user_data._email_verify_token_expires_at).toBeUndefined();
|
||||
expect(emailSpy).not.toHaveBeenCalled();
|
||||
expect(saveSpy).toHaveBeenCalled();
|
||||
expect(sendEmailOptions).toBeUndefined();
|
||||
expect(verifySpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can conditionally send emails and allow conditional login', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
const verifyUserEmails = {
|
||||
method(req) {
|
||||
expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']);
|
||||
if (req.object.get('username') === 'no_email') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: verifyUserEmails.method,
|
||||
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
const user = new Parse.User();
|
||||
user.setUsername('no_email');
|
||||
user.setPassword('expiringToken');
|
||||
user.set('email', 'user@example.com');
|
||||
await user.signUp();
|
||||
expect(sendEmailOptions).toBeUndefined();
|
||||
expect(user.getSessionToken()).toBeDefined();
|
||||
expect(verifySpy).toHaveBeenCalledTimes(2);
|
||||
const user2 = new Parse.User();
|
||||
user2.setUsername('email');
|
||||
user2.setPassword('expiringToken');
|
||||
user2.set('email', 'user2@example.com');
|
||||
await user2.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(user2.getSessionToken()).toBeUndefined();
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
expect(verifySpy).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it('can conditionally send user email verification', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
const sendVerificationEmail = {
|
||||
method(req) {
|
||||
expect(req.user).toBeDefined();
|
||||
expect(req.master).toBeDefined();
|
||||
return false;
|
||||
},
|
||||
};
|
||||
const sendSpy = spyOn(sendVerificationEmail, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
sendUserEmailVerification: sendVerificationEmail.method,
|
||||
});
|
||||
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
|
||||
const newUser = new Parse.User();
|
||||
newUser.setUsername('unsets_email_verify_token_expires_at');
|
||||
newUser.setPassword('expiringToken');
|
||||
newUser.set('email', 'user@example.com');
|
||||
await newUser.signUp();
|
||||
await Parse.User.requestEmailVerification('user@example.com');
|
||||
await jasmine.timeout();
|
||||
expect(sendSpy).toHaveBeenCalledTimes(2);
|
||||
expect(emailSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('provides full user object in email verification function on email and username change', async () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
const sendVerificationEmail = {
|
||||
method(req) {
|
||||
expect(req.user).toBeDefined();
|
||||
expect(req.user.id).toBeDefined();
|
||||
expect(req.user.get('createdAt')).toBeDefined();
|
||||
expect(req.user.get('updatedAt')).toBeDefined();
|
||||
expect(req.master).toBeDefined();
|
||||
return false;
|
||||
},
|
||||
};
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
sendUserEmailVerification: sendVerificationEmail.method,
|
||||
});
|
||||
const user = new Parse.User();
|
||||
user.setPassword('password');
|
||||
user.setUsername('new@example.com');
|
||||
user.setEmail('user@example.com');
|
||||
await user.save(null, { useMasterKey: true });
|
||||
|
||||
// Update email and username
|
||||
user.setUsername('new@example.com');
|
||||
user.setEmail('new@example.com');
|
||||
await user.save(null, { useMasterKey: true });
|
||||
});
|
||||
|
||||
it('beforeSave options do not change existing behaviour', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough();
|
||||
const newUser = new Parse.User();
|
||||
newUser.setUsername('unsets_email_verify_token_expires_at');
|
||||
newUser.setPassword('expiringToken');
|
||||
newUser.set('email', 'user@parse.com');
|
||||
await newUser.signUp();
|
||||
await jasmine.timeout();
|
||||
const response = await request({
|
||||
url: sendEmailOptions.link,
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(response.status).toEqual(302);
|
||||
const config = Config.get('test');
|
||||
const results = await config.database.find('_User', {
|
||||
username: 'unsets_email_verify_token_expires_at',
|
||||
});
|
||||
|
||||
expect(results.length).toBe(1);
|
||||
const user = results[0];
|
||||
expect(typeof user).toBe('object');
|
||||
expect(user.emailVerified).toEqual(true);
|
||||
expect(typeof user._email_verify_token).toBe('undefined');
|
||||
expect(typeof user._email_verify_token_expires_at).toBe('undefined');
|
||||
expect(emailSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('unsets the _email_verify_token_expires_at and _email_verify_token fields in the User class if email verification is successful', done => {
|
||||
const user = new Parse.User();
|
||||
let sendEmailOptions;
|
||||
@@ -311,6 +532,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -370,6 +592,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
return request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -587,6 +810,9 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toBe(200);
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
expect(sendVerificationEmailCallCount).toBe(2);
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
|
||||
@@ -616,6 +842,41 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('provides function arguments in verifyUserEmails on verificationEmailRequest', async () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('pass');
|
||||
user.set('email', 'test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const verifyUserEmails = {
|
||||
method: async (params) => {
|
||||
expect(params.object).toBeInstanceOf(Parse.User);
|
||||
expect(params.ip).toBeDefined();
|
||||
expect(params.master).toBeDefined();
|
||||
expect(params.installationId).toBeDefined();
|
||||
expect(params.resendRequest).toBeTrue();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const verifyUserEmailsSpy = spyOn(verifyUserEmails, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
verifyUserEmails: verifyUserEmails.method,
|
||||
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
|
||||
preventSignupWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
await expectAsync(Parse.User.requestEmailVerification('test@example.com')).toBeResolved();
|
||||
expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw with invalid emailVerifyTokenReuseIfValid', async done => {
|
||||
const sendEmailOptions = [];
|
||||
const emailAdapter = {
|
||||
@@ -683,7 +944,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
const config = Config.get('test');
|
||||
const [userBeforeRequest] = await config.database.find('_User', {
|
||||
username: 'resends_verification_token',
|
||||
});
|
||||
}, {}, Auth.maintenance(config));
|
||||
// store this user before we make our email request
|
||||
expect(sendVerificationEmailCallCount).toBe(1);
|
||||
await new Promise(resolve => {
|
||||
@@ -703,20 +964,21 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
await jasmine.timeout();
|
||||
expect(response.status).toBe(200);
|
||||
expect(sendVerificationEmailCallCount).toBe(2);
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
|
||||
const [userAfterRequest] = await config.database.find('_User', {
|
||||
username: 'resends_verification_token',
|
||||
});
|
||||
}, {}, Auth.maintenance(config));
|
||||
|
||||
// verify that our token & expiration has been changed for this new request
|
||||
// Verify that token & expiration haven't been changed for this new request
|
||||
expect(typeof userAfterRequest).toBe('object');
|
||||
expect(userBeforeRequest._email_verify_token).toBeDefined();
|
||||
expect(userBeforeRequest._email_verify_token).toEqual(userAfterRequest._email_verify_token);
|
||||
expect(userBeforeRequest._email_verify_token_expires_at).toEqual(
|
||||
userAfterRequest._email_verify_token_expires_at
|
||||
);
|
||||
expect(userBeforeRequest._email_verify_token_expires_at).toBeDefined();
|
||||
expect(userBeforeRequest._email_verify_token_expires_at).toEqual(userAfterRequest._email_verify_token_expires_at);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -745,6 +1007,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
return request({
|
||||
url: sendEmailOptions.link,
|
||||
@@ -983,6 +1246,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
|
||||
@@ -55,6 +55,8 @@ describe('FilesController', () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
expect(config.database.adapter._mongoOptions.retryWrites).toBeTrue();
|
||||
expect(config.filesController.adapter._mongoOptions.retryWrites).toBeTrue();
|
||||
expect(config.filesController.adapter._mongoOptions.enableSchemaHooks).toBeUndefined();
|
||||
expect(config.filesController.adapter._mongoOptions.schemaCacheTtl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create a server log on failure', done => {
|
||||
|
||||
@@ -20,6 +20,22 @@ describe_only_db('mongo')('GridFSBucket', () => {
|
||||
await db.dropDatabase();
|
||||
});
|
||||
|
||||
it('should connect to mongo with the supported database options', async () => {
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI, {
|
||||
retryWrites: true,
|
||||
// these are not supported by the mongo client
|
||||
enableSchemaHooks: true,
|
||||
schemaCacheTtl: 5000,
|
||||
maxTimeMS: 30000,
|
||||
});
|
||||
|
||||
const db = await gfsAdapter._connect();
|
||||
const status = await db.admin().serverStatus();
|
||||
expect(status.connections.current > 0).toEqual(true);
|
||||
expect(db.options?.retryWrites).toEqual(true);
|
||||
});
|
||||
|
||||
it('should save an encrypted file that can only be decrypted by a GridFS adapter with the encryptionKey', async () => {
|
||||
const unencryptedAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
const encryptedAdapter = new GridFSBucketAdapter(
|
||||
|
||||
@@ -45,10 +45,6 @@ describe('Idempotency', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
|
||||
});
|
||||
|
||||
// Tests
|
||||
it('should enforce idempotency for cloud code function', async () => {
|
||||
let counter = 0;
|
||||
|
||||
+159
-35
@@ -1,10 +1,19 @@
|
||||
const middlewares = require('../lib/middlewares');
|
||||
const AppCache = require('../lib/cache').AppCache;
|
||||
const { BlockList } = require('net');
|
||||
|
||||
const AppCachePut = (appId, config) =>
|
||||
AppCache.put(appId, {
|
||||
...config,
|
||||
maintenanceKeyIpsStore: new Map(),
|
||||
masterKeyIpsStore: new Map(),
|
||||
});
|
||||
|
||||
describe('middlewares', () => {
|
||||
let fakeReq, fakeRes;
|
||||
beforeEach(() => {
|
||||
fakeReq = {
|
||||
ip: '127.0.0.1',
|
||||
originalUrl: 'http://example.com/parse/',
|
||||
url: 'http://example.com/',
|
||||
body: {
|
||||
@@ -16,7 +25,7 @@ describe('middlewares', () => {
|
||||
},
|
||||
};
|
||||
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status']);
|
||||
AppCache.put(fakeReq.body._ApplicationId, {});
|
||||
AppCachePut(fakeReq.body._ApplicationId, {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -24,6 +33,9 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should use _ContentType if provided', done => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['127.0.0.1'],
|
||||
});
|
||||
expect(fakeReq.headers['content-type']).toEqual(undefined);
|
||||
const contentType = 'image/jpeg';
|
||||
fakeReq.body._ContentType = contentType;
|
||||
@@ -35,7 +47,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but no key supplied', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
@@ -44,7 +56,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but supplied key is incorrect', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
@@ -54,7 +66,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should give invalid response when keys are configured but different key is supplied', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
});
|
||||
@@ -64,7 +76,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should succeed when any one of the configured keys supplied', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
clientKey: 'clientKey',
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
@@ -77,7 +89,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should succeed when client key supplied but empty', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
clientKey: '',
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey',
|
||||
@@ -90,7 +102,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should succeed when no keys are configured and none supplied', done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
});
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
@@ -117,7 +129,7 @@ describe('middlewares', () => {
|
||||
otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey'
|
||||
);
|
||||
it(`it should pull ${bodyKey} into req.info`, done => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
});
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
@@ -138,48 +150,55 @@ describe('middlewares', () => {
|
||||
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, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
});
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaster).toBe(false);
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual(`unauthorized`);
|
||||
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 not succeed if the ip does not belong to masterKeyIps list', async () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
});
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
fakeReq.headers['x-parse-master-key'] = 'masterKey';
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
expect(fakeReq.auth.isMaster).toBe(false);
|
||||
});
|
||||
|
||||
it('should not succeed if the ip does not belong to maintenanceKeyIps list', async () => {
|
||||
it('should not succeed and log 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, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
maintenanceKey: 'masterKey',
|
||||
maintenanceKeyIps: ['10.0.0.0', '10.0.0.1'],
|
||||
});
|
||||
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);
|
||||
|
||||
let error;
|
||||
|
||||
try {
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toEqual(`unauthorized`);
|
||||
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 ip does belong to masterKeyIps list', async () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['10.0.0.1'],
|
||||
});
|
||||
@@ -190,7 +209,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should allow any ip to use masterKey if masterKeyIps is empty', async () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
masterKeyIps: ['0.0.0.0/0'],
|
||||
});
|
||||
@@ -221,7 +240,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should set default Access-Control-Allow-Headers if allowHeaders are empty', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: undefined,
|
||||
});
|
||||
const headers = {};
|
||||
@@ -234,7 +253,7 @@ describe('middlewares', () => {
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
expect(headers['Access-Control-Allow-Headers']).toContain(middlewares.DEFAULT_ALLOWED_HEADERS);
|
||||
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: [],
|
||||
});
|
||||
allowCrossDomain(fakeReq, res, () => {});
|
||||
@@ -242,7 +261,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should append custom headers to Access-Control-Allow-Headers if allowHeaders provided', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
allowHeaders: ['Header-1', 'Header-2'],
|
||||
});
|
||||
const headers = {};
|
||||
@@ -258,7 +277,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should set default Access-Control-Allow-Origin if allowOrigin is empty', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
allowOrigin: undefined,
|
||||
});
|
||||
const headers = {};
|
||||
@@ -273,7 +292,7 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should set custom origin to Access-Control-Allow-Origin if allowOrigin is provided', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
allowOrigin: 'https://parseplatform.org/',
|
||||
});
|
||||
const headers = {};
|
||||
@@ -287,8 +306,37 @@ describe('middlewares', () => {
|
||||
expect(headers['Access-Control-Allow-Origin']).toEqual('https://parseplatform.org/');
|
||||
});
|
||||
|
||||
it('should use user provided on field userFromJWT', done => {
|
||||
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 => {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
});
|
||||
fakeReq.userFromJWT = 'fake-user';
|
||||
@@ -299,11 +347,87 @@ describe('middlewares', () => {
|
||||
});
|
||||
|
||||
it('should give invalid response when upload file without x-parse-application-id in header', () => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
AppCachePut(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey',
|
||||
});
|
||||
fakeReq.body = Buffer.from('fake-file');
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes);
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(403);
|
||||
});
|
||||
|
||||
it('should match address', () => {
|
||||
const ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
|
||||
const anotherIpv6 = '::ffff:101.10.0.1';
|
||||
const ipv4 = '192.168.0.101';
|
||||
const localhostV6 = '::1';
|
||||
const localhostV62 = '::ffff:127.0.0.1';
|
||||
const localhostV4 = '127.0.0.1';
|
||||
|
||||
const v6 = [ipv6, anotherIpv6];
|
||||
v6.forEach(ip => {
|
||||
expect(middlewares.checkIp(ip, ['::/0'], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ip, ['::'], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ip, ['0.0.0.0'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ip, ['0.0.0.0/0'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ip, ['123.123.123.123'], new Map())).toBe(false);
|
||||
});
|
||||
|
||||
expect(middlewares.checkIp(ipv6, [anotherIpv6], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ipv6, [ipv6], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ipv6, ['2001:db8:85a3:0:0:8a2e:0:0/100'], new Map())).toBe(true);
|
||||
|
||||
expect(middlewares.checkIp(ipv4, ['::'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ipv4, ['::/0'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ipv4, ['0.0.0.0'], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ipv4, ['0.0.0.0/0'], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ipv4, ['123.123.123.123'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(ipv4, [ipv4], new Map())).toBe(true);
|
||||
expect(middlewares.checkIp(ipv4, ['192.168.0.0/24'], new Map())).toBe(true);
|
||||
|
||||
expect(middlewares.checkIp(localhostV4, ['::1'], new Map())).toBe(false);
|
||||
expect(middlewares.checkIp(localhostV6, ['::1'], new Map())).toBe(true);
|
||||
// ::ffff:127.0.0.1 is a padded ipv4 address but not ::1
|
||||
expect(middlewares.checkIp(localhostV62, ['::1'], new Map())).toBe(false);
|
||||
// ::ffff:127.0.0.1 is a padded ipv4 address and is a match for 127.0.0.1
|
||||
expect(middlewares.checkIp(localhostV62, ['127.0.0.1'], new Map())).toBe(true);
|
||||
});
|
||||
|
||||
it('should match address with cache', () => {
|
||||
const ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
|
||||
const cache1 = new Map();
|
||||
const spyBlockListCheck = spyOn(BlockList.prototype, 'check').and.callThrough();
|
||||
expect(middlewares.checkIp(ipv6, ['::'], cache1)).toBe(true);
|
||||
expect(cache1.get('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).toBe(undefined);
|
||||
expect(cache1.get('allowAllIpv6')).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(0);
|
||||
|
||||
const cache2 = new Map();
|
||||
expect(middlewares.checkIp('::1', ['::1'], cache2)).toBe(true);
|
||||
expect(cache2.get('::1')).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
expect(middlewares.checkIp('::1', ['::1'], cache2)).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
spyBlockListCheck.calls.reset();
|
||||
|
||||
const cache3 = new Map();
|
||||
expect(middlewares.checkIp('127.0.0.1', ['127.0.0.1'], cache3)).toBe(true);
|
||||
expect(cache3.get('127.0.0.1')).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
expect(middlewares.checkIp('127.0.0.1', ['127.0.0.1'], cache3)).toBe(true);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
spyBlockListCheck.calls.reset();
|
||||
|
||||
const cache4 = new Map();
|
||||
const ranges = ['127.0.0.1', '192.168.0.0/24'];
|
||||
// should not cache negative match
|
||||
expect(middlewares.checkIp('123.123.123.123', ranges, cache4)).toBe(false);
|
||||
expect(cache4.get('123.123.123.123')).toBe(undefined);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
spyBlockListCheck.calls.reset();
|
||||
|
||||
// should not cache cidr
|
||||
expect(middlewares.checkIp('192.168.0.101', ranges, cache4)).toBe(true);
|
||||
expect(cache4.get('192.168.0.101')).toBe(undefined);
|
||||
expect(spyBlockListCheck).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,12 +248,53 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
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});
|
||||
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('upserts with $setOnInsert', async () => {
|
||||
const uuid = require('uuid');
|
||||
const uuid1 = uuid.v4();
|
||||
const uuid2 = uuid.v4();
|
||||
const schema = {
|
||||
className: 'MyClass',
|
||||
fields: {
|
||||
x: { type: 'Number' },
|
||||
count: { type: 'Number' },
|
||||
},
|
||||
classLevelPermissions: {},
|
||||
};
|
||||
|
||||
const myClassSchema = new Parse.Schema(schema.className);
|
||||
myClassSchema.setCLP(schema.classLevelPermissions);
|
||||
await myClassSchema.save();
|
||||
|
||||
const query = {
|
||||
x: 1,
|
||||
};
|
||||
const update = {
|
||||
objectId: {
|
||||
__op: 'SetOnInsert',
|
||||
amount: uuid1,
|
||||
},
|
||||
count: {
|
||||
__op: 'Increment',
|
||||
amount: 1,
|
||||
},
|
||||
};
|
||||
await Parse.Server.database.update('MyClass', query, update, { upsert: true });
|
||||
update.objectId.amount = uuid2;
|
||||
await Parse.Server.database.update('MyClass', query, update, { upsert: true });
|
||||
|
||||
const res = await Parse.Server.database.find(schema.className, {}, {});
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].objectId).toBe(uuid1);
|
||||
expect(res[0].count).toBe(2);
|
||||
expect(res[0].x).toBe(1);
|
||||
});
|
||||
|
||||
it('handles updating a single object with array, object date', done => {
|
||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||
|
||||
|
||||
+2
-2
@@ -87,7 +87,7 @@ describe('OAuth', function () {
|
||||
done();
|
||||
}
|
||||
|
||||
it('GET request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
xit('GET request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
/*
|
||||
This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication.
|
||||
Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates.
|
||||
@@ -105,7 +105,7 @@ describe('OAuth', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('POST request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
xit('POST request for a resource that requires OAuth should fail with invalid credentials', done => {
|
||||
/*
|
||||
This endpoint has been chosen to make a request to an endpoint that requires OAuth which fails due to missing authentication.
|
||||
Any other endpoint from the Twitter API that requires OAuth can be used instead in case the currently used endpoint deprecates.
|
||||
|
||||
@@ -749,6 +749,7 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
@@ -777,6 +778,7 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
@@ -830,6 +832,7 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkWithLocale = new URL(link);
|
||||
@@ -846,6 +849,8 @@ describe('Pages Router', () => {
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
await jasmine.timeout();
|
||||
|
||||
const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(locale).toBe(exampleLocale);
|
||||
@@ -1190,6 +1195,7 @@ describe('Pages Router', () => {
|
||||
user.setPassword('examplePassword');
|
||||
user.set('email', 'mail@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
|
||||
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
||||
const linkResponse = await request({
|
||||
@@ -1197,7 +1203,6 @@ describe('Pages Router', () => {
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(linkResponse.status).toBe(200);
|
||||
|
||||
const pagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(pagePath).toMatch(new RegExp(`\/${pages.emailVerificationSuccess.defaultFile}`));
|
||||
});
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
const request = require('../lib/request');
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const pushCompleted = async pushId => {
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
query.equalTo('objectId', pushId);
|
||||
let result = await query.first({ useMasterKey: true });
|
||||
while (!(result && result.get('status') === 'succeeded')) {
|
||||
await sleep(100);
|
||||
await jasmine.timeout();
|
||||
result = await query.first({ useMasterKey: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1267,6 +1267,26 @@ describe('miscellaneous', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('test cloud function query parameters with array of pointers', async () => {
|
||||
await reconfigureServer({ encodeParseObjectInCloudFunction: false });
|
||||
Parse.Cloud.define('echoParams', req => {
|
||||
return req.params;
|
||||
});
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Javascript-Key': 'test',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/functions/echoParams',
|
||||
body: '{"arr": [{ "__type": "Pointer", "className": "PointerTest" }]}',
|
||||
});
|
||||
const res = response.data.result;
|
||||
expect(res.arr.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('can handle null params in cloud functions (regression test for #1742)', done => {
|
||||
Parse.Cloud.define('func', request => {
|
||||
expect(request.params.nullParam).toEqual(null);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
const Config = require('../lib/Config');
|
||||
const ParseServer = require('../lib/index').ParseServer;
|
||||
|
||||
describe('Config Keys', () => {
|
||||
const tests = [
|
||||
{
|
||||
name: 'Invalid Root Keys',
|
||||
options: { unknow: 'val', masterKeyIPs: '' },
|
||||
error: 'unknow, masterKeyIPs',
|
||||
},
|
||||
{ name: 'Invalid Schema Keys', options: { schema: { Strict: 'val' } }, error: 'schema.Strict' },
|
||||
{
|
||||
name: 'Invalid Pages Keys',
|
||||
options: { pages: { customUrls: { EmailVerificationSendFail: 'val' } } },
|
||||
error: 'pages.customUrls.EmailVerificationSendFail',
|
||||
},
|
||||
{
|
||||
name: 'Invalid LiveQueryServerOptions Keys',
|
||||
options: { liveQueryServerOptions: { MasterKey: 'value' } },
|
||||
error: 'liveQueryServerOptions.MasterKey',
|
||||
},
|
||||
{
|
||||
name: 'Invalid RateLimit Keys - Array Item',
|
||||
options: { rateLimit: [{ RequestPath: '' }, { RequestTimeWindow: '' }] },
|
||||
error: 'rateLimit[0].RequestPath, rateLimit[1].RequestTimeWindow',
|
||||
},
|
||||
];
|
||||
|
||||
tests.forEach(test => {
|
||||
it(test.name, async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callThrough();
|
||||
spyOn(Config, 'validateOptions').and.callFake(() => {});
|
||||
|
||||
new ParseServer({
|
||||
...defaultConfiguration,
|
||||
...test.options,
|
||||
});
|
||||
expect(logger.error).toHaveBeenCalledWith(`Invalid Option Keys Found: ${test.error}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should run fine', async () => {
|
||||
try {
|
||||
await reconfigureServer({
|
||||
...defaultConfiguration,
|
||||
});
|
||||
} catch (err) {
|
||||
fail('Should run without error');
|
||||
}
|
||||
});
|
||||
});
|
||||
+293
-25
@@ -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 => {
|
||||
@@ -1298,6 +1304,268 @@ 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('default should allow common types', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
},
|
||||
});
|
||||
for (const type of ['plain', 'txt', 'png', 'jpg', 'gif', 'doc']) {
|
||||
const file = new Parse.File(`parse-server-logo.${type}`, { base64: 'ParseA==' });
|
||||
await file.save();
|
||||
}
|
||||
});
|
||||
|
||||
it('works with a period in the file name', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
const values = ['file.png.html', 'file.txt.png.html', 'file.png.txt.html'];
|
||||
|
||||
for (const value of values) {
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: `http://localhost:8378/1/files/${value}`,
|
||||
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 to stop invalid filenames', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
const values = [
|
||||
'!invalid.png',
|
||||
'.png',
|
||||
'.html',
|
||||
' .html',
|
||||
'.png.html',
|
||||
'~invalid.png',
|
||||
'-invalid.png',
|
||||
];
|
||||
|
||||
for (const value of values) {
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: `http://localhost:8378/1/files/${value}`,
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_FILE_NAME, `Filename contains invalid characters.`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows file without extension', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
|
||||
},
|
||||
});
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
const values = ['filenamewithoutextension'];
|
||||
|
||||
for (const value of values) {
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: `http://localhost:8378/1/files/${value}`,
|
||||
body: '<html></html>\n',
|
||||
}).catch(e => {
|
||||
throw new Error(e.data.error);
|
||||
})
|
||||
).toBeResolved();
|
||||
}
|
||||
});
|
||||
|
||||
it('works with array', async () => {
|
||||
await reconfigureServer({
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
fileExtensions: ['jpg', 'wav'],
|
||||
},
|
||||
});
|
||||
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.`)
|
||||
);
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'image/jpg',
|
||||
base64: 'PGh0bWw+PC9odG1sPgo=',
|
||||
}),
|
||||
})
|
||||
).toBeResolved();
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/files/file',
|
||||
body: JSON.stringify({
|
||||
_ApplicationId: 'test',
|
||||
_JavaScriptKey: 'test',
|
||||
_ContentType: 'audio/wav',
|
||||
base64: 'UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA',
|
||||
}),
|
||||
})
|
||||
).toBeResolved();
|
||||
});
|
||||
|
||||
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$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+241
-13
@@ -49,7 +49,9 @@ describe('ParseGraphQLServer', () => {
|
||||
let parseGraphQLServer;
|
||||
|
||||
beforeEach(async () => {
|
||||
parseServer = await global.reconfigureServer({});
|
||||
parseServer = await global.reconfigureServer({
|
||||
maxUploadSize: '1kb',
|
||||
});
|
||||
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
|
||||
graphQLPath: '/graphql',
|
||||
playgroundPath: '/playground',
|
||||
@@ -122,15 +124,16 @@ describe('ParseGraphQLServer', () => {
|
||||
info: new Object(),
|
||||
config: new Object(),
|
||||
auth: new Object(),
|
||||
get: () => {},
|
||||
};
|
||||
const res = {
|
||||
set: () => {},
|
||||
};
|
||||
|
||||
it("should return schema and context with req's info, config and auth", async () => {
|
||||
const options = await parseGraphQLServer._getGraphQLOptions();
|
||||
expect(options.multipart).toEqual({
|
||||
fileSize: 20971520,
|
||||
});
|
||||
expect(options.schema).toEqual(parseGraphQLServer.parseGraphQLSchema.graphQLSchema);
|
||||
const contextResponse = options.context({ req });
|
||||
const contextResponse = await options.context({ req, res });
|
||||
expect(contextResponse.info).toEqual(req.info);
|
||||
expect(contextResponse.config).toEqual(req.config);
|
||||
expect(contextResponse.auth).toEqual(req.auth);
|
||||
@@ -5275,7 +5278,6 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
it('should only count', async () => {
|
||||
await prepareData();
|
||||
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const where = {
|
||||
@@ -6833,7 +6835,7 @@ describe('ParseGraphQLServer', () => {
|
||||
|
||||
describe('Files Mutations', () => {
|
||||
describe('Create', () => {
|
||||
it_only_node_version('<17')('should return File object', async () => {
|
||||
it('should return File object', async () => {
|
||||
const clientMutationId = uuidv4();
|
||||
|
||||
parseServer = await global.reconfigureServer({
|
||||
@@ -9299,7 +9301,7 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(result6[0].node.name).toEqual('imACountry3');
|
||||
});
|
||||
|
||||
it_only_node_version('<17')('should support files', async () => {
|
||||
it('should support files', async () => {
|
||||
try {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
@@ -9341,7 +9343,6 @@ describe('ParseGraphQLServer', () => {
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
const result = JSON.parse(await res.text());
|
||||
|
||||
expect(result.data.createFile.fileInfo.name).toEqual(
|
||||
jasmine.stringMatching(/_myFileName.txt$/)
|
||||
);
|
||||
@@ -9547,9 +9548,233 @@ describe('ParseGraphQLServer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it_only_node_version('<17')('should not upload if file is too large', async () => {
|
||||
parseGraphQLServer.parseServer.config.maxUploadSize = '1kb';
|
||||
it('should support files on required file', async () => {
|
||||
try {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
const schemaController = await parseServer.config.databaseController.loadSchema();
|
||||
await schemaController.addClassIfNotExists('SomeClassWithRequiredFile', {
|
||||
someField: { type: 'File', required: true },
|
||||
});
|
||||
await resetGraphQLCache();
|
||||
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
|
||||
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `
|
||||
mutation CreateSomeObject(
|
||||
$fields: CreateSomeClassWithRequiredFileFieldsInput
|
||||
) {
|
||||
createSomeClassWithRequiredFile(
|
||||
input: { fields: $fields }
|
||||
) {
|
||||
someClassWithRequiredFile {
|
||||
id
|
||||
someField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
fields: {
|
||||
someField: { upload: null },
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
body.append('map', JSON.stringify({ 1: ['variables.fields.someField.upload'] }));
|
||||
body.append('1', 'My File Content', {
|
||||
filename: 'myFileName.txt',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
const res = await fetch('http://localhost:13377/graphql', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
const resText = await res.text();
|
||||
const result = JSON.parse(resText);
|
||||
expect(
|
||||
result.data.createSomeClassWithRequiredFile.someClassWithRequiredFile.someField.name
|
||||
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
|
||||
expect(
|
||||
result.data.createSomeClassWithRequiredFile.someClassWithRequiredFile.someField.url
|
||||
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should support file upload for on fly creation through pointer and relation', async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
const schema = new Parse.Schema('SomeClass');
|
||||
schema.addFile('someFileField');
|
||||
schema.addPointer('somePointerField', 'SomeClass');
|
||||
schema.addRelation('someRelationField', 'SomeClass');
|
||||
await schema.save();
|
||||
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `
|
||||
mutation UploadFiles(
|
||||
$fields: CreateSomeClassFieldsInput
|
||||
) {
|
||||
createSomeClass(
|
||||
input: { fields: $fields }
|
||||
) {
|
||||
someClass {
|
||||
id
|
||||
someFileField {
|
||||
name
|
||||
url
|
||||
}
|
||||
somePointerField {
|
||||
id
|
||||
someFileField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
someRelationField {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
someFileField {
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
fields: {
|
||||
someFileField: { upload: null },
|
||||
somePointerField: {
|
||||
createAndLink: {
|
||||
someFileField: { upload: null },
|
||||
},
|
||||
},
|
||||
someRelationField: {
|
||||
createAndAdd: [
|
||||
{
|
||||
someFileField: { upload: null },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
body.append(
|
||||
'map',
|
||||
JSON.stringify({
|
||||
1: ['variables.fields.someFileField.upload'],
|
||||
2: ['variables.fields.somePointerField.createAndLink.someFileField.upload'],
|
||||
3: ['variables.fields.someRelationField.createAndAdd.0.someFileField.upload'],
|
||||
})
|
||||
);
|
||||
body.append('1', 'My File Content someFileField', {
|
||||
filename: 'someFileField.txt',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
body.append('2', 'My File Content somePointerField', {
|
||||
filename: 'somePointerField.txt',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
body.append('3', 'My File Content someRelationField', {
|
||||
filename: 'someRelationField.txt',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
const res = await fetch('http://localhost:13377/graphql', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
const result = await res.json();
|
||||
expect(result.data.createSomeClass.someClass.someFileField.name).toEqual(
|
||||
jasmine.stringMatching(/_someFileField.txt$/)
|
||||
);
|
||||
expect(result.data.createSomeClass.someClass.somePointerField.someFileField.name).toEqual(
|
||||
jasmine.stringMatching(/_somePointerField.txt$/)
|
||||
);
|
||||
expect(
|
||||
result.data.createSomeClass.someClass.someRelationField.edges[0].node.someFileField.name
|
||||
).toEqual(jasmine.stringMatching(/_someRelationField.txt$/));
|
||||
});
|
||||
|
||||
it('should support files and add extension from mimetype', async () => {
|
||||
try {
|
||||
parseServer = await global.reconfigureServer({
|
||||
publicServerURL: 'http://localhost:13377/parse',
|
||||
});
|
||||
|
||||
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', 'My File Content', {
|
||||
// No extension, the system should add it from mimetype
|
||||
filename: 'myFileName',
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
const res = await fetch('http://localhost:13377/graphql', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
const result = JSON.parse(await res.text());
|
||||
expect(result.data.createFile.fileInfo.name).toEqual(
|
||||
jasmine.stringMatching(/_myFileName.txt$/)
|
||||
);
|
||||
expect(result.data.createFile.fileInfo.url).toEqual(
|
||||
jasmine.stringMatching(/_myFileName.txt$/)
|
||||
);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not upload if file is too large', async () => {
|
||||
const body = new FormData();
|
||||
body.append(
|
||||
'operations',
|
||||
@@ -9574,6 +9799,7 @@ describe('ParseGraphQLServer', () => {
|
||||
body.append('map', JSON.stringify({ 1: ['variables.input.upload'] }));
|
||||
body.append(
|
||||
'1',
|
||||
// In this test file parse server is setup with 1kb limit
|
||||
Buffer.alloc(parseGraphQLServer._transformMaxUploadSizeToBytes('2kb'), 1),
|
||||
{
|
||||
filename: 'myFileName.txt',
|
||||
@@ -9588,8 +9814,10 @@ describe('ParseGraphQLServer', () => {
|
||||
});
|
||||
|
||||
const result = JSON.parse(await res.text());
|
||||
expect(res.status).toEqual(500);
|
||||
expect(result.errors[0].message).toEqual('File size limit exceeded: 1024 bytes');
|
||||
expect(res.status).toEqual(200);
|
||||
expect(result.errors[0].message).toEqual(
|
||||
'File truncated as it exceeds the 1024 byte size limit.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should support object values', async () => {
|
||||
|
||||
+35
-2
@@ -190,7 +190,7 @@ describe('Hooks', () => {
|
||||
|
||||
it('should fail trying to create two times the same function', done => {
|
||||
Parse.Hooks.createFunction('my_new_function', 'http://url.com')
|
||||
.then(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||
.then(() => jasmine.timeout())
|
||||
.then(
|
||||
() => {
|
||||
return Parse.Hooks.createFunction('my_new_function', 'http://url.com');
|
||||
@@ -208,7 +208,7 @@ describe('Hooks', () => {
|
||||
expect(err).not.toBe(null);
|
||||
if (err) {
|
||||
expect(err.code).toBe(143);
|
||||
expect(err.message).toBe('function name: my_new_function already exits');
|
||||
expect(err.message).toBe('function name: my_new_function already exists');
|
||||
}
|
||||
return Parse.Hooks.removeFunction('my_new_function');
|
||||
}
|
||||
@@ -694,3 +694,36 @@ describe('triggers', () => {
|
||||
expect(req.context).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizing names', () => {
|
||||
const invalidNames = [
|
||||
`test'%3bdeclare%20@q%20varchar(99)%3bset%20@q%3d'%5c%5cxxxxxxxxxxxxxxx.yyyyy'%2b'fy.com%5cxus'%3b%20exec%20master.dbo.xp_dirtree%20@q%3b--%20`,
|
||||
`test.function.name`,
|
||||
];
|
||||
|
||||
it('should not crash server and return error on invalid Cloud Function name', async () => {
|
||||
for (const invalidName of invalidNames) {
|
||||
let error;
|
||||
try {
|
||||
await Parse.Cloud.run(invalidName);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toMatch(/Invalid function/);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not crash server and return error on invalid Cloud Job name', async () => {
|
||||
for (const invalidName of invalidNames) {
|
||||
let error;
|
||||
try {
|
||||
await Parse.Cloud.startJob(invalidName);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toMatch(/Invalid job/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+100
-81
@@ -2,6 +2,9 @@
|
||||
const Auth = require('../lib/Auth');
|
||||
const UserController = require('../lib/Controllers/UserController').UserController;
|
||||
const Config = require('../lib/Config');
|
||||
const ParseServer = require('../lib/index').ParseServer;
|
||||
const triggers = require('../lib/triggers');
|
||||
const { resolvingPromise, sleep } = require('./support/testUtils');
|
||||
const validatorFail = () => {
|
||||
throw 'you are not authorized';
|
||||
};
|
||||
@@ -12,19 +15,9 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
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));
|
||||
await client.close();
|
||||
});
|
||||
it('access user on onLiveQueryEvent disconnect', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const requestedUser = new Parse.User();
|
||||
requestedUser.setUsername('username');
|
||||
requestedUser.setPassword('password');
|
||||
@@ -41,18 +34,10 @@ describe('ParseLiveQuery', function () {
|
||||
const query = new Parse.Query(TestObject);
|
||||
await query.subscribe();
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
client.close();
|
||||
await client.close();
|
||||
});
|
||||
|
||||
it('can subscribe to query', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
@@ -118,14 +103,6 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('expect afterEvent payload', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
@@ -145,14 +122,6 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('expect afterEvent enter', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
expect(req.event).toBe('enter');
|
||||
expect(req.user).toBeUndefined();
|
||||
@@ -176,14 +145,6 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('expect afterEvent leave', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
expect(req.event).toBe('leave');
|
||||
expect(req.user).toBeUndefined();
|
||||
@@ -208,14 +169,6 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('expect afterEvent delete', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
expect(req.event).toBe('delete');
|
||||
expect(req.user).toBeUndefined();
|
||||
@@ -363,30 +316,18 @@ describe('ParseLiveQuery', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle afterEvent sendEvent to false', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
it('can handle afterEvent sendEvent to false', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const promise = resolvingPromise();
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
const current = req.object;
|
||||
const original = req.original;
|
||||
|
||||
setTimeout(() => {
|
||||
done();
|
||||
}, 2000);
|
||||
|
||||
if (current.get('foo') != original.get('foo')) {
|
||||
req.sendEvent = false;
|
||||
}
|
||||
promise.resolve();
|
||||
});
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
@@ -400,9 +341,10 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
await promise;
|
||||
});
|
||||
|
||||
xit('can handle live query with fields - enable upon JS SDK support', async () => {
|
||||
it('can handle live query with fields', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['Test'],
|
||||
@@ -440,7 +382,7 @@ describe('ParseLiveQuery', function () {
|
||||
await obj2.save();
|
||||
obj2.set('foo', 'bart');
|
||||
await obj2.save();
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await sleep(2000);
|
||||
expect(createSpy).toHaveBeenCalledTimes(1);
|
||||
expect(updateSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -769,14 +711,6 @@ describe('ParseLiveQuery', function () {
|
||||
});
|
||||
|
||||
it('can handle select beforeSubscribe query', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.beforeSubscribe(TestObject, request => {
|
||||
const query = request.query;
|
||||
query.select('yolo');
|
||||
@@ -855,7 +789,7 @@ describe('ParseLiveQuery', function () {
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
await object.destroy();
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
await sleep(200);
|
||||
for (const key in calls) {
|
||||
expect(calls[key]).toHaveBeenCalled();
|
||||
}
|
||||
@@ -889,9 +823,9 @@ describe('ParseLiveQuery', function () {
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save(null, { useMasterKey: true });
|
||||
role.getUsers().add(user);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await sleep(1000);
|
||||
await role.save();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await sleep(1000);
|
||||
object.set('foo', 'yolo');
|
||||
await Promise.all([
|
||||
new Promise(resolve => {
|
||||
@@ -922,7 +856,6 @@ describe('ParseLiveQuery', function () {
|
||||
const subscription = await query.subscribe();
|
||||
|
||||
subscription.on('create', async obj => {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
expect(obj.get('user').id).toBe(user.id);
|
||||
expect(obj.get('createdWith')).toEqual({ action: 'login', authProvider: 'password' });
|
||||
expect(obj.get('expiresAt')).toBeInstanceOf(Date);
|
||||
@@ -1212,4 +1145,90 @@ describe('ParseLiveQuery', function () {
|
||||
object.set({ location: secondPoint });
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('does shutdown liveQuery server', async () => {
|
||||
await reconfigureServer({ appId: 'test_app_id' });
|
||||
const config = {
|
||||
appId: 'hello_test',
|
||||
masterKey: 'world',
|
||||
port: 1345,
|
||||
mountPath: '/1',
|
||||
serverURL: 'http://localhost:1345/1',
|
||||
liveQuery: {
|
||||
classNames: ['Yolo'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
};
|
||||
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
|
||||
config.databaseAdapter = new databaseAdapter.constructor({
|
||||
uri: databaseURI,
|
||||
collectionPrefix: 'test_',
|
||||
});
|
||||
config.filesAdapter = defaultConfiguration.filesAdapter;
|
||||
}
|
||||
const server = await ParseServer.startApp(config);
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
client.serverURL = 'ws://localhost:1345/1';
|
||||
const query = await new Parse.Query('Yolo').subscribe();
|
||||
await Promise.all([
|
||||
server.handleShutdown(),
|
||||
new Promise(resolve => query.on('close', resolve)),
|
||||
]);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(server.liveQueryServer.server.address()).toBeNull();
|
||||
expect(server.liveQueryServer.subscriber.isOpen).toBeFalse();
|
||||
await new Promise(resolve => server.server.close(resolve));
|
||||
});
|
||||
|
||||
it('prevent afterSave trigger if not exists', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
spyOn(triggers, 'maybeRunTrigger').and.callThrough();
|
||||
const object1 = new TestObject();
|
||||
const object2 = new TestObject();
|
||||
const object3 = new TestObject();
|
||||
await Parse.Object.saveAll([object1, object2, object3]);
|
||||
|
||||
expect(triggers.maybeRunTrigger).toHaveBeenCalledTimes(0);
|
||||
expect(object1.id).toBeDefined();
|
||||
expect(object2.id).toBeDefined();
|
||||
expect(object3.id).toBeDefined();
|
||||
});
|
||||
|
||||
it('triggers query event with constraint not equal to null', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const spy = {
|
||||
create(obj) {
|
||||
expect(obj.attributes.foo).toEqual('bar');
|
||||
},
|
||||
};
|
||||
const createSpy = spyOn(spy, 'create');
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.notEqualTo('foo', null);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('create', spy.create);
|
||||
|
||||
const object1 = new TestObject();
|
||||
object1.set('foo', 'bar');
|
||||
await object1.save();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(createSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
|
||||
});
|
||||
it('can connect', async () => {
|
||||
await reconfigureServer({
|
||||
appId: 'redis_live_query',
|
||||
startLiveQueryServer: true,
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
@@ -36,6 +37,7 @@ if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
|
||||
|
||||
it('can call connect twice', async () => {
|
||||
const server = await reconfigureServer({
|
||||
appId: 'redis_live_query',
|
||||
startLiveQueryServer: true,
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
|
||||
@@ -115,6 +115,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
});
|
||||
|
||||
describe_only_db('mongo')('initialization', () => {
|
||||
beforeEach(() => reconfigureServer({ appId: 'mongo_init_test' }));
|
||||
it('can be initialized through ParseServer without liveQueryServerOptions', async () => {
|
||||
const parseServer = await ParseServer.startApp({
|
||||
appId: 'hello',
|
||||
@@ -355,7 +356,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
// Make sure we add subscriptionInfo to the client
|
||||
const args = client.addSubscriptionInfo.calls.first().args;
|
||||
expect(args[0]).toBe(requestId);
|
||||
expect(args[1].fields).toBe(query.fields);
|
||||
expect(args[1].keys).toBe(query.keys);
|
||||
expect(args[1].sessionToken).toBe(request.sessionToken);
|
||||
// Make sure we send subscribe response to the client
|
||||
expect(client.pushSubscribe).toHaveBeenCalledWith(requestId);
|
||||
@@ -416,7 +417,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
// Make sure we add subscriptionInfo to the client 2
|
||||
args = clientAgain.addSubscriptionInfo.calls.mostRecent().args;
|
||||
expect(args[0]).toBe(requestIdAgain);
|
||||
expect(args[1].fields).toBe(queryAgain.fields);
|
||||
expect(args[1].keys).toBe(queryAgain.keys);
|
||||
});
|
||||
|
||||
it('can handle unsubscribe command without clientId', function () {
|
||||
@@ -754,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
|
||||
@@ -1037,7 +1081,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle create command with fields', async done => {
|
||||
it('can handle create command with keys', async done => {
|
||||
jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client');
|
||||
const Client = require('../lib/LiveQuery/Client').Client;
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
@@ -1087,62 +1131,6 @@ 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;
|
||||
@@ -1945,6 +1933,7 @@ describe('ParseLiveQueryServer', function () {
|
||||
} else {
|
||||
subscription.clientRequestIds = new Map([[clientId, [requestId]]]);
|
||||
}
|
||||
subscription.query = query.where;
|
||||
return subscription;
|
||||
}
|
||||
|
||||
|
||||
@@ -568,6 +568,70 @@ describe('Parse.Object testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('can increment array nested fields', async () => {
|
||||
const obj = new TestObject();
|
||||
obj.set('items', [ { value: 'a', count: 5 }, { value: 'b', count: 1 } ]);
|
||||
await obj.save();
|
||||
obj.increment('items.0.count', 15);
|
||||
obj.increment('items.1.count', 4);
|
||||
await obj.save();
|
||||
expect(obj.toJSON().items[0].value).toBe('a');
|
||||
expect(obj.toJSON().items[1].value).toBe('b');
|
||||
expect(obj.toJSON().items[0].count).toBe(20);
|
||||
expect(obj.toJSON().items[1].count).toBe(5);
|
||||
const query = new Parse.Query(TestObject);
|
||||
const result = await query.get(obj.id);
|
||||
expect(result.get('items')[0].value).toBe('a');
|
||||
expect(result.get('items')[1].value).toBe('b');
|
||||
expect(result.get('items')[0].count).toBe(20);
|
||||
expect(result.get('items')[1].count).toBe(5);
|
||||
expect(result.get('items')).toEqual(obj.get('items'));
|
||||
});
|
||||
|
||||
it_only_db('mongo')('can increment array nested fields missing index', async () => {
|
||||
const obj = new TestObject();
|
||||
obj.set('items', []);
|
||||
await obj.save();
|
||||
obj.increment('items.1.count', 15);
|
||||
await obj.save();
|
||||
expect(obj.toJSON().items[0]).toBe(null);
|
||||
expect(obj.toJSON().items[1].count).toBe(15);
|
||||
const query = new Parse.Query(TestObject);
|
||||
const result = await query.get(obj.id);
|
||||
expect(result.get('items')[0]).toBe(null);
|
||||
expect(result.get('items')[1].count).toBe(15);
|
||||
expect(result.get('items')).toEqual(obj.get('items'));
|
||||
});
|
||||
|
||||
it('can query array nested fields', async () => {
|
||||
const objects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const obj = new TestObject();
|
||||
obj.set('items', [i, { value: i }]);
|
||||
objects.push(obj);
|
||||
}
|
||||
await Parse.Object.saveAll(objects);
|
||||
let query = new Parse.Query(TestObject);
|
||||
query.greaterThan('items.1.value', 5);
|
||||
let result = await query.find();
|
||||
expect(result.length).toBe(4);
|
||||
|
||||
query = new Parse.Query(TestObject);
|
||||
query.lessThan('items.0', 3);
|
||||
result = await query.find();
|
||||
expect(result.length).toBe(3);
|
||||
|
||||
query = new Parse.Query(TestObject);
|
||||
query.equalTo('items.0', 5);
|
||||
result = await query.find();
|
||||
expect(result.length).toBe(1);
|
||||
|
||||
query = new Parse.Query(TestObject);
|
||||
query.notEqualTo('items.0', 5);
|
||||
result = await query.find();
|
||||
expect(result.length).toBe(9);
|
||||
});
|
||||
|
||||
it('addUnique with object', function (done) {
|
||||
const x1 = new Parse.Object('X');
|
||||
x1.set('stuff', [1, { hello: 'world' }, { foo: 'bar' }]);
|
||||
@@ -2055,4 +2119,56 @@ describe('Parse.Object testing', () => {
|
||||
const object = new Parse.Object('CloudCodeIsNew');
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('should not change the json field to array in afterSave', async () => {
|
||||
Parse.Cloud.beforeSave('failingJSONTestCase', req => {
|
||||
expect(req.object.get('jsonField')).toEqual({ '123': 'test' });
|
||||
});
|
||||
|
||||
Parse.Cloud.afterSave('failingJSONTestCase', req => {
|
||||
expect(req.object.get('jsonField')).toEqual({ '123': 'test' });
|
||||
});
|
||||
|
||||
const object = new Parse.Object('failingJSONTestCase');
|
||||
object.set('jsonField', { '123': 'test' });
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('returns correct field values', async () => {
|
||||
const values = [
|
||||
{ field: 'string', value: 'string' },
|
||||
{ field: 'number', value: 1 },
|
||||
{ field: 'boolean', value: true },
|
||||
{ field: 'array', value: [0, 1, 2] },
|
||||
{ field: 'array', value: [1, 2, 3] },
|
||||
{ field: 'array', value: [{ '0': 'a' }, 2, 3] },
|
||||
{ field: 'object', value: { key: 'value' } },
|
||||
{ field: 'object', value: { key1: 'value1', key2: 'value2' } },
|
||||
{ field: 'object', value: { key1: 1, key2: 2 } },
|
||||
{ field: 'object', value: { '1x1': 1 } },
|
||||
{ field: 'object', value: { '1x1': 1, '2': 2 } },
|
||||
{ field: 'object', value: { '0': 0 } },
|
||||
{ field: 'object', value: { '1': 1 } },
|
||||
{ field: 'object', value: { '0': { '0': 'a', '1': 'b' } } },
|
||||
{ field: 'date', value: new Date() },
|
||||
{
|
||||
field: 'file',
|
||||
value: Parse.File.fromJSON({
|
||||
__type: 'File',
|
||||
name: 'name',
|
||||
url: 'http://localhost:8378/1/files/test/name',
|
||||
}),
|
||||
},
|
||||
{ field: 'geoPoint', value: new Parse.GeoPoint(40, -30) },
|
||||
{ field: 'bytes', value: { __type: 'Bytes', base64: 'ZnJveW8=' } },
|
||||
];
|
||||
for (const value of values) {
|
||||
const object = new TestObject();
|
||||
object.set(value.field, value.value);
|
||||
await object.save();
|
||||
const query = new Parse.Query(TestObject);
|
||||
const objectAgain = await query.get(object.id);
|
||||
expect(objectAgain.get(value.field)).toEqual(value.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const Config = require('../lib/Config');
|
||||
const { MongoClient } = require('mongodb');
|
||||
const databaseURI = 'mongodb://localhost:27017/';
|
||||
const request = require('../lib/request');
|
||||
|
||||
let config, client, database;
|
||||
|
||||
const masterKeyHeaders = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const masterKeyOptions = {
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
};
|
||||
|
||||
const profileLevel = 2;
|
||||
describe_only_db('mongo')('Parse.Query with comment testing', () => {
|
||||
beforeAll(async () => {
|
||||
config = Config.get('test');
|
||||
client = await MongoClient.connect(databaseURI, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
});
|
||||
database = client.db('parseServerMongoAdapterTestDatabase');
|
||||
let profiler = await database.command({ profile: 0 });
|
||||
expect(profiler.was).toEqual(0);
|
||||
// console.log(`Disabling profiler : ${profiler.was}`);
|
||||
profiler = await database.command({ profile: profileLevel });
|
||||
profiler = await database.command({ profile: -1 });
|
||||
// console.log(`Enabling profiler : ${profiler.was}`);
|
||||
profiler = await database.command({ profile: -1 });
|
||||
expect(profiler.was).toEqual(profileLevel);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const profiler = await database.command({ profile: -1 });
|
||||
expect(profiler.was).toEqual(profileLevel);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await database.command({ profile: 0 });
|
||||
await client.close();
|
||||
});
|
||||
|
||||
it('send comment with query through REST', async () => {
|
||||
const comment = 'Hello Parse';
|
||||
const object = new TestObject();
|
||||
object.set('name', 'object');
|
||||
await object.save();
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
comment: comment,
|
||||
},
|
||||
});
|
||||
await request(options);
|
||||
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
|
||||
expect(result.command.explain.comment).toBe(comment);
|
||||
});
|
||||
|
||||
it('send comment with query', async () => {
|
||||
const comment = 'Hello Parse';
|
||||
const object = new TestObject();
|
||||
object.set('name', 'object');
|
||||
await object.save();
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
await collection._rawFind({ name: 'object' }, { comment: comment });
|
||||
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
|
||||
expect(result.command.comment).toBe(comment);
|
||||
});
|
||||
|
||||
it('send a comment with a count query', async () => {
|
||||
const comment = 'Hello Parse';
|
||||
const object = new TestObject();
|
||||
object.set('name', 'object');
|
||||
await object.save();
|
||||
|
||||
const object2 = new TestObject();
|
||||
object2.set('name', 'object');
|
||||
await object2.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
const countResult = await collection.count({ name: 'object' }, { comment: comment });
|
||||
expect(countResult).toEqual(2);
|
||||
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
|
||||
expect(result.command.comment).toBe(comment);
|
||||
});
|
||||
|
||||
it('attach a comment to an aggregation', async () => {
|
||||
const comment = 'Hello Parse';
|
||||
const object = new TestObject();
|
||||
object.set('name', 'object');
|
||||
await object.save();
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
await collection.aggregate([{ $group: { _id: '$name' } }], {
|
||||
explain: true,
|
||||
comment: comment,
|
||||
});
|
||||
const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } });
|
||||
expect(result.command.explain.comment).toBe(comment);
|
||||
});
|
||||
});
|
||||
@@ -86,6 +86,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
|
||||
|
||||
@@ -93,6 +94,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
hint: '_id_',
|
||||
explain: true,
|
||||
});
|
||||
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -631,4 +631,58 @@ describe('ParseServerRESTController', () => {
|
||||
expect(sessions[0].get('installationId')).toBe(installationId);
|
||||
expect(sessions[0].get('sessionToken')).toBe(loggedUser.sessionToken);
|
||||
});
|
||||
|
||||
it('returns a statusId when running jobs', async () => {
|
||||
Parse.Cloud.job('CloudJob', () => {
|
||||
return 'Cloud job completed';
|
||||
});
|
||||
const res = await RESTController.request(
|
||||
'POST',
|
||||
'/jobs/CloudJob',
|
||||
{},
|
||||
{ useMasterKey: true, returnStatus: true }
|
||||
);
|
||||
const jobStatusId = res._headers['X-Parse-Job-Status-Id'];
|
||||
expect(jobStatusId).toBeDefined();
|
||||
const result = await Parse.Cloud.getJobStatus(jobStatusId);
|
||||
expect(result.id).toBe(jobStatusId);
|
||||
});
|
||||
|
||||
it('returns a statusId when running push notifications', async () => {
|
||||
const payload = {
|
||||
data: { alert: 'We return status!' },
|
||||
where: { deviceType: 'ios' },
|
||||
};
|
||||
const res = await RESTController.request('POST', '/push', payload, {
|
||||
useMasterKey: true,
|
||||
returnStatus: true,
|
||||
});
|
||||
const pushStatusId = res._headers['X-Parse-Push-Status-Id'];
|
||||
expect(pushStatusId).toBeDefined();
|
||||
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result.id).toBe(pushStatusId);
|
||||
});
|
||||
|
||||
it('returns a statusId when running batch push notifications', async () => {
|
||||
const payload = {
|
||||
data: { alert: 'We return status!' },
|
||||
where: { deviceType: 'ios' },
|
||||
};
|
||||
const res = await RESTController.request('POST', 'batch', {
|
||||
requests: [{
|
||||
method: 'POST',
|
||||
path: '/push',
|
||||
body: payload,
|
||||
}],
|
||||
}, {
|
||||
useMasterKey: true,
|
||||
returnStatus: true,
|
||||
});
|
||||
const pushStatusId = res[0]._headers['X-Parse-Push-Status-Id'];
|
||||
expect(pushStatusId).toBeDefined();
|
||||
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result.id).toBe(pushStatusId);
|
||||
});
|
||||
});
|
||||
|
||||
+92
-67
@@ -15,51 +15,18 @@ 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(() => {});
|
||||
it('should default false', async () => {
|
||||
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);
|
||||
expect(Config.get(Parse.applicationId).allowExpiredAuthDataToken).toBe(false);
|
||||
});
|
||||
|
||||
it('should enforce boolean values', async () => {
|
||||
@@ -107,6 +74,36 @@ describe('Parse.User testing', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('user login with context', async () => {
|
||||
let hit = 0;
|
||||
const context = { foo: 'bar' };
|
||||
Parse.Cloud.beforeLogin(req => {
|
||||
expect(req.context).toEqual(context);
|
||||
hit++;
|
||||
});
|
||||
Parse.Cloud.afterLogin(req => {
|
||||
expect(req.context).toEqual(context);
|
||||
hit++;
|
||||
});
|
||||
await Parse.User.signUp('asdf', 'zxcv');
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/login',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Cloud-Context': JSON.stringify(context),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
_method: 'GET',
|
||||
username: 'asdf',
|
||||
password: 'zxcv',
|
||||
},
|
||||
});
|
||||
expect(hit).toBe(2);
|
||||
});
|
||||
|
||||
it('user login with non-string username with REST API', async done => {
|
||||
await Parse.User.signUp('asdf', 'zxcv');
|
||||
request({
|
||||
@@ -1848,7 +1845,7 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow login with expired authData token by default', async () => {
|
||||
it('should not allow login with expired authData token since allowExpiredAuthDataToken is set to false by default', async () => {
|
||||
const provider = {
|
||||
authData: {
|
||||
id: '12345',
|
||||
@@ -1874,37 +1871,7 @@ describe('Parse.User testing', () => {
|
||||
// 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();
|
||||
await expectAsync(Parse.User._logInWith('shortLivedAuth', {})).toBeRejected();
|
||||
});
|
||||
|
||||
it('should allow PUT request with stale auth Data', done => {
|
||||
@@ -3037,6 +3004,7 @@ describe('Parse.User testing', () => {
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
expect(emailCalled).toBe(true);
|
||||
expect(emailOptions).not.toBeUndefined();
|
||||
@@ -3194,6 +3162,35 @@ describe('Parse.User testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('should return current session with expired expiration date', async () => {
|
||||
await Parse.User.signUp('buser', 'somepass', null);
|
||||
const response = await request({
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8378/1/classes/_Session',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
});
|
||||
const body = response.data;
|
||||
const id = body.results[0].objectId;
|
||||
const expiresAt = new Date(new Date().setYear(2015));
|
||||
await request({
|
||||
method: 'PUT',
|
||||
url: 'http://localhost:8378/1/classes/_Session/' + id,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
expiresAt: { __type: 'Date', iso: expiresAt.toISOString() },
|
||||
},
|
||||
});
|
||||
const session = await Parse.Session.current();
|
||||
expect(session.get('expiresAt')).toEqual(expiresAt);
|
||||
});
|
||||
|
||||
it('should not create extraneous session tokens', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
config.database
|
||||
@@ -4405,3 +4402,31 @@ describe('login as other user', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowClientClassCreation option', () => {
|
||||
it('should enforce boolean values', async () => {
|
||||
const options = [[], 'a', '', 0, 1, {}, 'true', 'false'];
|
||||
for (const option of options) {
|
||||
await expectAsync(reconfigureServer({ allowClientClassCreation: option })).toBeRejected();
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept true value', async () => {
|
||||
await reconfigureServer({ allowClientClassCreation: true });
|
||||
expect(Config.get(Parse.applicationId).allowClientClassCreation).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept false value', async () => {
|
||||
await reconfigureServer({ allowClientClassCreation: false });
|
||||
expect(Config.get(Parse.applicationId).allowClientClassCreation).toBe(false);
|
||||
});
|
||||
|
||||
it('should default false', async () => {
|
||||
// remove predefined allowClientClassCreation:true on global defaultConfiguration
|
||||
delete defaultConfiguration.allowClientClassCreation;
|
||||
await reconfigureServer(defaultConfiguration);
|
||||
expect(Config.get(Parse.applicationId).allowClientClassCreation).toBe(false);
|
||||
// Need to set it back to true to avoid other test fails
|
||||
defaultConfiguration.allowClientClassCreation = true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,14 +26,12 @@ const successfulIOS = function (body, installations) {
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const pushCompleted = async pushId => {
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
query.equalTo('objectId', pushId);
|
||||
let result = await query.first({ useMasterKey: true });
|
||||
while (!(result && result.get('status') === 'succeeded')) {
|
||||
await sleep(100);
|
||||
await jasmine.timeout();
|
||||
result = await query.first({ useMasterKey: true });
|
||||
}
|
||||
};
|
||||
@@ -562,7 +560,7 @@ describe('PushController', () => {
|
||||
});
|
||||
const pushStatusId = await sendPush(payload, {}, config, auth);
|
||||
// it is enqueued so it can take time
|
||||
await sleep(1000);
|
||||
await jasmine.timeout(1000);
|
||||
Parse.serverURL = 'http://localhost:8378/1'; // GOOD url
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result).toBeDefined();
|
||||
@@ -801,7 +799,7 @@ describe('PushController', () => {
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
await pushController.sendPush(payload, {}, config, auth);
|
||||
await sleep(1000);
|
||||
await jasmine.timeout(1000);
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
const results = await query.find({ useMasterKey: true });
|
||||
expect(results.length).toBe(1);
|
||||
@@ -856,7 +854,7 @@ describe('PushController', () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
await Parse.Object.saveAll(installations);
|
||||
await pushController.sendPush(payload, {}, config, auth);
|
||||
await sleep(1000);
|
||||
await jasmine.timeout(1000);
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
const results = await query.find({ useMasterKey: true });
|
||||
expect(results.length).toBe(1);
|
||||
|
||||
@@ -125,6 +125,35 @@ describe('matchesQuery', function () {
|
||||
expect(matchesQuery(obj, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches queries with eq constraint', function () {
|
||||
const obj = {
|
||||
objectId: 'Person2',
|
||||
score: 12,
|
||||
name: 'Tom',
|
||||
};
|
||||
|
||||
const q1 = {
|
||||
objectId: {
|
||||
$eq: 'Person2',
|
||||
},
|
||||
};
|
||||
|
||||
const q2 = {
|
||||
score: {
|
||||
$eq: 12,
|
||||
},
|
||||
};
|
||||
|
||||
const q3 = {
|
||||
name: {
|
||||
$eq: 'Tom',
|
||||
},
|
||||
};
|
||||
expect(matchesQuery(obj, q1)).toBe(true);
|
||||
expect(matchesQuery(obj, q2)).toBe(true);
|
||||
expect(matchesQuery(obj, q3)).toBe(true);
|
||||
});
|
||||
|
||||
it('matches on equality queries', function () {
|
||||
const day = new Date();
|
||||
const location = new Parse.GeoPoint({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default;
|
||||
describe('rate limit', () => {
|
||||
it('can limit cloud functions', async () => {
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
@@ -334,6 +335,99 @@ describe('rate limit', () => {
|
||||
await Parse.Cloud.run('test2');
|
||||
});
|
||||
|
||||
describe('zone', () => {
|
||||
const middlewares = require('../lib/middlewares');
|
||||
it('can use global zone', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: {
|
||||
requestPath: '*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
zone: Parse.Server.RateLimitZone.global,
|
||||
},
|
||||
});
|
||||
const fakeReq = {
|
||||
originalUrl: 'http://example.com/parse/',
|
||||
url: 'http://example.com/',
|
||||
body: {
|
||||
_ApplicationId: 'test',
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
get: key => {
|
||||
return fakeReq.headers[key];
|
||||
},
|
||||
};
|
||||
fakeReq.ip = '127.0.0.1';
|
||||
let fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status', 'setHeader', 'json']);
|
||||
await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve));
|
||||
fakeReq.ip = '127.0.0.2';
|
||||
fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status', 'setHeader']);
|
||||
let resolvingPromise;
|
||||
const promise = new Promise(resolve => {
|
||||
resolvingPromise = resolve;
|
||||
});
|
||||
fakeRes.json = jasmine.createSpy('json').and.callFake(resolvingPromise);
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
throw 'Should not call next';
|
||||
});
|
||||
await promise;
|
||||
expect(fakeRes.status).toHaveBeenCalledWith(429);
|
||||
expect(fakeRes.json).toHaveBeenCalledWith({
|
||||
code: Parse.Error.CONNECTION_FAILED,
|
||||
error: 'Too many requests',
|
||||
});
|
||||
});
|
||||
|
||||
it('can use session zone', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: {
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
zone: Parse.Server.RateLimitZone.session,
|
||||
},
|
||||
});
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await Parse.User.signUp('username', 'password');
|
||||
await Parse.Cloud.run('test');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
await Parse.User.logIn('username', 'password');
|
||||
await Parse.Cloud.run('test');
|
||||
});
|
||||
|
||||
it('can use user zone', async () => {
|
||||
await reconfigureServer({
|
||||
rateLimit: {
|
||||
requestPath: '/functions/*',
|
||||
requestTimeWindow: 10000,
|
||||
requestCount: 1,
|
||||
errorResponseMessage: 'Too many requests',
|
||||
includeInternalRequests: true,
|
||||
zone: Parse.Server.RateLimitZone.user,
|
||||
},
|
||||
});
|
||||
Parse.Cloud.define('test', () => 'Abc');
|
||||
await Parse.User.signUp('username', 'password');
|
||||
await Parse.Cloud.run('test');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
await Parse.User.logIn('username', 'password');
|
||||
await expectAsync(Parse.Cloud.run('test')).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.CONNECTION_FAILED, 'Too many requests')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('can validate rateLimit', async () => {
|
||||
const Config = require('../lib/Config');
|
||||
const validateRateLimit = ({ rateLimit }) => Config.validateRateLimit(rateLimit);
|
||||
@@ -349,6 +443,11 @@ describe('rate limit', () => {
|
||||
expect(() =>
|
||||
validateRateLimit({ rateLimit: [{ requestTimeWindow: [], requestPath: 'a' }] })
|
||||
).toThrow('rateLimit.requestTimeWindow must be a number');
|
||||
expect(() =>
|
||||
validateRateLimit({
|
||||
rateLimit: [{ requestPath: 'a', requestTimeWindow: 1000, requestCount: 3, zone: 'abc' }],
|
||||
})
|
||||
).toThrow('rateLimit.zone must be one of global, session, user, or ip');
|
||||
expect(() =>
|
||||
validateRateLimit({
|
||||
rateLimit: [
|
||||
@@ -388,4 +487,33 @@ describe('rate limit', () => {
|
||||
})
|
||||
).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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,4 +167,18 @@ describe_only(() => {
|
||||
.then(() => expect(getQueueCount(cache)).toEqual(0))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('should start and connect cache adapter', async () => {
|
||||
const server = await reconfigureServer({
|
||||
cacheAdapter: {
|
||||
module: `${__dirname.replace('/spec', '')}/lib/Adapters/Cache/RedisCacheAdapter`,
|
||||
options: {
|
||||
url: 'redis://127.0.0.1:6379/1',
|
||||
},
|
||||
},
|
||||
});
|
||||
const symbol = Object.getOwnPropertySymbols(server.config.cacheController);
|
||||
const client = server.config.cacheController[symbol[0]].client;
|
||||
expect(client.isOpen).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
+24
-20
@@ -399,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);
|
||||
@@ -438,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 => {
|
||||
|
||||
@@ -337,5 +337,33 @@ describe('Security Check', () => {
|
||||
expect(logSpy.calls.all()[0].args[0]).toContain(title);
|
||||
}
|
||||
});
|
||||
|
||||
it('does update featuresRouter', async () => {
|
||||
let response = await request({
|
||||
url: 'http://localhost:8378/1/serverInfo',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
});
|
||||
expect(response.data.features.settings.securityCheck).toBeTrue();
|
||||
await reconfigureServer({
|
||||
security: {
|
||||
enableCheck: false,
|
||||
},
|
||||
});
|
||||
response = await request({
|
||||
url: 'http://localhost:8378/1/serverInfo',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
});
|
||||
expect(response.data.features.settings.securityCheck).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('SessionTokenCache', function () {
|
||||
|
||||
sessionTokenCache.getUserId('sessionToken').then(userIdFromCache => {
|
||||
expect(userIdFromCache).toBe('userId');
|
||||
expect(sessionTokenCache.cache.length).toBe(1);
|
||||
expect(sessionTokenCache.cache.size).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
+50
-29
@@ -1,55 +1,76 @@
|
||||
const UserController = require('../lib/Controllers/UserController').UserController;
|
||||
const emailAdapter = require('./support/MockEmailAdapter');
|
||||
const Config = require('../lib/Config');
|
||||
const Auth = require('../lib/Auth');
|
||||
|
||||
describe('UserController', () => {
|
||||
const user = {
|
||||
_email_verify_token: 'testToken',
|
||||
username: 'testUser',
|
||||
email: 'test@example.com',
|
||||
};
|
||||
|
||||
describe('sendVerificationEmail', () => {
|
||||
describe('parseFrameURL not provided', () => {
|
||||
it('uses publicServerURL', async done => {
|
||||
it('uses publicServerURL', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://www.example.com',
|
||||
customPages: {
|
||||
parseFrameURL: undefined,
|
||||
},
|
||||
});
|
||||
emailAdapter.sendVerificationEmail = options => {
|
||||
expect(options.link).toEqual(
|
||||
'http://www.example.com/apps/test/verify_email?token=testToken&username=testUser'
|
||||
);
|
||||
emailAdapter.sendVerificationEmail = () => Promise.resolve();
|
||||
done();
|
||||
};
|
||||
const userController = new UserController(emailAdapter, 'test', {
|
||||
verifyUserEmails: true,
|
||||
emailAdapter,
|
||||
appName: 'test',
|
||||
});
|
||||
userController.sendVerificationEmail(user);
|
||||
|
||||
let emailOptions;
|
||||
emailAdapter.sendVerificationEmail = options => {
|
||||
emailOptions = options;
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const username = 'verificationUser';
|
||||
const user = new Parse.User();
|
||||
user.setUsername(username);
|
||||
user.setPassword('pass');
|
||||
user.setEmail('verification@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const config = Config.get('test');
|
||||
const rawUser = await config.database.find('_User', { username }, {}, Auth.maintenance(config));
|
||||
const rawUsername = rawUser[0].username;
|
||||
const rawToken = rawUser[0]._email_verify_token;
|
||||
expect(rawToken).toBeDefined();
|
||||
expect(rawUsername).toBe(username);
|
||||
expect(emailOptions.link).toEqual(`http://www.example.com/apps/test/verify_email?token=${rawToken}&username=${username}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseFrameURL provided', () => {
|
||||
it('uses parseFrameURL and includes the destination in the link parameter', async done => {
|
||||
it('uses parseFrameURL and includes the destination in the link parameter', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://www.example.com',
|
||||
customPages: {
|
||||
parseFrameURL: 'http://someother.example.com/handle-parse-iframe',
|
||||
},
|
||||
});
|
||||
emailAdapter.sendVerificationEmail = options => {
|
||||
expect(options.link).toEqual(
|
||||
'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser'
|
||||
);
|
||||
emailAdapter.sendVerificationEmail = () => Promise.resolve();
|
||||
done();
|
||||
};
|
||||
const userController = new UserController(emailAdapter, 'test', {
|
||||
verifyUserEmails: true,
|
||||
emailAdapter,
|
||||
appName: 'test',
|
||||
});
|
||||
userController.sendVerificationEmail(user);
|
||||
|
||||
let emailOptions;
|
||||
emailAdapter.sendVerificationEmail = options => {
|
||||
emailOptions = options;
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const username = 'verificationUser';
|
||||
const user = new Parse.User();
|
||||
user.setUsername(username);
|
||||
user.setPassword('pass');
|
||||
user.setEmail('verification@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const config = Config.get('test');
|
||||
const rawUser = await config.database.find('_User', { username }, {}, Auth.maintenance(config));
|
||||
const rawUsername = rawUser[0].username;
|
||||
const rawToken = rawUser[0]._email_verify_token;
|
||||
expect(rawToken).toBeDefined();
|
||||
expect(rawUsername).toBe(username);
|
||||
expect(emailOptions.link).toEqual(`http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=${rawToken}&username=${username}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.setUsername('zxcv');
|
||||
user.setEmail('testIfEnabled@parse.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
|
||||
user.fetch().then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
@@ -137,6 +138,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
spyOn(emailAdapter, 'sendVerificationEmail').and.callFake(options => {
|
||||
expect(options.link).not.toBeNull();
|
||||
expect(options.link).not.toMatch(/token=undefined/);
|
||||
expect(options.link).not.toMatch(/username=undefined/);
|
||||
Promise.resolve();
|
||||
});
|
||||
const user = new Parse.User();
|
||||
@@ -183,6 +185,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.setUsername('zxcv');
|
||||
user.set('email', 'testSendSimpleAdapter@parse.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(calls).toBe(1);
|
||||
user
|
||||
.fetch()
|
||||
@@ -242,8 +245,67 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows user to login only after user clicks on the link to confirm email address if preventLoginWithUnverifiedEmail is set to true', done => {
|
||||
it('prevents user from signup and login if email is not verified and preventLoginWithUnverifiedEmail is set to function returning true', async () => {
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
verifyUserEmails: async () => true,
|
||||
preventLoginWithUnverifiedEmail: async () => true,
|
||||
preventSignupWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setPassword('asdf');
|
||||
user.setUsername('zxcv');
|
||||
user.set('email', 'testInvalidConfig@parse.com');
|
||||
const signupRes = await user.signUp(null).catch(e => e);
|
||||
expect(signupRes.message).toEqual('User email is not verified.');
|
||||
|
||||
const loginRes = await Parse.User.logIn('zxcv', 'asdf').catch(e => e);
|
||||
expect(loginRes.message).toEqual('User email is not verified.');
|
||||
});
|
||||
|
||||
it('provides function arguments in verifyUserEmails on login', async () => {
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('pass');
|
||||
user.set('email', 'test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const verifyUserEmails = {
|
||||
method: async (params) => {
|
||||
expect(params.object).toBeInstanceOf(Parse.User);
|
||||
expect(params.ip).toBeDefined();
|
||||
expect(params.master).toBeDefined();
|
||||
expect(params.installationId).toBeDefined();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const verifyUserEmailsSpy = spyOn(verifyUserEmails, 'method').and.callThrough();
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
verifyUserEmails: verifyUserEmails.method,
|
||||
preventLoginWithUnverifiedEmail: verifyUserEmails.method,
|
||||
preventSignupWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
const res = await Parse.User.logIn('user', 'pass').catch(e => e);
|
||||
expect(res.code).toBe(205);
|
||||
expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('allows user to login only after user clicks on the link to confirm email address if preventLoginWithUnverifiedEmail is set to true', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
@@ -252,59 +314,33 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
reconfigureServer({
|
||||
await reconfigureServer({
|
||||
appName: 'emailing app',
|
||||
verifyUserEmails: true,
|
||||
preventLoginWithUnverifiedEmail: true,
|
||||
emailAdapter: emailAdapter,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
})
|
||||
.then(() => {
|
||||
user.setPassword('other-password');
|
||||
user.setUsername('user');
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
expect(sendEmailOptions).not.toBeUndefined();
|
||||
request({
|
||||
url: sendEmailOptions.link,
|
||||
followRedirects: false,
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user'
|
||||
);
|
||||
user
|
||||
.fetch({ useMasterKey: true })
|
||||
.then(
|
||||
() => {
|
||||
expect(user.get('emailVerified')).toEqual(true);
|
||||
|
||||
Parse.User.logIn('user', 'other-password').then(
|
||||
user => {
|
||||
expect(typeof user).toBe('object');
|
||||
expect(user.get('emailVerified')).toBe(true);
|
||||
done();
|
||||
},
|
||||
() => {
|
||||
fail('login should have succeeded');
|
||||
done();
|
||||
}
|
||||
);
|
||||
},
|
||||
err => {
|
||||
jfail(err);
|
||||
fail('this should not fail');
|
||||
done();
|
||||
}
|
||||
)
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
let user = new Parse.User();
|
||||
user.setPassword('other-password');
|
||||
user.setUsername('user');
|
||||
user.set('email', 'user@example.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(sendEmailOptions).not.toBeUndefined();
|
||||
const response = await request({
|
||||
url: sendEmailOptions.link,
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user'
|
||||
);
|
||||
user = await new Parse.Query(Parse.User).first({ useMasterKey: true });
|
||||
expect(user.get('emailVerified')).toEqual(true);
|
||||
user = await Parse.User.logIn('user', 'other-password');
|
||||
expect(typeof user).toBe('object');
|
||||
expect(user.get('emailVerified')).toBe(true);
|
||||
});
|
||||
|
||||
it('allows user to login if email is not verified but preventLoginWithUnverifiedEmail is set to false', done => {
|
||||
@@ -345,6 +381,35 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not allow signup with preventSignupWithUnverified', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
appName: 'test',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
verifyUserEmails: true,
|
||||
preventLoginWithUnverifiedEmail: true,
|
||||
preventSignupWithUnverifiedEmail: true,
|
||||
emailAdapter,
|
||||
});
|
||||
const newUser = new Parse.User();
|
||||
newUser.setPassword('asdf');
|
||||
newUser.setUsername('zxcv');
|
||||
newUser.set('email', 'test@example.com');
|
||||
await expectAsync(newUser.signUp()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.')
|
||||
);
|
||||
const user = await new Parse.Query(Parse.User).first({ useMasterKey: true });
|
||||
expect(user).toBeDefined();
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
});
|
||||
|
||||
it('fails if you include an emailAdapter, set a publicServerURL, but have no appName and send a password reset email', done => {
|
||||
reconfigureServer({
|
||||
appName: undefined,
|
||||
@@ -573,6 +638,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.setUsername('zxcv');
|
||||
user.set('email', 'user@parse.com');
|
||||
await user.signUp();
|
||||
await jasmine.timeout();
|
||||
expect(emailSent).toBe(true);
|
||||
done();
|
||||
});
|
||||
@@ -600,6 +666,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => jasmine.timeout())
|
||||
.then(() => {
|
||||
expect(sendEmailOptions).not.toBeUndefined();
|
||||
request({
|
||||
|
||||
@@ -353,8 +353,9 @@ describe('Verify User Password', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('fails to verify password when preventLoginWithUnverifiedEmail is set to true REST API', done => {
|
||||
reconfigureServer({
|
||||
|
||||
it('fails to verify password when preventLoginWithUnverifiedEmail is set to true REST API', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://localhost:8378/',
|
||||
appName: 'emailVerify',
|
||||
verifyUserEmails: true,
|
||||
@@ -364,28 +365,21 @@ describe('Verify User Password', () => {
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
})
|
||||
.then(() => {
|
||||
const user = new Parse.User();
|
||||
return user.save({
|
||||
username: 'unverified-user',
|
||||
password: 'mypass',
|
||||
email: 'unverified-email@user.com',
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return verifyPassword('unverified-email@user.com', 'mypass', true);
|
||||
})
|
||||
.then(res => {
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.text).toMatch('{"code":205,"error":"User email is not verified."}');
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
fail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
const user = new Parse.User();
|
||||
await user.save({
|
||||
username: 'unverified-user',
|
||||
password: 'mypass',
|
||||
email: 'unverified-email@example.com',
|
||||
});
|
||||
const res = await verifyPassword('unverified-email@example.com', 'mypass', true);
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.data).toEqual({
|
||||
code: Parse.Error.EMAIL_NOT_FOUND,
|
||||
error: 'User email is not verified.',
|
||||
});
|
||||
});
|
||||
|
||||
it('verify password lock account if failed verify password attempts are above threshold', done => {
|
||||
reconfigureServer({
|
||||
appName: 'lockout threshold',
|
||||
@@ -591,4 +585,83 @@ describe('Verify User Password', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('verify password of user with unverified email with master key and ignoreEmailVerification=true', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://localhost:8378/',
|
||||
appName: 'emailVerify',
|
||||
verifyUserEmails: true,
|
||||
preventLoginWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('pass');
|
||||
user.setEmail('test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const { data: res } = await request({
|
||||
method: 'POST',
|
||||
url: Parse.serverURL + '/verifyPassword',
|
||||
headers: {
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
ignoreEmailVerification: true,
|
||||
},
|
||||
json: true,
|
||||
});
|
||||
expect(res.objectId).toBe(user.id);
|
||||
expect(Object.prototype.hasOwnProperty.call(res, 'sessionToken')).toEqual(false);
|
||||
expect(Object.prototype.hasOwnProperty.call(res, 'password')).toEqual(false);
|
||||
});
|
||||
|
||||
it('fails to verify password of user with unverified email with master key and ignoreEmailVerification=false', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://localhost:8378/',
|
||||
appName: 'emailVerify',
|
||||
verifyUserEmails: true,
|
||||
preventLoginWithUnverifiedEmail: true,
|
||||
emailAdapter: MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
}),
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('user');
|
||||
user.setPassword('pass');
|
||||
user.setEmail('test@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const res = await request({
|
||||
method: 'POST',
|
||||
url: Parse.serverURL + '/verifyPassword',
|
||||
headers: {
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
ignoreEmailVerification: false,
|
||||
},
|
||||
json: true,
|
||||
}).catch(e => e);
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.text).toMatch(/User email is not verified/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"appName": "test",
|
||||
"appId": "test",
|
||||
"masterKey": "test",
|
||||
"logLevel": "error",
|
||||
"auth": {
|
||||
"facebook": {
|
||||
"appIds": "test"
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
-13
@@ -35,6 +35,7 @@ process.noDeprecation = true;
|
||||
const cache = require('../lib/cache').default;
|
||||
const defaults = require('../lib/defaults').default;
|
||||
const ParseServer = require('../lib/index').ParseServer;
|
||||
const loadAdapter = require('../lib/Adapters/AdapterLoader').loadAdapter;
|
||||
const path = require('path');
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
|
||||
@@ -53,7 +54,10 @@ let databaseAdapter;
|
||||
let databaseURI;
|
||||
// need to bind for mocking mocha
|
||||
|
||||
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
|
||||
if (process.env.PARSE_SERVER_DATABASE_ADAPTER) {
|
||||
databaseAdapter = JSON.parse(process.env.PARSE_SERVER_DATABASE_ADAPTER);
|
||||
databaseAdapter = loadAdapter(databaseAdapter);
|
||||
} else if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
|
||||
databaseURI = process.env.PARSE_SERVER_TEST_DATABASE_URI || postgresURI;
|
||||
databaseAdapter = new PostgresStorageAdapter({
|
||||
uri: databaseURI,
|
||||
@@ -103,11 +107,17 @@ const defaultConfiguration = {
|
||||
restAPIKey: 'rest',
|
||||
webhookKey: 'hook',
|
||||
masterKey: 'test',
|
||||
maintenanceKey: 'testing',
|
||||
readOnlyMasterKey: 'read-only-test',
|
||||
fileKey: 'test',
|
||||
directAccess: true,
|
||||
silent,
|
||||
verbose: !silent,
|
||||
logLevel,
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
fileUpload: {
|
||||
enableForPublic: true,
|
||||
enableForAnonymousUser: true,
|
||||
@@ -128,8 +138,20 @@ const defaultConfiguration = {
|
||||
},
|
||||
shortLivedAuth: mockShortLivedAuth(),
|
||||
},
|
||||
allowClientClassCreation: true,
|
||||
encodeParseObjectInCloudFunction: true,
|
||||
};
|
||||
|
||||
if (silent) {
|
||||
defaultConfiguration.logLevels = {
|
||||
cloudFunctionSuccess: 'silent',
|
||||
cloudFunctionError: 'silent',
|
||||
triggerAfter: 'silent',
|
||||
triggerBeforeError: 'silent',
|
||||
triggerBeforeSuccess: 'silent',
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
|
||||
defaultConfiguration.cacheAdapter = new RedisCacheAdapter();
|
||||
}
|
||||
@@ -146,15 +168,15 @@ const destroyAliveConnections = function () {
|
||||
}
|
||||
};
|
||||
// Set up a default API server for testing with default configuration.
|
||||
let server;
|
||||
|
||||
let parseServer;
|
||||
let didChangeConfiguration = false;
|
||||
|
||||
// Allows testing specific configurations of Parse Server
|
||||
const reconfigureServer = async (changedConfiguration = {}) => {
|
||||
if (server) {
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
server = undefined;
|
||||
if (parseServer) {
|
||||
destroyAliveConnections();
|
||||
await new Promise(resolve => parseServer.server.close(resolve));
|
||||
parseServer = undefined;
|
||||
return reconfigureServer(changedConfiguration);
|
||||
}
|
||||
didChangeConfiguration = Object.keys(changedConfiguration).length !== 0;
|
||||
@@ -163,14 +185,20 @@ const reconfigureServer = async (changedConfiguration = {}) => {
|
||||
port,
|
||||
});
|
||||
cache.clear();
|
||||
const parseServer = await ParseServer.startApp(newConfiguration);
|
||||
server = parseServer.server;
|
||||
parseServer = await ParseServer.startApp(newConfiguration);
|
||||
Parse.CoreManager.setRESTController(RESTController);
|
||||
parseServer.expressApp.use('/1', err => {
|
||||
console.error(err);
|
||||
fail('should not call next');
|
||||
});
|
||||
server.on('connection', connection => {
|
||||
parseServer.liveQueryServer?.server?.on('connection', connection => {
|
||||
const key = `${connection.remoteAddress}:${connection.remotePort}`;
|
||||
openConnections[key] = connection;
|
||||
connection.on('close', () => {
|
||||
delete openConnections[key];
|
||||
});
|
||||
});
|
||||
parseServer.server.on('connection', connection => {
|
||||
const key = `${connection.remoteAddress}:${connection.remotePort}`;
|
||||
openConnections[key] = connection;
|
||||
connection.on('close', () => {
|
||||
@@ -200,10 +228,10 @@ beforeAll(async () => {
|
||||
|
||||
afterEach(function (done) {
|
||||
const afterLogOut = async () => {
|
||||
if (Object.keys(openConnections).length > 0) {
|
||||
console.warn('There were open connections to the server left after the test finished');
|
||||
// Jasmine process uses one connection
|
||||
if (Object.keys(openConnections).length > 1) {
|
||||
console.warn(`There were ${Object.keys(openConnections).length} open connections to the server left after the test finished`);
|
||||
}
|
||||
destroyAliveConnections();
|
||||
await TestUtils.destroyAllDataPermanently(true);
|
||||
SchemaCache.clear();
|
||||
if (didChangeConfiguration) {
|
||||
@@ -214,6 +242,7 @@ afterEach(function (done) {
|
||||
done();
|
||||
};
|
||||
Parse.Cloud._removeAllHooks();
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient();
|
||||
defaults.protectedFields = { _User: { '*': ['email'] } };
|
||||
databaseAdapter
|
||||
.getAllClasses()
|
||||
@@ -422,6 +451,26 @@ global.it_exclude_dbs = excluded => {
|
||||
}
|
||||
};
|
||||
|
||||
let testExclusionList = [];
|
||||
try {
|
||||
// Fetch test exclusion list
|
||||
testExclusionList = require('./testExclusionList.json');
|
||||
console.log(`Using test exclusion list with ${testExclusionList.length} entries`);
|
||||
} catch (error) {
|
||||
if (error.code !== 'MODULE_NOT_FOUND') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable test if its UUID is found in testExclusionList
|
||||
global.it_id = (id, func) => {
|
||||
if (testExclusionList.includes(id)) {
|
||||
return xit;
|
||||
} else {
|
||||
return func || it;
|
||||
}
|
||||
};
|
||||
|
||||
global.it_only_db = db => {
|
||||
if (
|
||||
process.env.PARSE_SERVER_TEST_DB === db ||
|
||||
@@ -541,6 +590,16 @@ global.describe_only_db = db => {
|
||||
}
|
||||
};
|
||||
|
||||
global.fdescribe_only_db = db => {
|
||||
if (process.env.PARSE_SERVER_TEST_DB == db) {
|
||||
return fdescribe;
|
||||
} else if (!process.env.PARSE_SERVER_TEST_DB && db == 'mongo') {
|
||||
return fdescribe;
|
||||
} else {
|
||||
return xdescribe;
|
||||
}
|
||||
};
|
||||
|
||||
global.describe_only = validator => {
|
||||
if (validator()) {
|
||||
return describe;
|
||||
@@ -566,4 +625,4 @@ jasmine.restoreLibrary = function (library, name) {
|
||||
require(library)[name] = libraryCache[library][name];
|
||||
};
|
||||
|
||||
jasmine.timeout = t => new Promise(resolve => setTimeout(resolve, t));
|
||||
jasmine.timeout = (t = 100) => new Promise(resolve => setTimeout(resolve, t));
|
||||
|
||||
+17
-1
@@ -367,6 +367,22 @@ describe('server', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when extendSessionOnUse is invalid', async () => {
|
||||
await expectAsync(
|
||||
reconfigureServer({
|
||||
extendSessionOnUse: 'yolo',
|
||||
})
|
||||
).toBeRejectedWith('extendSessionOnUse must be a boolean value');
|
||||
});
|
||||
|
||||
it('should throw when revokeSessionOnPasswordReset is invalid', async () => {
|
||||
await expectAsync(
|
||||
reconfigureServer({
|
||||
revokeSessionOnPasswordReset: 'yolo',
|
||||
})
|
||||
).toBeRejectedWith('revokeSessionOnPasswordReset must be a boolean value');
|
||||
});
|
||||
|
||||
it('fails if the session length is not a number', done => {
|
||||
reconfigureServer({ sessionLength: 'test' })
|
||||
.then(done.fail)
|
||||
@@ -558,7 +574,7 @@ describe('server', () => {
|
||||
});
|
||||
|
||||
it('can get starting state', async () => {
|
||||
await reconfigureServer({ appId: 'test2', silent: false });
|
||||
await reconfigureServer({ appId: 'test2' });
|
||||
const parseServer = new ParseServer.ParseServer({
|
||||
...defaultConfiguration,
|
||||
appId: 'test2',
|
||||
|
||||
@@ -136,6 +136,117 @@ describe('rest create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('with maintenance key', () => {
|
||||
let req;
|
||||
|
||||
async function getObject(id) {
|
||||
const res = await request({
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/classes/TestObject/${id}`,
|
||||
});
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Maintenance-Key': 'testing',
|
||||
},
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
};
|
||||
});
|
||||
|
||||
it('allows createdAt', async () => {
|
||||
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
|
||||
req.body = { createdAt };
|
||||
|
||||
const res = await request(req);
|
||||
expect(res.data.createdAt).toEqual(createdAt.iso);
|
||||
});
|
||||
|
||||
it('allows createdAt and updatedAt', async () => {
|
||||
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
|
||||
const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' };
|
||||
req.body = { createdAt, updatedAt };
|
||||
|
||||
const res = await request(req);
|
||||
|
||||
const obj = await getObject(res.data.objectId);
|
||||
expect(obj.createdAt).toEqual(createdAt.iso);
|
||||
expect(obj.updatedAt).toEqual(updatedAt.iso);
|
||||
});
|
||||
|
||||
it('allows createdAt, updatedAt, and additional field', async () => {
|
||||
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
|
||||
const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' };
|
||||
req.body = { createdAt, updatedAt, testing: 123 };
|
||||
|
||||
const res = await request(req);
|
||||
|
||||
const obj = await getObject(res.data.objectId);
|
||||
expect(obj.createdAt).toEqual(createdAt.iso);
|
||||
expect(obj.updatedAt).toEqual(updatedAt.iso);
|
||||
expect(obj.testing).toEqual(123);
|
||||
});
|
||||
|
||||
it('cannot set updatedAt dated before createdAt', async () => {
|
||||
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
|
||||
const updatedAt = { __type: 'Date', iso: '2018-12-01T00:00:00.000Z' };
|
||||
req.body = { createdAt, updatedAt };
|
||||
|
||||
try {
|
||||
await request(req);
|
||||
fail();
|
||||
} catch (err) {
|
||||
expect(err.data.code).toEqual(Parse.Error.VALIDATION_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
it('cannot set updatedAt without createdAt', async () => {
|
||||
const updatedAt = { __type: 'Date', iso: '2018-12-01T00:00:00.000Z' };
|
||||
req.body = { updatedAt };
|
||||
|
||||
const res = await request(req);
|
||||
|
||||
const obj = await getObject(res.data.objectId);
|
||||
expect(obj.updatedAt).not.toEqual(updatedAt.iso);
|
||||
});
|
||||
|
||||
it('handles bad types for createdAt and updatedAt', async () => {
|
||||
const createdAt = 12345;
|
||||
const updatedAt = true;
|
||||
req.body = { createdAt, updatedAt };
|
||||
|
||||
try {
|
||||
await request(req);
|
||||
fail();
|
||||
} catch (err) {
|
||||
expect(err.data.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
||||
}
|
||||
});
|
||||
|
||||
it('cannot set createdAt or updatedAt without maintenance key', async () => {
|
||||
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
|
||||
const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' };
|
||||
req.body = { createdAt, updatedAt };
|
||||
delete req.headers['X-Parse-Maintenance-Key'];
|
||||
|
||||
const res = await request(req);
|
||||
|
||||
expect(res.data.createdAt).not.toEqual(createdAt.iso);
|
||||
expect(res.data.updatedAt).not.toEqual(updatedAt.iso);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles array, object, date', done => {
|
||||
const now = new Date();
|
||||
const obj = {
|
||||
@@ -660,6 +771,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')
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = function (options) {
|
||||
return {
|
||||
options: options,
|
||||
send: function () {},
|
||||
getDatabaseURI: function () {
|
||||
return options.databaseURI;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -2,5 +2,5 @@
|
||||
"spec_dir": "spec",
|
||||
"spec_files": ["*spec.js"],
|
||||
"helpers": ["helper.js"],
|
||||
"random": true
|
||||
"random": false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Create Deferred Promise
|
||||
function resolvingPromise() {
|
||||
let res;
|
||||
let rej;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
res = resolve;
|
||||
rej = reject;
|
||||
});
|
||||
promise.resolve = res;
|
||||
promise.reject = rej;
|
||||
return promise;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
module.exports = { resolvingPromise, sleep };
|
||||
@@ -138,6 +138,71 @@ describe('Vulnerabilities', () => {
|
||||
);
|
||||
});
|
||||
|
||||
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');
|
||||
@@ -170,32 +235,6 @@ describe('Vulnerabilities', () => {
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
});
|
||||
|
||||
it('allows BSON type code data in write request with custom denylist', async () => {
|
||||
await reconfigureServer({
|
||||
requestKeywordDenylist: [],
|
||||
});
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const params = {
|
||||
headers: headers,
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/RCE',
|
||||
body: JSON.stringify({
|
||||
obj: {
|
||||
_bsontype: 'Code',
|
||||
code: 'delete Object.prototype.evalFunctions',
|
||||
},
|
||||
}),
|
||||
};
|
||||
const response = await request(params).catch(e => e);
|
||||
expect(response.status).toBe(201);
|
||||
const text = JSON.parse(response.text);
|
||||
expect(text.objectId).toBeDefined();
|
||||
});
|
||||
|
||||
it('denies write request with custom denylist of key/value', async () => {
|
||||
await reconfigureServer({
|
||||
requestKeywordDenylist: [{ key: 'a[K]ey', value: 'aValue[123]*' }],
|
||||
@@ -394,3 +433,28 @@ describe('Vulnerabilities', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Postgres regex sanitizater', () => {
|
||||
it('sanitizes the regex correctly to prevent Injection', async () => {
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'username');
|
||||
user.set('password', 'password');
|
||||
user.set('email', 'email@example.com');
|
||||
await user.signUp();
|
||||
|
||||
const response = await request({
|
||||
method: 'GET',
|
||||
url:
|
||||
"http://localhost:8378/1/classes/_User?where[username][$regex]=A'B'%3BSELECT+PG_SLEEP(3)%3B--",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.data.results).toEqual(jasmine.any(Array));
|
||||
expect(response.data.results.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,4 +46,21 @@ export function loadAdapter<T>(adapter, defaultAdapter, options): T {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
export async function loadModule(modulePath) {
|
||||
let module;
|
||||
try {
|
||||
module = require(modulePath);
|
||||
} catch (err) {
|
||||
if (err.code === 'ERR_REQUIRE_ESM') {
|
||||
module = await import(modulePath);
|
||||
if (module.default) {
|
||||
module = module.default;
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return module;
|
||||
}
|
||||
|
||||
export default loadAdapter;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/*eslint no-unused-vars: "off"*/
|
||||
/**
|
||||
* @module Adapters
|
||||
*/
|
||||
/**
|
||||
* @interface AnalyticsAdapter
|
||||
* @module Adapters
|
||||
*/
|
||||
export class AnalyticsAdapter {
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,9 @@ export class AuthAdapter {
|
||||
* Usage policy
|
||||
* @type {AuthPolicy}
|
||||
*/
|
||||
this.policy = 'default';
|
||||
if (!this.policy) {
|
||||
this.policy = 'default';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param appIds The specified app IDs in the configuration
|
||||
@@ -93,6 +95,24 @@ export class AuthAdapter {
|
||||
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;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
const jwksClient = require('jwks-rsa');
|
||||
const util = require('util');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authUtils = require('./utils');
|
||||
|
||||
@@ -17,11 +16,9 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
cacheMaxAge,
|
||||
});
|
||||
|
||||
const asyncGetSigningKeyFunction = util.promisify(client.getSigningKey);
|
||||
|
||||
let key;
|
||||
try {
|
||||
key = await asyncGetSigningKeyFunction(keyId);
|
||||
key = await authUtils.getSigningKey(client, keyId);
|
||||
} catch (error) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
const Parse = require('parse/node').Parse;
|
||||
const crypto = require('crypto');
|
||||
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';
|
||||
const TOKEN_ISSUER = 'https://www.facebook.com';
|
||||
|
||||
function getAppSecretPath(authData, options = {}) {
|
||||
const appSecret = options.appSecret;
|
||||
@@ -60,11 +59,9 @@ const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
cacheMaxAge,
|
||||
});
|
||||
|
||||
const asyncGetSigningKeyFunction = util.promisify(client.getSigningKey);
|
||||
|
||||
let key;
|
||||
try {
|
||||
key = await asyncGetSigningKeyFunction(keyId);
|
||||
key = await authUtils.getSigningKey(client, keyId);
|
||||
} catch (error) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
|
||||
@@ -9,6 +9,7 @@ const facebook = require('./facebook');
|
||||
const instagram = require('./instagram');
|
||||
const linkedin = require('./linkedin');
|
||||
const meetup = require('./meetup');
|
||||
import mfa from './mfa';
|
||||
const google = require('./google');
|
||||
const github = require('./github');
|
||||
const twitter = require('./twitter');
|
||||
@@ -44,6 +45,7 @@ const providers = {
|
||||
instagram,
|
||||
linkedin,
|
||||
meetup,
|
||||
mfa,
|
||||
google,
|
||||
github,
|
||||
twitter,
|
||||
@@ -75,7 +77,11 @@ function authDataValidator(provider, adapter, appIds, options) {
|
||||
if (appIds && typeof adapter.validateAppId === 'function') {
|
||||
await Promise.resolve(adapter.validateAppId(appIds, authData, options, requestObject));
|
||||
}
|
||||
if (adapter.policy && !authAdapterPolicies[adapter.policy]) {
|
||||
if (
|
||||
adapter.policy &&
|
||||
!authAdapterPolicies[adapter.policy] &&
|
||||
typeof adapter.policy !== 'function'
|
||||
) {
|
||||
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")'
|
||||
@@ -154,7 +160,8 @@ function loadAuthAdapter(provider, authOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const adapter = defaultAdapter instanceof AuthAdapter ? defaultAdapter : Object.assign({}, defaultAdapter);
|
||||
const adapter =
|
||||
defaultAdapter instanceof AuthAdapter ? defaultAdapter : Object.assign({}, defaultAdapter);
|
||||
const keys = [
|
||||
'validateAuthData',
|
||||
'validateAppId',
|
||||
@@ -162,12 +169,18 @@ function loadAuthAdapter(provider, authOptions) {
|
||||
'validateLogin',
|
||||
'validateUpdate',
|
||||
'challenge',
|
||||
'policy'
|
||||
'validateOptions',
|
||||
'policy',
|
||||
'afterFind',
|
||||
];
|
||||
const defaultAuthAdapter = new AuthAdapter();
|
||||
keys.forEach(key => {
|
||||
const existing = adapter?.[key];
|
||||
if (existing && typeof existing === 'function' && existing.toString() === defaultAuthAdapter[key].toString()) {
|
||||
if (
|
||||
existing &&
|
||||
typeof existing === 'function' &&
|
||||
existing.toString() === defaultAuthAdapter[key].toString()
|
||||
) {
|
||||
adapter[key] = null;
|
||||
}
|
||||
});
|
||||
@@ -184,6 +197,9 @@ function loadAuthAdapter(provider, authOptions) {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (adapter.validateOptions) {
|
||||
adapter.validateOptions(providerOptions);
|
||||
}
|
||||
|
||||
return { adapter, appIds, providerOptions };
|
||||
}
|
||||
@@ -204,9 +220,43 @@ module.exports = function (authOptions = {}, enableAnonymousUsers = true) {
|
||||
return { validator: authDataValidator(provider, adapter, appIds, providerOptions), adapter };
|
||||
};
|
||||
|
||||
const runAfterFind = async (req, authData) => {
|
||||
if (!authData) {
|
||||
return;
|
||||
}
|
||||
const adapters = Object.keys(authData);
|
||||
await Promise.all(
|
||||
adapters.map(async provider => {
|
||||
const authAdapter = getValidatorForProvider(provider);
|
||||
if (!authAdapter) {
|
||||
return;
|
||||
}
|
||||
const { adapter, providerOptions } = authAdapter;
|
||||
const afterFind = adapter.afterFind;
|
||||
if (afterFind && typeof afterFind === 'function') {
|
||||
const requestObject = {
|
||||
ip: req.config.ip,
|
||||
user: req.auth.user,
|
||||
master: req.auth.isMaster,
|
||||
};
|
||||
const result = afterFind.call(
|
||||
adapter,
|
||||
requestObject,
|
||||
authData[provider],
|
||||
providerOptions
|
||||
);
|
||||
if (result) {
|
||||
authData[provider] = result;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return Object.freeze({
|
||||
getValidatorForProvider,
|
||||
setEnableAnonymousUsers,
|
||||
runAfterFind,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ function searchForGroup(client, options, id, resolve, reject) {
|
||||
return reject(new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'LDAP group search failed'));
|
||||
}
|
||||
res.on('searchEntry', entry => {
|
||||
if (entry.object.cn === options.groupCn) {
|
||||
if (entry.pojo.attributes.find(obj => obj.type === 'cn').values.includes(options.groupCn)) {
|
||||
found = true;
|
||||
client.unbind();
|
||||
client.destroy();
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { TOTP, Secret } from 'otpauth';
|
||||
import { randomString } from '../../cryptoUtils';
|
||||
import AuthAdapter from './AuthAdapter';
|
||||
class MFAAdapter extends AuthAdapter {
|
||||
validateOptions(opts) {
|
||||
const validOptions = opts.options;
|
||||
if (!Array.isArray(validOptions)) {
|
||||
throw 'mfa.options must be an array';
|
||||
}
|
||||
this.sms = validOptions.includes('SMS');
|
||||
this.totp = validOptions.includes('TOTP');
|
||||
if (!this.sms && !this.totp) {
|
||||
throw 'mfa.options must include SMS or TOTP';
|
||||
}
|
||||
const digits = opts.digits || 6;
|
||||
const period = opts.period || 30;
|
||||
if (typeof digits !== 'number') {
|
||||
throw 'mfa.digits must be a number';
|
||||
}
|
||||
if (typeof period !== 'number') {
|
||||
throw 'mfa.period must be a number';
|
||||
}
|
||||
if (digits < 4 || digits > 10) {
|
||||
throw 'mfa.digits must be between 4 and 10';
|
||||
}
|
||||
if (period < 10) {
|
||||
throw 'mfa.period must be greater than 10';
|
||||
}
|
||||
const sendSMS = opts.sendSMS;
|
||||
if (this.sms && typeof sendSMS !== 'function') {
|
||||
throw 'mfa.sendSMS callback must be defined when using SMS OTPs';
|
||||
}
|
||||
this.smsCallback = sendSMS;
|
||||
this.digits = digits;
|
||||
this.period = period;
|
||||
this.algorithm = opts.algorithm || 'SHA1';
|
||||
}
|
||||
validateSetUp(mfaData) {
|
||||
if (mfaData.mobile && this.sms) {
|
||||
return this.setupMobileOTP(mfaData.mobile);
|
||||
}
|
||||
if (this.totp) {
|
||||
return this.setupTOTP(mfaData);
|
||||
}
|
||||
throw 'Invalid MFA data';
|
||||
}
|
||||
async validateLogin(loginData, _, req) {
|
||||
const saveResponse = {
|
||||
doNotSave: true,
|
||||
};
|
||||
const token = loginData.token;
|
||||
const auth = req.original.get('authData') || {};
|
||||
const { secret, recovery, mobile, token: saved, expiry } = auth.mfa || {};
|
||||
if (this.sms && mobile) {
|
||||
if (token === 'request') {
|
||||
const { token: sendToken, expiry } = await this.sendSMS(mobile);
|
||||
auth.mfa.token = sendToken;
|
||||
auth.mfa.expiry = expiry;
|
||||
req.object.set('authData', auth);
|
||||
await req.object.save(null, { useMasterKey: true });
|
||||
throw 'Please enter the token';
|
||||
}
|
||||
if (!saved || token !== saved) {
|
||||
throw 'Invalid MFA token 1';
|
||||
}
|
||||
if (new Date() > expiry) {
|
||||
throw 'Invalid MFA token 2';
|
||||
}
|
||||
delete auth.mfa.token;
|
||||
delete auth.mfa.expiry;
|
||||
return {
|
||||
save: auth.mfa,
|
||||
};
|
||||
}
|
||||
if (this.totp) {
|
||||
if (typeof token !== 'string') {
|
||||
throw 'Invalid MFA token';
|
||||
}
|
||||
if (!secret) {
|
||||
return saveResponse;
|
||||
}
|
||||
if (recovery[0] === token || recovery[1] === token) {
|
||||
return saveResponse;
|
||||
}
|
||||
const totp = new TOTP({
|
||||
algorithm: this.algorithm,
|
||||
digits: this.digits,
|
||||
period: this.period,
|
||||
secret: Secret.fromBase32(secret),
|
||||
});
|
||||
const valid = totp.validate({
|
||||
token,
|
||||
});
|
||||
if (valid === null) {
|
||||
throw 'Invalid MFA token';
|
||||
}
|
||||
}
|
||||
return saveResponse;
|
||||
}
|
||||
async validateUpdate(authData, _, req) {
|
||||
if (req.master) {
|
||||
return;
|
||||
}
|
||||
if (authData.mobile && this.sms) {
|
||||
if (!authData.token) {
|
||||
throw 'MFA is already set up on this account';
|
||||
}
|
||||
return this.confirmSMSOTP(authData, req.original.get('authData')?.mfa || {});
|
||||
}
|
||||
if (this.totp) {
|
||||
await this.validateLogin({ token: authData.old }, null, req);
|
||||
return this.validateSetUp(authData);
|
||||
}
|
||||
throw 'Invalid MFA data';
|
||||
}
|
||||
afterFind(req, authData) {
|
||||
if (req.master) {
|
||||
return;
|
||||
}
|
||||
if (this.totp && authData.secret) {
|
||||
return {
|
||||
status: 'enabled',
|
||||
};
|
||||
}
|
||||
if (this.sms && authData.mobile) {
|
||||
return {
|
||||
status: 'enabled',
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'disabled',
|
||||
};
|
||||
}
|
||||
|
||||
policy(req, auth) {
|
||||
if (this.sms && auth?.pending && Object.keys(auth).length === 1) {
|
||||
return 'default';
|
||||
}
|
||||
return 'additional';
|
||||
}
|
||||
|
||||
async setupMobileOTP(mobile) {
|
||||
const { token, expiry } = await this.sendSMS(mobile);
|
||||
return {
|
||||
save: {
|
||||
pending: {
|
||||
[mobile]: {
|
||||
token,
|
||||
expiry,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async sendSMS(mobile) {
|
||||
if (!/^[+]*[(]{0,1}[0-9]{1,3}[)]{0,1}[-\s\./0-9]*$/g.test(mobile)) {
|
||||
throw 'Invalid mobile number.';
|
||||
}
|
||||
let token = '';
|
||||
while (token.length < this.digits) {
|
||||
token += randomString(10).replace(/\D/g, '');
|
||||
}
|
||||
token = token.substring(0, this.digits);
|
||||
await Promise.resolve(this.smsCallback(token, mobile));
|
||||
const expiry = new Date(new Date().getTime() + this.period * 1000);
|
||||
return { token, expiry };
|
||||
}
|
||||
|
||||
async confirmSMSOTP(inputData, authData) {
|
||||
const { mobile, token } = inputData;
|
||||
if (!authData.pending?.[mobile]) {
|
||||
throw 'This number is not pending';
|
||||
}
|
||||
const pendingData = authData.pending[mobile];
|
||||
if (token !== pendingData.token) {
|
||||
throw 'Invalid MFA token';
|
||||
}
|
||||
if (new Date() > pendingData.expiry) {
|
||||
throw 'Invalid MFA token';
|
||||
}
|
||||
delete authData.pending[mobile];
|
||||
authData.mobile = mobile;
|
||||
return {
|
||||
save: authData,
|
||||
};
|
||||
}
|
||||
|
||||
setupTOTP(mfaData) {
|
||||
const { secret, token } = mfaData;
|
||||
if (!secret || !token || secret.length < 20) {
|
||||
throw 'Invalid MFA data';
|
||||
}
|
||||
const totp = new TOTP({
|
||||
algorithm: this.algorithm,
|
||||
digits: this.digits,
|
||||
period: this.period,
|
||||
secret: Secret.fromBase32(secret),
|
||||
});
|
||||
const valid = totp.validate({
|
||||
token,
|
||||
});
|
||||
if (valid === null) {
|
||||
throw 'Invalid MFA token';
|
||||
}
|
||||
const recovery = [randomString(30), randomString(30)];
|
||||
return {
|
||||
response: { recovery: recovery.join(', ') },
|
||||
save: { secret, recovery },
|
||||
};
|
||||
}
|
||||
}
|
||||
export default new MFAAdapter();
|
||||
@@ -1,4 +1,5 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const util = require('util');
|
||||
const Parse = require('parse/node').Parse;
|
||||
const getHeaderFromToken = token => {
|
||||
const decodedToken = jwt.decode(token, { complete: true });
|
||||
@@ -8,6 +9,16 @@ const getHeaderFromToken = token => {
|
||||
|
||||
return decodedToken.header;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the signing key from a JWKS client.
|
||||
* @param {Object} client The JWKS client.
|
||||
* @param {String} key The kid.
|
||||
*/
|
||||
async function getSigningKey(client, key) {
|
||||
return util.promisify(client.getSigningKey)(key);
|
||||
}
|
||||
module.exports = {
|
||||
getHeaderFromToken,
|
||||
getSigningKey,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/*eslint no-unused-vars: "off"*/
|
||||
/**
|
||||
* @module Adapters
|
||||
*/
|
||||
/**
|
||||
* @interface CacheAdapter
|
||||
* @interface
|
||||
* @memberof module:Adapters
|
||||
*/
|
||||
export class CacheAdapter {
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LRU from 'lru-cache';
|
||||
import { LRUCache as LRU } from 'lru-cache';
|
||||
import defaults from '../../defaults';
|
||||
|
||||
export class LRUCache {
|
||||
|
||||
@@ -17,6 +17,10 @@ export class RedisCacheAdapter {
|
||||
this.ttl = isValidTTL(ttl) ? ttl : DEFAULT_REDIS_TTL;
|
||||
this.client = createClient(redisCtx);
|
||||
this.queue = new KeyPromiseQueue();
|
||||
this.client.on('error', err => { logger.error('RedisCacheAdapter client error', { error: err }) });
|
||||
this.client.on('connect', () => {});
|
||||
this.client.on('reconnecting', () => {});
|
||||
this.client.on('ready', () => {});
|
||||
}
|
||||
|
||||
async connect() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,13 +28,21 @@ export class GridFSBucketAdapter extends FilesAdapter {
|
||||
this._algorithm = 'aes-256-gcm';
|
||||
this._encryptionKey =
|
||||
encryptionKey !== undefined
|
||||
? crypto.createHash('sha256').update(String(encryptionKey)).digest('base64').substr(0, 32)
|
||||
? crypto
|
||||
.createHash('sha256')
|
||||
.update(String(encryptionKey))
|
||||
.digest('base64')
|
||||
.substring(0, 32)
|
||||
: null;
|
||||
const defaultMongoOptions = {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
};
|
||||
this._mongoOptions = Object.assign(defaultMongoOptions, mongoOptions);
|
||||
const _mongoOptions = Object.assign(defaultMongoOptions, mongoOptions);
|
||||
for (const key of ['enableSchemaHooks', 'schemaCacheTtl', 'maxTimeMS']) {
|
||||
delete _mongoOptions[key];
|
||||
}
|
||||
this._mongoOptions = _mongoOptions;
|
||||
}
|
||||
|
||||
_connect() {
|
||||
@@ -134,8 +142,8 @@ export class GridFSBucketAdapter extends FilesAdapter {
|
||||
}
|
||||
|
||||
async rotateEncryptionKey(options = {}) {
|
||||
var fileNames = [];
|
||||
var oldKeyFileAdapter = {};
|
||||
let fileNames = [];
|
||||
let oldKeyFileAdapter = {};
|
||||
const bucket = await this._getBucket();
|
||||
if (options.oldKey !== undefined) {
|
||||
oldKeyFileAdapter = new GridFSBucketAdapter(
|
||||
@@ -154,51 +162,22 @@ export class GridFSBucketAdapter extends FilesAdapter {
|
||||
fileNames.push(file.filename);
|
||||
});
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
var fileNamesNotRotated = fileNames;
|
||||
var fileNamesRotated = [];
|
||||
var fileNameTotal = fileNames.length;
|
||||
var fileNameIndex = 0;
|
||||
fileNames.forEach(fileName => {
|
||||
oldKeyFileAdapter
|
||||
.getFileData(fileName)
|
||||
.then(plainTextData => {
|
||||
//Overwrite file with data encrypted with new key
|
||||
this.createFile(fileName, plainTextData)
|
||||
.then(() => {
|
||||
fileNamesRotated.push(fileName);
|
||||
fileNamesNotRotated = fileNamesNotRotated.filter(function (value) {
|
||||
return value !== fileName;
|
||||
});
|
||||
fileNameIndex += 1;
|
||||
if (fileNameIndex == fileNameTotal) {
|
||||
resolve({
|
||||
rotated: fileNamesRotated,
|
||||
notRotated: fileNamesNotRotated,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
fileNameIndex += 1;
|
||||
if (fileNameIndex == fileNameTotal) {
|
||||
resolve({
|
||||
rotated: fileNamesRotated,
|
||||
notRotated: fileNamesNotRotated,
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
fileNameIndex += 1;
|
||||
if (fileNameIndex == fileNameTotal) {
|
||||
resolve({
|
||||
rotated: fileNamesRotated,
|
||||
notRotated: fileNamesNotRotated,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
let fileNamesNotRotated = fileNames;
|
||||
const fileNamesRotated = [];
|
||||
for (const fileName of fileNames) {
|
||||
try {
|
||||
const plainTextData = await oldKeyFileAdapter.getFileData(fileName);
|
||||
// Overwrite file with data encrypted with new key
|
||||
await this.createFile(fileName, plainTextData);
|
||||
fileNamesRotated.push(fileName);
|
||||
fileNamesNotRotated = fileNamesNotRotated.filter(function (value) {
|
||||
return value !== fileName;
|
||||
});
|
||||
} catch (err) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return { rotated: fileNamesRotated, notRotated: fileNamesNotRotated };
|
||||
}
|
||||
|
||||
getFileLocation(config, filename) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/*eslint no-unused-vars: "off"*/
|
||||
/**
|
||||
* @module Adapters
|
||||
*/
|
||||
/**
|
||||
* @interface LoggerAdapter
|
||||
* @interface
|
||||
* @memberof module:Adapters
|
||||
* Logger Adapter
|
||||
* Allows you to change the logger mechanism
|
||||
* Default is WinstonLoggerAdapter.js
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/*eslint no-unused-vars: "off"*/
|
||||
/**
|
||||
* @module Adapters
|
||||
*/
|
||||
/**
|
||||
* @interface PubSubAdapter
|
||||
* @interface
|
||||
* @memberof module:Adapters
|
||||
*/
|
||||
export class PubSubAdapter {
|
||||
/**
|
||||
|
||||
@@ -12,10 +12,8 @@
|
||||
// android push and APNS for ios push.
|
||||
|
||||
/**
|
||||
* @module Adapters
|
||||
*/
|
||||
/**
|
||||
* @interface PushAdapter
|
||||
* @interface
|
||||
* @memberof module:Adapters
|
||||
*/
|
||||
export class PushAdapter {
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,18 @@ export default class MongoCollection {
|
||||
// idea. Or even if this behavior is a good idea.
|
||||
find(
|
||||
query,
|
||||
{ skip, limit, sort, keys, maxTimeMS, readPreference, hint, caseInsensitive, explain } = {}
|
||||
{
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
keys,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
} = {}
|
||||
) {
|
||||
// Support for Full Text Search - $text
|
||||
if (keys && keys.$score) {
|
||||
@@ -32,6 +43,7 @@ export default class MongoCollection {
|
||||
hint,
|
||||
caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
}).catch(error => {
|
||||
// Check for "no geoindex" error
|
||||
if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) {
|
||||
@@ -60,6 +72,7 @@ export default class MongoCollection {
|
||||
hint,
|
||||
caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -75,7 +88,18 @@ export default class MongoCollection {
|
||||
|
||||
_rawFind(
|
||||
query,
|
||||
{ skip, limit, sort, keys, maxTimeMS, readPreference, hint, caseInsensitive, explain } = {}
|
||||
{
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
keys,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
} = {}
|
||||
) {
|
||||
let findOperation = this._mongoCollection.find(query, {
|
||||
skip,
|
||||
@@ -83,6 +107,7 @@ export default class MongoCollection {
|
||||
sort,
|
||||
readPreference,
|
||||
hint,
|
||||
comment,
|
||||
});
|
||||
|
||||
if (keys) {
|
||||
@@ -100,7 +125,7 @@ export default class MongoCollection {
|
||||
return explain ? findOperation.explain(explain) : findOperation.toArray();
|
||||
}
|
||||
|
||||
count(query, { skip, limit, sort, maxTimeMS, readPreference, hint } = {}) {
|
||||
count(query, { skip, limit, sort, maxTimeMS, readPreference, hint, comment } = {}) {
|
||||
// If query is empty, then use estimatedDocumentCount instead.
|
||||
// This is due to countDocuments performing a scan,
|
||||
// which greatly increases execution time when being run on large collections.
|
||||
@@ -118,6 +143,7 @@ export default class MongoCollection {
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
comment,
|
||||
});
|
||||
|
||||
return countOperation;
|
||||
@@ -127,9 +153,9 @@ export default class MongoCollection {
|
||||
return this._mongoCollection.distinct(field, query);
|
||||
}
|
||||
|
||||
aggregate(pipeline, { maxTimeMS, readPreference, hint, explain } = {}) {
|
||||
aggregate(pipeline, { maxTimeMS, readPreference, hint, explain, comment } = {}) {
|
||||
return this._mongoCollection
|
||||
.aggregate(pipeline, { maxTimeMS, readPreference, hint, explain })
|
||||
.aggregate(pipeline, { maxTimeMS, readPreference, hint, explain, comment })
|
||||
.toArray();
|
||||
}
|
||||
|
||||
@@ -160,18 +186,10 @@ export default class MongoCollection {
|
||||
}
|
||||
|
||||
_ensureSparseUniqueIndexInBackground(indexRequest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._mongoCollection.createIndex(
|
||||
indexRequest,
|
||||
{ unique: true, background: true, sparse: true },
|
||||
error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
return this._mongoCollection.createIndex(indexRequest, {
|
||||
unique: true,
|
||||
background: true,
|
||||
sparse: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,6 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// parsing and re-formatting causes the auth value (if there) to get URI
|
||||
// encoded
|
||||
const encodedUri = formatUrl(parseUrl(this._uri));
|
||||
|
||||
this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions)
|
||||
.then(client => {
|
||||
// Starting mongoDB 3.0, the MongoClient.connect don't return a DB anymore but a client
|
||||
@@ -212,11 +211,12 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
throw error;
|
||||
}
|
||||
|
||||
handleShutdown() {
|
||||
async handleShutdown() {
|
||||
if (!this.client) {
|
||||
return Promise.resolve();
|
||||
return;
|
||||
}
|
||||
return this.client.close(false);
|
||||
await this.client.close(false);
|
||||
delete this.connectionPromise;
|
||||
}
|
||||
|
||||
_adaptiveCollection(name: string) {
|
||||
@@ -603,7 +603,17 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
{ skip, limit, sort, keys, readPreference, hint, caseInsensitive, explain }: QueryOptions
|
||||
{
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
keys,
|
||||
readPreference,
|
||||
hint,
|
||||
caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
}: QueryOptions
|
||||
): Promise<any> {
|
||||
validateExplainValue(explain);
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
@@ -646,6 +656,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
hint,
|
||||
caseInsensitive,
|
||||
explain,
|
||||
comment,
|
||||
})
|
||||
)
|
||||
.then(objects => {
|
||||
@@ -686,13 +697,8 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
};
|
||||
|
||||
return this._adaptiveCollection(className)
|
||||
.then(
|
||||
collection =>
|
||||
new Promise((resolve, reject) =>
|
||||
collection._mongoCollection.createIndex(indexCreationRequest, indexOptions, error =>
|
||||
error ? reject(error) : resolve()
|
||||
)
|
||||
)
|
||||
.then(collection =>
|
||||
collection._mongoCollection.createIndex(indexCreationRequest, indexOptions)
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
@@ -740,7 +746,9 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
readPreference: ?string,
|
||||
hint: ?mixed
|
||||
_estimate: ?boolean,
|
||||
hint: ?mixed,
|
||||
comment: ?string
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
readPreference = this._parseReadPreference(readPreference);
|
||||
@@ -750,6 +758,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
comment,
|
||||
})
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
@@ -782,7 +791,8 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
pipeline: any,
|
||||
readPreference: ?string,
|
||||
hint: ?mixed,
|
||||
explain?: boolean
|
||||
explain?: boolean,
|
||||
comment: ?string
|
||||
) {
|
||||
validateExplainValue(explain);
|
||||
let isPointerField = false;
|
||||
@@ -816,6 +826,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
hint,
|
||||
explain,
|
||||
comment,
|
||||
})
|
||||
)
|
||||
.then(results => {
|
||||
|
||||
@@ -457,6 +457,7 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) =>
|
||||
);
|
||||
}
|
||||
value = mapValues(restValue, transformInteriorValue);
|
||||
|
||||
return { key: restKey, value };
|
||||
};
|
||||
|
||||
@@ -986,6 +987,13 @@ function transformUpdateOperator({ __op, amount, objects }, flatten) {
|
||||
return { __op: '$inc', arg: amount };
|
||||
}
|
||||
|
||||
case 'SetOnInsert':
|
||||
if (flatten) {
|
||||
return amount;
|
||||
} else {
|
||||
return { __op: '$setOnInsert', arg: amount };
|
||||
}
|
||||
|
||||
case 'Add':
|
||||
case 'AddUnique':
|
||||
if (!(objects instanceof Array)) {
|
||||
|
||||
@@ -58,7 +58,7 @@ function getDatabaseOptionsFromURI(uri) {
|
||||
databaseOptions.fallback_application_name = queryParams.fallback_application_name;
|
||||
|
||||
if (queryParams.poolSize) {
|
||||
databaseOptions.poolSize = parseInt(queryParams.poolSize) || 10;
|
||||
databaseOptions.max = parseInt(queryParams.poolSize) || 10;
|
||||
}
|
||||
if (queryParams.max) {
|
||||
databaseOptions.max = parseInt(queryParams.max) || 10;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user