mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed0baa87af | ||
|
|
ba2b0a9cb9 | ||
|
|
a8aef820af | ||
|
|
af4a0417a9 | ||
|
|
852bb4782a | ||
|
|
9c414804ac | ||
|
|
ff5b39123b | ||
|
|
2aeae76d80 | ||
|
|
065141f00e | ||
|
|
0575952509 | ||
|
|
bc07a3ff3a | ||
|
|
c5cf282d11 | ||
|
|
e2333ba262 | ||
|
|
892c6f94d5 | ||
|
|
0d6f9e951d | ||
|
|
765cfd02dc | ||
|
|
ccd3d86137 | ||
|
|
693d72060d | ||
|
|
2b7f772a1b | ||
|
|
443a509905 | ||
|
|
2830021990 | ||
|
|
d2d12d3d69 | ||
|
|
1099ebbcd8 | ||
|
|
4c50c4d11c | ||
|
|
e9d23830a3 | ||
|
|
01096b360a | ||
|
|
effed92cab | ||
|
|
9cdc6ca5b3 | ||
|
|
158a974c89 | ||
|
|
1593575a87 | ||
|
|
ef9ee66640 | ||
|
|
d5db318db8 | ||
|
|
9fa80eb4ed | ||
|
|
ff16839450 | ||
|
|
0c1b75fcbe | ||
|
|
69781ce0cc | ||
|
|
2eebc68f21 | ||
|
|
79e00e1dee | ||
|
|
46c9a91627 | ||
|
|
33dcf6dd7b | ||
|
|
1f6e19fcce | ||
|
|
b2a2a7e1f5 | ||
|
|
50072bdd38 | ||
|
|
2dceec7359 | ||
|
|
f5ef2e9162 | ||
|
|
23a3488f15 | ||
|
|
3e68491df0 | ||
|
|
5ace99d542 | ||
|
|
9b344da9d0 | ||
|
|
e569f402b1 | ||
|
|
972b800ae4 | ||
|
|
6bc021ee14 | ||
|
|
f235412c1b | ||
|
|
d35cd47c08 | ||
|
|
971adb5438 | ||
|
|
8a126fc1a2 | ||
|
|
ad5d17953b | ||
|
|
5ca2288823 | ||
|
|
9806912045 | ||
|
|
4af253d1f8 | ||
|
|
2a6cec173e | ||
|
|
c7012012aa | ||
|
|
8cece4a2a8 | ||
|
|
7a30d7c885 | ||
|
|
b6a20d9e9f | ||
|
|
82c3015dc0 | ||
|
|
a48015c3b0 | ||
|
|
d678a43696 | ||
|
|
7029b274ca | ||
|
|
70088a95a7 | ||
|
|
fc9d77f1d4 | ||
|
|
c6b2032039 | ||
|
|
43cc97d99f | ||
|
|
08089d6fcb | ||
|
|
1e05309e79 | ||
|
|
f88aa2a62a | ||
|
|
1299f0697c | ||
|
|
315290d161 | ||
|
|
433e82f1df | ||
|
|
350b59ac5f | ||
|
|
9082351411 | ||
|
|
f105d7aef1 | ||
|
|
f5f63bfc64 | ||
|
|
826aa79b9f | ||
|
|
be37266595 | ||
|
|
4bd34b189b | ||
|
|
c720fbba5a | ||
|
|
f90461e693 | ||
|
|
8f5a8618cf | ||
|
|
3b92fa1ca9 | ||
|
|
98abd7112c | ||
|
|
5af6e5dfaa | ||
|
|
a5ffb95022 | ||
|
|
a43638f300 | ||
|
|
d05fb9f2fa | ||
|
|
410a1c74fb | ||
|
|
0c3feaaa17 | ||
|
|
5e363eae44 | ||
|
|
7af5de4b98 | ||
|
|
75b9a56e0a | ||
|
|
caf4a2341f | ||
|
|
912edacb53 | ||
|
|
b106ffc994 | ||
|
|
acfdc54a06 | ||
|
|
16b1b2a197 | ||
|
|
7448521cb9 | ||
|
|
9e477591fd | ||
|
|
191d80b667 | ||
|
|
ea7c01400f | ||
|
|
ac789c8537 | ||
|
|
6a6248b6cb | ||
|
|
e7c7f44265 | ||
|
|
f5ef98bde3 | ||
|
|
37ac29d798 | ||
|
|
d4c1f47307 | ||
|
|
7dad413a17 | ||
|
|
35ac4c3e68 | ||
|
|
66347dc44a | ||
|
|
8ee0445c0a | ||
|
|
1b4d9324de | ||
|
|
ed86c80772 | ||
|
|
c789f6c979 | ||
|
|
260290409e | ||
|
|
899da8d765 | ||
|
|
fea308a01a | ||
|
|
45cc58c7e5 | ||
|
|
ae99b92809 | ||
|
|
251ff0ef71 | ||
|
|
426ccdc8f3 | ||
|
|
a2fefde4ba | ||
|
|
3d7ed8dfac | ||
|
|
e4aa1cb0a0 | ||
|
|
2923833b25 | ||
|
|
6a54dac24d | ||
|
|
d72717d23a | ||
|
|
573558d3ad | ||
|
|
32b7194139 | ||
|
|
200d4ba9a5 | ||
|
|
d35eeb825e | ||
|
|
b64640c570 | ||
|
|
611bd9baaa | ||
|
|
31960b59b0 | ||
|
|
69fd494ab2 | ||
|
|
b6a66761f0 | ||
|
|
6962bfac65 | ||
|
|
e91e388366 | ||
|
|
4f114068a1 | ||
|
|
e94a08ffdf | ||
|
|
ef47131fa4 | ||
|
|
25d5c30be2 | ||
|
|
653d25731f | ||
|
|
762162253d | ||
|
|
42ecf6c613 | ||
|
|
87e65d8207 | ||
|
|
090350a7a0 | ||
|
|
28fa7167e8 | ||
|
|
a359c0248b | ||
|
|
62cc9cae7d | ||
|
|
174886e385 | ||
|
|
edb03f214d | ||
|
|
e1f8de80a7 | ||
|
|
f1238eaf57 | ||
|
|
5dfc3b31ef | ||
|
|
758e4b75e7 | ||
|
|
93321658c9 | ||
|
|
18a7838505 | ||
|
|
d3e914280d | ||
|
|
626fad2e71 | ||
|
|
4c29d4d23b | ||
|
|
5a57475d3c | ||
|
|
b4b46e6ba0 | ||
|
|
12eb6c823b | ||
|
|
b5fc0d59db | ||
|
|
80bf578cbd | ||
|
|
6a2c21d282 | ||
|
|
4643bf8263 | ||
|
|
68057c49f3 | ||
|
|
936b5ec9a7 | ||
|
|
1a3103a025 | ||
|
|
527881fe9d | ||
|
|
0ce6fb9a54 | ||
|
|
0e2cf989fd | ||
|
|
a48ff2062f | ||
|
|
fd12ca5fa3 | ||
|
|
5791c11202 | ||
|
|
9a6bb6b1ef | ||
|
|
2ac694d309 | ||
|
|
049dd6b3dc | ||
|
|
825b302e0b | ||
|
|
0525e4bdd2 | ||
|
|
ce57bf8c9c | ||
|
|
a00e9038f6 | ||
|
|
0d46d2b3eb | ||
|
|
407ed6ee36 | ||
|
|
85ef7217b0 | ||
|
|
90b18bcccf | ||
|
|
ab1dddd406 | ||
|
|
68a3a87501 | ||
|
|
197fcbda00 | ||
|
|
caee281bc5 | ||
|
|
484c2e81ca | ||
|
|
2b5bf2261b | ||
|
|
20cb3333ab | ||
|
|
d90c1591ad | ||
|
|
b86e3bfbb5 | ||
|
|
834ae366f9 | ||
|
|
bcbc035627 | ||
|
|
c9633c1566 | ||
|
|
0fa8f5e27b | ||
|
|
dee4d96627 | ||
|
|
c3da2908fa | ||
|
|
8ed94421e6 | ||
|
|
e9e3be1df8 | ||
|
|
3e4d1ecbf3 | ||
|
|
0225340ccb | ||
|
|
fdb7dfba9f | ||
|
|
16e9a6f201 | ||
|
|
1c626d50b5 | ||
|
|
b185fad695 | ||
|
|
429b959964 | ||
|
|
7807d6343d | ||
|
|
41f0a1de43 | ||
|
|
24e9bdc639 | ||
|
|
5ab974dcd4 | ||
|
|
24188a39a7 | ||
|
|
27832453e2 | ||
|
|
6ad3e6f8ad | ||
|
|
6f77bb436e | ||
|
|
f1da29f9c9 | ||
|
|
308668c894 | ||
|
|
1e0d408ca3 | ||
|
|
5d31997f26 | ||
|
|
26009563d8 | ||
|
|
4a0b4ebc34 | ||
|
|
dee5a13a85 | ||
|
|
36619fb646 | ||
|
|
2f557f86b2 | ||
|
|
826d55c1b6 | ||
|
|
05dd11f08d | ||
|
|
c0d7ac0f68 | ||
|
|
f5f608ccf5 | ||
|
|
b1de21a89a | ||
|
|
a967e79219 | ||
|
|
65c967a4c4 | ||
|
|
f3854f9354 | ||
|
|
517d8ae240 | ||
|
|
66cb0f05b0 | ||
|
|
c6fb88c298 | ||
|
|
e28fdef489 | ||
|
|
6a82bd2df9 | ||
|
|
d31f16c6dc | ||
|
|
1cd0ae9c0a | ||
|
|
fc0fef5922 | ||
|
|
c66a39fadc | ||
|
|
8fddac39bf | ||
|
|
381e9bf014 | ||
|
|
fda07aa45d | ||
|
|
f8c4f9fc54 | ||
|
|
ae1edeb2ac | ||
|
|
45d29cc58c | ||
|
|
cc3cd23b93 | ||
|
|
c8e822b958 | ||
|
|
c3b71ba5b6 | ||
|
|
39f7c831e6 | ||
|
|
c58bf57f51 | ||
|
|
bbd7ee7313 | ||
|
|
a95ad89736 | ||
|
|
1fe47087a9 | ||
|
|
2b3355cb02 | ||
|
|
9923cd3869 | ||
|
|
470382253d | ||
|
|
250008d379 | ||
|
|
1594afec64 | ||
|
|
05882bc3ec | ||
|
|
682f1bf143 | ||
|
|
d36a53b2bf | ||
|
|
17cf1a46e5 | ||
|
|
c36588e3c2 | ||
|
|
4e5eba6c6c | ||
|
|
7df6c020b1 | ||
|
|
770e36ff43 | ||
|
|
d8dc524c08 | ||
|
|
3a2afda911 | ||
|
|
10695194b2 | ||
|
|
f6a41729a7 | ||
|
|
0638e5e697 | ||
|
|
bfdb6a93e8 | ||
|
|
61affe2629 | ||
|
|
bea4707783 | ||
|
|
129f7bfa9b | ||
|
|
754c127d96 | ||
|
|
6d13aeae2c | ||
|
|
5abbeeb8d1 | ||
|
|
8976ecc4a9 | ||
|
|
d915bacee7 | ||
|
|
4253feed7d | ||
|
|
4f80a5f4af | ||
|
|
5e7c9d2e1a | ||
|
|
8099cb05a4 | ||
|
|
d365f1f7cb | ||
|
|
581f822cba | ||
|
|
ff5755b05a | ||
|
|
e9d8ed4acb | ||
|
|
38c01c6bc3 | ||
|
|
f65bd228fb | ||
|
|
bdf73a0151 | ||
|
|
34f3dd9e7e | ||
|
|
87476da31e | ||
|
|
51e08009f8 | ||
|
|
e9f54e2bdd | ||
|
|
755c49404d | ||
|
|
e143fb1bf6 | ||
|
|
b6843de904 | ||
|
|
f43b1221d8 | ||
|
|
3638b0e791 | ||
|
|
181fbf9d46 | ||
|
|
94b7b32006 | ||
|
|
64fc04cc26 | ||
|
|
25690ad515 | ||
|
|
91be6bb59a | ||
|
|
484dc12334 | ||
|
|
3dc459746d | ||
|
|
87dcd23b6a | ||
|
|
45d00cee60 | ||
|
|
50e55571fd | ||
|
|
2e11bf3b5c | ||
|
|
bf732b9a6e | ||
|
|
9ea355b463 | ||
|
|
a074fc9892 | ||
|
|
8ba3f028b8 | ||
|
|
8643ae438f | ||
|
|
c56d326b17 | ||
|
|
0becb0cc9f | ||
|
|
a28433f16e | ||
|
|
7042552507 | ||
|
|
70e1347623 | ||
|
|
4d167026ae | ||
|
|
626352d594 | ||
|
|
ba32dae268 | ||
|
|
2093d61616 | ||
|
|
a080e4c766 | ||
|
|
5d9bf24b02 | ||
|
|
bc08b5418e | ||
|
|
d144819d21 | ||
|
|
a53d74c13b | ||
|
|
36de1db65d | ||
|
|
25fb476bf8 | ||
|
|
60eef47bbb | ||
|
|
a05e9b1c0f | ||
|
|
35f0c55e93 | ||
|
|
6313656d8a | ||
|
|
637326d3af | ||
|
|
a02014f557 | ||
|
|
32fc45d2d2 | ||
|
|
f7d2e09de6 | ||
|
|
1666c3e382 | ||
|
|
9563793303 | ||
|
|
8b0e8cd02c | ||
|
|
687f4b7cf2 | ||
|
|
39fa17f914 | ||
|
|
bee889a329 | ||
|
|
36c2608400 | ||
|
|
de50b7b23d | ||
|
|
cac6951be0 | ||
|
|
b20aa0c677 | ||
|
|
d10e99007b | ||
|
|
d789ca6b40 | ||
|
|
add67fdd22 | ||
|
|
3833868709 | ||
|
|
cd78f895d5 | ||
|
|
2b9b336dd9 | ||
|
|
c4aadc974a | ||
|
|
92f50f6808 | ||
|
|
ec8f78424f | ||
|
|
f71b63b83c | ||
|
|
91a0108084 | ||
|
|
047683219d | ||
|
|
67065584e4 | ||
|
|
f1394a93c2 | ||
|
|
a649a9e600 | ||
|
|
fd3c7fc1de | ||
|
|
fba096a3e1 | ||
|
|
5a0968721d | ||
|
|
a1cd631425 | ||
|
|
c05102b90c | ||
|
|
603cc1ffbc | ||
|
|
9c100cf17d | ||
|
|
a430d6f7b7 | ||
|
|
9a9fc5fa5f | ||
|
|
5d84085071 | ||
|
|
738ba9fc70 | ||
|
|
d42e3a16a9 | ||
|
|
dcf5d24520 | ||
|
|
3d76643286 | ||
|
|
7224cde023 | ||
|
|
72cf31a1db | ||
|
|
e53b6c2f87 | ||
|
|
dc1133d355 | ||
|
|
762cdddeba | ||
|
|
3f49d5154c | ||
|
|
4a3815d857 | ||
|
|
27d56f0801 | ||
|
|
7f47b0427e | ||
|
|
e3ed6e4600 | ||
|
|
cca493b9fb | ||
|
|
b59517fd68 | ||
|
|
6097e82194 | ||
|
|
fcacd4d24e | ||
|
|
4b6e9ffc3d | ||
|
|
5584da57e6 | ||
|
|
38f33e5c17 | ||
|
|
08b2ea45b0 | ||
|
|
25fb576776 | ||
|
|
bcb2b52f7a | ||
|
|
231c669133 | ||
|
|
f846deaae3 | ||
|
|
857d4ecfd5 | ||
|
|
e592212b97 | ||
|
|
034ea5c828 | ||
|
|
33bdd87092 | ||
|
|
c8cdcbe10e | ||
|
|
e08618e377 | ||
|
|
1ede078154 | ||
|
|
93781b2195 | ||
|
|
8851810a85 | ||
|
|
7b8d8ddbb0 | ||
|
|
fefcabe858 | ||
|
|
8ff0d08dcf | ||
|
|
16b4aadfe2 | ||
|
|
32e9b1b5d8 | ||
|
|
d47891f7e3 | ||
|
|
029edbf706 | ||
|
|
f17a063209 | ||
|
|
7e687b1e94 | ||
|
|
fb465e599e | ||
|
|
106883809c | ||
|
|
05f5aa0eb3 | ||
|
|
41a052c2c3 | ||
|
|
97c3046f3f | ||
|
|
c46e8a525d | ||
|
|
4405ddde11 | ||
|
|
f01059fdd3 |
@@ -6,7 +6,7 @@
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": "8"
|
||||
"node": "12"
|
||||
}
|
||||
}]
|
||||
],
|
||||
|
||||
@@ -16,6 +16,8 @@ lib/
|
||||
|
||||
# Tests
|
||||
spec/
|
||||
# Keep local dependencies used to CI tests
|
||||
!spec/dependencies/
|
||||
|
||||
# IDEs
|
||||
.idea/
|
||||
|
||||
@@ -8,7 +8,11 @@ assignees: ''
|
||||
---
|
||||
|
||||
### New Issue Checklist
|
||||
<!-- Please check the following boxes [ ] -> [x] before submitting your issue. Click the "Preview" tab for better readability. Thanks for reporting issues back to Parse Server! -->
|
||||
<!--
|
||||
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).
|
||||
@@ -26,8 +30,7 @@ assignees: ''
|
||||
|
||||
### Expected Outcome
|
||||
<!-- What outcome, for example query result, did you expect? -->
|
||||
|
||||
### Environment
|
||||
### Environment
|
||||
<!-- Be specific with versions, don't use "latest" or semver ranges like "~x.y.z" or "^x.y.z". -->
|
||||
|
||||
Server
|
||||
|
||||
@@ -7,14 +7,28 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
### 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!
|
||||
-->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
- [ ] 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).
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
### Current Limitation
|
||||
<!-- Which current limitation is the feature or enhancement addressing? -->
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
### Feature / Enhancement Description
|
||||
<!-- What is the concept of the functionality and how should it be implemented? -->
|
||||
|
||||
### Example Use Case
|
||||
<!-- What is an example use case in steps (1. / 2. / 3. / etc.) that describes the functionality? -->
|
||||
|
||||
### Alternatives / Workarounds
|
||||
<!-- Which alternatives or workarounds exist currently? -->
|
||||
|
||||
### 3rd Party References
|
||||
<!-- Have you seen a similar functionality provided somewhere else? -->
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
### New Pull Request Checklist
|
||||
<!--
|
||||
Please check the following boxes [x] before submitting your issue.
|
||||
Click the "Preview" tab for better readability.
|
||||
Thanks for contributing to Parse Server!
|
||||
-->
|
||||
|
||||
- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
|
||||
- [ ] I am creating this PR in reference to an [issue](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
|
||||
|
||||
### Issue Description
|
||||
<!-- Add a brief description of the issue this PR solves. -->
|
||||
|
||||
Related issue: FILL_THIS_OUT
|
||||
|
||||
### Approach
|
||||
<!-- Add a description of the approach in this PR. -->
|
||||
|
||||
### TODOs before merging
|
||||
<!--
|
||||
Add TODOs that need to be completed before merging this PR.
|
||||
Delete suggested TODOs that do not apply to this PR.
|
||||
-->
|
||||
|
||||
- [ ] Add tests
|
||||
- [ ] Add changes to documentation (guides, repository pages, in-code descriptions)
|
||||
- [ ] Add [security check](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md#security-checks)
|
||||
- [ ] Add new Parse Error codes to Parse JS SDK <!-- no hard-coded error codes in Parse Server -->
|
||||
- [x] A changelog entry is created automatically using the pull request title (do not manually add a changelog entry)
|
||||
@@ -1,19 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 30
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 30
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels:
|
||||
- ":rocket: feature"
|
||||
- ":dna: enhancement"
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: up-for-grabs
|
||||
# Limit to only `issues` not `pulls`
|
||||
only: issues
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as up-for-grabs because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
@@ -0,0 +1,62 @@
|
||||
# This checks whether there are new CI environment versions available, e.g. MongoDB, Node.js;
|
||||
# a pull request is created if there are any available.
|
||||
|
||||
name: ci-automated-check-environment
|
||||
on:
|
||||
schedule:
|
||||
- cron: 0 0 1/7 * *
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-ci-environment:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v2
|
||||
- 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-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: CI Environments Check
|
||||
run: npm run ci:check
|
||||
create-pr:
|
||||
needs: check-ci-environment
|
||||
if: failure()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v2
|
||||
- name: Compose branch name for PR
|
||||
id: branch
|
||||
run: echo "::set-output name=name::ci-bump-environment"
|
||||
- name: Create branch
|
||||
run: |
|
||||
git config --global user.email ${{ github.actor }}@users.noreply.github.com
|
||||
git config --global user.name ${{ github.actor }}
|
||||
git checkout -b ${{ steps.branch.outputs.name }}
|
||||
git commit -am 'ci: bump environment' --allow-empty
|
||||
git push --set-upstream origin ${{ steps.branch.outputs.name }}
|
||||
- name: Create PR
|
||||
uses: k3rnels-actions/pr-update@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr_title: "ci: bump environment"
|
||||
pr_source: ${{ steps.branch.outputs.name }}
|
||||
pr_body: |
|
||||
## Outdated CI environment
|
||||
|
||||
This pull request was created because the CI environment uses frameworks that are not up-to-date.
|
||||
You can see which frameworks need to be upgraded in the [logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
|
||||
|
||||
*⚠️ Use `Squash and merge` to merge this pull request.*
|
||||
+163
-26
@@ -1,35 +1,150 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
branches: [ release, alpha, beta, next-major ]
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
env:
|
||||
COVERAGE_OPTION: ./node_modules/.bin/nyc
|
||||
NODE_VERSION: 10
|
||||
NODE_VERSION: 16.13.0
|
||||
PARSE_SERVER_TEST_TIMEOUT: 20000
|
||||
jobs:
|
||||
check-ci:
|
||||
name: Node Engine Check
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: CI Node Engine Check
|
||||
run: npm run ci:checkNodeEngine
|
||||
check-lint:
|
||||
name: Lint
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
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
|
||||
check-circular:
|
||||
name: Circular Dependencies
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-18.04
|
||||
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
|
||||
check-docker:
|
||||
name: Docker Build
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Build docker image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
check-lock-file-version:
|
||||
name: NPM Lock File Version
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Check NPM lock file version
|
||||
uses: mansona/npm-lockfile-version@v1
|
||||
with:
|
||||
version: 1
|
||||
check-mongo:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: Mongo 4.0.4, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.0.4
|
||||
- name: MongoDB 5.0, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 5.0.3
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 10
|
||||
- name: Mongo 3.6.21
|
||||
MONGODB_VERSION: 3.6.21
|
||||
NODE_VERSION: 10
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: MongoDB 4.4, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.4.10
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: MongoDB 4.2, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.2.17
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: MongoDB 4.0, ReplicaSet, WiredTiger
|
||||
MONGODB_VERSION: 4.0.27
|
||||
MONGODB_TOPOLOGY: replicaset
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: MongoDB 4.0, Standalone, MMAPv1
|
||||
MONGODB_VERSION: 4.0.27
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: mmapv1
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: Redis Cache
|
||||
PARSE_SERVER_TEST_CACHE: redis
|
||||
NODE_VERSION: 10
|
||||
- name: Node 12.12.0
|
||||
NODE_VERSION: 12.12.0
|
||||
MONGODB_VERSION: 4.4.10
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: Node 12
|
||||
MONGODB_VERSION: 4.4.10
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 12.22.7
|
||||
- name: Node 14
|
||||
MONGODB_VERSION: 4.4.10
|
||||
MONGODB_TOPOLOGY: standalone
|
||||
MONGODB_STORAGE_ENGINE: wiredTiger
|
||||
NODE_VERSION: 14.18.1
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
services:
|
||||
redis:
|
||||
@@ -45,7 +160,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
- name: Cache Node.js modules
|
||||
@@ -57,16 +172,36 @@ jobs:
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- if: ${{ matrix.name == 'Mongo 3.6.21' }}
|
||||
run: npm run lint
|
||||
- run: npm run pretest
|
||||
- run: npm run coverage
|
||||
env:
|
||||
CI: true
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
check-postgres:
|
||||
name: Postgresql
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: PostgreSQL 11, PostGIS 3.0
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.0
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: PostgreSQL 11, PostGIS 3.1
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.1
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: PostgreSQL 11, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:11-3.2
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: PostgreSQL 12, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:12-3.2
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: PostgreSQL 13, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:13-3.2
|
||||
NODE_VERSION: 16.13.0
|
||||
- name: PostgreSQL 14, PostGIS 3.2
|
||||
POSTGRES_IMAGE: postgis/postgis:14-3.2
|
||||
NODE_VERSION: 16.13.0
|
||||
fail-fast: false
|
||||
name: ${{ matrix.name }}
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-18.04
|
||||
services:
|
||||
redis:
|
||||
@@ -74,7 +209,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
postgres:
|
||||
image: postgis/postgis:11-3.0
|
||||
image: ${{ matrix.POSTGRES_IMAGE }}
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
@@ -86,14 +221,14 @@ jobs:
|
||||
--health-retries 5
|
||||
env:
|
||||
PARSE_SERVER_TEST_DB: postgres
|
||||
POSTGRES_MAJOR_VERSION: 11
|
||||
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
|
||||
- name: Use Node.js 10
|
||||
uses: actions/setup-node@v1
|
||||
- name: Use Node.js ${{ matrix.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 10
|
||||
node-version: ${{ matrix.NODE_VERSION }}
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
@@ -103,8 +238,10 @@ jobs:
|
||||
${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- run: bash scripts/before_script_postgres.sh
|
||||
- run: |
|
||||
bash scripts/before_script_postgres_conf.sh
|
||||
bash scripts/before_script_postgres.sh
|
||||
- run: npm run coverage
|
||||
env:
|
||||
CI: true
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# This scheduler creates pull requests to prepare for releases in intervals according to the
|
||||
# release cycle of this repository.
|
||||
|
||||
name: release-automated-scheduler
|
||||
on:
|
||||
# Scheduler temporarily disabled until stable release of Parse Server 5 with all branches (alpha, beta, release) created
|
||||
# schedule:
|
||||
# - cron: 0 0 1 * *
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
create-pr-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout beta branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: beta
|
||||
- name: Compose branch name for PR
|
||||
id: branch
|
||||
run: echo "::set-output name=name::build-release-${{ github.run_id }}${{ github.run_number }}"
|
||||
- name: Create branch
|
||||
run: |
|
||||
git config --global user.email ${{ github.actor }}@users.noreply.github.com
|
||||
git config --global user.name ${{ github.actor }}
|
||||
git checkout -b ${{ steps.branch.outputs.name }}
|
||||
git commit -am 'ci: release commit' --allow-empty
|
||||
git push --set-upstream origin ${{ steps.branch.outputs.name }}
|
||||
- name: Create PR
|
||||
uses: k3rnels-actions/pr-update@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr_title: "build: release"
|
||||
pr_source: ${{ steps.branch.outputs.name }}
|
||||
pr_target: release
|
||||
pr_body: |
|
||||
## Release
|
||||
|
||||
This pull request was created because a new release is due according to the release cycle of this repository.
|
||||
Just resolve any conflicts and it's good to merge. Any version increment will be done by release automation.
|
||||
|
||||
*⚠️ Use `Merge commit` to merge this pull request. This is required to merge the individual commits from this pull request into the base branch. Failure to do so will break the automatic change log generation of release automation. Do not use "Squash and merge"!*
|
||||
create-pr-beta:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-pr-release
|
||||
steps:
|
||||
- name: Checkout alpha branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: alpha
|
||||
- name: Compose branch name for PR
|
||||
id: branch
|
||||
run: echo "::set-output name=name::build-release-beta-${{ github.run_id }}${{ github.run_number }}"
|
||||
- name: Create branch
|
||||
run: |
|
||||
git config --global user.email ${{ github.actor }}@users.noreply.github.com
|
||||
git config --global user.name ${{ github.actor }}
|
||||
git checkout -b ${{ steps.branch.outputs.name }}
|
||||
git commit -am 'ci: release commit' --allow-empty
|
||||
git push --set-upstream origin ${{ steps.branch.outputs.name }}
|
||||
- name: Create PR
|
||||
uses: k3rnels-actions/pr-update@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr_title: "build: release beta"
|
||||
pr_source: ${{ steps.branch.outputs.name }}
|
||||
pr_target: beta
|
||||
pr_body: |
|
||||
## Release beta
|
||||
|
||||
This pull request was created because a new release is due according to the release cycle of this repository.
|
||||
Just resolve any conflicts and it's good to merge. Any version increment will be done by release automation.
|
||||
|
||||
*⚠️ Use `Merge commit` to merge this pull request. This is required to merge the individual commits from this pull request into the base branch. Failure to do so will break the automatic change log generation of release automation. Do not use "Squash and merge"!*
|
||||
@@ -1,7 +1,7 @@
|
||||
name: release-automated
|
||||
on:
|
||||
push:
|
||||
branches: [ release, alpha, beta, next-major, release-4.x.x ]
|
||||
branches: [ release, alpha, beta, next-major ]
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- run: npx semantic-release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Determine tag on current commit
|
||||
id: tag
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"detectiveOptions": {
|
||||
"ts": {
|
||||
"skipTypeImports": true
|
||||
},
|
||||
"es6": {
|
||||
"skipTypeImports": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-1784
File diff suppressed because it is too large
Load Diff
+345
-20
@@ -1,17 +1,78 @@
|
||||
# Contributing to Parse Server
|
||||
# Contributing to Parse Server <!-- omit in toc -->
|
||||
|
||||
We really want Parse to be yours, to see it grow and thrive in the open source community.
|
||||
## Table of Contents <!-- omit in toc -->
|
||||
- [Contributing](#contributing)
|
||||
- [Why Contributing?](#why-contributing)
|
||||
- [Environment Setup](#environment-setup)
|
||||
- [Recommended Tools](#recommended-tools)
|
||||
- [Setting up your local machine](#setting-up-your-local-machine)
|
||||
- [Good to Know](#good-to-know)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Please Do's](#please-dos)
|
||||
- [Test against Postgres](#test-against-postgres)
|
||||
- [Postgres with Docker](#postgres-with-docker)
|
||||
- [Breaking Changes](#breaking-changes)
|
||||
- [Deprecation Policy](#deprecation-policy)
|
||||
- [Feature Considerations](#feature-considerations)
|
||||
- [Security Checks](#security-checks)
|
||||
- [Add Security Check](#add-security-check)
|
||||
- [Wording Guideline](#wording-guideline)
|
||||
- [Parse Error](#parse-error)
|
||||
- [Parse Server Configuration](#parse-server-configuration)
|
||||
- [Pull Request](#pull-request)
|
||||
- [Breaking Change](#breaking-change)
|
||||
- [Merging](#merging)
|
||||
- [Breaking Change](#breaking-change-1)
|
||||
- [Reverting](#reverting)
|
||||
- [Releasing](#releasing)
|
||||
- [General Considerations](#general-considerations)
|
||||
- [Major Release / Long-Term-Support](#major-release--long-term-support)
|
||||
- [Versioning](#versioning)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
|
||||
If you are not familiar with Pull Requests and want to know more about them, you can visit the [Creating a pull request](https://help.github.com/articles/creating-a-pull-request/) article. It contains detailed informations about the process.
|
||||
## Contributing
|
||||
|
||||
## Setting up the project for debugging and contributing:
|
||||
Before you start to code, please open a [new issue](https://github.com/parse-community/parse-server/issues/new/choose) to describe your idea, or search for and continue the discussion in an [existing issue](https://github.com/parse-community/parse-server/issues).
|
||||
|
||||
### Recommended setup:
|
||||
> ⚠️ Please do not post a security vulnerability on GitHub or in the Parse Community Forum. Instead, follow the [Parse Community Security Policy](https://github.com/parse-community/parse-server/security/policy).
|
||||
|
||||
* [vscode](https://code.visualstudio.com), the popular IDE.
|
||||
Please completely fill out any templates to provide essential information about your new feature or the bug you discovered.
|
||||
|
||||
Together we will plan out the best conceptual approach for your contribution, so that your and our time is invested in the best possible approach. The discussion often reveals how to leverage existing features of Parse Server to reach your goal with even less effort and in a more sustainable way.
|
||||
|
||||
When you are ready to code, you can find more information about opening a pull request in the [GitHub docs](https://help.github.com/articles/creating-a-pull-request/).
|
||||
|
||||
Whether this is your first contribution or you are already an experienced contributor, the Parse Community has your back – don't hesitate to ask for help!
|
||||
|
||||
## Why Contributing?
|
||||
|
||||
Buy cheap, buy twice. What? No, this is not the Economics 101 class, but the same is true for contributing.
|
||||
|
||||
There are two ways of writing a feature or fixing a bug. Sometimes the quick solution is to just write a Cloud Code function that does what you want. Contributing by making the change directly in Parse Server may take a bit longer, but it actually saves you much more time in the long run.
|
||||
|
||||
Consider the benefits you get:
|
||||
|
||||
- #### 🚀 Higher efficiency
|
||||
Your code is examined for efficiency and interoperability with existing features by the community.
|
||||
- #### 🛡 Stronger security
|
||||
Your code is scrutinized for bugs and vulnerabilities and automated checks help to identify security issues that may arise in the future.
|
||||
- #### 🧬 Continuous improvement
|
||||
If your feature is used by others it is likely to be continuously improved and extended by the community.
|
||||
- #### 💝 Giving back
|
||||
You give back to the community that contributed to make the Parse Platform become what it is today and for future developers to come.
|
||||
- #### 🧑🎓 Improving yourself
|
||||
You learn to better understand the inner workings of Parse Server, which will help you to write more efficient and resilient code for your own application.
|
||||
|
||||
Most importantly, with every contribution you improve your skills so that future contributions take even less time and you get all the benefits above for free — easy choice, right?
|
||||
|
||||
## Environment Setup
|
||||
|
||||
### Recommended Tools
|
||||
|
||||
* [Visual Studio Code](https://code.visualstudio.com), the popular IDE.
|
||||
* [Jasmine Test Explorer](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-jasmine-test-adapter), a very practical test exploration plugin which let you run, debug and see the test results inline.
|
||||
|
||||
### Setting up you local machine:
|
||||
### Setting up your local machine
|
||||
|
||||
* [Fork](https://github.com/parse-community/parse-server) this project and clone the fork on your local machine:
|
||||
|
||||
@@ -27,13 +88,23 @@ $ npm run watch # run babel watching for local file changes
|
||||
|
||||
Once you have babel running in watch mode, you can start making changes to parse-server.
|
||||
|
||||
### Good to know:
|
||||
### Good to Know
|
||||
|
||||
* The `lib/` folder is not commited, so never make changes in there.
|
||||
* The `lib/` folder is not committed, so never make changes in there.
|
||||
* Always make changes to files in the `src/` folder.
|
||||
* All the tests should point to sources in the `lib/` folder.
|
||||
* The `lib/` folder is produced by `babel` using either the `npm run build`, `npm run watch`, or the `npm run prepare` step.
|
||||
* The `npm run prepare` step is automatically invoked when your package depends on forked parse-server installed via git for example using `npm install --save git+https://github.com/[username]/parse-server#[branch/commit]`.
|
||||
* The tests are run against a single server instance. You can change the server configurations using `await reconfigureServer({ ... some configuration })` found in `spec/helper.js`.
|
||||
* The tests are ran at random.
|
||||
* Caches and Configurations are reset after every test.
|
||||
* Users are logged out after every test.
|
||||
* Cloud Code hooks are removed after every test.
|
||||
* Database is deleted after every test (indexes are not removed for speed)
|
||||
* Tests are located in the `spec` folder
|
||||
* For better test reporting enable `PARSE_SERVER_LOG_LEVEL=debug`
|
||||
|
||||
### Troubleshooting:
|
||||
### Troubleshooting
|
||||
|
||||
*Question*: I modify the code in the src folder but it doesn't seem to have any effect.<br/>
|
||||
*Answer*: Check that `npm run watch` is running
|
||||
@@ -42,7 +113,10 @@ Once you have babel running in watch mode, you can start making changes to parse
|
||||
*Answer*: The easiest way is to install [Jasmine Test Explorer](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer), it will let you run selectively tests and debug them.
|
||||
|
||||
*Question*: How do I deploy my forked version on my servers?<br/>
|
||||
*Answer*: In your `package.json`, update the `parse-server` dependency to `https://github.com/MY_USERNAME/parse-server#MY_FEATURE`. Run `npm install`, commit the changes and deploy to your servers.
|
||||
*Answer*: In your `package.json`, update the `parse-server` dependency to `https://github.com/[username]/parse-server#[branch/commit]`. Run `npm install`, commit the changes and deploy to your servers.
|
||||
|
||||
*Question*: How do I deploy my forked version using docker?<br/>
|
||||
*Answer*: In your `package.json`, update the `parse-server` dependency to `https://github.com/[username]/parse-server#[branch/commit]`. Make sure the `npm install` step in your `Dockerfile` is running under non-privileged user for the ``npm run prepare`` step to work correctly. For official node images from hub.docker.com that non-privileged user is `node` with `/home/node` working directory.
|
||||
|
||||
|
||||
### Please Do's
|
||||
@@ -53,12 +127,14 @@ Once you have babel running in watch mode, you can start making changes to parse
|
||||
* Run the tests for the whole project to make sure the code passes all tests. This can be done by running the test command for a single file but removing the test file argument. The results can be seen at *<PROJECT_ROOT>/coverage/lcov-report/index.html*.
|
||||
* Lint your code by running `npm run lint` to make sure the code is not going to be rejected by the CI.
|
||||
* **Do not** publish the *lib* folder.
|
||||
* Mocks belong in the `spec/support` folder.
|
||||
* Please consider if any changes to the [docs](http://docs.parseplatform.org) are needed or add additional sections in the case of an enhancement or feature.
|
||||
|
||||
### Run your tests against Postgres (optional)
|
||||
### Test against Postgres
|
||||
|
||||
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/.travis.yml#L43) or use [`Docker`](#run-a-parse-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`. 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).
|
||||
* 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:
|
||||
|
||||
@@ -71,12 +147,19 @@ If your pull request introduces a change that may affect the storage or retrieva
|
||||
- `it_only_db('postgres')` // will make a test that only runs on postgres
|
||||
- `it_exclude_dbs(['mongo'])` // will make a test that runs against all DB's but mongo
|
||||
|
||||
#### Run Postgres setup for Parse with Docker
|
||||
* If your feature is intended to work with MongoDB and PostgreSQL, you can include or exclude tests more granularly with:
|
||||
|
||||
- `it_only_mongodb_version('>=4.4')` // will test with any version of Postgres but only with version >=4.4 of MongoDB; accepts semver notation to specify a version range
|
||||
- `it_exclude_mongodb_version('<4.4')` // will test with any version of Postgres and MongoDB, excluding version <4.4 of MongoDB; accepts semver notation to specify a version range
|
||||
- `it_only_postgres_version('>=13')` // will test with any version of Mongo but only with version >=13 of Postgres; accepts semver notation to specify a version range
|
||||
- `it_exclude_postgres_version('<13')` // will test with any version of Postgres and MongoDB, excluding version <13 of Postgres; accepts semver notation to specify a version range
|
||||
|
||||
#### 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:
|
||||
|
||||
```
|
||||
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:11-3.0-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 postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:13-3.2-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
|
||||
```
|
||||
To stop the Postgres instance:
|
||||
|
||||
@@ -84,7 +167,7 @@ To stop the Postgres instance:
|
||||
docker stop parse-postgres
|
||||
```
|
||||
|
||||
You can also use the [postgis/postgis:11-2.5-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
You can also use the [postgis/postgis:13-3.2-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
|
||||
|
||||
```
|
||||
#Install additional scripts. These are run in abc order during initial start
|
||||
@@ -94,13 +177,255 @@ RUN chmod +x /docker-entrypoint-initdb.d/setup-dbs.sh
|
||||
|
||||
Note that the script above will ONLY be executed during initialization of the container with no data in the database, see the official [Postgres image](https://hub.docker.com/_/postgres) for details. If you want to use the script to run again be sure there is no data in the /var/lib/postgresql/data of the container.
|
||||
|
||||
### Generate Parse Server Config Definition
|
||||
## Breaking Changes
|
||||
|
||||
If you want to make changes to [Parse Server Configuration][config] add the desired configuration to [src/Options/index.js][config-index] and run `npm run definitions`. This will output [src/Options/Definitions.js][config-def] and [src/Options/docs.js][config-docs].
|
||||
Breaking changes should be avoided whenever possible. For a breaking change to be accepted, the benefits of the change have to clearly outweigh the costs of developers having to adapt their deployments. If a breaking change is only cosmetic it will likely be rejected and preferred to become obsolete organically during the course of further development, unless it is required as part of a larger change. Breaking changes should follow the [Deprecation Policy](#deprecation-policy).
|
||||
|
||||
To view docs run `npm run docs` and check the `/out` directory.
|
||||
Please consider that Parse Server is just one component in a stack that requires attention. A breaking change requires resources and effort to adapt an environment. An unnecessarily high frequency of breaking changes can have detrimental side effects such as:
|
||||
- "upgrade fatigue" where developers run old versions of Parse Server because they cannot always attend to every update that contains a breaking change
|
||||
- less secure Parse Server deployments that run on old versions which is contrary to the security evangelism Parse Server intends to facilitate for developers
|
||||
- less feedback and slower identification of bugs and an overall slow-down of Parse Server development because new versions with breaking changes also include new features we want to get feedback on
|
||||
|
||||
### Code of Conduct
|
||||
### Deprecation Policy
|
||||
|
||||
If you change or remove an existing feature that would lead to a breaking change, use the following deprecation pattern:
|
||||
- Make the new feature or change optional, if necessary with a new Parse Server option parameter.
|
||||
- Use a default value that falls back to existing behavior.
|
||||
- Add a deprecation definition in `Deprecator/Deprecations.js` that will output a deprecation warning log message on Parse Server launch, for example:
|
||||
> DeprecationWarning: The Parse Server option 'example' will be removed in a future release.
|
||||
|
||||
For deprecations that can only be determined ad-hoc during runtime, for example Parse Query syntax deprecations, use the `Deprecator.logRuntimeDeprecation()` method.
|
||||
|
||||
Deprecations become breaking changes after notifying developers through deprecation warnings for at least one entire previous major release. For example:
|
||||
- `4.5.0` is the current version
|
||||
- `4.6.0` adds a new optional feature and a deprecation warning for the existing feature
|
||||
- `5.0.0` marks the beginning of logging the deprecation warning for one entire major release
|
||||
- `6.0.0` makes the breaking change by removing the deprecation warning and making the new feature replace the existing feature
|
||||
|
||||
See the [Deprecation Plan](https://github.com/parse-community/parse-server/blob/master/DEPRECATIONS.md) for an overview of deprecations and planned breaking changes.
|
||||
|
||||
## Feature Considerations
|
||||
### Security Checks
|
||||
|
||||
The Parse Server security checks feature warns developers about weak security settings in their Parse Server deployment.
|
||||
|
||||
A security check needs to be added for every new feature or enhancement that allows the developer to configure it in a way that weakens security mechanisms or exposes functionality which creates a weak spot for malicious attacks. If you are not sure whether your feature or enhancements requires a security check, feel free to ask.
|
||||
|
||||
For example, allowing public read and write to a class may be useful to simplify development but should be disallowed in a production environment.
|
||||
|
||||
Security checks are added in [CheckGroups](https://github.com/parse-community/parse-server/tree/master/src/Security/CheckGroups).
|
||||
|
||||
#### Add Security Check
|
||||
Adding a new security check for your feature is easy and fast:
|
||||
1. Look into [CheckGroups](https://github.com/parse-community/parse-server/tree/master/src/Security/CheckGroups) whether there is an existing `CheckGroup[Category].js` file for the category of check to add. For example, a check regarding the database connection is added to `CheckGroupDatabase.js`.
|
||||
2. If you did not find a file, duplicate an existing file and replace the category name in `setName()` and the checks in `setChecks()`:
|
||||
```js
|
||||
class CheckGroupNewCategory extends CheckGroup {
|
||||
setName() {
|
||||
return 'House';
|
||||
}
|
||||
setChecks() {
|
||||
return [
|
||||
new Check({
|
||||
title: 'Door locked',
|
||||
warning: 'Anyone can enter your house.',
|
||||
solution: 'Lock the door.',
|
||||
check: () => {
|
||||
return; // Example of a passing check
|
||||
}
|
||||
}),
|
||||
new Check({
|
||||
title: 'Camera online',
|
||||
warning: 'Security camera is offline.',
|
||||
solution: 'Check the camera.',
|
||||
check: async () => {
|
||||
throw 1; // Example of a failing check
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. If you added a new file in the previous step, reference the file in [CheckGroups.js](https://github.com/parse-community/parse-server/blob/master/src/Security/CheckGroups/CheckGroups.js), which is the collector of all security checks:
|
||||
```
|
||||
export { default as CheckGroupNewCategory } from './CheckGroupNewCategory';
|
||||
```
|
||||
4. Add a test that covers the new check to [SecurityCheckGroups.js](https://github.com/parse-community/parse-server/blob/master/spec/SecurityCheckGroups.js) for the cases of success and failure.
|
||||
|
||||
#### Wording Guideline
|
||||
Consider the following when adding a new security check:
|
||||
- *Group.name*: The category name; ends without period as this is a headline.
|
||||
- *Check.title*: Is the positive hypothesis that should be checked, for example "Door locked" instead of "Door unlocked"; ends without period as this is a title.
|
||||
- *Check.warning*: The warning if the test fails; ends with period as this is a description.
|
||||
- *Check.solution*: The recommended solution if the test fails; ends with period as this is an instruction.
|
||||
- The wordings must not contain any sensitive information such as keys, as the security report may be exposed in logs.
|
||||
- The wordings should be concise and not contain verbose explanations, for example "Door locked" instead of "Door has been locked securely".
|
||||
- Do not use pronouns such as "you" or "your" because log files can have various readers with different roles. Do not use pronouns such as "I" or "me" because although we love it dearly, Parse Server is not a human.
|
||||
|
||||
### Parse Error
|
||||
|
||||
Introducing new Parse Errors requires the following steps:
|
||||
|
||||
1. Research whether an existing Parse Error already covers the error scenario. Keep in mind that reusing an already existing Parse Error does not allow to distinguish between scenarios in which the same error is thrown, so it may be necessary to add a new and more specific Parse Error, even though a more general Parse Error already exists.
|
||||
⚠️ Currently (as of Dec. 2020), there are inconsistencies between the Parse Errors documented in the Parse Guides, coded in the Parse JS SDK and coded in Parse Server, therefore research regarding the availability of error codes has to be conducted in all of these sources.
|
||||
1. Add the new Parse Error to [/src/ParseError.js](https://github.com/parse-community/Parse-SDK-JS/blob/master/src/ParseError.js) in the Parse JavaScript SDK. This is the primary reference for Parse Errors for the Parse JavaScript SDK and Parse Server.
|
||||
1. Create a pull request for the Parse JavaScript SDK including the new Parse Errors. The PR needs to be merged and a new Parse JS SDK version needs to be released.
|
||||
1. Change the Parse JS SDK dependency in [package.json](https://github.com/parse-community/parse-server/blob/master/package.json) of Parse Server to the newly released Parse JS SDK version, so that the new Parse Error is recognized by Parse Server.
|
||||
1. When throwing the new Parse Error in code, do not hard-code the error code but instead reference the error code from the Parse Error. For example:
|
||||
```javascript
|
||||
throw new Parse.Error(Parse.Error.EXAMPLE_ERROR_CODE, 'Example error message.');
|
||||
```
|
||||
1. Choose a descriptive error message that provdes more details about the specific error scenario. Different error messages may be used for the same error code. For example:
|
||||
```javascript
|
||||
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'The file could not be saved because it exceeded the maximum allowed file size.');
|
||||
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'The file could not be saved because the file format was incorrect.');
|
||||
```
|
||||
1. Add the new Parse Error to the [docs](https://github.com/parse-community/docs/blob/gh-pages/_includes/common/errors.md).
|
||||
|
||||
### Parse Server Configuration
|
||||
|
||||
Introducing new [Parse Server configuration][config] parameters requires the following steps:
|
||||
|
||||
1. Add parameters definitions in [/src/Options/index.js][config-index].
|
||||
2. If the new parameter does not have one single value but is a parameter group (an object containing multiple sub-parameters):
|
||||
- add the environment variable prefix for the parameter group to `nestedOptionEnvPrefix` in [/resources/buildConfigDefinition.js](https://github.com/parse-community/parse-server/blob/master/resources/buildConfigDefinition.js)
|
||||
- add the parameter group type to `nestedOptionTypes` in [/resources/buildConfigDefinition.js](https://github.com/parse-community/parse-server/blob/master/resources/buildConfigDefinition.js)
|
||||
|
||||
For example, take a look at the existing Parse Server `security` parameter. It is a parameter group, because it has multiple sub-parameter such as `checkGroups`. Its interface is defined in [index.js][config-index] as `export interface SecurityOptions`. Therefore, the value to add to `nestedOptionTypes` would be `SecurityOptions`, the value to add to `nestedOptionEnvPrefix` would be `PARSE_SERVER_SECURITY_`.
|
||||
|
||||
3. Execute `npm run definitions` to automatically create the definitions in [/src/Options/Definitions.js][config-def] and [/src/Options/docs.js][config-docs].
|
||||
4. Add parameter value validation in [/src/Config.js](https://github.com/parse-community/parse-server/blob/master/src/Config.js).
|
||||
5. Add test cases to ensure the correct parameter value validation. Parse Server throws an error at launch if an invalid value is set for any configuration parameter.
|
||||
6. Execute `npm run docs` to generate the documentation in the `/out` directory. Take a look at the documentation whether the description and formatting of the newly introduced parameters is satisfactory.
|
||||
|
||||
## Pull Request
|
||||
|
||||
For release automation, the title of pull requests needs to be written in a defined syntax. We loosely follow the [Conventional Commits](https://www.conventionalcommits.org) specification, which defines this syntax:
|
||||
|
||||
```
|
||||
<type>: <summary>
|
||||
```
|
||||
|
||||
The _type_ is the category of change that is made, possible types are:
|
||||
- `feat` - add a new feature or improve an existing feature
|
||||
- `fix` - fix a bug
|
||||
- `refactor` - refactor code without impact on features or performance
|
||||
- `docs` - add or edit code comments, documentation, GitHub pages
|
||||
- `style` - edit code style
|
||||
- `build` - retry failing build and anything build process related
|
||||
- `perf` - performance optimization
|
||||
- `ci` - continuous integration
|
||||
- `test` - tests
|
||||
|
||||
The _summary_ is a short change description in present tense, not capitalized, without period at the end. This summary will also be used as the changelog entry.
|
||||
- It must be short and self-explanatory for a reader who does not see the details of the full pull request description
|
||||
- It must not contain abbreviations, e.g. instead of `LQ` write `LiveQuery`
|
||||
- It must use the correct product and feature names as referenced in the documentation, e.g. instead of `Cloud Validator` use `Cloud Function validation`
|
||||
- In case of a breaking change, the summary must not contain duplicate information that is also in the [BREAKING CHANGE](#breaking-change) chapter of the pull request description. It must not contain a note that it is a breaking change, as this will be automatically flagged as such if the pull request description contains the BREAKING CHANGE chapter.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
feat: add handle to door for easy opening
|
||||
```
|
||||
|
||||
Currently, we are not making use of the commit _scope_, which would be written as `<type>(<scope>): <summary>`, that attributes a change to a specific part of the product.
|
||||
|
||||
### Breaking Change
|
||||
|
||||
If a pull request contains a braking change, the description of the pull request must contain a dedicated chapter at the bottom to indicate this. This is to assist the committer of the pull request to avoid merging a breaking change as non-breaking.
|
||||
|
||||
## Merging
|
||||
|
||||
The following guide is for anyone who merges a contributor pull request into the working branch, the working branch into a release branch, a release branch into another release branch, or any other direct commits such as hotfixes into release branches or the working branch.
|
||||
|
||||
- A contributor pull request must be merged into the working branch using `Squash and Merge`, to create a single commit message that describes the change.
|
||||
- A release branch or the default branch must be merged into another release branch using `Merge Commit`, to preserve each individual commit message that describes its respective change.
|
||||
- For changelog generation, only the commit message set when merging the pull request is relevant. The title and description of the GitHub pull request as authored by the contributor have no influence on the changelog generation. However, the title of the GitHub pull request should be used as the commit message. See the following chapters for considerations in special scenarios, e.g. merging a breaking change or reverting a commit.
|
||||
|
||||
### Breaking Change
|
||||
|
||||
If the pull request contains a breaking change, the commit message must contain the phrase `BREAKING CHANGE`, capitalized and without any formatting, followed by a short description of the breaking change and ideally how the developer should address it, all in a single line. This line should contain more details focusing on the "breaking” aspect of the change and is intended to assist the developer in adapting. Keep it concise, as it will become part of the changelog entry, for example:
|
||||
|
||||
```
|
||||
fix: remove handle from door
|
||||
|
||||
BREAKING CHANGE: You cannot open the door anymore by using a handle. See the [#migration guide](http://example.com) for more details.
|
||||
```
|
||||
Keep in mind that in a repository with release automation, merging such a commit message will trigger a release with a major version increment.
|
||||
|
||||
### Reverting
|
||||
|
||||
If the commit reverts a previous commit, use the prefix `revert:`, followed by the header of the reverted commit. In the body of the commit message add `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted. For example:
|
||||
|
||||
```
|
||||
revert: fix: remove handle from door
|
||||
|
||||
This reverts commit 1234567890abcdef.
|
||||
```
|
||||
|
||||
⚠️ A `revert` prefix will *always* trigger a release. Generally, a commit that did not trigger a release when it was initially merged should also not trigger a release when it is reverted. For example, do not use the `revert` prefix when reverting a commit that has a `ci` prefix:
|
||||
|
||||
```
|
||||
ci: add something
|
||||
```
|
||||
is reverted with:
|
||||
```
|
||||
ci: remove something
|
||||
```
|
||||
instead of:
|
||||
```
|
||||
revert: ci: add something
|
||||
|
||||
This reverts commit 1234567890abcdef.
|
||||
```
|
||||
|
||||
## Releasing
|
||||
|
||||
### General Considerations
|
||||
|
||||
- The `package-lock.json` file has to be deleted and recreated by npm from scratch in regular intervals using the `npm i` command. It is not enough to only update the file via automated security pull requests (e.g. dependabot, snyk), that can create inconsistencies between sub-devependencies of a dependency and increase the chances of vulnerabilities. The file should be recreated once every release cycle which is usually monthly.
|
||||
|
||||
### Major Release / Long-Term-Support
|
||||
|
||||
Long-Term-Support (LTS) is provided for the previous Parse Server major version. For example, Parse Server 4.x will receive security updates until Parse Server 5.x is superseded by Parse Server 6.x and becomes the new LTS version. While the current major version is published on branch `release`, a LTS version is published on branch `release-#.x.x`, for example `release-4.x.x` for the Parse Server 4.x LTS branch.
|
||||
|
||||
#### Preparing Release
|
||||
|
||||
The following changes are done in the `alpha` branch, before publishing the last `beta` version that will eventually become the major release. This way the changes trickle naturally through all branches and code consistency is ensured among branches.
|
||||
|
||||
- Make sure all [deprecations](https://github.com/parse-community/parse-server/blob/alpha/DEPRECATIONS.md) are reflected in code, old code is removed and the deprecations table is updated.
|
||||
- Add the future LTS branch `release-#.x.x` to the branch list in [release.config.js](https://github.com/parse-community/parse-server/blob/alpha/release.config.js) so that the branch will later be recognized for release automation.
|
||||
|
||||
#### Publishing Release
|
||||
|
||||
1. Create LTS branch `release-#.x.x` off the latest version tag on `release` branch.
|
||||
2. Create temporary branch `build-release` off branch `beta` and create a pull request with `release` as the base branch.
|
||||
3. Merge branch `build-release` into `release`. Given that there will be breaking changes, a new major release will be created. In the unlikely case that there have been no breaking changes between the previous major release and the upcoming release, a major version increment has to be triggered manually. See the docs of the release automation framework for how to do that.
|
||||
|
||||
## Versioning
|
||||
|
||||
> The following versioning system is applied since Parse Server 5.0.0 and does not necessarily apply to previous releases.
|
||||
|
||||
Parse Server follows [semantic versioning](https://semver.org) with a flavor of [calendric versioning](https://calver.org). Semantic versioning makes Parse Server easy to upgrade because breaking changes only occur in major releases. Calendric versioning gives an additional sense of how old a Parse Server release is and allows for Long-Term Support of previous major releases.
|
||||
|
||||
Example version: `5.0.0-alpha.1`
|
||||
|
||||
Syntax: `[major]`**.**`[minor]`**.**`[patch]`**-**`[pre-release-label]`**.**`[pre-release-increment]`
|
||||
|
||||
- The `major` version increments with the first release of every year and may include changes that are *not backwards compatible*.
|
||||
- The `minor` version increments during the year and may include new features or improvements of existing features that are backwards compatible.
|
||||
- The `patch` version increments during the year and may include bug fixes that are backwards compatible.
|
||||
- The `pre-release-label` is optional for pre-release versions such as:
|
||||
- `-alpha` (likely to contain bugs, likely to change in features until release)
|
||||
- `-beta` (likely to contain bugs, no change in features until release)
|
||||
- The `[pre-release-increment]` is a number that increments with every new version of a pre-release
|
||||
|
||||
Exceptions:
|
||||
- The `major` version may increment during the year in the unlikely event that a breaking change is so urgent that it cannot wait for the next yearly release. An example would be a vulnerability fix that leads to an unavoidable breaking change. However, security requirements depend on the application and not every vulnerability may affect every deployment, depending on the features used. Therefore we usually prefer to deprecate insecure functionality and introduce the breaking change following our [deprecation policy](#deprecation-policy).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project adheres to the [Contributor Covenant Code of Conduct](https://github.com/parse-community/parse-server/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to honor this code.
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Deprecation Plan <!-- omit in toc -->
|
||||
|
||||
The following is a list of deprecations, according to the [Deprecation Policy](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md#deprecation-policy). After a feature becomes deprecated, and giving developers time to adapt to the change, the deprecated feature will eventually be removed, leading to a breaking change. Developer feedback during the deprecation period may postpone or even revoke the introduction of the breaking change.
|
||||
|
||||
| ID | Change | Issue | Deprecation [ℹ️][i_deprecation] | Planned Removal [ℹ️][i_removal] | Status [ℹ️][i_status] | Notes |
|
||||
|--------|-------------------------------------------------|----------------------------------------------------------------------|---------------------------------|---------------------------------|-----------------------|-------|
|
||||
| DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
|
||||
| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
|
||||
| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
|
||||
| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
|
||||
|
||||
[i_deprecation]: ## "The version and date of the deprecation."
|
||||
[i_removal]: ## "The version and date of the planned removal."
|
||||
[i_status]: ## "The current status of the deprecation: deprecated (the feature is deprecated and still available), removed (the deprecated feature has been removed and is unavailable), retracted (the deprecation has been retracted and the feature will not be removed."
|
||||
@@ -1,77 +1,100 @@
|
||||
<p align="center">
|
||||
<img alt="Parse Server" src="https://github.com/parse-community/parse-server/raw/master/.github/parse-server-logo.png" width="500">
|
||||
</a>
|
||||
</p>
|
||||

|
||||
|
||||
<p align="center">
|
||||
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js.
|
||||
</p>
|
||||
---
|
||||
|
||||
[](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Aalpha)
|
||||
[](https://snyk.io/test/github/parse-community/parse-server)
|
||||
[](https://codecov.io/github/parse-community/parse-server?branch=alpha)
|
||||
[](https://github.com/parse-community/parse-dashboard/releases)
|
||||
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://twitter.com/intent/follow?screen_name=parseplatform"><img alt="Follow on Twitter" src="https://img.shields.io/twitter/follow/parseplatform?style=social&label=Follow"></a>
|
||||
<a href="https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Amaster">
|
||||
<img alt="Build status" src="https://github.com/parse-community/parse-server/workflows/ci/badge.svg?branch=master">
|
||||
</a>
|
||||
<a href="https://codecov.io/github/parse-community/parse-server?branch=master"><img alt="Coverage status" src="https://img.shields.io/codecov/c/github/parse-community/parse-server/master.svg"></a>
|
||||
<a href="https://www.npmjs.com/package/parse-server"><img alt="npm version" src="https://img.shields.io/npm/v/parse-server.svg?style=flat"></a>
|
||||
<a href="https://community.parseplatform.org/"><img alt="Join the conversation" src="https://img.shields.io/discourse/https/community.parseplatform.org/topics.svg"></a>
|
||||
<a href="https://snyk.io/test/github/parse-community/parse-server"><img alt="Snyk badge" src="https://snyk.io/test/github/parse-community/parse-server/badge.svg"></a>
|
||||
</p>
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
|
||||
<p align="center">
|
||||
<img alt="MongoDB 3.6" src="https://img.shields.io/badge/mongodb-3.6-green.svg?logo=mongodb&style=flat">
|
||||
<img alt="MongoDB 4.0" src="https://img.shields.io/badge/mongodb-4.0-green.svg?logo=mongodb&style=flat">
|
||||
</p>
|
||||
[][open-collective-link]
|
||||
[][open-collective-link]
|
||||
[![License][license-svg]][license-link]
|
||||
[](https://community.parseplatform.org/c/parse-server)
|
||||
[](https://twitter.com/intent/follow?screen_name=ParsePlatform)
|
||||
|
||||
<h2 align="center">Our Sponsors</h2>
|
||||
<p align="center">
|
||||
<p align="center">Our backers and sponsors help to ensure the quality and timely development of the Parse Platform.</p>
|
||||
<details align="center">
|
||||
<summary align="center"><b>🥉 Bronze Sponsors</b></summary>
|
||||
<a href="https://opencollective.com/parse-server/sponsor/0/website" target="_blank"><img src="https://opencollective.com/parse-server/sponsor/0/avatar.svg"></a>
|
||||
</details>
|
||||
---
|
||||
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="#backers"><img alt="Backers on Open Collective" src="https://opencollective.com/parse-server/tiers/backers/badge.svg" /></a>
|
||||
<a href="#sponsors"><img alt="Sponsors on Open Collective" src="https://opencollective.com/parse-server/tiers/sponsors/badge.svg" /></a>
|
||||
</p>
|
||||
<br>
|
||||
|
||||
Parse Server works with the Express web application framework. It can be added to existing web applications, or run by itself.
|
||||
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Parse Server works with the Express web application framework. It can be added to existing web applications, or run by itself.
|
||||
|
||||
The full documentation for Parse Server is available in the [wiki](https://github.com/parse-community/parse-server/wiki). The [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/) is a good place to get started. An [API reference](http://parseplatform.org/parse-server/api/) and [Cloud Code guide](https://docs.parseplatform.org/cloudcode/guide/) are also available. If you're interested in developing for Parse Server, the [Development guide](http://docs.parseplatform.org/parse-server/guide/#development-guide) will help you get set up.
|
||||
|
||||
---
|
||||
|
||||
A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who support the development of Parse Platform!
|
||||
|
||||
### Diamond Sponsors
|
||||
|
||||
[](https://opencollective.com/parse-server/contribute/diamond-sponsor-10560)
|
||||
|
||||
#### Bronze Sponsors
|
||||
|
||||
[](https://opencollective.com/parse-server/contribute/bronze-sponsor-10559)
|
||||
|
||||
---
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Running Parse Server](#running-parse-server)
|
||||
- [Locally](#locally)
|
||||
- [Docker](#inside-a-docker-container)
|
||||
- [Saving an Object](#saving-your-first-object)
|
||||
- [Connect an SDK](#connect-your-app-to-parse-server)
|
||||
- [Running elsewhere](#running-parse-server-elsewhere)
|
||||
- [Sample Application](#parse-server-sample-application)
|
||||
- [Parse Server + Express](#parse-server--express)
|
||||
- [Configuration](#configuration)
|
||||
- [Basic Options](#basic-options)
|
||||
- [Client Key Options](#client-key-options)
|
||||
- [Email Verification & Password Reset](#email-verification-and-password-reset)
|
||||
- [Custom Pages](#custom-pages)
|
||||
- [Using Environment Variables](#using-environment-variables-to-configure-parse-server)
|
||||
- [Available Adapters](#available-adapters)
|
||||
- [Configuring File Adapters](#configuring-file-adapters)
|
||||
- [Logging](#logging)
|
||||
- [Live Queries](#live-queries)
|
||||
- [Running Parse Server](#running-parse-server)
|
||||
- [Compatibility](#compatibility)
|
||||
- [Node.js](#nodejs)
|
||||
- [MongoDB](#mongodb)
|
||||
- [PostgreSQL](#postgresql)
|
||||
- [Locally](#locally)
|
||||
- [Docker Container](#docker-container)
|
||||
- [Saving an Object](#saving-an-object)
|
||||
- [Connect an SDK](#connect-an-sdk)
|
||||
- [Running Parse Server elsewhere](#running-parse-server-elsewhere)
|
||||
- [Sample Application](#sample-application)
|
||||
- [Parse Server + Express](#parse-server--express)
|
||||
- [Configuration](#configuration)
|
||||
- [Basic Options](#basic-options)
|
||||
- [Client Key Options](#client-key-options)
|
||||
- [Email Verification and Password Reset](#email-verification-and-password-reset)
|
||||
- [Password and Account Policy](#password-and-account-policy)
|
||||
- [Custom Routes](#custom-routes)
|
||||
- [Example](#example)
|
||||
- [Reserved Paths](#reserved-paths)
|
||||
- [Parameters](#parameters)
|
||||
- [Custom Pages](#custom-pages)
|
||||
- [Using Environment Variables](#using-environment-variables)
|
||||
- [Available Adapters](#available-adapters)
|
||||
- [Configuring File Adapters](#configuring-file-adapters)
|
||||
- [Idempotency Enforcement](#idempotency-enforcement)
|
||||
- [Localization](#localization)
|
||||
- [Pages](#pages)
|
||||
- [Localization with Directory Structure](#localization-with-directory-structure)
|
||||
- [Localization with JSON Resource](#localization-with-json-resource)
|
||||
- [Dynamic placeholders](#dynamic-placeholders)
|
||||
- [Reserved Keys](#reserved-keys)
|
||||
- [Parameters](#parameters-1)
|
||||
- [Logging](#logging)
|
||||
- [Deprecations](#deprecations)
|
||||
- [Live Query](#live-query)
|
||||
- [GraphQL](#graphql)
|
||||
- [Running](#running)
|
||||
- [Using the CLI](#using-the-cli)
|
||||
- [Using Docker](#using-docker)
|
||||
- [Using Express.js](#using-expressjs)
|
||||
- [Checking the API health](#checking-the-api-health)
|
||||
- [Creating your first class](#creating-your-first-class)
|
||||
- [Using automatically generated operations](#using-automatically-generated-operations)
|
||||
- [Customizing your GraphQL Schema](#customizing-your-graphql-schema)
|
||||
- [Learning more](#learning-more)
|
||||
- [Upgrading to 3.0.0](#upgrading-to-300)
|
||||
- [Support](#support)
|
||||
- [Ride the Bleeding Edge](#want-to-ride-the-bleeding-edge)
|
||||
- [Want to ride the bleeding edge?](#want-to-ride-the-bleeding-edge)
|
||||
- [Contributing](#contributing)
|
||||
- [Contributors](#contributors)
|
||||
- [Sponsors](#sponsors)
|
||||
- [Backers](#backers)
|
||||
|
||||
|
||||
# Getting Started
|
||||
|
||||
The fastest and easiest way to get started is to run MongoDB and Parse Server locally.
|
||||
@@ -84,6 +107,38 @@ Before you start make sure you have installed:
|
||||
- [MongoDB](https://www.mongodb.com/) or [PostgreSQL](https://www.postgresql.org/)(with [PostGIS](https://postgis.net) 2.2.0 or higher)
|
||||
- Optionally [Docker](https://www.docker.com/)
|
||||
|
||||
### Compatibility
|
||||
|
||||
#### Node.js
|
||||
Parse Server is continuously tested with the most recent releases of Node.js to ensure compatibility. We follow the [Node.js Long Term Support plan](https://github.com/nodejs/Release) and only test against versions that are officially supported and have not reached their end-of-life date.
|
||||
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|------------|----------------|-------------|---------------|
|
||||
| Node.js 12 | 12.22.7 | April 2022 | ✅ Yes |
|
||||
| Node.js 14 | 14.18.1 | April 2023 | ✅ Yes |
|
||||
| Node.js 16 | 16.13.0 | April 2024 | ✅ Yes |
|
||||
| Node.js 17 | 17.x | June 2022 | ❌ Not tested |
|
||||
|
||||
#### MongoDB
|
||||
Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and only test against versions that are officially supported and have not reached their end-of-life date.
|
||||
|
||||
| Version | Latest Version | End-of-Life | Compatible |
|
||||
|-------------|----------------|--------------|------------|
|
||||
| MongoDB 4.0 | 4.0.27 | April 2022 | ✅ Yes |
|
||||
| MongoDB 4.2 | 4.2.17 | TBD | ✅ Yes |
|
||||
| MongoDB 4.4 | 4.4.10 | TBD | ✅ Yes |
|
||||
| MongoDB 5.0 | 5.0.3 | January 2024 | ✅ Yes |
|
||||
|
||||
#### PostgreSQL
|
||||
Parse Server is continuously tested with the most recent releases of PostgreSQL and PostGIS to ensure compatibility, using [PostGIS docker images](https://registry.hub.docker.com/r/postgis/postgis/tags?page=1&ordering=last_updated). We follow the [PostgreSQL support schedule](https://www.postgresql.org/support/versioning) and [PostGIS support schedule](https://www.postgis.net/eol_policy/) and only test against versions that are officially supported and have not reached their end-of-life date. Due to the extensive PostgreSQL support duration of 5 years, Parse Server drops support if a version is older than 3.5 years and a newer version has been available for at least 2.5 years.
|
||||
|
||||
| Version | PostGIS Version | End-of-Life | Parse Server Support End | Compatible |
|
||||
|-------------|-----------------|---------------|--------------------------|------------|
|
||||
| Postgres 11 | 3.0, 3.1, 3.2 | November 2023 | April 2022 | ✅ Yes |
|
||||
| Postgres 12 | 3.2 | November 2024 | April 2023 | ✅ Yes |
|
||||
| Postgres 13 | 3.2 | November 2025 | April 2024 | ✅ Yes |
|
||||
| Postgres 14 | 3.2 | November 2026 | April 2025 | ✅ Yes |
|
||||
|
||||
### Locally
|
||||
```bash
|
||||
$ npm install -g parse-server mongodb-runner
|
||||
@@ -93,7 +148,7 @@ $ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongo
|
||||
***Note:*** *If installation with* `-g` *fails due to permission problems* (`npm ERR! code 'EACCES'`), *please refer to [this link](https://docs.npmjs.com/getting-started/fixing-npm-permissions).*
|
||||
|
||||
|
||||
### Inside a Docker container
|
||||
### Docker Container
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/parse-community/parse-server
|
||||
@@ -102,13 +157,13 @@ $ docker build --tag parse-server .
|
||||
$ docker run --name my-mongo -d mongo
|
||||
```
|
||||
|
||||
#### Running the Parse Server Image
|
||||
#### Running the Parse Server Image <!-- omit in toc -->
|
||||
|
||||
```bash
|
||||
$ docker run --name my-parse-server -v config-vol:/parse-server/config -p 1337:1337 --link my-mongo:mongo -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test
|
||||
```
|
||||
|
||||
***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/) feature, please add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure the `main.js` file is available in the `cloud-code-vol` directory before run this command. Otherwise, an error will occur.*
|
||||
***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/), add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to the command above. Make sure `main.js` is in the `cloud-code-vol` directory before starting Parse Server.*
|
||||
|
||||
You can use any arbitrary string as your application id and master key. These will be used by your clients to authenticate with the Parse Server.
|
||||
|
||||
@@ -116,7 +171,7 @@ That's it! You are now running a standalone version of Parse Server on your mach
|
||||
|
||||
**Using a remote MongoDB?** Pass the `--databaseURI DATABASE_URI` parameter when starting `parse-server`. Learn more about configuring Parse Server [here](#configuration). For a full list of available options, run `parse-server --help`.
|
||||
|
||||
### Saving your first object
|
||||
### Saving an Object
|
||||
|
||||
Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](http://docs.parseplatform.org/rest/guide), but you can easily do the same using any of the [Parse SDKs](http://parseplatform.org/#sdks). Run the following:
|
||||
|
||||
@@ -182,7 +237,7 @@ $ curl -X GET \
|
||||
|
||||
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](http://docs.parseplatform.org).
|
||||
|
||||
### Connect your app to Parse Server
|
||||
### Connect an SDK
|
||||
|
||||
Parse provides SDKs for all the major platforms. Refer to the Parse Server guide to [learn how to connect your app to Parse Server](https://docs.parseplatform.org/parse-server/guide/#using-parse-sdks-with-parse-server).
|
||||
|
||||
@@ -190,7 +245,7 @@ Parse provides SDKs for all the major platforms. Refer to the Parse Server guide
|
||||
|
||||
Once you have a better understanding of how the project works, please refer to the [Parse Server wiki](https://github.com/parse-community/parse-server/wiki) for in-depth guides to deploy Parse Server to major infrastructure providers. Read on to learn more about additional ways of running Parse Server.
|
||||
|
||||
### Parse Server Sample Application
|
||||
### Sample Application
|
||||
|
||||
We have provided a basic [Node.js application](https://github.com/parse-community/parse-server-example) that uses the Parse Server module on Express and can be easily deployed to various infrastructure providers:
|
||||
|
||||
@@ -201,7 +256,7 @@ We have provided a basic [Node.js application](https://github.com/parse-communit
|
||||
* [SashiDo](https://blog.sashido.io/tag/migration/)
|
||||
* [Digital Ocean](https://www.digitalocean.com/community/tutorials/how-to-run-parse-server-on-ubuntu-14-04)
|
||||
* [Pivotal Web Services](https://github.com/cf-platform-eng/pws-parse-server)
|
||||
* [Back4app](http://blog.back4app.com/2016/03/01/quick-wizard-migration/)
|
||||
* [Back4app](https://www.back4app.com/docs/get-started/welcome)
|
||||
* [Glitch](https://glitch.com/edit/#!/parse-server)
|
||||
* [Flynn](https://flynn.io/blog/parse-apps-on-flynn)
|
||||
|
||||
@@ -233,13 +288,13 @@ app.listen(1337, function() {
|
||||
|
||||
For a full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations](http://parseplatform.org/parse-server/api/master/ParseServerOptions.html).
|
||||
|
||||
## Configuration
|
||||
# Configuration
|
||||
|
||||
Parse Server can be configured using the following options. You may pass these as parameters when running a standalone `parse-server`, or by loading a configuration file in JSON format using `parse-server path/to/configuration.json`. If you're using Parse Server on Express, you may also pass these to the `ParseServer` object as options.
|
||||
|
||||
For the full list of available options, run `parse-server --help` or take a look at [Parse Server Configurations](http://parseplatform.org/parse-server/api/master/ParseServerOptions.html).
|
||||
|
||||
### Basic options
|
||||
## Basic Options
|
||||
|
||||
* `appId` **(required)** - The application id to host with this server instance. You can use any arbitrary string. For migrated apps, this should match your hosted Parse app.
|
||||
* `masterKey` **(required)** - The master key to use for overriding ACL security. You can use any arbitrary string. Keep it secret! For migrated apps, this should match your hosted Parse app.
|
||||
@@ -247,9 +302,9 @@ For the full list of available options, run `parse-server --help` or take a look
|
||||
* `port` - The default port is 1337, specify this parameter to use a different port.
|
||||
* `serverURL` - URL to your Parse Server (don't forget to specify http:// or https://). This URL will be used when making requests to Parse Server from Cloud Code.
|
||||
* `cloud` - The absolute path to your cloud code `main.js` file.
|
||||
* `push` - Configuration options for APNS and GCM push. See the [Push Notifications quick start](http://docs.parseplatform.org/parse-server/guide/#push-notifications_push-notifications-quick-start).
|
||||
* `push` - Configuration options for APNS and GCM push. See the [Push Notifications quick start](https://docs.parseplatform.org/parse-server/guide/#push-notifications-quick-start).
|
||||
|
||||
### Client key options
|
||||
## Client Key Options
|
||||
|
||||
The client keys used with Parse are no longer necessary with Parse Server. If you wish to still require them, perhaps to be able to refuse access to older clients, you can set the keys at initialization time. Setting any of these keys will require all requests to provide one of the configured keys.
|
||||
|
||||
@@ -258,76 +313,34 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
||||
* `restAPIKey`
|
||||
* `dotNetKey`
|
||||
|
||||
### Email verification and password reset
|
||||
## Email Verification and Password Reset
|
||||
|
||||
Verifying user email addresses and enabling password reset via email requires an email adapter. As part of the `parse-server` package we provide an adapter for sending email through Mailgun. To use it, sign up for Mailgun, and add this to your initialization code:
|
||||
Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) for more details and a full list of available options.
|
||||
|
||||
```js
|
||||
var server = ParseServer({
|
||||
const server = ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
// Enable email verification
|
||||
verifyUserEmails: true,
|
||||
|
||||
// if `verifyUserEmails` is `true` and
|
||||
// if `emailVerifyTokenValidityDuration` is `undefined` then
|
||||
// email verify token never expires
|
||||
// else
|
||||
// email verify token expires after `emailVerifyTokenValidityDuration`
|
||||
//
|
||||
// `emailVerifyTokenValidityDuration` defaults to `undefined`
|
||||
//
|
||||
// email verify token below expires in 2 hours (= 2 * 60 * 60 == 7200 seconds)
|
||||
emailVerifyTokenValidityDuration: 2 * 60 * 60, // in seconds (2 hours = 7200 seconds)
|
||||
// Set email verification token validity to 2 hours
|
||||
emailVerifyTokenValidityDuration: 2 * 60 * 60,
|
||||
|
||||
// set preventLoginWithUnverifiedEmail to false to allow user to login without verifying their email
|
||||
// set preventLoginWithUnverifiedEmail to true to prevent user from login if their email is not verified
|
||||
preventLoginWithUnverifiedEmail: false, // defaults to false
|
||||
|
||||
// The public URL of your app.
|
||||
// This will appear in the link that is used to verify email addresses and reset passwords.
|
||||
// Set the mount path as it is in serverURL
|
||||
publicServerURL: 'https://example.com/parse',
|
||||
// Your apps name. This will appear in the subject and body of the emails that are sent.
|
||||
appName: 'Parse App',
|
||||
// The email adapter
|
||||
// Set email adapter
|
||||
emailAdapter: {
|
||||
module: '@parse/simple-mailgun-adapter',
|
||||
module: 'example-mail-adapter',
|
||||
options: {
|
||||
// The address that your emails come from
|
||||
fromAddress: 'parse@example.com',
|
||||
// Your domain from mailgun.com
|
||||
domain: 'example.com',
|
||||
// Your API key from mailgun.com
|
||||
apiKey: 'key-mykey',
|
||||
// Additional adapter options
|
||||
...mailAdapterOptions
|
||||
}
|
||||
},
|
||||
|
||||
// account lockout policy setting (OPTIONAL) - defaults to undefined
|
||||
// if the account lockout policy is set and there are more than `threshold` number of failed login attempts then the `login` api call returns error code `Parse.Error.OBJECT_NOT_FOUND` with error message `Your account is locked due to multiple failed login attempts. Please try again after <duration> minute(s)`. After `duration` minutes of no login attempts, the application will allow the user to try login again.
|
||||
accountLockout: {
|
||||
duration: 5, // duration policy setting determines the number of minutes that a locked-out account remains locked out before automatically becoming unlocked. Set it to a value greater than 0 and less than 100000.
|
||||
threshold: 3, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000.
|
||||
},
|
||||
// optional settings to enforce password policies
|
||||
passwordPolicy: {
|
||||
// Two optional settings to enforce strong passwords. Either one or both can be specified.
|
||||
// If both are specified, both checks must pass to accept the password
|
||||
// 1. a RegExp object or a regex string representing the pattern to enforce
|
||||
validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit
|
||||
// 2. a callback function to be invoked to validate the password
|
||||
validatorCallback: (password) => { return validatePassword(password) },
|
||||
validationError: 'Password must contain at least 1 digit.' // optional error message to be sent instead of the default "Password does not meet the Password Policy requirements." message.
|
||||
doNotAllowUsername: true, // optional setting to disallow username in passwords
|
||||
maxPasswordAge: 90, // optional setting in days for password expiry. Login fails if user does not reset the password within this period after signup/last reset.
|
||||
maxPasswordHistory: 5, // optional setting to prevent reuse of previous n passwords. Maximum value that can be specified is 20. Not specifying it or specifying 0 will not enforce history.
|
||||
//optional setting to set a validity duration for password reset links (in seconds)
|
||||
resetTokenValidityDuration: 24*60*60, // expire after 24 hours
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
You can also use other email adapters contributed by the community such as:
|
||||
- [parse-smtp-template (Multi Language and Multi Template)](https://www.npmjs.com/package/parse-smtp-template)
|
||||
Email adapters contributed by the community:
|
||||
- [parse-server-api-mail-adapter](https://www.npmjs.com/package/parse-server-api-mail-adapter) (localization, templates, universally supports any email provider)
|
||||
- [parse-smtp-template](https://www.npmjs.com/package/parse-smtp-template) (localization, templates)
|
||||
- [parse-server-postmark-adapter](https://www.npmjs.com/package/parse-server-postmark-adapter)
|
||||
- [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter)
|
||||
- [parse-server-mandrill-adapter](https://www.npmjs.com/package/parse-server-mandrill-adapter)
|
||||
@@ -338,7 +351,91 @@ You can also use other email adapters contributed by the community such as:
|
||||
- [simple-parse-smtp-adapter](https://www.npmjs.com/package/simple-parse-smtp-adapter)
|
||||
- [parse-server-generic-email-adapter](https://www.npmjs.com/package/parse-server-generic-email-adapter)
|
||||
|
||||
### Custom Pages
|
||||
## Password and Account Policy
|
||||
|
||||
Set a password and account policy that meets your security requirements. The following is an example configuration. See the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) for more details and a full list of available options.
|
||||
|
||||
```js
|
||||
const server = ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
// The account lock policy
|
||||
accountLockout: {
|
||||
// Lock the account for 5 minutes.
|
||||
duration: 5,
|
||||
// Lock an account after 3 failed log-in attempts
|
||||
threshold: 3,
|
||||
// Unlock the account after a successful password reset
|
||||
unlockOnPasswordReset: true,
|
||||
},
|
||||
|
||||
// The password policy
|
||||
passwordPolicy: {
|
||||
// Enforce a password of at least 8 characters which contain at least 1 lower case, 1 upper case and 1 digit
|
||||
validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/,
|
||||
// Do not allow the username as part of the password
|
||||
doNotAllowUsername: true,
|
||||
// Do not allow to re-use the last 5 passwords when setting a new password
|
||||
maxPasswordHistory: 5,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Routes
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
Custom routes allow to build user flows with webpages, similar to the existing password reset and email verification features. Custom routes are defined with the `pages` option in the Parse Server configuration:
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true, // Enables the experimental feature; required for custom routes
|
||||
customRoutes: [{
|
||||
method: 'GET',
|
||||
path: 'custom_route',
|
||||
handler: async request => {
|
||||
// custom logic
|
||||
// ...
|
||||
// then, depending on the outcome, return a HTML file as response
|
||||
return { file: 'custom_page.html' };
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The above route can be invoked by sending a `GET` request to:
|
||||
`https://[parseServerPublicUrl]/[parseMount]/[pagesEndpoint]/[appId]/[customRoute]`
|
||||
|
||||
The `handler` receives the `request` and returns a `custom_page.html` webpage from the `pages.pagesPath` directory as response. The advantage of building a custom route this way is that it automatically makes use of Parse Server's built-in capabilities, such as [page localization](#pages) and [dynamic placeholders](#dynamic-placeholders).
|
||||
|
||||
### Reserved Paths
|
||||
The following paths are already used by Parse Server's built-in features and are therefore not available for custom routes. Custom routes with an identical combination of `path` and `method` are ignored.
|
||||
|
||||
| Path | HTTP Method | Feature |
|
||||
|-----------------------------|-------------|--------------------|
|
||||
| `verify_email` | `GET` | email verification |
|
||||
| `resend_verification_email` | `POST` | email verification |
|
||||
| `choose_password` | `GET` | password reset |
|
||||
| `request_password_reset` | `GET` | password reset |
|
||||
| `request_password_reset` | `POST` | password reset |
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
|------------------------------|----------|-----------------|---------------|-----------------------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** |
|
||||
| `pages.customRoutes` | yes | `Array` | `[]` | - | `PARSE_SERVER_PAGES_CUSTOM_ROUTES` | The custom routes. The routes are added in the order they are defined here, which has to be considered since requests traverse routes in an ordered manner. Custom routes are traversed after build-in routes such as password reset and email verification. |
|
||||
| `pages.customRoutes.method` | | `String` | - | `GET`, `POST` | - | The HTTP method of the custom route. |
|
||||
| `pages.customRoutes.path` | | `String` | - | `custom_page` | - | The path of the custom route. Note that the same path can used if the `method` is different, for example a path `custom_page` can have two routes, a `GET` and `POST` route, which will be invoked depending on the HTTP request method. |
|
||||
| `pages.customRoutes.handler` | | `AsyncFunction` | - | `async () => { ... }` | - | The route handler that is invoked when the route matches the HTTP request. If the handler does not return a page, the request is answered with a 404 `Not found.` response. |
|
||||
|
||||
## Custom Pages
|
||||
|
||||
It’s possible to change the default pages of the app and redirect the user to another path or domain.
|
||||
|
||||
@@ -359,7 +456,7 @@ var server = ParseServer({
|
||||
})
|
||||
```
|
||||
|
||||
### Using environment variables to configure Parse Server
|
||||
## Using Environment Variables
|
||||
|
||||
You may configure the Parse Server using environment variables:
|
||||
|
||||
@@ -380,37 +477,38 @@ $ PORT=8080 parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
|
||||
|
||||
For the full list of configurable environment variables, run `parse-server --help` or take a look at [Parse Server Configuration](https://github.com/parse-community/parse-server/blob/master/src/Options/Definitions.js).
|
||||
|
||||
### Available Adapters
|
||||
## Available Adapters
|
||||
|
||||
All official adapters are distributed as scoped pacakges on [npm (@parse)](https://www.npmjs.com/search?q=scope%3Aparse).
|
||||
All official adapters are distributed as scoped packages on [npm (@parse)](https://www.npmjs.com/search?q=scope%3Aparse).
|
||||
|
||||
Some well maintained adapters are also available on the [Parse Server Modules](https://github.com/parse-server-modules) organization.
|
||||
|
||||
You can also find more adapters maintained by the community by searching on [npm](https://www.npmjs.com/search?q=parse-server%20adapter&page=1&ranking=optimal).
|
||||
|
||||
### Configuring File Adapters
|
||||
## Configuring File Adapters
|
||||
|
||||
Parse Server allows developers to choose from several options when hosting files:
|
||||
|
||||
* `GridFSBucketAdapter`, which is backed by MongoDB;
|
||||
* `S3Adapter`, which is backed by [Amazon S3](https://aws.amazon.com/s3/); or
|
||||
* `GCSAdapter`, which is backed by [Google Cloud Storage](https://cloud.google.com/storage/)
|
||||
* `GridFSBucketAdapter` - which is backed by MongoDB
|
||||
* `S3Adapter` - which is backed by [Amazon S3](https://aws.amazon.com/s3/)
|
||||
* `GCSAdapter` - which is backed by [Google Cloud Storage](https://cloud.google.com/storage/)
|
||||
* `FSAdapter` - local file storage
|
||||
|
||||
`GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters).
|
||||
`GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using Amazon S3, Google Cloud Storage, or local file storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters).
|
||||
|
||||
### Idempodency Enforcement
|
||||
## Idempotency Enforcement
|
||||
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
This feature deduplicates identical requests that are received by Parse Server mutliple times, typically due to network issues or network adapter access restrictions on mobile operating systems.
|
||||
This feature deduplicates identical requests that are received by Parse Server multiple times, typically due to network issues or network adapter access restrictions on mobile operating systems.
|
||||
|
||||
Identical requests are identified by their request header `X-Parse-Request-Id`. Therefore a client request has to include this header for deduplication to be applied. Requests that do not contain this header cannot be deduplicated and are processed normally by Parse Server. This means rolling out this feature to clients is seamless as Parse Server still processes request without this header when this feature is enbabled.
|
||||
Identical requests are identified by their request header `X-Parse-Request-Id`. Therefore a client request has to include this header for deduplication to be applied. Requests that do not contain this header cannot be deduplicated and are processed normally by Parse Server. This means rolling out this feature to clients is seamless as Parse Server still processes requests without this header when this feature is enabled.
|
||||
|
||||
> This feature needs to be enabled on the client side to send the header and on the server to process the header. Refer to the specific Parse SDK docs to see whether the feature is supported yet.
|
||||
|
||||
Deduplication is only done for object creation and update (`POST` and `PUT` requests). Deduplication is not done for object finding and deletion (`GET` and `DELETE` requests), as these operations are already idempotent by definition.
|
||||
|
||||
#### Configuration example
|
||||
### Configuration example <!-- omit in toc -->
|
||||
```
|
||||
let api = new ParseServer({
|
||||
idempotencyOptions: {
|
||||
@@ -419,19 +517,234 @@ let api = new ParseServer({
|
||||
}
|
||||
}
|
||||
```
|
||||
### Parameters <!-- omit in toc -->
|
||||
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
|----------------------------|----------|-----------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `idempotencyOptions` | yes | `Object` | `undefined` | | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS | Setting this enables idempotency enforcement for the specified paths. |
|
||||
| `idempotencyOptions.paths` | yes | `Array<String>` | `[]` | `.*` (all paths, includes the examples below), <br>`functions/.*` (all functions), <br>`jobs/.*` (all jobs), <br>`classes/.*` (all classes), <br>`functions/.*` (all functions), <br>`users` (user creation / update), <br>`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specify the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. |
|
||||
| `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. |
|
||||
|
||||
### Postgres <!-- omit in toc -->
|
||||
|
||||
To use this feature in Postgres, you will need to create a cron job using [pgAdmin](https://www.pgadmin.org/docs/pgadmin4/development/pgagent_jobs.html) or similar to call the Postgres function `idempotency_delete_expired_records()` that deletes expired idempotency records. You can find an example script below. Make sure the script has the same privileges to log into Postgres as Parse Server.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
SELECT idempotency_delete_expired_records();
|
||||
EOSQL
|
||||
|
||||
exec "$@"
|
||||
```
|
||||
|
||||
Assuming the script above is named, `parse_idempotency_delete_expired_records.sh`, a cron job that runs the script every 2 minutes may look like:
|
||||
|
||||
```bash
|
||||
2 * * * * /root/parse_idempotency_delete_expired_records.sh >/dev/null 2>&1
|
||||
```
|
||||
|
||||
## Localization
|
||||
|
||||
### Pages
|
||||
**Caution, this is an experimental feature that may not be appropriate for production.**
|
||||
|
||||
Custom pages as well as feature pages (e.g. password reset, email verification) can be localized with the `pages` option in the Parse Server configuration:
|
||||
|
||||
```js
|
||||
const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableLocalization: true,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Localization is achieved by matching a request-supplied `locale` parameter with localized page content. The locale can be supplied in either the request query, body or header with the following keys:
|
||||
- query: `locale`
|
||||
- body: `locale`
|
||||
- header: `x-parse-page-param-locale`
|
||||
|
||||
For example, a password reset link with the locale parameter in the query could look like this:
|
||||
```
|
||||
http://example.com/parse/apps/[appId]/request_password_reset?token=[token]&username=[username]&locale=de-AT
|
||||
```
|
||||
|
||||
- Localization is only available for pages in the pages directory as set with `pages.pagesPath`.
|
||||
- Localization for feature pages (e.g. password reset, email verification) is disabled if `pages.customUrls` are set, even if the custom URLs point to the pages within the pages path.
|
||||
- Only `.html` files are considered for localization when localizing custom pages.
|
||||
|
||||
Pages can be localized in two ways:
|
||||
|
||||
#### Localization with Directory Structure
|
||||
|
||||
Pages are localized by using the corresponding file in the directory structure where the files are placed in subdirectories named after the locale or language. The file in the base directory is the default file.
|
||||
|
||||
**Example Directory Structure:**
|
||||
```js
|
||||
root/
|
||||
├── public/ // pages base path
|
||||
│ ├── example.html // default file
|
||||
│ └── de/ // de language folder
|
||||
│ │ └── example.html // de localized file
|
||||
│ └── de-AT/ // de-AT locale folder
|
||||
│ │ └── example.html // de-AT localized file
|
||||
```
|
||||
|
||||
Files are matched with the locale in the following order:
|
||||
1. Locale match, e.g. locale `de-AT` matches file in folder `de-AT`.
|
||||
1. Language match, e.g. locale `de-CH` matches file in folder `de`.
|
||||
1. Default; file in base folder is returned.
|
||||
|
||||
**Configuration Example:**
|
||||
```js
|
||||
const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableLocalization: true,
|
||||
customUrls: {
|
||||
passwordReset: 'https://example.com/page.html'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pros:
|
||||
- All files are complete in their content and can be easily opened and previewed by viewing the file in a browser.
|
||||
|
||||
Cons:
|
||||
- In most cases, a localized page differs only slightly from the default page, which could cause a lot of duplicate code that is difficult to maintain.
|
||||
|
||||
#### Localization with JSON Resource
|
||||
|
||||
Pages are localized by adding placeholders in the HTML files and providing a JSON resource that contains the translations to fill into the placeholders.
|
||||
|
||||
**Example Directory Structure:**
|
||||
```js
|
||||
root/
|
||||
├── public/ // pages base path
|
||||
│ ├── example.html // the page containing placeholders
|
||||
├── private/ // folder outside of public scope
|
||||
│ └── translations.json // JSON resource file
|
||||
```
|
||||
|
||||
The JSON resource file loosely follows the [i18next](https://github.com/i18next/i18next) syntax, which is a syntax that is often supported by translation platforms, making it easy to manage translations, exporting them for use in Parse Server, and even to automate this workflow.
|
||||
|
||||
**Example JSON Content:**
|
||||
```json
|
||||
{
|
||||
"en": { // resource for language `en` (English)
|
||||
"translation": {
|
||||
"greeting": "Hello!"
|
||||
}
|
||||
},
|
||||
"de": { // resource for language `de` (German)
|
||||
"translation": {
|
||||
"greeting": "Hallo!"
|
||||
}
|
||||
}
|
||||
"de-AT": { // resource for locale `de-AT` (Austrian German)
|
||||
"translation": {
|
||||
"greeting": "Servus!"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Configuration Example:**
|
||||
```js
|
||||
const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
enableLocalization: true,
|
||||
localizationJsonPath: './private/localization.json',
|
||||
localizationFallbackLocale: 'en'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pros:
|
||||
- There is only one HTML file to maintain that contains the placeholders that are filled with the translations according to the locale.
|
||||
|
||||
Cons:
|
||||
- Files cannot be easily previewed by viewing the file in a browser because the content contains only placeholders and even HTML or CSS changes may be dynamically applied, e.g. when a localization requires a Right-To-Left layout direction.
|
||||
- Style and other fundamental layout changes may be more difficult to apply.
|
||||
|
||||
#### Dynamic placeholders
|
||||
|
||||
In addition to feature related default parameters such as `appId` and the translations provided via JSON resource, it is possible to define custom dynamic placeholders as part of the router configuration. This works independently of localization and, also if `enableLocalization` is disabled.
|
||||
|
||||
**Configuration Example:**
|
||||
```js
|
||||
const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
placeholders: {
|
||||
exampleKey: 'exampleValue'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
The placeholders can also be provided as function or as async function, with the `locale` and other feature related parameters passed through, to allow for dynamic placeholder values:
|
||||
|
||||
```js
|
||||
const api = new ParseServer({
|
||||
...otherOptions,
|
||||
|
||||
pages: {
|
||||
enableRouter: true, // Enables the experimental feature; required for localization
|
||||
placeholders: async (params) => {
|
||||
const value = await doSomething(params.locale);
|
||||
return {
|
||||
exampleKey: value
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Reserved Keys
|
||||
|
||||
The following parameter and placeholder keys are reserved because they are used related to features such as password reset or email verification. They should not be used as translation keys in the JSON resource or as manually defined placeholder keys in the configuration: `appId`, `appName`, `email`, `error`, `locale`, `publicServerUrl`, `token`, `username`.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
|-----------|----------|--------|---------------|-----------|-----------|-------------|
|
||||
| `idempotencyOptions` | yes | `Object` | `undefined` | | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS | Setting this enables idempotency enforcement for the specified paths. |
|
||||
| `idempotencyOptions.paths`| yes | `Array<String>` | `[]` | `.*` (all paths, includes the examples below), <br>`functions/.*` (all functions), <br>`jobs/.*` (all jobs), <br>`classes/.*` (all classes), <br>`functions/.*` (all functions), <br>`users` (user creation / update), <br>`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specifiy the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. |
|
||||
| `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. |
|
||||
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
|
||||
|-------------------------------------------------|----------|---------------------------------------|----------------------------------------|------------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. |
|
||||
| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** |
|
||||
| `pages.enableLocalization` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_LOCALIZATION` | Is true if pages should be localized; this has no effect on custom page redirects. |
|
||||
| `pages.localizationJsonPath` | yes | `String` | `undefined` | `./private/translations.json` | `PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH` | The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale. |
|
||||
| `pages.localizationFallbackLocale` | yes | `String` | `en` | `en`, `en-GB`, `default` | `PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE` | The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file. |
|
||||
| `pages.placeholders` | yes | `Object`, `Function`, `AsyncFunction` | `undefined` | `{ exampleKey: 'exampleValue' }` | `PARSE_SERVER_PAGES_PLACEHOLDERS` | The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function. |
|
||||
| `pages.forceRedirect` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_FORCE_REDIRECT` | Is `true` if responses should always be redirects and never content, `false` if the response type should depend on the request type (`GET` request -> content response; `POST` request -> redirect response). |
|
||||
| `pages.pagesPath` | yes | `String` | `./public` | `./files/pages`, `../../pages` | `PARSE_SERVER_PAGES_PAGES_PATH` | The path to the pages directory; this also defines where the static endpoint `/apps` points to. |
|
||||
| `pages.pagesEndpoint` | yes | `String` | `apps` | - | `PARSE_SERVER_PAGES_PAGES_ENDPOINT` | The API endpoint for the pages. |
|
||||
| `pages.customUrls` | yes | `Object` | `{}` | `{ passwordReset: 'https://example.com/page.html' }` | `PARSE_SERVER_PAGES_CUSTOM_URLS` | The URLs to the custom pages |
|
||||
| `pages.customUrls.passwordReset` | yes | `String` | `password_reset.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET` | The URL to the custom page for password reset. |
|
||||
| `pages.customUrls.passwordResetSuccess` | yes | `String` | `password_reset_success.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_SUCCESS` | The URL to the custom page for password reset -> success. |
|
||||
| `pages.customUrls.passwordResetLinkInvalid` | yes | `String` | `password_reset_link_invalid.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_PASSWORD_RESET_LINK_INVALID` | The URL to the custom page for password reset -> link invalid. |
|
||||
| `pages.customUrls.emailVerificationSuccess` | yes | `String` | `email_verification_success.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SUCCESS` | The URL to the custom page for email verification -> success. |
|
||||
| `pages.customUrls.emailVerificationSendFail` | yes | `String` | `email_verification_send_fail.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_FAIL` | The URL to the custom page for email verification -> link send fail. |
|
||||
| `pages.customUrls.emailVerificationSendSuccess` | yes | `String` | `email_verification_send_success.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_SEND_SUCCESS` | The URL to the custom page for email verification -> resend link -> success. |
|
||||
| `pages.customUrls.emailVerificationLinkInvalid` | yes | `String` | `email_verification_link_invalid.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_INVALID` | The URL to the custom page for email verification -> link invalid. |
|
||||
| `pages.customUrls.emailVerificationLinkExpired` | yes | `String` | `email_verification_link_expired.html` | - | `PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_EXPIRED` | The URL to the custom page for email verification -> link expired. |
|
||||
|
||||
#### Notes
|
||||
### Notes <!-- omit in toc -->
|
||||
|
||||
- This feature is currently only available for MongoDB and not for Postgres.
|
||||
- In combination with the [Parse Server API Mail Adapter](https://www.npmjs.com/package/parse-server-api-mail-adapter) Parse Server provides a fully localized flow (emails -> pages) for the user. The email adapter sends a localized email and adds a locale parameter to the password reset or email verification link, which is then used to respond with localized pages.
|
||||
|
||||
### Logging
|
||||
## Logging
|
||||
|
||||
Parse Server will, by default, log:
|
||||
* to the console
|
||||
@@ -441,13 +754,17 @@ Logs are also viewable in Parse Dashboard.
|
||||
|
||||
**Want to log each request and response?** Set the `VERBOSE` environment variable when starting `parse-server`. Usage :- `VERBOSE='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
|
||||
|
||||
**Want logs to be in placed in a different folder?** Pass the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server`. Usage :- `PARSE_SERVER_LOGS_FOLDER='<path-to-logs-folder>' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
|
||||
**Want logs to be placed in a different folder?** Pass the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server`. Usage :- `PARSE_SERVER_LOGS_FOLDER='<path-to-logs-folder>' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
|
||||
|
||||
**Want to log specific levels?** Pass the `logLevel` parameter when starting `parse-server`. Usage :- `parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --logLevel LOG_LEVEL`
|
||||
|
||||
**Want new line delimited JSON error logs (for consumption by CloudWatch, Google Cloud Logging, etc)?** Pass the `JSON_LOGS` environment variable when starting `parse-server`. Usage :- `JSON_LOGS='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
|
||||
|
||||
# Live Queries
|
||||
# Deprecations
|
||||
|
||||
See the [Deprecation Plan](https://github.com/parse-community/parse-server/blob/master/DEPRECATIONS.md) for an overview of deprecations and planned breaking changes.
|
||||
|
||||
# Live Query
|
||||
|
||||
Live queries are meant to be used in real-time reactive applications, where just using the traditional query paradigm could cause several problems, like increased response time and high network and server usage. Live queries should be used in cases where you need to continuously update a page with fresh data coming from the database, which often happens in (but is not limited to) online games, messaging clients and shared to-do lists.
|
||||
|
||||
@@ -484,13 +801,13 @@ $ docker build --tag parse-server .
|
||||
$ docker run --name my-mongo -d mongo
|
||||
```
|
||||
|
||||
#### Running the Parse Server Image
|
||||
#### Running the Parse Server Image <!-- omit in toc -->
|
||||
|
||||
```bash
|
||||
$ docker run --name my-parse-server --link my-mongo:mongo -v config-vol:/parse-server/config -p 1337:1337 -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test --publicServerURL http://localhost:1337/parse --mountGraphQL --mountPlayground
|
||||
```
|
||||
|
||||
***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/) feature, please add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure the `main.js` file is available in the `cloud-code-vol` directory before run this command. Otherwise, an error will occur.*
|
||||
***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/), add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to the command above. Make sure `main.js` is in the `cloud-code-vol` directory before starting Parse Server.*
|
||||
|
||||
After starting the server, you can visit http://localhost:1337/playground in your browser to start playing with your GraphQL API.
|
||||
|
||||
@@ -731,7 +1048,7 @@ To start creating your custom schema, you need to code a `schema.graphql` file a
|
||||
$ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://localhost/test --publicServerURL http://localhost:1337/parse --cloud ./cloud/main.js --graphQLSchema ./cloud/schema.graphql --mountGraphQL --mountPlayground
|
||||
```
|
||||
|
||||
### Creating your first custom query
|
||||
### Creating your first custom query <!-- omit in toc -->
|
||||
|
||||
Use the code below for your `schema.graphql` and `main.js` files. Then restart your Parse Server.
|
||||
|
||||
@@ -793,7 +1110,7 @@ directly on this branch:
|
||||
npm install parse-community/parse-server.git#master
|
||||
```
|
||||
|
||||
## Experimenting
|
||||
## Experimenting <!-- omit in toc -->
|
||||
|
||||
You can also use your own forks, and work in progress branches by specifying them:
|
||||
|
||||
@@ -857,3 +1174,7 @@ Support us with a monthly donation and help us continue our activities. [Become
|
||||
-----
|
||||
|
||||
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
|
||||
|
||||
[license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg
|
||||
[license-link]: LICENSE
|
||||
[open-collective-link]: https://opencollective.com/parse-server
|
||||
|
||||
+1
-83
@@ -16,86 +16,4 @@ We ask that:
|
||||
|
||||
# Communicating with us
|
||||
|
||||
All vulnerabilities should be privately reported to either [Node Security](https://nodesecurity.io/report) or directly to us at the following address [security at parseplatform dot org](mailto:security@parseplatform.org)
|
||||
|
||||
You can use our PGP public key, which is also uploaded [here](hkp://pgp.mit.edu):
|
||||
|
||||
```
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBFbZHTcBEADMJledXkBantsiKc5fbln3j+Bj3R2fP6xcUZ4N6RdKj/19G8e4
|
||||
+Lwso/SEDlKKuh+1ORHrcXbYBPNRTi+syf0dtL6uqNKVS+jzuS48qd7G04Foe+qs
|
||||
rg5k80TfRLboCoESIS4C8E6sdjCMKEj8b+QQU8YyzL470+gYwgg7bfvHyECuS4AD
|
||||
lPssBi03cQdVlYjxNWQZAfVMZ+5zcvpS4P5KOCZPT082rzlgQEmVpmNuTyBELNtl
|
||||
TBcVK9Sq6/KlNNSXMbGfJlMMq0kgAzVxrSyx3y0gOnRx1DR+a5jJSecPtdVJYno8
|
||||
9mwRT6Z1B/boN6GmEhC3vikmsOmA+umaLoscQcwjQj7jK5rPTF8ypuDfVNa+kAUS
|
||||
ONFrayDQljwMEVHZ5/lk9TfEwrnarN8q0fRs2MXaJsD/YlTHG5/9LJs3mMk5yQpq
|
||||
VGq0sydprnubW36nbP0SkH2LMRrLhQWoLEvtjkz7EaqGLWKO6N0Nr+BT1YBy5gM+
|
||||
evc5mUeHUTPqflDht1crHn0rdfWmtDzEsNUWc9GR1hK2+x8U43YUPDmmgRYZyCGP
|
||||
iKdmrF0kUDlh2mmok3dXlQCZesXaeFvSbIFMfL7midhbiWyCfDtAIQPfBTKNtfc3
|
||||
qbaAoEHmYS2Yjri0rRqK9zbFqDgOR7Ap/ExeoOuaAMx1bvjV0QBm0W8q+QARAQAB
|
||||
tC1BcnRodXIgQ2luYWRlciAoR2l0aHViKSA8YXJ0aHVyQHBvcHN1Z2FyLmNvbT6J
|
||||
Aj0EEwEKACcFAloYZqECGwMFCQeGH4AFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AA
|
||||
CgkQgZHETYyfyECCKA/8CbpKrMJn+UhP4s5eUisx6wSfqDWuHGkvhecxTWLRGGRT
|
||||
yycDm7PJxSb3AdJ//sUTGemG88kpLXmEGt3HpINqB0B4J+aqTB/Ei0+1g/FH0LXP
|
||||
RlCehH0RpLHJmplkEbd2VZ8wFN9+tW1u4jhG+LCZD8pAVy7f36QixCZA3fdlt9GN
|
||||
K2Jq2456dMpHmaLdUbrYERcDSKmDVKBRa8/CTe9hAkA83kAt0xgWjr/Byxw+L3wi
|
||||
Ar4/twAwLAHCzl7HTVvbWOXYehM8dpybE7rFV/1OACg3i2uppLE1oGeS2s4HBv84
|
||||
WYNx0oBlBzEefpDAxz1NQI4HnKtBopt8jNUs5GEa1GR4eSNdMf9SmX7MRBNgDKuY
|
||||
PsvZQLUBqG8GYZR214NzK9wf0VkQDkZ+PwG+L5pnpKtc7RwsR49z2qyti/nZfPP7
|
||||
y9gJanTNPkzgx2YAk+UBrKL7435XfFAW6mo2y5LLbD6ouT2hGDfnhsSuMrS4bAdM
|
||||
7ua9B8vs2cnwYXUFM7ydAueaPvfP0x5i0ZQrphls3ZUpKRpWORSXa0fTNinSpzqW
|
||||
YzTmPxJsHsyioPlRsl2/r97I9XJ9i5gjMDkNI3TQpGKFy/YNMk7rkk1dp3hq3aP/
|
||||
xt0P/2yL/MJEj9Jus9FTKGqVtOn73e8oSOsu0ngpllYasYaLkO19MJ2lemSW+CC0
|
||||
LEFydGh1ciBDaW5hZGVyIChHaXRodWIpIDxhY2luYWRlckBnbWFpbC5jb20+iQI9
|
||||
BBMBCgAnBQJaGGJcAhsDBQkHhh+ABQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJ
|
||||
EIGRxE2Mn8hAGVcP/RkqkER8/AKWfPFQs40Epe3mocuLyEW1CHX5LkFTjya42GAM
|
||||
0BKk+bStRrMQ4rBGOmdKGxphysQFZn4bscRUVMmJd/frJ0s8ConSfWzaweL7rbQO
|
||||
UgGnL4mSNUvQkNCoO/RgKJapq9G/+jA9RRYEoSncE1/i3FQ+96JWfRRYy3MGYi9H
|
||||
WmH3UFQ8cJ1JAFMIGaxuHuNQ20mStVDSuK3Zm8KVxk8rWHb2O8lye4bcBi7OLXYx
|
||||
oZEoLrbLQinMbuccNaMq2j3ZNLOPYUDyyv5O81WzN70A7r0rkipOaJx4LiXE2/NT
|
||||
3vz1CyT7i+2/GlLL113DP0DA8neMjx6MzpxOo7MgT+ZBHRRZh+tWoqfJKclh6Duw
|
||||
rAJ9BOxSCm1y4BxTxuWrb5mU/RDCe3oC7PTA6wIMbJThqxtRpjqa17oWn2UXyJOH
|
||||
aEXvt6jH6YqqFV9liArwkjZZl4KKyiqZ8UFKLteIVSK5xlwQ/ICW3uPYRpYhIFj0
|
||||
fMaqN5SFcMOxtD4L5SP4k7HRn8l/gVoWQyIMJMip87sPCw7mRe5jq91n9s33stHr
|
||||
vByL0ownS5MmvKXLLAyAltw2FcIyafcn6mKNGMUBunM14/j5uXaMcgz3MQtYjkvk
|
||||
Fh6uX1OqLt/rpOhsRTeDRvjGvAFtdLt1QtDEz4i9kGN4h4B/XqwEbVNMWyv4tDlB
|
||||
cnRodXIgQ2luYWRlciA8NzAwNTcyK2FjaW5hZGVyQHVzZXJzLm5vcmVwbHkuZ2l0
|
||||
aHViLmNvbT6JAj0EEwEKACcFAloYaaMCGwMFCQeGH4AFCwkIBwMFFQoJCAsFFgID
|
||||
AQACHgECF4AACgkQgZHETYyfyEANHBAAuOkRMEoCuRjN3Dz/bP7SpWSFnBjOWW42
|
||||
Lbie3bXbT1SYRltd7AM3ICu2M8OzjATzrDimmGi7K4qxFIGnz+sjp9NRr6x7Ohgi
|
||||
bPwmU1OMIjuARPhsauUyyUNI+wKbRG9/tO0YxOUBadsKcVYY+6JxhsjrO5qb9NUI
|
||||
WaNvwfCPlSBDcvsKCOVu6weyw9FGpaaKZcscge8tPPEQCf7FYKy6NYPVK6/D7qn6
|
||||
myaKe/dh/HwozZ0o2NhW3uIAdd4OIvmWE7rh97B7afKXTiIfiqWqtkFhH0RxdR2q
|
||||
Damg0BiGjdARqSnneLKDPgIwr904yM1RD36BkPcP8WH3ommsK95mrUKrZtLAQA6J
|
||||
J6uESkuHNtcy5XTx4eF2cD2uaJTcRjlbAHFBMEI/+vr4umo+8wt38JhY+XtSot6W
|
||||
rS99JU6Ht1/SMYdz/rFisOWHb6hS69DOSCEK68lne6n0u1AnsWnDHwbQxcaSEreR
|
||||
axXMzgMtRuM5R4ncLpx0nUwhxlRoIyo2GN6aghXcCrZt1fsLXBilag1moxZgh+YE
|
||||
RaVOsBASuqO/5m609Mi8AGLbuLU+39Ekb/b2ozw/MRvGPNfXC1XIqPe4asEE9GNL
|
||||
XdVqvrHhEexBpv7El9yQ9qyllzEEdv5+soMcUQmjJAVabx+0gtLb5x3QHD4V8ttT
|
||||
kA8kUPG5MyO5Ag0EVtkdNwEQAKssJS3MZiu6WkBact/HvDjJrq+S1HcxeTLYbFXK
|
||||
lEsolW5sw0IX5ORM9+Z9LfUTyVcyU6w/UbM91IecjNnFQkMvIQy8lVhrqO20FL46
|
||||
Vu6G5HezIf2hg/1vgt891hrKMrySQDDyGo68f6uF3U+SJLeNPRoB4O8qL2RHXfC3
|
||||
3ti6FAoOFfRGe/CNB35viK/L//6O3pCFz/nrckEaMzH/GOrcZ8xlrFyeKhsOjtoR
|
||||
S2MDSNpIJfZP+pbtBgVW5lA5HDlyy5s52jXgd0+1Ktw1FV1uCjsgaX9xfbfXG8o1
|
||||
SxpKpj1dI8WQ/7ZuCTxu0phyJsQPmfIHb5kBvZjm4vqpnCfbbFWxsQE+T01PRsV+
|
||||
rWdh1EG4dlTMkvZtMfAnDZV+Cqf6FELb/KhrbRqlCjHeC99tn6YP9EpvLNIgUnD6
|
||||
qiV2QVHMKZ+wRfRUAYUBtvbFYqbbEqLySpW0ahPB/UmLUMjvArzrQkxvKFM20nb8
|
||||
HnAAKAZpgjhXTO9OBiNErCfiORooZLEs1MBeR1u8932GL/uKSDX0RhTYBBFDVoNy
|
||||
zGj3lW3YfnCurVIjCoj+jAZGMSVi67GnUuhm0Vj2K4mdSbq40TwhXxKlp8G0uSU4
|
||||
SmCm+yjTVcgQj+Xj+fsFJh6YGIgkcLEpbZS6kCLKcnx+44U3nZYPZch0+3/m8Uaf
|
||||
i3e5ABEBAAGJAiUEGAEKAA8FAlbZHTcCGwwFCQeGH4AACgkQgZHETYyfyEBEcQ/8
|
||||
DXyIYahE4JmY4REkdSnTQQ09etNmlqZbnMo1y7aYqDgqoixGpZAyE5U3oxGMeNBD
|
||||
P+XEaZGDav9wfiOlnofMXBa65kbtWoz/+dLc+sTAjNdWvucuzP0yiE0+RNkOtvmY
|
||||
5BlGgIQS9PTRaw86aRFOE5LilAoR/jv+mOMPt1dcLfHksmCpW+3OzPyxCA703fE5
|
||||
l7xOXYOAhPGMco30EftebbZkiaAmoZFese92pRenTJXi007ALhMpjPbk5D7717DZ
|
||||
4/g2gqT+Zs8fZe4tUHjo8LSQrFh/i3TpyBoAIouJsuvVvXy0r+iucKvfBjB4vdQb
|
||||
b33Fft2DYVBMpVVfnjRg1Y+p5IFNWByI5NYfFsf8AWLHhOWargYmiUjHMdDFXuea
|
||||
3QUTzHARp4HsqoZocjhKEoW5+j0MTVM6q7cTGgkNvAUmlPEzpvjQP84zkeM7gskP
|
||||
vaKjgp0gIaCMlzP2fRSKqQ2f84LhKj0mZDy7HQNhtKme1l014HgTbbP7GDJ2UMse
|
||||
uHgdaLLljuHFbHYAgGI7Uck225weDESF8enizh1ZF1itRliN47ICsef1RQJCgrJb
|
||||
dkoPBN52k7VhS3vUIQhA1P1sLSEtPMuJ8SDq0CuA008WpU/xHdm1b+xcBxrabuoz
|
||||
6jfgzgnAZveF5DMisrOnbi4GHVIiHXvWrrIglA6o1sM=
|
||||
=paxU
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
```
|
||||
All vulnerabilities should be privately reported to us by going to [https://report.parseplatform.org](https://report.parseplatform.org). Alternatively, you can send an email to [security@parseplatform.org](mailto:security@parseplatform.org).
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
# [5.2.0-alpha.3](https://github.com/parse-community/parse-server/compare/5.2.0-alpha.2...5.2.0-alpha.3) (2022-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security bump minimist from 1.2.5 to 1.2.6 ([#7884](https://github.com/parse-community/parse-server/issues/7884)) ([c5cf282](https://github.com/parse-community/parse-server/commit/c5cf282d11ffdc023764f8e7539a2bd6bc246fe1))
|
||||
|
||||
# [5.2.0-alpha.2](https://github.com/parse-community/parse-server/compare/5.2.0-alpha.1...5.2.0-alpha.2) (2022-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* sensitive keyword detection may produce false positives ([#7881](https://github.com/parse-community/parse-server/issues/7881)) ([0d6f9e9](https://github.com/parse-community/parse-server/commit/0d6f9e951d9e186e95e96d8869066ce7022bad02))
|
||||
|
||||
# [5.2.0-alpha.1](https://github.com/parse-community/parse-server/compare/5.1.1...5.2.0-alpha.1) (2022-03-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improved LiveQuery error logging with additional information ([#7837](https://github.com/parse-community/parse-server/issues/7837)) ([443a509](https://github.com/parse-community/parse-server/commit/443a5099059538d379fe491793a5871fcbb4f377))
|
||||
|
||||
# [5.0.0-alpha.29](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.28...5.0.0-alpha.29) (2022-03-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* bump required node engine to >=12.22.10 ([#7846](https://github.com/parse-community/parse-server/issues/7846)) ([5ace99d](https://github.com/parse-community/parse-server/commit/5ace99d542a11e422af46d9fd6b1d3d2513b34cf))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This requires Node.js version >=12.22.10. ([5ace99d](5ace99d))
|
||||
|
||||
# [5.0.0-alpha.28](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.27...5.0.0-alpha.28) (2022-03-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security vulnerability that allows remote code execution (GHSA-p6h4-93qp-jhcm) ([#7844](https://github.com/parse-community/parse-server/issues/7844)) ([e569f40](https://github.com/parse-community/parse-server/commit/e569f402b1fd8648fb0d1523b71b2a03273902a5))
|
||||
|
||||
# [5.0.0-alpha.27](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.26...5.0.0-alpha.27) (2022-03-12)
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* update node engine to 2.22.0 ([#7827](https://github.com/parse-community/parse-server/issues/7827)) ([f235412](https://github.com/parse-community/parse-server/commit/f235412c1b6c2b173b7531f285429ea7214b56a2))
|
||||
|
||||
# [5.0.0-alpha.26](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.25...5.0.0-alpha.26) (2022-02-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* package.json & package-lock.json to reduce vulnerabilities ([#7823](https://github.com/parse-community/parse-server/issues/7823)) ([5ca2288](https://github.com/parse-community/parse-server/commit/5ca228882332b65f3ac05407e6e4da1ee3ef3749))
|
||||
|
||||
# [5.0.0-alpha.25](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.24...5.0.0-alpha.25) (2022-02-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* upgrade winston from 3.5.0 to 3.5.1 ([#7820](https://github.com/parse-community/parse-server/issues/7820)) ([4af253d](https://github.com/parse-community/parse-server/commit/4af253d1f8654a6f57b5137ad310cdacadc922cc))
|
||||
|
||||
# [5.0.0-alpha.24](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.23...5.0.0-alpha.24) (2022-02-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade follow-redirects from 1.14.7 to 1.14.8 ([#7801](https://github.com/parse-community/parse-server/issues/7801)) ([70088a9](https://github.com/parse-community/parse-server/commit/70088a95a78393da2a4ac68be81e63107747626a))
|
||||
|
||||
# [5.0.0-alpha.23](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.22...5.0.0-alpha.23) (2022-02-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* server crash using GraphQL due to missing @apollo/client peer dependency ([#7787](https://github.com/parse-community/parse-server/issues/7787)) ([08089d6](https://github.com/parse-community/parse-server/commit/08089d6fcbb215412448ce7d92b21b9fe6c929f2))
|
||||
|
||||
# [5.0.0-alpha.22](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.21...5.0.0-alpha.22) (2022-02-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* upgrade to MongoDB Node.js driver 4.x for MongoDB 5.0 support ([#7794](https://github.com/parse-community/parse-server/issues/7794)) ([f88aa2a](https://github.com/parse-community/parse-server/commit/f88aa2a62a533e5344d1c13dd38c5a0b283a480a))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* The MongoDB GridStore adapter has been removed. By default, Parse Server already uses GridFS, so if you do not manually use the GridStore adapter, you can ignore this change. ([f88aa2a](f88aa2a))
|
||||
|
||||
# [5.0.0-alpha.21](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.20...5.0.0-alpha.21) (2022-01-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Cloud Code context to `ParseObject.fetch` ([#7779](https://github.com/parse-community/parse-server/issues/7779)) ([315290d](https://github.com/parse-community/parse-server/commit/315290d16110110938f80a6b779cc2d1db58c552))
|
||||
|
||||
# [5.0.0-alpha.20](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.19...5.0.0-alpha.20) (2022-01-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump node-fetch from 2.6.1 to 3.1.1 ([#7782](https://github.com/parse-community/parse-server/issues/7782)) ([9082351](https://github.com/parse-community/parse-server/commit/90823514113a1a085ebc818f7109b3fd7591346f))
|
||||
|
||||
# [5.0.0-alpha.19](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.18...5.0.0-alpha.19) (2022-01-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump nanoid from 3.1.25 to 3.2.0 ([#7781](https://github.com/parse-community/parse-server/issues/7781)) ([f5f63bf](https://github.com/parse-community/parse-server/commit/f5f63bfc64d3481ed944ceb5e9f50b33dccd1ce9))
|
||||
|
||||
# [5.0.0-alpha.18](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.17...5.0.0-alpha.18) (2022-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade follow-redirects from 1.14.6 to 1.14.7 ([#7769](https://github.com/parse-community/parse-server/issues/7769)) ([8f5a861](https://github.com/parse-community/parse-server/commit/8f5a8618cfa7ed9a2a239a095abffa8f3fd8d31a))
|
||||
|
||||
# [5.0.0-alpha.17](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.16...5.0.0-alpha.17) (2022-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* schema cache not cleared in some cases ([#7678](https://github.com/parse-community/parse-server/issues/7678)) ([5af6e5d](https://github.com/parse-community/parse-server/commit/5af6e5dfaa129b1a350afcba4fb381b21c4cc35d))
|
||||
|
||||
# [5.0.0-alpha.16](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.15...5.0.0-alpha.16) (2022-01-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Idempotency to Postgres ([#7750](https://github.com/parse-community/parse-server/issues/7750)) ([0c3feaa](https://github.com/parse-community/parse-server/commit/0c3feaaa1751964c0db89f25674935c3354b1538))
|
||||
|
||||
# [5.0.0-alpha.15](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.14...5.0.0-alpha.15) (2022-01-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support `postgresql` protocol in database URI ([#7757](https://github.com/parse-community/parse-server/issues/7757)) ([caf4a23](https://github.com/parse-community/parse-server/commit/caf4a2341f554b28e3918c53e7e897a3ca47bf8b))
|
||||
|
||||
# [5.0.0-alpha.14](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.13...5.0.0-alpha.14) (2022-01-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support relativeTime query constraint on Postgres ([#7747](https://github.com/parse-community/parse-server/issues/7747)) ([16b1b2a](https://github.com/parse-community/parse-server/commit/16b1b2a19714535ca805f2dbb3b561d8f6a519a7))
|
||||
|
||||
# [5.0.0-alpha.13](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.12...5.0.0-alpha.13) (2021-12-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* node engine compatibility did not include node 16 ([#7739](https://github.com/parse-community/parse-server/issues/7739)) ([ea7c014](https://github.com/parse-community/parse-server/commit/ea7c01400f992a1263543706fe49b6174758a2d6))
|
||||
|
||||
# [5.0.0-alpha.12](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.11...5.0.0-alpha.12) (2021-12-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding or modifying a nested property requires addField permissions ([#7679](https://github.com/parse-community/parse-server/issues/7679)) ([6a6248b](https://github.com/parse-community/parse-server/commit/6a6248b6cb2e732d17131e18e659943b894ed2f1))
|
||||
|
||||
# [5.0.0-alpha.11](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.10...5.0.0-alpha.11) (2021-11-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* upgrade mime from 2.5.2 to 3.0.0 ([#7725](https://github.com/parse-community/parse-server/issues/7725)) ([f5ef98b](https://github.com/parse-community/parse-server/commit/f5ef98bde32083403c0e30a12162fcc1e52cac37))
|
||||
|
||||
# [5.0.0-alpha.10](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.9...5.0.0-alpha.10) (2021-11-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* upgrade parse from 3.3.1 to 3.4.0 ([#7723](https://github.com/parse-community/parse-server/issues/7723)) ([d4c1f47](https://github.com/parse-community/parse-server/commit/d4c1f473073764cb0570c633fc4a30669c2ce889))
|
||||
|
||||
# [5.0.0-alpha.9](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.8...5.0.0-alpha.9) (2021-11-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* unable to use objectId size higher than 19 on GraphQL API ([#7627](https://github.com/parse-community/parse-server/issues/7627)) ([ed86c80](https://github.com/parse-community/parse-server/commit/ed86c807721cc52a1a5a9dea0b768717eec269ed))
|
||||
|
||||
# [5.0.0-alpha.8](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.7...5.0.0-alpha.8) (2021-11-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for Node 16 ([#7707](https://github.com/parse-community/parse-server/issues/7707)) ([45cc58c](https://github.com/parse-community/parse-server/commit/45cc58c7e5e640a46c5d508019a3aa81242964b1))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Removes official Node 15 support which has reached it end-of-life date. ([45cc58c](45cc58c))
|
||||
|
||||
# [5.0.0-alpha.7](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.6...5.0.0-alpha.7) (2021-11-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* node engine range has no upper limit to exclude incompatible node versions ([#7692](https://github.com/parse-community/parse-server/issues/7692)) ([573558d](https://github.com/parse-community/parse-server/commit/573558d3adcbcc6222c92003829867e1a73eef94))
|
||||
|
||||
# [5.0.0-alpha.6](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.5...5.0.0-alpha.6) (2021-11-10)
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* refactor: allow ES import for cloud string if package type is module ([b64640c](https://github.com/parse-community/parse-server/commit/b64640c5705f733798783e68d216e957044ef23c))
|
||||
|
||||
# [5.0.0-alpha.5](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.4...5.0.0-alpha.5) (2021-11-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add user-defined schema and migrations ([#7418](https://github.com/parse-community/parse-server/issues/7418)) ([25d5c30](https://github.com/parse-community/parse-server/commit/25d5c30be2111be332eb779eb0697774a17da7af))
|
||||
|
||||
# [5.0.0-alpha.4](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.3...5.0.0-alpha.4) (2021-10-31)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for Postgres 14 ([#7644](https://github.com/parse-community/parse-server/issues/7644)) ([090350a](https://github.com/parse-community/parse-server/commit/090350a7a0fac945394ca1cb24b290316ef06aa7))
|
||||
|
||||
# [5.0.0-alpha.3](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.2...5.0.0-alpha.3) (2021-10-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* combined `and` query with relational query condition returns incorrect results ([#7593](https://github.com/parse-community/parse-server/issues/7593)) ([174886e](https://github.com/parse-community/parse-server/commit/174886e385e091c6bbd4a84891ef95f80b50d05c))
|
||||
|
||||
# [5.0.0-alpha.2](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.1...5.0.0-alpha.2) (2021-10-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* setting a field to null does not delete it via GraphQL API ([#7649](https://github.com/parse-community/parse-server/issues/7649)) ([626fad2](https://github.com/parse-community/parse-server/commit/626fad2e71017dcc62196c487de5f908fa43000b))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* To delete a field via the GraphQL API, the field value has to be set to `null`. Previously, setting a field value to `null` would save a null value in the database, which was not according to the [GraphQL specs](https://spec.graphql.org/June2018/#sec-Null-Value). To delete a file field use `file: null`, the previous way of using `file: { file: null }` has become obsolete. ([626fad2](626fad2))
|
||||
|
||||
# [5.0.0-alpha.1](https://github.com/parse-community/parse-server/compare/4.10.4...5.0.0-alpha.1) (2021-10-12)
|
||||
|
||||
## Breaking Changes
|
||||
- Improved schema caching through database real-time hooks. Reduces DB queries, decreases Parse Query execution time and fixes a potential schema memory leak. If multiple Parse Server instances connect to the same DB (for example behind a load balancer), set the [Parse Server Option](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) `databaseOptions.enableSchemaHooks: true` to enable this feature and keep the schema in sync across all instances. Failing to do so will cause a schema change to not propagate to other instances and re-syncing will only happen when these instances restart. The options `enableSingleSchemaCache` and `schemaCacheTTL` have been removed. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required. (Diamond Lewis, SebC) [#7214](https://github.com/parse-community/parse-server/issues/7214)
|
||||
- Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) (dblythy, Manuel Trezza) [#7071](https://github.com/parse-community/parse-server/pull/7071)
|
||||
- Removed [parse-server-simple-mailgun-adapter](https://github.com/parse-community/parse-server-simple-mailgun-adapter) dependency; to continue using the adapter it has to be explicitly installed (Manuel Trezza) [#7321](https://github.com/parse-community/parse-server/pull/7321)
|
||||
- Remove support for MongoDB 3.6 which has reached its End-of-Life date and PostgreSQL 10 (Manuel Trezza) [#7315](https://github.com/parse-community/parse-server/pull/7315)
|
||||
- Remove support for Node 10 which has reached its End-of-Life date (Manuel Trezza) [#7314](https://github.com/parse-community/parse-server/pull/7314)
|
||||
- Remove S3 Files Adapter from Parse Server, instead install separately as `@parse/s3-files-adapter` (Manuel Trezza) [#7324](https://github.com/parse-community/parse-server/pull/7324)
|
||||
- Remove Session field `restricted`; the field was a code artifact from a feature that never existed in Open Source Parse Server; if you have been using this field for custom purposes, consider that for new Parse Server installations the field does not exist anymore in the schema, and for existing installations the field default value `false` will not be set anymore when creating a new session (Manuel Trezza) [#7543](https://github.com/parse-community/parse-server/pull/7543)
|
||||
- ci: add node engine version check (Manuel Trezza) [#7574](https://github.com/parse-community/parse-server/pull/7574)
|
||||
|
||||
## Notable Changes
|
||||
- Alphabetical ordered GraphQL API, improved GraphQL Schema cache system and fix GraphQL input reassign issue (Moumouls) [#7344](https://github.com/parse-community/parse-server/issues/7344)
|
||||
- Added Parse Server Security Check to report weak security settings (Manuel Trezza, dblythy) [#7247](https://github.com/parse-community/parse-server/issues/7247)
|
||||
- EXPERIMENTAL: Added new page router with placeholder rendering and localization of custom and feature pages such as password reset and email verification (Manuel Trezza) [#7128](https://github.com/parse-community/parse-server/pull/7128)
|
||||
- EXPERIMENTAL: Added custom routes to easily customize flows for password reset, email verification or build entirely new flows (Manuel Trezza) [#7231](https://github.com/parse-community/parse-server/pull/7231)
|
||||
- Added Deprecation Policy to govern the introduction of breaking changes in a phased pattern that is more predictable for developers (Manuel Trezza) [#7199](https://github.com/parse-community/parse-server/pull/7199)
|
||||
- Add REST API endpoint `/loginAs` to create session of any user with master key; allows to impersonate another user. (GormanFletcher) [#7406](https://github.com/parse-community/parse-server/pull/7406)
|
||||
- Add official support for MongoDB 5.0 (Manuel Trezza) [#7469](https://github.com/parse-community/parse-server/pull/7469)
|
||||
- Added Parse Server Configuration `enforcePrivateUsers`, which will remove public access by default on new Parse.Users (dblythy) [#7319](https://github.com/parse-community/parse-server/pull/7319)
|
||||
|
||||
## Other Changes
|
||||
- Support native mongodb syntax in aggregation pipelines (Raschid JF Rafeally) [#7339](https://github.com/parse-community/parse-server/pull/7339)
|
||||
- Fix error when a not yet inserted job is updated (Antonio Davi Macedo Coelho de Castro) [#7196](https://github.com/parse-community/parse-server/pull/7196)
|
||||
- request.context for afterFind triggers (dblythy) [#7078](https://github.com/parse-community/parse-server/pull/7078)
|
||||
- Winston Logger interpolating stdout to console (dplewis) [#7114](https://github.com/parse-community/parse-server/pull/7114)
|
||||
- Added convenience method `Parse.Cloud.sendEmail(...)` to send email via email adapter in Cloud Code (dblythy) [#7089](https://github.com/parse-community/parse-server/pull/7089)
|
||||
- LiveQuery support for $and, $nor, $containedBy, $geoWithin, $geoIntersects queries (dplewis) [#7113](https://github.com/parse-community/parse-server/pull/7113)
|
||||
- Supporting patterns in LiveQuery server's config parameter `classNames` (Nes-si) [#7131](https://github.com/parse-community/parse-server/pull/7131)
|
||||
- Added `requireAnyUserRoles` and `requireAllUserRoles` for Parse Cloud validator (dblythy) [#7097](https://github.com/parse-community/parse-server/pull/7097)
|
||||
- Support Facebook Limited Login (miguel-s) [#7219](https://github.com/parse-community/parse-server/pull/7219)
|
||||
- Removed Stage name check on aggregate pipelines (BRETT71) [#7237](https://github.com/parse-community/parse-server/pull/7237)
|
||||
- Retry transactions on MongoDB when it fails due to transient error (Antonio Davi Macedo Coelho de Castro) [#7187](https://github.com/parse-community/parse-server/pull/7187)
|
||||
- Bump tests to use Mongo 4.4.4 (Antonio Davi Macedo Coelho de Castro) [#7184](https://github.com/parse-community/parse-server/pull/7184)
|
||||
- Added new account lockout policy option `accountLockout.unlockOnPasswordReset` to automatically unlock account on password reset (Manuel Trezza) [#7146](https://github.com/parse-community/parse-server/pull/7146)
|
||||
- Test Parse Server continuously against all recent MongoDB versions that have not reached their end-of-life support date, added MongoDB compatibility table to Parse Server docs (Manuel Trezza) [#7161](https://github.com/parse-community/parse-server/pull/7161)
|
||||
- Test Parse Server continuously against all recent Node.js versions that have not reached their end-of-life support date, added Node.js compatibility table to Parse Server docs (Manuel Trezza) [7161](https://github.com/parse-community/parse-server/pull/7177)
|
||||
- Throw error on invalid Cloud Function validation configuration (dblythy) [#7154](https://github.com/parse-community/parse-server/pull/7154)
|
||||
- Allow Cloud Validator `options` to be async (dblythy) [#7155](https://github.com/parse-community/parse-server/pull/7155)
|
||||
- Optimize queries on classes with pointer permissions (Pedro Diaz) [#7061](https://github.com/parse-community/parse-server/pull/7061)
|
||||
- Test Parse Server continuously against all relevant Postgres versions (minor versions), added Postgres compatibility table to Parse Server docs (Corey Baker) [#7176](https://github.com/parse-community/parse-server/pull/7176)
|
||||
- Randomize test suite (Diamond Lewis) [#7265](https://github.com/parse-community/parse-server/pull/7265)
|
||||
- LDAP: Properly unbind client on group search error (Diamond Lewis) [#7265](https://github.com/parse-community/parse-server/pull/7265)
|
||||
- Improve data consistency in Push and Job Status update (Diamond Lewis) [#7267](https://github.com/parse-community/parse-server/pull/7267)
|
||||
- Excluding keys that have trailing edges.node when performing GraphQL resolver (Chris Bland) [#7273](https://github.com/parse-community/parse-server/pull/7273)
|
||||
- Added centralized feature deprecation with standardized warning logs (Manuel Trezza) [#7303](https://github.com/parse-community/parse-server/pull/7303)
|
||||
- Use Node.js 15.13.0 in CI (Olle Jonsson) [#7312](https://github.com/parse-community/parse-server/pull/7312)
|
||||
- Fix file upload issue for S3 compatible storage (Linode, DigitalOcean) by avoiding empty tags property when creating a file (Ali Oguzhan Yildiz) [#7300](https://github.com/parse-community/parse-server/pull/7300)
|
||||
- Add building Docker image as CI check (Manuel Trezza) [#7332](https://github.com/parse-community/parse-server/pull/7332)
|
||||
- Add NPM package-lock version check to CI (Manuel Trezza) [#7333](https://github.com/parse-community/parse-server/pull/7333)
|
||||
- Fix incorrect LiveQuery events triggered for multiple subscriptions on the same class with different events [#7341](https://github.com/parse-community/parse-server/pull/7341)
|
||||
- Fix select and excludeKey queries to properly accept JSON string arrays. Also allow nested fields in exclude (Corey Baker) [#7242](https://github.com/parse-community/parse-server/pull/7242)
|
||||
- Fix LiveQuery server crash when using $all query operator on a missing object key (Jason Posthuma) [#7421](https://github.com/parse-community/parse-server/pull/7421)
|
||||
- Added runtime deprecation warnings (Manuel Trezza) [#7451](https://github.com/parse-community/parse-server/pull/7451)
|
||||
- Add ability to pass context of an object via a header, X-Parse-Cloud-Context, for Cloud Code triggers. The header addition allows client SDK's to add context without injecting _context in the body of JSON objects (Corey Baker) [#7437](https://github.com/parse-community/parse-server/pull/7437)
|
||||
- Add CI check to add changelog entry (Manuel Trezza) [#7512](https://github.com/parse-community/parse-server/pull/7512)
|
||||
- Refactor: uniform issue templates across repos (Manuel Trezza) [#7528](https://github.com/parse-community/parse-server/pull/7528)
|
||||
- ci: bump ci environment (Manuel Trezza) [#7539](https://github.com/parse-community/parse-server/pull/7539)
|
||||
- CI now pushes docker images to Docker Hub (Corey Baker) [#7548](https://github.com/parse-community/parse-server/pull/7548)
|
||||
- Allow afterFind and afterLiveQueryEvent to set unsaved pointers and keys (dblythy) [#7310](https://github.com/parse-community/parse-server/pull/7310)
|
||||
- Allow setting descending sort to full text queries (dblythy) [#7496](https://github.com/parse-community/parse-server/pull/7496)
|
||||
- Allow cloud string for ES modules (Daniel Blyth) [#7560](https://github.com/parse-community/parse-server/pull/7560)
|
||||
- docs: Introduce deprecation ID for reference in comments and online search (Manuel Trezza) [#7562](https://github.com/parse-community/parse-server/pull/7562)
|
||||
- refactor: deprecate `Parse.Cloud.httpRequest`; it is recommended to use a HTTP library instead. (Daniel Blyth) [#7595](https://github.com/parse-community/parse-server/pull/7595)
|
||||
- refactor: Modernize HTTPRequest tests (brandongregoryscott) [#7604](https://github.com/parse-community/parse-server/pull/7604)
|
||||
- Allow liveQuery on Session class (Daniel Blyth) [#7554](https://github.com/parse-community/parse-server/pull/7554)
|
||||
@@ -0,0 +1,191 @@
|
||||
# [5.2.0-beta.2](https://github.com/parse-community/parse-server/compare/5.2.0-beta.1...5.2.0-beta.2) (2022-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security bump minimist from 1.2.5 to 1.2.6 ([#7884](https://github.com/parse-community/parse-server/issues/7884)) ([c5cf282](https://github.com/parse-community/parse-server/commit/c5cf282d11ffdc023764f8e7539a2bd6bc246fe1))
|
||||
* sensitive keyword detection may produce false positives ([#7881](https://github.com/parse-community/parse-server/issues/7881)) ([0d6f9e9](https://github.com/parse-community/parse-server/commit/0d6f9e951d9e186e95e96d8869066ce7022bad02))
|
||||
|
||||
# [5.2.0-beta.1](https://github.com/parse-community/parse-server/compare/5.1.1...5.2.0-beta.1) (2022-03-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improved LiveQuery error logging with additional information ([#7837](https://github.com/parse-community/parse-server/issues/7837)) ([443a509](https://github.com/parse-community/parse-server/commit/443a5099059538d379fe491793a5871fcbb4f377))
|
||||
|
||||
# [5.0.0-beta.10](https://github.com/parse-community/parse-server/compare/5.0.0-beta.9...5.0.0-beta.10) (2022-03-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding or modifying a nested property requires addField permissions ([#7679](https://github.com/parse-community/parse-server/issues/7679)) ([6a6248b](https://github.com/parse-community/parse-server/commit/6a6248b6cb2e732d17131e18e659943b894ed2f1))
|
||||
* bump nanoid from 3.1.25 to 3.2.0 ([#7781](https://github.com/parse-community/parse-server/issues/7781)) ([f5f63bf](https://github.com/parse-community/parse-server/commit/f5f63bfc64d3481ed944ceb5e9f50b33dccd1ce9))
|
||||
* bump node-fetch from 2.6.1 to 3.1.1 ([#7782](https://github.com/parse-community/parse-server/issues/7782)) ([9082351](https://github.com/parse-community/parse-server/commit/90823514113a1a085ebc818f7109b3fd7591346f))
|
||||
* node engine compatibility did not include node 16 ([#7739](https://github.com/parse-community/parse-server/issues/7739)) ([ea7c014](https://github.com/parse-community/parse-server/commit/ea7c01400f992a1263543706fe49b6174758a2d6))
|
||||
* node engine range has no upper limit to exclude incompatible node versions ([#7692](https://github.com/parse-community/parse-server/issues/7692)) ([573558d](https://github.com/parse-community/parse-server/commit/573558d3adcbcc6222c92003829867e1a73eef94))
|
||||
* package.json & package-lock.json to reduce vulnerabilities ([#7823](https://github.com/parse-community/parse-server/issues/7823)) ([5ca2288](https://github.com/parse-community/parse-server/commit/5ca228882332b65f3ac05407e6e4da1ee3ef3749))
|
||||
* schema cache not cleared in some cases ([#7678](https://github.com/parse-community/parse-server/issues/7678)) ([5af6e5d](https://github.com/parse-community/parse-server/commit/5af6e5dfaa129b1a350afcba4fb381b21c4cc35d))
|
||||
* security upgrade follow-redirects from 1.14.6 to 1.14.7 ([#7769](https://github.com/parse-community/parse-server/issues/7769)) ([8f5a861](https://github.com/parse-community/parse-server/commit/8f5a8618cfa7ed9a2a239a095abffa8f3fd8d31a))
|
||||
* security upgrade follow-redirects from 1.14.7 to 1.14.8 ([#7801](https://github.com/parse-community/parse-server/issues/7801)) ([70088a9](https://github.com/parse-community/parse-server/commit/70088a95a78393da2a4ac68be81e63107747626a))
|
||||
* security vulnerability that allows remote code execution (GHSA-p6h4-93qp-jhcm) ([#7844](https://github.com/parse-community/parse-server/issues/7844)) ([e569f40](https://github.com/parse-community/parse-server/commit/e569f402b1fd8648fb0d1523b71b2a03273902a5))
|
||||
* server crash using GraphQL due to missing @apollo/client peer dependency ([#7787](https://github.com/parse-community/parse-server/issues/7787)) ([08089d6](https://github.com/parse-community/parse-server/commit/08089d6fcbb215412448ce7d92b21b9fe6c929f2))
|
||||
* unable to use objectId size higher than 19 on GraphQL API ([#7627](https://github.com/parse-community/parse-server/issues/7627)) ([ed86c80](https://github.com/parse-community/parse-server/commit/ed86c807721cc52a1a5a9dea0b768717eec269ed))
|
||||
* upgrade mime from 2.5.2 to 3.0.0 ([#7725](https://github.com/parse-community/parse-server/issues/7725)) ([f5ef98b](https://github.com/parse-community/parse-server/commit/f5ef98bde32083403c0e30a12162fcc1e52cac37))
|
||||
* upgrade parse from 3.3.1 to 3.4.0 ([#7723](https://github.com/parse-community/parse-server/issues/7723)) ([d4c1f47](https://github.com/parse-community/parse-server/commit/d4c1f473073764cb0570c633fc4a30669c2ce889))
|
||||
* upgrade winston from 3.5.0 to 3.5.1 ([#7820](https://github.com/parse-community/parse-server/issues/7820)) ([4af253d](https://github.com/parse-community/parse-server/commit/4af253d1f8654a6f57b5137ad310cdacadc922cc))
|
||||
|
||||
### Features
|
||||
|
||||
* add Cloud Code context to `ParseObject.fetch` ([#7779](https://github.com/parse-community/parse-server/issues/7779)) ([315290d](https://github.com/parse-community/parse-server/commit/315290d16110110938f80a6b779cc2d1db58c552))
|
||||
* add Idempotency to Postgres ([#7750](https://github.com/parse-community/parse-server/issues/7750)) ([0c3feaa](https://github.com/parse-community/parse-server/commit/0c3feaaa1751964c0db89f25674935c3354b1538))
|
||||
* add support for Node 16 ([#7707](https://github.com/parse-community/parse-server/issues/7707)) ([45cc58c](https://github.com/parse-community/parse-server/commit/45cc58c7e5e640a46c5d508019a3aa81242964b1))
|
||||
* bump required node engine to >=12.22.10 ([#7846](https://github.com/parse-community/parse-server/issues/7846)) ([5ace99d](https://github.com/parse-community/parse-server/commit/5ace99d542a11e422af46d9fd6b1d3d2513b34cf))
|
||||
* support `postgresql` protocol in database URI ([#7757](https://github.com/parse-community/parse-server/issues/7757)) ([caf4a23](https://github.com/parse-community/parse-server/commit/caf4a2341f554b28e3918c53e7e897a3ca47bf8b))
|
||||
* support relativeTime query constraint on Postgres ([#7747](https://github.com/parse-community/parse-server/issues/7747)) ([16b1b2a](https://github.com/parse-community/parse-server/commit/16b1b2a19714535ca805f2dbb3b561d8f6a519a7))
|
||||
* upgrade to MongoDB Node.js driver 4.x for MongoDB 5.0 support ([#7794](https://github.com/parse-community/parse-server/issues/7794)) ([f88aa2a](https://github.com/parse-community/parse-server/commit/f88aa2a62a533e5344d1c13dd38c5a0b283a480a))
|
||||
|
||||
### Reverts
|
||||
|
||||
* refactor: allow ES import for cloud string if package type is module ([b64640c](https://github.com/parse-community/parse-server/commit/b64640c5705f733798783e68d216e957044ef23c))
|
||||
* update node engine to 2.22.0 ([#7827](https://github.com/parse-community/parse-server/issues/7827)) ([f235412](https://github.com/parse-community/parse-server/commit/f235412c1b6c2b173b7531f285429ea7214b56a2))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This requires Node.js version >=12.22.10. ([5ace99d](5ace99d))
|
||||
* The MongoDB GridStore adapter has been removed. By default, Parse Server already uses GridFS, so if you do not manually use the GridStore adapter, you can ignore this change. ([f88aa2a](f88aa2a))
|
||||
* Removes official Node 15 support which has reached it end-of-life date. ([45cc58c](45cc58c))
|
||||
|
||||
# [5.0.0-beta.9](https://github.com/parse-community/parse-server/compare/5.0.0-beta.8...5.0.0-beta.9) (2022-03-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* bump required node engine to >=12.22.10 ([#7848](https://github.com/parse-community/parse-server/issues/7848)) ([23a3488](https://github.com/parse-community/parse-server/commit/23a3488f15511fafbe0e1d7ff0ef8355f9cb0215))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This requires Node.js version >=12.22.10. ([23a3488](23a3488))
|
||||
|
||||
# [5.0.0-beta.8](https://github.com/parse-community/parse-server/compare/5.0.0-beta.7...5.0.0-beta.8) (2022-03-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security vulnerability that allows remote code execution (GHSA-p6h4-93qp-jhcm) ([#7843](https://github.com/parse-community/parse-server/issues/7843)) ([971adb5](https://github.com/parse-community/parse-server/commit/971adb54387b0ede31be05ca407d5f35b4575c83))
|
||||
|
||||
# [5.0.0-beta.7](https://github.com/parse-community/parse-server/compare/5.0.0-beta.6...5.0.0-beta.7) (2022-02-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade follow-redirects from 1.14.7 to 1.14.8 ([#7802](https://github.com/parse-community/parse-server/issues/7802)) ([7029b27](https://github.com/parse-community/parse-server/commit/7029b274ca87bc8058617f29865d683dc3b351a1))
|
||||
|
||||
# [5.0.0-beta.6](https://github.com/parse-community/parse-server/compare/5.0.0-beta.5...5.0.0-beta.6) (2022-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade follow-redirects from 1.14.2 to 1.14.7 ([#7772](https://github.com/parse-community/parse-server/issues/7772)) ([4bd34b1](https://github.com/parse-community/parse-server/commit/4bd34b189bc9f5aa2e70b7e7c1a456e91b6de773))
|
||||
|
||||
# [5.0.0-beta.5](https://github.com/parse-community/parse-server/compare/5.0.0-beta.4...5.0.0-beta.5) (2022-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* schema cache not cleared in some cases ([#7771](https://github.com/parse-community/parse-server/issues/7771)) ([3b92fa1](https://github.com/parse-community/parse-server/commit/3b92fa1ca9e8889127a32eba913d68309397ca2c))
|
||||
|
||||
# [5.0.0-beta.4](https://github.com/parse-community/parse-server/compare/5.0.0-beta.3...5.0.0-beta.4) (2021-11-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* unable to use objectId size higher than 19 on GraphQL API ([#7722](https://github.com/parse-community/parse-server/issues/7722)) ([8ee0445](https://github.com/parse-community/parse-server/commit/8ee0445c0aeeb88dff2559b46ade408071d22143))
|
||||
|
||||
# [5.0.0-beta.3](https://github.com/parse-community/parse-server/compare/5.0.0-beta.2...5.0.0-beta.3) (2021-11-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* node engine range has no upper limit to exclude incompatible node versions ([#7693](https://github.com/parse-community/parse-server/issues/7693)) ([6a54dac](https://github.com/parse-community/parse-server/commit/6a54dac24d9fb63a44f311b8d414f4aa64140f32))
|
||||
|
||||
# [5.0.0-beta.2](https://github.com/parse-community/parse-server/compare/5.0.0-beta.1...5.0.0-beta.2) (2021-11-10)
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* refactor: allow ES import for cloud string if package type is module ([#7691](https://github.com/parse-community/parse-server/issues/7691)) ([200d4ba](https://github.com/parse-community/parse-server/commit/200d4ba9a527016a65668738c7728696f443bd53))
|
||||
|
||||
# [5.0.0-beta.1](https://github.com/parse-community/parse-server/compare/4.5.0...5.0.0-beta.1) (2021-11-01)
|
||||
|
||||
### BREAKING CHANGES
|
||||
- Improved schema caching through database real-time hooks. Reduces DB queries, decreases Parse Query execution time and fixes a potential schema memory leak. If multiple Parse Server instances connect to the same DB (for example behind a load balancer), set the [Parse Server Option](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) `databaseOptions.enableSchemaHooks: true` to enable this feature and keep the schema in sync across all instances. Failing to do so will cause a schema change to not propagate to other instances and re-syncing will only happen when these instances restart. The options `enableSingleSchemaCache` and `schemaCacheTTL` have been removed. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required. (Diamond Lewis, SebC) [#7214](https://github.com/parse-community/parse-server/issues/7214)
|
||||
- Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) (dblythy, Manuel Trezza) [#7071](https://github.com/parse-community/parse-server/pull/7071)
|
||||
- Removed [parse-server-simple-mailgun-adapter](https://github.com/parse-community/parse-server-simple-mailgun-adapter) dependency; to continue using the adapter it has to be explicitly installed (Manuel Trezza) [#7321](https://github.com/parse-community/parse-server/pull/7321)
|
||||
- Remove support for MongoDB 3.6 which has reached its End-of-Life date and PostgreSQL 10 (Manuel Trezza) [#7315](https://github.com/parse-community/parse-server/pull/7315)
|
||||
- Remove support for Node 10 which has reached its End-of-Life date (Manuel Trezza) [#7314](https://github.com/parse-community/parse-server/pull/7314)
|
||||
- Remove S3 Files Adapter from Parse Server, instead install separately as `@parse/s3-files-adapter` (Manuel Trezza) [#7324](https://github.com/parse-community/parse-server/pull/7324)
|
||||
- Remove Session field `restricted`; the field was a code artifact from a feature that never existed in Open Source Parse Server; if you have been using this field for custom purposes, consider that for new Parse Server installations the field does not exist anymore in the schema, and for existing installations the field default value `false` will not be set anymore when creating a new session (Manuel Trezza) [#7543](https://github.com/parse-community/parse-server/pull/7543)
|
||||
- ci: add node engine version check (Manuel Trezza) [#7574](https://github.com/parse-community/parse-server/pull/7574)
|
||||
- To delete a field via the GraphQL API, the field value has to be set to `null`. Previously, setting a field value to `null` would save a null value in the database, which was not according to the [GraphQL specs](https://spec.graphql.org/June2018/#sec-Null-Value). To delete a file field use `file: null`, the previous way of using `file: { file: null }` has become obsolete. ([626fad2](626fad2))
|
||||
|
||||
### Notable Changes
|
||||
- Alphabetical ordered GraphQL API, improved GraphQL Schema cache system and fix GraphQL input reassign issue (Moumouls) [#7344](https://github.com/parse-community/parse-server/issues/7344)
|
||||
- Added Parse Server Security Check to report weak security settings (Manuel Trezza, dblythy) [#7247](https://github.com/parse-community/parse-server/issues/7247)
|
||||
- EXPERIMENTAL: Added new page router with placeholder rendering and localization of custom and feature pages such as password reset and email verification (Manuel Trezza) [#7128](https://github.com/parse-community/parse-server/pull/7128)
|
||||
- EXPERIMENTAL: Added custom routes to easily customize flows for password reset, email verification or build entirely new flows (Manuel Trezza) [#7231](https://github.com/parse-community/parse-server/pull/7231)
|
||||
- Added Deprecation Policy to govern the introduction of breaking changes in a phased pattern that is more predictable for developers (Manuel Trezza) [#7199](https://github.com/parse-community/parse-server/pull/7199)
|
||||
- Add REST API endpoint `/loginAs` to create session of any user with master key; allows to impersonate another user. (GormanFletcher) [#7406](https://github.com/parse-community/parse-server/pull/7406)
|
||||
- Add official support for MongoDB 5.0 (Manuel Trezza) [#7469](https://github.com/parse-community/parse-server/pull/7469)
|
||||
- Added Parse Server Configuration `enforcePrivateUsers`, which will remove public access by default on new Parse.Users (dblythy) [#7319](https://github.com/parse-community/parse-server/pull/7319)
|
||||
* add support for Postgres 14 ([#7644](https://github.com/parse-community/parse-server/issues/7644)) ([090350a](https://github.com/parse-community/parse-server/commit/090350a7a0fac945394ca1cb24b290316ef06aa7))
|
||||
* add user-defined schema and migrations ([#7418](https://github.com/parse-community/parse-server/issues/7418)) ([25d5c30](https://github.com/parse-community/parse-server/commit/25d5c30be2111be332eb779eb0697774a17da7af))
|
||||
* setting a field to null does not delete it via GraphQL API ([#7649](https://github.com/parse-community/parse-server/issues/7649)) ([626fad2](https://github.com/parse-community/parse-server/commit/626fad2e71017dcc62196c487de5f908fa43000b))
|
||||
* combined `and` query with relational query condition returns incorrect results ([#7593](https://github.com/parse-community/parse-server/issues/7593)) ([174886e](https://github.com/parse-community/parse-server/commit/174886e385e091c6bbd4a84891ef95f80b50d05c))
|
||||
|
||||
### Other Changes
|
||||
- Support native mongodb syntax in aggregation pipelines (Raschid JF Rafeally) [#7339](https://github.com/parse-community/parse-server/pull/7339)
|
||||
- Fix error when a not yet inserted job is updated (Antonio Davi Macedo Coelho de Castro) [#7196](https://github.com/parse-community/parse-server/pull/7196)
|
||||
- request.context for afterFind triggers (dblythy) [#7078](https://github.com/parse-community/parse-server/pull/7078)
|
||||
- Winston Logger interpolating stdout to console (dplewis) [#7114](https://github.com/parse-community/parse-server/pull/7114)
|
||||
- Added convenience method `Parse.Cloud.sendEmail(...)` to send email via email adapter in Cloud Code (dblythy) [#7089](https://github.com/parse-community/parse-server/pull/7089)
|
||||
- LiveQuery support for $and, $nor, $containedBy, $geoWithin, $geoIntersects queries (dplewis) [#7113](https://github.com/parse-community/parse-server/pull/7113)
|
||||
- Supporting patterns in LiveQuery server's config parameter `classNames` (Nes-si) [#7131](https://github.com/parse-community/parse-server/pull/7131)
|
||||
- Added `requireAnyUserRoles` and `requireAllUserRoles` for Parse Cloud validator (dblythy) [#7097](https://github.com/parse-community/parse-server/pull/7097)
|
||||
- Support Facebook Limited Login (miguel-s) [#7219](https://github.com/parse-community/parse-server/pull/7219)
|
||||
- Removed Stage name check on aggregate pipelines (BRETT71) [#7237](https://github.com/parse-community/parse-server/pull/7237)
|
||||
- Retry transactions on MongoDB when it fails due to transient error (Antonio Davi Macedo Coelho de Castro) [#7187](https://github.com/parse-community/parse-server/pull/7187)
|
||||
- Bump tests to use Mongo 4.4.4 (Antonio Davi Macedo Coelho de Castro) [#7184](https://github.com/parse-community/parse-server/pull/7184)
|
||||
- Added new account lockout policy option `accountLockout.unlockOnPasswordReset` to automatically unlock account on password reset (Manuel Trezza) [#7146](https://github.com/parse-community/parse-server/pull/7146)
|
||||
- Test Parse Server continuously against all recent MongoDB versions that have not reached their end-of-life support date, added MongoDB compatibility table to Parse Server docs (Manuel Trezza) [#7161](https://github.com/parse-community/parse-server/pull/7161)
|
||||
- Test Parse Server continuously against all recent Node.js versions that have not reached their end-of-life support date, added Node.js compatibility table to Parse Server docs (Manuel Trezza) [7161](https://github.com/parse-community/parse-server/pull/7177)
|
||||
- Throw error on invalid Cloud Function validation configuration (dblythy) [#7154](https://github.com/parse-community/parse-server/pull/7154)
|
||||
- Allow Cloud Validator `options` to be async (dblythy) [#7155](https://github.com/parse-community/parse-server/pull/7155)
|
||||
- Optimize queries on classes with pointer permissions (Pedro Diaz) [#7061](https://github.com/parse-community/parse-server/pull/7061)
|
||||
- Test Parse Server continuously against all relevant Postgres versions (minor versions), added Postgres compatibility table to Parse Server docs (Corey Baker) [#7176](https://github.com/parse-community/parse-server/pull/7176)
|
||||
- Randomize test suite (Diamond Lewis) [#7265](https://github.com/parse-community/parse-server/pull/7265)
|
||||
- LDAP: Properly unbind client on group search error (Diamond Lewis) [#7265](https://github.com/parse-community/parse-server/pull/7265)
|
||||
- Improve data consistency in Push and Job Status update (Diamond Lewis) [#7267](https://github.com/parse-community/parse-server/pull/7267)
|
||||
- Excluding keys that have trailing edges.node when performing GraphQL resolver (Chris Bland) [#7273](https://github.com/parse-community/parse-server/pull/7273)
|
||||
- Added centralized feature deprecation with standardized warning logs (Manuel Trezza) [#7303](https://github.com/parse-community/parse-server/pull/7303)
|
||||
- Use Node.js 15.13.0 in CI (Olle Jonsson) [#7312](https://github.com/parse-community/parse-server/pull/7312)
|
||||
- Fix file upload issue for S3 compatible storage (Linode, DigitalOcean) by avoiding empty tags property when creating a file (Ali Oguzhan Yildiz) [#7300](https://github.com/parse-community/parse-server/pull/7300)
|
||||
- Add building Docker image as CI check (Manuel Trezza) [#7332](https://github.com/parse-community/parse-server/pull/7332)
|
||||
- Add NPM package-lock version check to CI (Manuel Trezza) [#7333](https://github.com/parse-community/parse-server/pull/7333)
|
||||
- Fix incorrect LiveQuery events triggered for multiple subscriptions on the same class with different events [#7341](https://github.com/parse-community/parse-server/pull/7341)
|
||||
- Fix select and excludeKey queries to properly accept JSON string arrays. Also allow nested fields in exclude (Corey Baker) [#7242](https://github.com/parse-community/parse-server/pull/7242)
|
||||
- Fix LiveQuery server crash when using $all query operator on a missing object key (Jason Posthuma) [#7421](https://github.com/parse-community/parse-server/pull/7421)
|
||||
- Added runtime deprecation warnings (Manuel Trezza) [#7451](https://github.com/parse-community/parse-server/pull/7451)
|
||||
- Add ability to pass context of an object via a header, X-Parse-Cloud-Context, for Cloud Code triggers. The header addition allows client SDK's to add context without injecting _context in the body of JSON objects (Corey Baker) [#7437](https://github.com/parse-community/parse-server/pull/7437)
|
||||
- Add CI check to add changelog entry (Manuel Trezza) [#7512](https://github.com/parse-community/parse-server/pull/7512)
|
||||
- Refactor: uniform issue templates across repos (Manuel Trezza) [#7528](https://github.com/parse-community/parse-server/pull/7528)
|
||||
- ci: bump ci environment (Manuel Trezza) [#7539](https://github.com/parse-community/parse-server/pull/7539)
|
||||
- CI now pushes docker images to Docker Hub (Corey Baker) [#7548](https://github.com/parse-community/parse-server/pull/7548)
|
||||
- Allow afterFind and afterLiveQueryEvent to set unsaved pointers and keys (dblythy) [#7310](https://github.com/parse-community/parse-server/pull/7310)
|
||||
- Allow setting descending sort to full text queries (dblythy) [#7496](https://github.com/parse-community/parse-server/pull/7496)
|
||||
- Allow cloud string for ES modules (Daniel Blyth) [#7560](https://github.com/parse-community/parse-server/pull/7560)
|
||||
- docs: Introduce deprecation ID for reference in comments and online search (Manuel Trezza) [#7562](https://github.com/parse-community/parse-server/pull/7562)
|
||||
- refactor: deprecate `Parse.Cloud.httpRequest`; it is recommended to use a HTTP library instead. (Daniel Blyth) [#7595](https://github.com/parse-community/parse-server/pull/7595)
|
||||
- refactor: Modernize HTTPRequest tests (brandongregoryscott) [#7604](https://github.com/parse-community/parse-server/pull/7604)
|
||||
- Allow liveQuery on Session class (Daniel Blyth) [#7554](https://github.com/parse-community/parse-server/pull/7554)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,290 @@
|
||||
const core = require('@actions/core');
|
||||
const semver = require('semver');
|
||||
const yaml = require('yaml');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
/**
|
||||
* This checks the CI version of an environment variable in a YAML file
|
||||
* against a list of released versions of a package.
|
||||
*/
|
||||
class CiVersionCheck {
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
* @param {Object} config The config.
|
||||
* @param {String} config.packageName The package name to check.
|
||||
* @param {String} config.packageSupportUrl The URL to the package website
|
||||
* that shows the End-of-Life support dates.
|
||||
* @param {String} config.yamlFilePath The path to the GitHub workflow YAML
|
||||
* file that contains the tests.
|
||||
* @param {String} config.ciEnvironmentsKeyPath The key path in the CI YAML
|
||||
* file to the environment specifications.
|
||||
* @param {String} config.ciVersionKey The key in the CI YAML file to
|
||||
* determine the package version.
|
||||
* @param {Array<String>} config.releasedVersions The released versions of
|
||||
* the package to check against.
|
||||
* @param {Array<String>} config.ignoreReleasedVersions The versions to
|
||||
* ignore when checking whether the CI tests against the latest versions.
|
||||
* This can be used in case there is a package release for which Parse
|
||||
* Server compatibility is not required.
|
||||
* @param {String} [config.latestComponent='patch'] The version component
|
||||
* (`major`, `minor`, `patch`) that must be the latest released version.
|
||||
* Default is `patch`.
|
||||
*
|
||||
* For example:
|
||||
* - Released versions: 1.0.0, 1.2.0, 1.2.1, 1.3.0, 1.3.1, 2.0.0
|
||||
* - Tested version: 1.2.0
|
||||
*
|
||||
* If the latest version component is `patch`, then the check would
|
||||
* fail and recommend an upgrade to version 1.2.1 and to add additional
|
||||
* tests against 1.3.1 and 2.0.0.
|
||||
* If the latest version component is `minor` then the check would
|
||||
* fail and recommend an upgrade to version 1.3.0 and to add an additional
|
||||
* test against 2.0.0.
|
||||
* If the latest version component is `major` then the check would
|
||||
* fail and recommend an upgrade to version 2.0.0.
|
||||
*/
|
||||
constructor(config) {
|
||||
const {
|
||||
packageName,
|
||||
packageSupportUrl,
|
||||
yamlFilePath,
|
||||
ciEnvironmentsKeyPath,
|
||||
ciVersionKey,
|
||||
releasedVersions,
|
||||
ignoreReleasedVersions = [],
|
||||
latestComponent = CiVersionCheck.versionComponents.patch,
|
||||
} = config;
|
||||
|
||||
// Ensure required params are set
|
||||
if ([
|
||||
packageName,
|
||||
packageSupportUrl,
|
||||
yamlFilePath,
|
||||
ciEnvironmentsKeyPath,
|
||||
ciVersionKey,
|
||||
releasedVersions,
|
||||
].includes(undefined)) {
|
||||
throw 'invalid configuration';
|
||||
}
|
||||
|
||||
if (!Object.keys(CiVersionCheck.versionComponents).includes(latestComponent)) {
|
||||
throw 'invalid configuration for latestComponent';
|
||||
}
|
||||
|
||||
this.packageName = packageName;
|
||||
this.packageSupportUrl = packageSupportUrl;
|
||||
this.yamlFilePath = yamlFilePath;
|
||||
this.ciEnvironmentsKeyPath = ciEnvironmentsKeyPath;
|
||||
this.ciVersionKey = ciVersionKey;
|
||||
this.releasedVersions = releasedVersions;
|
||||
this.ignoreReleasedVersions = ignoreReleasedVersions;
|
||||
this.latestComponent = latestComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition of version components.
|
||||
*/
|
||||
static get versionComponents() {
|
||||
return Object.freeze({
|
||||
major: 'major',
|
||||
minor: 'minor',
|
||||
patch: 'patch',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the test environments as specified in the YAML file.
|
||||
*/
|
||||
async getTests() {
|
||||
try {
|
||||
// Get CI workflow
|
||||
const ciYaml = await fs.readFile(this.yamlFilePath, 'utf-8');
|
||||
const ci = yaml.parse(ciYaml);
|
||||
|
||||
// Extract package versions
|
||||
let versions = this.ciEnvironmentsKeyPath.split('.').reduce((o,k) => o !== undefined ? o[k] : undefined, ci);
|
||||
versions = Object.entries(versions)
|
||||
.map(entry => entry[1])
|
||||
.filter(entry => entry[this.ciVersionKey]);
|
||||
|
||||
return versions;
|
||||
} catch (e) {
|
||||
throw `Failed to determine ${this.packageName} versions from CI YAML file with error: ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the package versions which are missing in the CI environment.
|
||||
* @param {Array<String>} releasedVersions The released versions; need to
|
||||
* be sorted descending.
|
||||
* @param {Array<String>} testedVersions The tested versions.
|
||||
* @param {String} versionComponent The latest version component.
|
||||
* @returns {Array<String>} The untested versions.
|
||||
*/
|
||||
getUntestedVersions(releasedVersions, testedVersions, versionComponent) {
|
||||
// Use these example values for debugging the version range logic below
|
||||
// versionComponent = CiVersionCheck.versionComponents.patch;
|
||||
// this.ignoreReleasedVersions = ['<4.4.0', '~4.7.0'];
|
||||
// testedVersions = ['4.4.3'];
|
||||
// releasedVersions = [
|
||||
// '5.0.0-rc0',
|
||||
// '5.0.0',
|
||||
// '4.9.1',
|
||||
// '4.9.0',
|
||||
// '4.8.1',
|
||||
// '4.8.0',
|
||||
// '4.7.1',
|
||||
// '4.7.0',
|
||||
// '4.4.3',
|
||||
// '4.4.2',
|
||||
// '4.4.0',
|
||||
// '4.1.0',
|
||||
// '3.5.0',
|
||||
// ];
|
||||
|
||||
// Determine operator for range comparison
|
||||
const operator = versionComponent == CiVersionCheck.versionComponents.major
|
||||
? '>='
|
||||
: versionComponent == CiVersionCheck.versionComponents.minor
|
||||
? '^'
|
||||
: '~'
|
||||
|
||||
// Get all untested versions
|
||||
const untestedVersions = releasedVersions.reduce((m, v) => {
|
||||
// If the version should be ignored, skip it
|
||||
if (this.ignoreReleasedVersions.length > 0 && semver.satisfies(v, this.ignoreReleasedVersions.join(' || '))) {
|
||||
return m;
|
||||
}
|
||||
// If the version is a pre-release, skip it
|
||||
if ((semver.prerelease(v) || []).length > 0) {
|
||||
return m;
|
||||
}
|
||||
// If a satisfying version has already been added to untested, skip it
|
||||
if (semver.maxSatisfying(m, `${operator}${v}`)) {
|
||||
return m;
|
||||
}
|
||||
// If a satisfying version is already tested, skip it
|
||||
if (semver.maxSatisfying(testedVersions, `${operator}${v}`)) {
|
||||
return m;
|
||||
}
|
||||
// Add version
|
||||
m.push(v);
|
||||
return m;
|
||||
}, []);
|
||||
|
||||
return untestedVersions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest version for a given version and component.
|
||||
* @param {Array<String>} versions The versions in which to search.
|
||||
* @param {String} version The version for which a newer version
|
||||
* should be searched.
|
||||
* @param {String} versionComponent The version component up to
|
||||
* which the latest version should be checked.
|
||||
* @returns {String|undefined} The newer version.
|
||||
*/
|
||||
getNewerVersion(versions, version, versionComponent) {
|
||||
// Determine operator for range comparison
|
||||
const operator = versionComponent == CiVersionCheck.versionComponents.major
|
||||
? '>='
|
||||
: versionComponent == CiVersionCheck.versionComponents.minor
|
||||
? '^'
|
||||
: '~'
|
||||
const latest = semver.maxSatisfying(versions, `${operator}${version}`);
|
||||
|
||||
// If the version should be ignored, skip it
|
||||
if (this.ignoreReleasedVersions.length > 0 && semver.satisfies(latest, this.ignoreReleasedVersions.join(' || '))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Return the latest version if it is newer than any currently used version
|
||||
return semver.gt(latest, version) ? latest : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This validates that the given versions strictly follow semver
|
||||
* syntax.
|
||||
* @param {Array<String>} versions The versions to check.
|
||||
*/
|
||||
_validateVersionSyntax(versions) {
|
||||
for (const version of versions) {
|
||||
if (!semver.valid(version)) {
|
||||
throw version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the check.
|
||||
*/
|
||||
async check() {
|
||||
try {
|
||||
console.log(`\nChecking ${this.packageName} versions in CI environments...`);
|
||||
|
||||
// Validate released versions syntax
|
||||
try {
|
||||
this._validateVersionSyntax(this.releasedVersions);
|
||||
} catch (e) {
|
||||
core.setFailed(`Failed to check ${this.packageName} versions because released version '${e}' does not follow semver syntax (x.y.z).`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort versions descending
|
||||
semver.sort(this.releasedVersions).reverse()
|
||||
|
||||
// Get tested package versions from CI
|
||||
const tests = await this.getTests();
|
||||
|
||||
// Is true if any of the checks failed
|
||||
let failed = false;
|
||||
|
||||
// Check whether each tested version is the latest patch
|
||||
for (const test of tests) {
|
||||
const version = test[this.ciVersionKey];
|
||||
|
||||
// Validate version syntax
|
||||
try {
|
||||
this._validateVersionSyntax([version]);
|
||||
} catch (e) {
|
||||
core.setFailed(`Failed to check ${this.packageName} versions because environment version '${e}' does not follow semver syntax (x.y.z).`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newer = this.getNewerVersion(this.releasedVersions, version, this.latestComponent);
|
||||
if (newer) {
|
||||
console.log(`❌ CI environment '${test.name}' uses an old ${this.packageName} ${this.latestComponent} version ${version} instead of ${newer}.`);
|
||||
failed = true;
|
||||
} else {
|
||||
console.log(`✅ CI environment '${test.name}' uses the latest ${this.packageName} ${this.latestComponent} version ${version}.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether there is a newer component version available that is not tested
|
||||
const testedVersions = tests.map(test => test[this.ciVersionKey]);
|
||||
const untested = this.getUntestedVersions(this.releasedVersions, testedVersions, this.latestComponent);
|
||||
if (untested.length > 0) {
|
||||
console.log(`❌ CI does not have environments using the following versions of ${this.packageName}: ${untested.join(', ')}.`);
|
||||
failed = true;
|
||||
} else {
|
||||
console.log(`✅ CI has environments using all recent versions of ${this.packageName}.`);
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
core.setFailed(
|
||||
`CI environments are not up-to-date with the latest ${this.packageName} versions.` +
|
||||
`\n\nCheck the error messages above and update the ${this.packageName} versions in the CI YAML ` +
|
||||
`file.\n\nℹ️ Additionally, there may be versions of ${this.packageName} that have reached their official end-of-life ` +
|
||||
`support date and should be removed from the CI, see ${this.packageSupportUrl}.`
|
||||
);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
const msg = `Failed to check ${this.packageName} versions with error: ${e}`;
|
||||
core.setFailed(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CiVersionCheck;
|
||||
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
const CiVersionCheck = require('./CiVersionCheck');
|
||||
const mongoVersionList = require('mongodb-version-list');
|
||||
const allNodeVersions = require('all-node-versions');
|
||||
|
||||
async function check() {
|
||||
// Run checks
|
||||
await checkMongoDbVersions();
|
||||
await checkNodeVersions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the MongoDB versions used in test environments.
|
||||
*/
|
||||
async function checkMongoDbVersions() {
|
||||
const releasedVersions = await new Promise((resolve, reject) => {
|
||||
mongoVersionList(function (error, versions) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
resolve(versions);
|
||||
});
|
||||
});
|
||||
|
||||
await new CiVersionCheck({
|
||||
packageName: 'MongoDB',
|
||||
packageSupportUrl: 'https://www.mongodb.com/support-policy',
|
||||
yamlFilePath: './.github/workflows/ci.yml',
|
||||
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
|
||||
ciVersionKey: 'MONGODB_VERSION',
|
||||
releasedVersions,
|
||||
latestComponent: CiVersionCheck.versionComponents.path,
|
||||
ignoreReleasedVersions: [
|
||||
'<4.0.0', // Versions reached their MongoDB end-of-life support date
|
||||
'~4.1.0', // Development release according to MongoDB support
|
||||
'~4.3.0', // Development release according to MongoDB support
|
||||
'~4.7.0', // Development release according to MongoDB support
|
||||
|
||||
'4.0.26', // Temporarily disabled because not yet available for download via mongodb-runner
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the Nodejs versions used in test environments.
|
||||
*/
|
||||
async function checkNodeVersions() {
|
||||
const allVersions = await allNodeVersions();
|
||||
const releasedVersions = allVersions.versions;
|
||||
|
||||
await new CiVersionCheck({
|
||||
packageName: 'Node.js',
|
||||
packageSupportUrl: 'https://github.com/nodejs/node/blob/master/CHANGELOG.md',
|
||||
yamlFilePath: './.github/workflows/ci.yml',
|
||||
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
|
||||
ciVersionKey: 'NODE_VERSION',
|
||||
releasedVersions,
|
||||
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
|
||||
'>=17.0.0', // These versions are not officially supported yet
|
||||
],
|
||||
}).check();
|
||||
}
|
||||
|
||||
check();
|
||||
@@ -0,0 +1,190 @@
|
||||
const core = require('@actions/core');
|
||||
const semver = require('semver');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* This checks whether any package dependency requires a minimum node engine
|
||||
* version higher than the host package.
|
||||
*/
|
||||
class NodeEngineCheck {
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
* @param {Object} config The config.
|
||||
* @param {String} config.nodeModulesPath The path to the node_modules directory.
|
||||
* @param {String} config.packageJsonPath The path to the parent package.json file.
|
||||
*/
|
||||
constructor(config) {
|
||||
const {
|
||||
nodeModulesPath,
|
||||
packageJsonPath,
|
||||
} = config;
|
||||
|
||||
// Ensure required params are set
|
||||
if ([
|
||||
nodeModulesPath,
|
||||
packageJsonPath,
|
||||
].includes(undefined)) {
|
||||
throw 'invalid configuration';
|
||||
}
|
||||
|
||||
this.nodeModulesPath = nodeModulesPath;
|
||||
this.packageJsonPath = packageJsonPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of `package.json` files under the given path and subdirectories.
|
||||
* @param {String} [basePath] The base path for recursive directory search.
|
||||
*/
|
||||
async getPackageFiles(basePath = this.nodeModulesPath) {
|
||||
try {
|
||||
// Declare file list
|
||||
const files = []
|
||||
|
||||
// Get files
|
||||
const dirents = await fs.readdir(basePath, { withFileTypes: true });
|
||||
const validFiles = dirents.filter(d => d.name.toLowerCase() == 'package.json').map(d => path.join(basePath, d.name));
|
||||
files.push(...validFiles);
|
||||
|
||||
// For each directory entry
|
||||
for (const dirent of dirents) {
|
||||
if (dirent.isDirectory()) {
|
||||
const subFiles = await this.getPackageFiles(path.join(basePath, dirent.name));
|
||||
files.push(...subFiles);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
} catch (e) {
|
||||
throw `Failed to get package.json files in ${this.nodeModulesPath} with error: ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and returns the node engine versions of the given package.json
|
||||
* files.
|
||||
* @param {String[]} files The package.json files.
|
||||
* @param {Boolean} clean Is true if packages with undefined node versions
|
||||
* should be removed from the results.
|
||||
* @returns {Object[]} A list of results.
|
||||
*/
|
||||
async getNodeVersion({ files, clean = false }) {
|
||||
|
||||
// Declare response
|
||||
let response = [];
|
||||
|
||||
// For each file
|
||||
for (const file of files) {
|
||||
|
||||
// Get node version
|
||||
const contentString = await fs.readFile(file, 'utf-8');
|
||||
const contentJson = JSON.parse(contentString);
|
||||
const version = ((contentJson || {}).engines || {}).node;
|
||||
|
||||
// Add response
|
||||
response.push({
|
||||
file: file,
|
||||
nodeVersion: version
|
||||
});
|
||||
}
|
||||
|
||||
// If results should be cleaned by removing undefined node versions
|
||||
if (clean) {
|
||||
response = response.filter(r => r.nodeVersion !== undefined);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the highest semver definition that satisfies all versions
|
||||
* in the given list.
|
||||
* @param {String[]} versions The list of semver version ranges.
|
||||
* @param {String} baseVersion The base version of which higher versions should be
|
||||
* determined; as a version (1.2.3), not a range (>=1.2.3).
|
||||
* @returns {String} The highest semver version.
|
||||
*/
|
||||
getHigherVersions({ versions, baseVersion }) {
|
||||
// Add min satisfying node versions
|
||||
const minVersions = versions.map(v => {
|
||||
v.nodeMinVersion = semver.minVersion(v.nodeVersion)
|
||||
return v;
|
||||
});
|
||||
|
||||
// Sort by min version
|
||||
const sortedMinVersions = minVersions.sort((v1, v2) => semver.compare(v1.nodeMinVersion, v2.nodeMinVersion));
|
||||
|
||||
// Filter by higher versions
|
||||
const higherVersions = sortedMinVersions.filter(v => semver.gt(v.nodeMinVersion, baseVersion));
|
||||
// console.log(`getHigherVersions: ${JSON.stringify(higherVersions)}`);
|
||||
return higherVersions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the node version of the parent package.
|
||||
* @return {Object} The parent package info.
|
||||
*/
|
||||
async getParentVersion() {
|
||||
// Get parent package.json version
|
||||
const version = await this.getNodeVersion({ files: [ this.packageJsonPath ], clean: true });
|
||||
// console.log(`getParentVersion: ${JSON.stringify(version)}`);
|
||||
return version[0];
|
||||
}
|
||||
}
|
||||
|
||||
async function check() {
|
||||
// Define paths
|
||||
const nodeModulesPath = path.join(__dirname, '../node_modules');
|
||||
const packageJsonPath = path.join(__dirname, '../package.json');
|
||||
|
||||
// Create check
|
||||
const check = new NodeEngineCheck({
|
||||
nodeModulesPath,
|
||||
packageJsonPath,
|
||||
});
|
||||
|
||||
// Get package node version of parent package
|
||||
const parentVersion = await check.getParentVersion();
|
||||
|
||||
// If parent node version could not be determined
|
||||
if (parentVersion === undefined) {
|
||||
core.setFailed(`Failed to determine node engine version of parent package at ${this.packageJsonPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine parent min version
|
||||
const parentMinVersion = semver.minVersion(parentVersion.nodeVersion);
|
||||
|
||||
// Get package.json files
|
||||
const files = await check.getPackageFiles();
|
||||
core.info(`Checking the minimum node version requirement of ${files.length} dependencies`);
|
||||
|
||||
// Get node versions
|
||||
const versions = await check.getNodeVersion({ files, clean: true });
|
||||
|
||||
// Get are dependencies that require a higher node version than the parent package
|
||||
const higherVersions = check.getHigherVersions({ versions, baseVersion: parentMinVersion });
|
||||
|
||||
// Get highest version
|
||||
const highestVersion = higherVersions.map(v => v.nodeMinVersion).pop();
|
||||
|
||||
// If there are higher versions
|
||||
if (higherVersions.length > 0) {
|
||||
console.log(`\nThere are ${higherVersions.length} dependencies that require a higher node engine version than the parent package (${parentVersion.nodeVersion}):`);
|
||||
|
||||
// For each dependency
|
||||
for (const higherVersion of higherVersions) {
|
||||
|
||||
// Get package name
|
||||
const _package = higherVersion.file.split('node_modules/').pop().replace('/package.json', '');
|
||||
console.log(`- ${_package} requires at least node ${higherVersion.nodeMinVersion} (${higherVersion.nodeVersion})`);
|
||||
}
|
||||
console.log('');
|
||||
core.setFailed(`❌ Upgrade the node engine version in package.json to at least '${highestVersion}' to satisfy the dependencies.`);
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`✅ All dependencies satisfy the node version requirement of the parent package (${parentVersion.nodeVersion}).`);
|
||||
}
|
||||
|
||||
check();
|
||||
Generated
+4750
-5128
File diff suppressed because it is too large
Load Diff
+66
-51
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "4.10.7",
|
||||
"version": "5.2.2",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -19,60 +19,65 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@apollographql/graphql-playground-html": "1.6.26",
|
||||
"@graphql-tools/links": "6.2.5",
|
||||
"@apollographql/graphql-playground-html": "1.6.29",
|
||||
"@graphql-tools/links": "8.2.2",
|
||||
"@apollo/client": "3.5.8",
|
||||
"@graphql-tools/stitch": "6.2.4",
|
||||
"@graphql-tools/utils": "6.2.4",
|
||||
"@parse/fs-files-adapter": "1.2.0",
|
||||
"@parse/push-adapter": "3.4.1",
|
||||
"@parse/s3-files-adapter": "1.6.2",
|
||||
"@parse/simple-mailgun-adapter": "1.1.0",
|
||||
"apollo-server-express": "2.19.0",
|
||||
"@parse/fs-files-adapter": "1.2.1",
|
||||
"@parse/push-adapter": "4.1.0",
|
||||
"apollo-server-express": "2.25.2",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.19.0",
|
||||
"body-parser": "1.19.1",
|
||||
"commander": "5.1.0",
|
||||
"cors": "2.8.5",
|
||||
"deepcopy": "2.1.0",
|
||||
"express": "4.17.1",
|
||||
"express": "4.17.2",
|
||||
"follow-redirects": "1.14.8",
|
||||
"graphql": "15.5.1",
|
||||
"graphql": "15.7.1",
|
||||
"graphql-list-fields": "2.0.2",
|
||||
"graphql-relay": "0.7.0",
|
||||
"graphql-tag": "2.12.5",
|
||||
"graphql-tag": "2.12.6",
|
||||
"graphql-upload": "11.0.0",
|
||||
"intersect": "1.0.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"jwks-rsa": "1.12.3",
|
||||
"ldapjs": "2.2.2",
|
||||
"jwks-rsa": "2.0.5",
|
||||
"ldapjs": "2.3.1",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "5.1.1",
|
||||
"mime": "2.4.6",
|
||||
"mongodb": "3.6.11",
|
||||
"parse": "3.3.0",
|
||||
"pg-promise": "10.8.1",
|
||||
"lru-cache": "6.0.0",
|
||||
"mime": "3.0.0",
|
||||
"mongodb": "4.3.1",
|
||||
"mustache": "4.2.0",
|
||||
"parse": "3.4.1",
|
||||
"pg-monitor": "1.4.1",
|
||||
"pg-promise": "10.11.1",
|
||||
"pluralize": "8.0.0",
|
||||
"redis": "3.1.2",
|
||||
"semver": "7.3.2",
|
||||
"subscriptions-transport-ws": "0.10.0",
|
||||
"semver": "7.3.5",
|
||||
"subscriptions-transport-ws": "0.11.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "8.3.1",
|
||||
"winston": "3.3.3",
|
||||
"winston-daily-rotate-file": "4.5.0",
|
||||
"ws": "7.5.3"
|
||||
"uuid": "8.3.2",
|
||||
"winston": "3.5.1",
|
||||
"winston-daily-rotate-file": "4.6.0",
|
||||
"ws": "8.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.14.8",
|
||||
"@actions/core": "1.2.6",
|
||||
"@babel/cli": "7.10.0",
|
||||
"@babel/core": "7.10.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.10.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "7.9.0",
|
||||
"@babel/preset-env": "7.10.0",
|
||||
"@parse/minami": "1.0.0",
|
||||
"apollo-cache-inmemory": "1.6.6",
|
||||
"apollo-client": "2.6.10",
|
||||
"apollo-link": "1.2.14",
|
||||
"apollo-link-http": "1.5.17",
|
||||
"apollo-link-ws": "1.0.20",
|
||||
"apollo-upload-client": "13.0.0",
|
||||
"@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": "8.0.0",
|
||||
"apollo-upload-client": "16.0.0",
|
||||
"apollo-utilities": "1.3.4",
|
||||
"babel-eslint": "10.1.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
@@ -85,46 +90,56 @@
|
||||
"husky": "4.2.5",
|
||||
"jasmine": "3.5.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"jsdoc": "3.6.7",
|
||||
"jsdoc": "3.6.3",
|
||||
"jsdoc-babel": "0.5.0",
|
||||
"lint-staged": "10.2.3",
|
||||
"mongodb-runner": "mongodb-js/runner",
|
||||
"node-fetch": "2.6.1",
|
||||
"madge": "5.0.1",
|
||||
"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-fetch": "3.1.1",
|
||||
"nyc": "15.1.0",
|
||||
"prettier": "2.0.5",
|
||||
"@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",
|
||||
"semantic-release": "17.4.6"
|
||||
"semantic-release": "17.4.6",
|
||||
"yaml": "1.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"definitions": "node ./resources/buildConfigDefinitions.js",
|
||||
"ci:check": "node ./ci/ciCheck.js",
|
||||
"ci:checkNodeEngine": "node ./ci/nodeEngineCheck.js",
|
||||
"definitions": "node ./resources/buildConfigDefinitions.js && prettier --write 'src/Options/*.js'",
|
||||
"docs": "jsdoc -c ./jsdoc-conf.json",
|
||||
"lint": "flow && eslint --cache ./",
|
||||
"lint-fix": "eslint --fix --cache ./",
|
||||
"build": "babel src/ -d lib/ --copy-files",
|
||||
"watch": "babel --watch src/ -d lib/ --copy-files",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} mongodb-runner start",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} TESTING=1 jasmine",
|
||||
"test:mongodb:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
|
||||
"test:mongodb:testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine",
|
||||
"test:mongodb": "npm run test:mongodb:runnerstart --dbversion=$npm_config_dbversion && npm run test:mongodb:testonly --dbversion=$npm_config_dbversion",
|
||||
"test:mongodb:4.0.27": "npm run test:mongodb --dbversion=4.0.27",
|
||||
"test:mongodb:4.2.17": "npm run test:mongodb --dbversion=4.2.17",
|
||||
"test:mongodb:4.4.10": "npm run test:mongodb --dbversion=4.4.10",
|
||||
"test:mongodb:5.0.5": "npm run test:mongodb --dbversion=5.0.5",
|
||||
"posttest:mongodb": "mongodb-runner stop",
|
||||
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
|
||||
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine",
|
||||
"test": "npm run testonly",
|
||||
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} mongodb-runner stop",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} TESTING=1 nyc jasmine",
|
||||
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 nyc jasmine",
|
||||
"start": "node ./bin/parse-server",
|
||||
"prettier": "prettier --write '{src,spec}/{**/*,*}.js'",
|
||||
"prettier": "prettier --write {src,spec}/{**/*,*}.js",
|
||||
"prepare": "npm run build",
|
||||
"postinstall": "node -p 'require(\"./postinstall.js\")()'"
|
||||
"postinstall": "node -p 'require(\"./postinstall.js\")()'",
|
||||
"madge:circular": "node_modules/.bin/madge ./src --circular"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
"node": ">=12.22.10 <17"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "bin/parse-server"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@node-rs/bcrypt": "0.4.1"
|
||||
"@node-rs/bcrypt": "1.1.0"
|
||||
},
|
||||
"collective": {
|
||||
"type": "opencollective",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page demonstrates how to localize using a JSON file that contains the
|
||||
translations for each placeholder.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>{{title}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{heading}}</h1>
|
||||
<p>{{body}}</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"en": {
|
||||
"translation": {
|
||||
"title": "Hello!",
|
||||
"heading": "Welcome to {{appName}}!",
|
||||
"body": "We are delighted to welcome you on board."
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"translation": {
|
||||
"title": "Hallo!",
|
||||
"heading": "Willkommen bei {{appName}}!",
|
||||
"body": "Wir freuen uns, dich begrüßen zu dürfen."
|
||||
}
|
||||
},
|
||||
"de-AT": {
|
||||
"translation": {
|
||||
"title": "Servus!",
|
||||
"heading": "Willkommen bei {{appName}}!",
|
||||
"body": "Wir freuen uns, dich begrüßen zu dürfen."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page demonstrates how to use custom pages in custom routes.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>{{appName}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link with a security
|
||||
token that is expired or incorrect. This can either mean the user has clicked
|
||||
on a stale link (i.e. re-clicked on the link) or this could be a sign of a
|
||||
malicious user trying to tamper with your app.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Expired verification link!</h1>
|
||||
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
|
||||
<input name="username" type="hidden" value="{{{username}}}">
|
||||
<input name="locale" type="hidden" value="{{{locale}}}">
|
||||
<button type="submit">Resend Link</button>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link with parameters
|
||||
that are missing or incorrect. This can either mean the user has incorrectly
|
||||
entered a link or this could be a sign of a malicious user trying to tamper
|
||||
with your app.
|
||||
If the link contains an expired security token (or the email has already
|
||||
been verified), this page is not displayed, there is another page for that.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Invalid verification link!</h1>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link with a security
|
||||
token that is expired or incorrect, then requests to receive another link,
|
||||
but it fails because the username is invalid or the email has already been
|
||||
verified. This can either mean the user has previously verified the email
|
||||
or this could be a sign of a malicious user trying to tamper with your app.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Invalid link!</h1>
|
||||
<p>No link sent. User not found or email already verified.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link with a
|
||||
security token that is expired, then requests to receive another email
|
||||
with a new link and the email is sent successfully.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Link sent!</h1>
|
||||
<p>A new link has been sent. Check your email.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link and the
|
||||
email gets verified successfully.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Email verified!</h1>
|
||||
<p>Successfully verified your email for account: {{username}}.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when someone clicks a valid 'reset password' link.
|
||||
Users should feel free to add to this page (i.e. branding or security widgets)
|
||||
but should be sure not to delete any of the form inputs or the javascript from the
|
||||
template file. This javascript is what adds the necessary values to authenticate
|
||||
this session with Parse.
|
||||
The query params 'username' and 'app' hold the friendly names for your current user and
|
||||
your app. You should feel free to incorporate their values to make the page more personal.
|
||||
If you are missing form parameters in your POST, Parse will navigate back to this page and
|
||||
add an 'error' query parameter.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Password Reset</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Reset Your Password</h1>
|
||||
<noscript><p>We apologize, but resetting your password requires javascript</p></noscript>
|
||||
<p>You can set a new Password for your account: {{username}}</p>
|
||||
<br />
|
||||
<p>{{error}}</p>
|
||||
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
|
||||
<input name='utf-8' type='hidden' value='✓' />
|
||||
<input name="username" type="hidden" id="username" value="{{{username}}}" />
|
||||
<input name="token" type="hidden" id="token" value="{{{token}}}" />
|
||||
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
|
||||
|
||||
<p>New Password</p>
|
||||
<input name="new_password" type="password" id="password" />
|
||||
<p>Confirm New Password</p>
|
||||
<input name="confirm_new_password" type="password" id="password_confirm" />
|
||||
<br />
|
||||
<p id="password_match_info"></p>
|
||||
<br />
|
||||
<button id="change_password">Change Password</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
document.getElementById("password").oninput = validatePassword;
|
||||
document.getElementById("password_confirm").oninput = validatePassword;
|
||||
document.getElementById("change_password").disabled = true;
|
||||
|
||||
function validatePassword() {
|
||||
var pass2 = document.getElementById("password").value;
|
||||
var pass1 = document.getElementById("password_confirm").value;
|
||||
if(pass1 !== pass2) {
|
||||
if(document.getElementById("password_confirm").value) {
|
||||
document.getElementById("change_password").disabled = true;
|
||||
document.getElementById("password_match_info").innerHTML = "Must match the previous entry";
|
||||
}
|
||||
} else {
|
||||
document.getElementById("change_password").disabled = false;
|
||||
document.getElementById("password_match_info").innerHTML = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a password reset link with parameters
|
||||
that are missing or incorrect. This can either mean the user has incorrectly
|
||||
entered a link or this could be a sign of a malicious user trying to tamper
|
||||
with your app.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Password Reset</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Invalid password reset link!</h1>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a reset password link, then submits
|
||||
the form with a new password and the password gets updated successfully.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Password Reset</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Success!</h1>
|
||||
<p>Your password has been updated.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link with a security
|
||||
token that is expired or incorrect. This can either mean the user has clicked
|
||||
on a stale link (i.e. re-clicked on the link) or this could be a sign of a
|
||||
malicious user trying to tamper with your app.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Expired verification link!</h1>
|
||||
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
|
||||
<input name="username" type="hidden" value="{{{username}}}">
|
||||
<input name="locale" type="hidden" value="{{{locale}}}">
|
||||
<button type="submit">Resend Link</button>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link with parameters
|
||||
that are missing or incorrect. This can either mean the user has incorrectly
|
||||
entered a link or this could be a sign of a malicious user trying to tamper
|
||||
with your app.
|
||||
If the link contains an expired security token (or the email has already
|
||||
been verified), this page is not displayed, there is another page for that.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Invalid verification link!</h1>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link with a security
|
||||
token that is expired or incorrect, then requests to receive another link,
|
||||
but it fails because the username is invalid or the email has already been
|
||||
verified. This can either mean the user has previously verified the email
|
||||
or this could be a sign of a malicious user trying to tamper with your app.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Invalid link!</h1>
|
||||
<p>No link sent. User not found or email already verified.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link with a
|
||||
security token that is expired, then requests to receive another email
|
||||
with a new link and the email is sent successfully.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Link sent!</h1>
|
||||
<p>A new link has been sent. Check your email.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link and the
|
||||
email gets verified successfully.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Email verified!</h1>
|
||||
<p>Successfully verified your email for account: {{username}}.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when someone clicks a valid 'reset password' link.
|
||||
Users should feel free to add to this page (i.e. branding or security widgets)
|
||||
but should be sure not to delete any of the form inputs or the javascript from the
|
||||
template file. This javascript is what adds the necessary values to authenticate
|
||||
this session with Parse.
|
||||
The query params 'username' and 'app' hold the friendly names for your current user and
|
||||
your app. You should feel free to incorporate their values to make the page more personal.
|
||||
If you are missing form parameters in your POST, Parse will navigate back to this page and
|
||||
add an 'error' query parameter.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Password Reset</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Reset Your Password</h1>
|
||||
<noscript><p>We apologize, but resetting your password requires javascript</p></noscript>
|
||||
<p>You can set a new Password for your account: {{username}}</p>
|
||||
<br />
|
||||
<p>{{error}}</p>
|
||||
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
|
||||
<input name='utf-8' type='hidden' value='✓' />
|
||||
<input name="username" type="hidden" id="username" value="{{{username}}}" />
|
||||
<input name="token" type="hidden" id="token" value="{{{token}}}" />
|
||||
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
|
||||
|
||||
<p>New Password</p>
|
||||
<input name="new_password" type="password" id="password" />
|
||||
<p>Confirm New Password</p>
|
||||
<input name="confirm_new_password" type="password" id="password_confirm" />
|
||||
<br />
|
||||
<p id="password_match_info"></p>
|
||||
<br />
|
||||
<button id="change_password">Change Password</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
document.getElementById("password").oninput = validatePassword;
|
||||
document.getElementById("password_confirm").oninput = validatePassword;
|
||||
document.getElementById("change_password").disabled = true;
|
||||
|
||||
function validatePassword() {
|
||||
var pass2 = document.getElementById("password").value;
|
||||
var pass1 = document.getElementById("password_confirm").value;
|
||||
if(pass1 !== pass2) {
|
||||
if(document.getElementById("password_confirm").value) {
|
||||
document.getElementById("change_password").disabled = true;
|
||||
document.getElementById("password_match_info").innerHTML = "Must match the previous entry";
|
||||
}
|
||||
} else {
|
||||
document.getElementById("change_password").disabled = false;
|
||||
document.getElementById("password_match_info").innerHTML = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a password reset link with parameters
|
||||
that are missing or incorrect. This can either mean the user has incorrectly
|
||||
entered a link or this could be a sign of a malicious user trying to tamper
|
||||
with your app.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Password Reset</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Invalid password reset link!</h1>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a reset password link, then submits
|
||||
the form with a new password and the password gets updated successfully.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Password Reset</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Success!</h1>
|
||||
<p>Your password has been updated.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link with a security
|
||||
token that is expired or incorrect. This can either mean the user has clicked
|
||||
on a stale link (i.e. re-clicked on the link) or this could be a sign of a
|
||||
malicious user trying to tamper with your app.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Expired verification link!</h1>
|
||||
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
|
||||
<input name="username" type="hidden" value="{{{username}}}">
|
||||
<input name="locale" type="hidden" value="{{{locale}}}">
|
||||
<button type="submit">Resend Link</button>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link with parameters
|
||||
that are missing or incorrect. This can either mean the user has incorrectly
|
||||
entered a link or this could be a sign of a malicious user trying to tamper
|
||||
with your app.
|
||||
If the link contains an expired security token (or the email has already
|
||||
been verified), this page is not displayed, there is another page for that.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Invalid verification link!</h1>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link with a security
|
||||
token that is expired or incorrect, then requests to receive another link,
|
||||
but it fails because the username is invalid or the email has already been
|
||||
verified. This can either mean the user has previously verified the email
|
||||
or this could be a sign of a malicious user trying to tamper with your app.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Invalid link!</h1>
|
||||
<p>No link sent. User not found or email already verified.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link with a
|
||||
security token that is expired, then requests to receive another email
|
||||
with a new link and the email is sent successfully.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Link sent!</h1>
|
||||
<p>A new link has been sent. Check your email.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a verify email link and the
|
||||
email gets verified successfully.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Email verified!</h1>
|
||||
<p>Successfully verified your email for account: {{username}}.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when someone clicks a valid 'reset password' link.
|
||||
Users should feel free to add to this page (i.e. branding or security widgets)
|
||||
but should be sure not to delete any of the form inputs or the javascript from the
|
||||
template file. This javascript is what adds the necessary values to authenticate
|
||||
this session with Parse.
|
||||
The query params 'username' and 'app' hold the friendly names for your current user and
|
||||
your app. You should feel free to incorporate their values to make the page more personal.
|
||||
If you are missing form parameters in your POST, Parse will navigate back to this page and
|
||||
add an 'error' query parameter.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Password Reset</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Reset Your Password</h1>
|
||||
<noscript><p>We apologize, but resetting your password requires javascript</p></noscript>
|
||||
<p>You can set a new Password for your account: {{username}}</p>
|
||||
<br />
|
||||
<p>{{error}}</p>
|
||||
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
|
||||
<input name='utf-8' type='hidden' value='✓' />
|
||||
<input name="username" type="hidden" id="username" value="{{{username}}}" />
|
||||
<input name="token" type="hidden" id="token" value="{{{token}}}" />
|
||||
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
|
||||
|
||||
<p>New Password</p>
|
||||
<input name="new_password" type="password" id="password" />
|
||||
<p>Confirm New Password</p>
|
||||
<input name="confirm_new_password" type="password" id="password_confirm" />
|
||||
<br />
|
||||
<p id="password_match_info"></p>
|
||||
<br />
|
||||
<button id="change_password">Change Password</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
document.getElementById("password").oninput = validatePassword;
|
||||
document.getElementById("password_confirm").oninput = validatePassword;
|
||||
document.getElementById("change_password").disabled = true;
|
||||
|
||||
function validatePassword() {
|
||||
var pass2 = document.getElementById("password").value;
|
||||
var pass1 = document.getElementById("password_confirm").value;
|
||||
if(pass1 !== pass2) {
|
||||
if(document.getElementById("password_confirm").value) {
|
||||
document.getElementById("change_password").disabled = true;
|
||||
document.getElementById("password_match_info").innerHTML = "Must match the previous entry";
|
||||
}
|
||||
} else {
|
||||
document.getElementById("change_password").disabled = false;
|
||||
document.getElementById("password_match_info").innerHTML = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a password reset link with parameters
|
||||
that are missing or incorrect. This can either mean the user has incorrectly
|
||||
entered a link or this could be a sign of a malicious user trying to tamper
|
||||
with your app.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Password Reset</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Invalid password reset link!</h1>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This page is displayed when a user opens a reset password link, then submits
|
||||
the form with a new password and the password gets updated successfully.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Password Reset</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>{{appName}}</h1>
|
||||
<h1>Success!</h1>
|
||||
<p>Your password has been updated.</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+14
-2
@@ -28,7 +28,7 @@ async function config() {
|
||||
console.log(`Running on branch: ${branch}`);
|
||||
|
||||
// Set changelog file
|
||||
const changelogFile = `./CHANGELOG.md`;
|
||||
const changelogFile = `./changelogs/CHANGELOG_${branch}.md`;
|
||||
console.log(`Changelog file output to: ${changelogFile}`);
|
||||
|
||||
// Load template file contents
|
||||
@@ -41,7 +41,10 @@ async function config() {
|
||||
{ name: 'beta', prerelease: true },
|
||||
'next-major',
|
||||
// Long-Term-Support branches
|
||||
{ name: 'release-4.x.x', range: '4.x.x', channel: '4.x.x' },
|
||||
// { name: 'release-1', range: '1.x.x', channel: '1.x' },
|
||||
// { name: 'release-2', range: '2.x.x', channel: '2.x' },
|
||||
// { name: 'release-3', range: '3.x.x', channel: '3.x' },
|
||||
// { name: 'release-4', range: '4.x.x', channel: '4.x' },
|
||||
],
|
||||
dryRun: false,
|
||||
debug: true,
|
||||
@@ -85,6 +88,15 @@ async function config() {
|
||||
labels: ['type:ci'],
|
||||
releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
|
||||
}],
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
"branches": [
|
||||
{ from: "beta", to: "alpha" },
|
||||
{ from: "release", to: "beta" },
|
||||
]
|
||||
}
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,37 @@
|
||||
*/
|
||||
const parsers = require('../src/Options/parsers');
|
||||
|
||||
/** The types of nested options. */
|
||||
const nestedOptionTypes = [
|
||||
'CustomPagesOptions',
|
||||
'DatabaseOptions',
|
||||
'FileUploadOptions',
|
||||
'IdempotencyOptions',
|
||||
'Object',
|
||||
'PagesCustomUrlsOptions',
|
||||
'PagesOptions',
|
||||
'PagesRoute',
|
||||
'PasswordPolicyOptions',
|
||||
'SecurityOptions',
|
||||
];
|
||||
|
||||
/** The prefix of environment variables for nested options. */
|
||||
const nestedOptionEnvPrefix = {
|
||||
'AccountLockoutOptions' : 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
|
||||
'CustomPagesOptions' : 'PARSE_SERVER_CUSTOM_PAGES_',
|
||||
'DatabaseOptions': 'PARSE_SERVER_DATABASE_',
|
||||
'FileUploadOptions' : 'PARSE_SERVER_FILE_UPLOAD_',
|
||||
'IdempotencyOptions' : 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
|
||||
'LiveQueryOptions' : 'PARSE_SERVER_LIVEQUERY_',
|
||||
'LiveQueryServerOptions' : 'PARSE_LIVE_QUERY_SERVER_',
|
||||
'PagesCustomUrlsOptions' : 'PARSE_SERVER_PAGES_CUSTOM_URL_',
|
||||
'PagesOptions' : 'PARSE_SERVER_PAGES_',
|
||||
'PagesRoute': 'PARSE_SERVER_PAGES_ROUTE_',
|
||||
'ParseServerOptions' : 'PARSE_SERVER_',
|
||||
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_',
|
||||
'SecurityOptions': 'PARSE_SERVER_SECURITY_',
|
||||
};
|
||||
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
@@ -40,17 +71,8 @@ function getCommentValue(comment) {
|
||||
}
|
||||
|
||||
function getENVPrefix(iface) {
|
||||
const options = {
|
||||
'ParseServerOptions' : 'PARSE_SERVER_',
|
||||
'CustomPagesOptions' : 'PARSE_SERVER_CUSTOM_PAGES_',
|
||||
'LiveQueryServerOptions' : 'PARSE_LIVE_QUERY_SERVER_',
|
||||
'LiveQueryOptions' : 'PARSE_SERVER_LIVEQUERY_',
|
||||
'IdempotencyOptions' : 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
|
||||
'AccountLockoutOptions' : 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
|
||||
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_'
|
||||
}
|
||||
if (options[iface.id.name]) {
|
||||
return options[iface.id.name]
|
||||
if (nestedOptionEnvPrefix[iface.id.name]) {
|
||||
return nestedOptionEnvPrefix[iface.id.name]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,14 +199,8 @@ function parseDefaultValue(elt, value, t) {
|
||||
if (type == 'NumberOrBoolean') {
|
||||
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
|
||||
}
|
||||
if (type == 'CustomPagesOptions') {
|
||||
const object = parsers.objectParser(value);
|
||||
const props = Object.keys(object).map((key) => {
|
||||
return t.objectProperty(key, object[value]);
|
||||
});
|
||||
literalValue = t.objectExpression(props);
|
||||
}
|
||||
if (type == 'IdempotencyOptions') {
|
||||
|
||||
if (nestedOptionTypes.includes(type)) {
|
||||
const object = parsers.objectParser(value);
|
||||
const props = Object.keys(object).map((key) => {
|
||||
return t.objectProperty(key, object[value]);
|
||||
@@ -234,7 +250,9 @@ function inject(t, list) {
|
||||
type = elt.typeAnnotation.id.name;
|
||||
}
|
||||
if (type === 'Array') {
|
||||
type = `${elt.typeAnnotation.elementType.type.replace('TypeAnnotation', '')}[]`;
|
||||
type = elt.typeAnnotation.elementType.id
|
||||
? `${elt.typeAnnotation.elementType.id.name}[]`
|
||||
: `${elt.typeAnnotation.elementType.type.replace('TypeAnnotation', '')}[]`;
|
||||
}
|
||||
if (type === 'NumberOrBoolean') {
|
||||
type = 'Number|Boolean';
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
set -e
|
||||
|
||||
echo "[SCRIPT] Before Script :: Setup Parse DB for Postgres ${POSTGRES_MAJOR_VERSION}"
|
||||
echo "[SCRIPT] Before Script :: Setup Parse DB for Postgres"
|
||||
|
||||
PGPASSWORD=postgres psql -v ON_ERROR_STOP=1 -h localhost -U postgres <<-EOSQL
|
||||
CREATE DATABASE parse_server_postgres_adapter_test_database;
|
||||
\c parse_server_postgres_adapter_test_database;
|
||||
CREATE EXTENSION pgcrypto;
|
||||
CREATE EXTENSION postgis;
|
||||
CREATE EXTENSION postgis_topology;
|
||||
EOSQL
|
||||
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "[SCRIPT] Before Script :: Setup Parse Postgres configuration file"
|
||||
|
||||
# DB Version: 13
|
||||
# OS Type: linux
|
||||
# DB Type: web
|
||||
# Total Memory (RAM): 6 GB
|
||||
# CPUs num: 1
|
||||
# Data Storage: ssd
|
||||
|
||||
PGPASSWORD=postgres psql -v ON_ERROR_STOP=1 -h localhost -U postgres <<-EOSQL
|
||||
ALTER SYSTEM SET max_connections TO '200';
|
||||
ALTER SYSTEM SET shared_buffers TO '1536MB';
|
||||
ALTER SYSTEM SET effective_cache_size TO '4608MB';
|
||||
ALTER SYSTEM SET maintenance_work_mem TO '384MB';
|
||||
ALTER SYSTEM SET checkpoint_completion_target TO '0.9';
|
||||
ALTER SYSTEM SET wal_buffers TO '16MB';
|
||||
ALTER SYSTEM SET default_statistics_target TO '100';
|
||||
ALTER SYSTEM SET random_page_cost TO '1.1';
|
||||
ALTER SYSTEM SET effective_io_concurrency TO '200';
|
||||
ALTER SYSTEM SET work_mem TO '3932kB';
|
||||
ALTER SYSTEM SET min_wal_size TO '1GB';
|
||||
ALTER SYSTEM SET max_wal_size TO '4GB';
|
||||
SELECT pg_reload_conf();
|
||||
EOSQL
|
||||
|
||||
exec "$@"
|
||||
@@ -16,6 +16,12 @@
|
||||
"expectAsync": true,
|
||||
"notEqual": true,
|
||||
"it_only_db": true,
|
||||
"it_only_mongodb_version": true,
|
||||
"it_only_postgres_version": true,
|
||||
"fit_only_mongodb_version": true,
|
||||
"it_exclude_mongodb_version": true,
|
||||
"it_exclude_postgres_version": true,
|
||||
"fit_exclude_mongodb_version": true,
|
||||
"it_exclude_dbs": true,
|
||||
"describe_only_db": true,
|
||||
"describe_only": true,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const Config = require('../lib/Config');
|
||||
const Definitions = require('../lib/Options/Definitions');
|
||||
const request = require('../lib/request');
|
||||
|
||||
const loginWithWrongCredentialsShouldFail = function (username, password) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -340,3 +342,125 @@ describe('Account Lockout Policy: ', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('lockout with password reset option', () => {
|
||||
let sendPasswordResetEmail;
|
||||
|
||||
async function setup(options = {}) {
|
||||
const accountLockout = Object.assign(
|
||||
{
|
||||
duration: 10000,
|
||||
threshold: 1,
|
||||
},
|
||||
options
|
||||
);
|
||||
const config = {
|
||||
appName: 'exampleApp',
|
||||
accountLockout: accountLockout,
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
emailAdapter: {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
},
|
||||
};
|
||||
await reconfigureServer(config);
|
||||
|
||||
sendPasswordResetEmail = spyOn(config.emailAdapter, 'sendPasswordResetEmail').and.callThrough();
|
||||
}
|
||||
|
||||
it('accepts valid unlockOnPasswordReset option', async () => {
|
||||
const values = [true, false];
|
||||
|
||||
for (const value of values) {
|
||||
await expectAsync(setup({ unlockOnPasswordReset: value })).toBeResolved();
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects invalid unlockOnPasswordReset option', async () => {
|
||||
const values = ['a', 0, {}, [], null];
|
||||
|
||||
for (const value of values) {
|
||||
await expectAsync(setup({ unlockOnPasswordReset: value })).toBeRejected();
|
||||
}
|
||||
});
|
||||
|
||||
it('uses default value if unlockOnPasswordReset is not set', async () => {
|
||||
await expectAsync(setup({ unlockOnPasswordReset: undefined })).toBeResolved();
|
||||
|
||||
const parseConfig = Config.get(Parse.applicationId);
|
||||
expect(parseConfig.accountLockout.unlockOnPasswordReset).toBe(
|
||||
Definitions.AccountLockoutOptions.unlockOnPasswordReset.default
|
||||
);
|
||||
});
|
||||
|
||||
it('allow login for locked account after password reset', async () => {
|
||||
await setup({ unlockOnPasswordReset: true });
|
||||
const config = Config.get(Parse.applicationId);
|
||||
|
||||
const user = new Parse.User();
|
||||
const username = 'exampleUsername';
|
||||
const password = 'examplePassword';
|
||||
user.setUsername(username);
|
||||
user.setPassword(password);
|
||||
user.setEmail('mail@example.com');
|
||||
await user.signUp();
|
||||
|
||||
await expectAsync(Parse.User.logIn(username, 'incorrectPassword')).toBeRejected();
|
||||
await expectAsync(Parse.User.logIn(username, password)).toBeRejected();
|
||||
|
||||
await Parse.User.requestPasswordReset(user.getEmail());
|
||||
await expectAsync(Parse.User.logIn(username, password)).toBeRejected();
|
||||
|
||||
const link = sendPasswordResetEmail.calls.all()[0].args[0].link;
|
||||
const linkUrl = new URL(link);
|
||||
const token = linkUrl.searchParams.get('token');
|
||||
const newPassword = 'newPassword';
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: `${config.publicServerURL}/apps/test/request_password_reset`,
|
||||
body: `new_password=${newPassword}&token=${token}&username=${username}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
followRedirects: false,
|
||||
});
|
||||
|
||||
await expectAsync(Parse.User.logIn(username, newPassword)).toBeResolved();
|
||||
});
|
||||
|
||||
it('reject login for locked account after password reset (default)', async () => {
|
||||
await setup();
|
||||
const config = Config.get(Parse.applicationId);
|
||||
|
||||
const user = new Parse.User();
|
||||
const username = 'exampleUsername';
|
||||
const password = 'examplePassword';
|
||||
user.setUsername(username);
|
||||
user.setPassword(password);
|
||||
user.setEmail('mail@example.com');
|
||||
await user.signUp();
|
||||
|
||||
await expectAsync(Parse.User.logIn(username, 'incorrectPassword')).toBeRejected();
|
||||
await expectAsync(Parse.User.logIn(username, password)).toBeRejected();
|
||||
|
||||
await Parse.User.requestPasswordReset(user.getEmail());
|
||||
await expectAsync(Parse.User.logIn(username, password)).toBeRejected();
|
||||
|
||||
const link = sendPasswordResetEmail.calls.all()[0].args[0].link;
|
||||
const linkUrl = new URL(link);
|
||||
const token = linkUrl.searchParams.get('token');
|
||||
const newPassword = 'newPassword';
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: `${config.publicServerURL}/apps/test/request_password_reset`,
|
||||
body: `new_password=${newPassword}&token=${token}&username=${username}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
followRedirects: false,
|
||||
});
|
||||
|
||||
await expectAsync(Parse.User.logIn(username, newPassword)).toBeRejected();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const loadAdapter = require('../lib/Adapters/AdapterLoader').loadAdapter;
|
||||
const FilesAdapter = require('@parse/fs-files-adapter').default;
|
||||
const S3Adapter = require('@parse/s3-files-adapter').default;
|
||||
const ParsePushAdapter = require('@parse/push-adapter').default;
|
||||
const MockFilesAdapter = require('mock-files-adapter');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
describe('AdapterLoader', () => {
|
||||
it('should instantiate an adapter from string in object', done => {
|
||||
const adapterPath = require('path').resolve('./spec/MockAdapter');
|
||||
const adapterPath = require('path').resolve('./spec/support/MockAdapter');
|
||||
|
||||
const adapter = loadAdapter({
|
||||
adapter: adapterPath,
|
||||
@@ -23,7 +23,7 @@ describe('AdapterLoader', () => {
|
||||
});
|
||||
|
||||
it('should instantiate an adapter from string', done => {
|
||||
const adapterPath = require('path').resolve('./spec/MockAdapter');
|
||||
const adapterPath = require('path').resolve('./spec/support/MockAdapter');
|
||||
const adapter = loadAdapter(adapterPath);
|
||||
|
||||
expect(adapter instanceof Object).toBe(true);
|
||||
@@ -119,7 +119,7 @@ describe('AdapterLoader', () => {
|
||||
});
|
||||
|
||||
it('should load custom push adapter from string (#3544)', done => {
|
||||
const adapterPath = require('path').resolve('./spec/MockPushAdapter');
|
||||
const adapterPath = require('path').resolve('./spec/support/MockPushAdapter');
|
||||
const options = {
|
||||
ios: {
|
||||
bundleId: 'bundle.id',
|
||||
@@ -142,12 +142,12 @@ describe('AdapterLoader', () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should load S3Adapter from direct passing', done => {
|
||||
it('should load file adapter from direct passing', done => {
|
||||
spyOn(console, 'warn').and.callFake(() => {});
|
||||
const s3Adapter = new S3Adapter('key', 'secret', 'bucket');
|
||||
const mockFilesAdapter = new MockFilesAdapter('key', 'secret', 'bucket');
|
||||
expect(() => {
|
||||
const adapter = loadAdapter(s3Adapter, FilesAdapter);
|
||||
expect(adapter).toBe(s3Adapter);
|
||||
const adapter = loadAdapter(mockFilesAdapter, FilesAdapter);
|
||||
expect(adapter).toBe(mockFilesAdapter);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const AggregateRouter = require('../lib/Routers/AggregateRouter').AggregateRouter;
|
||||
|
||||
describe('AggregateRouter', () => {
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Array', () => {
|
||||
const body = [
|
||||
{
|
||||
@@ -12,6 +13,7 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Object', () => {
|
||||
const body = {
|
||||
group: { objectId: {} },
|
||||
@@ -21,6 +23,7 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Pipeline Operator (Array)', () => {
|
||||
const body = {
|
||||
pipeline: [
|
||||
@@ -34,6 +37,7 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline from Pipeline Operator (Object)', () => {
|
||||
const body = {
|
||||
pipeline: {
|
||||
@@ -45,6 +49,7 @@ describe('AggregateRouter', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline fails multiple keys in Array stage ', () => {
|
||||
const body = [
|
||||
{
|
||||
@@ -59,6 +64,7 @@ describe('AggregateRouter', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get pipeline fails multiple keys in Pipeline Operator Array stage ', () => {
|
||||
const body = {
|
||||
pipeline: [
|
||||
@@ -74,4 +80,85 @@ describe('AggregateRouter', () => {
|
||||
expect(e.message).toBe('Pipeline stages should only have one key found group, match');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: update pipeline syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
it('get search pipeline from Pipeline Operator (Array)', () => {
|
||||
const body = {
|
||||
pipeline: {
|
||||
search: {},
|
||||
},
|
||||
};
|
||||
const expected = [{ $search: {} }];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('support stage name starting with `$`', () => {
|
||||
const body = {
|
||||
$match: { someKey: 'whatever' },
|
||||
};
|
||||
const expected = [{ $match: { someKey: 'whatever' } }];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('support nested stage names starting with `$`', () => {
|
||||
const body = [
|
||||
{
|
||||
lookup: {
|
||||
from: 'ACollection',
|
||||
let: { id: '_id' },
|
||||
as: 'results',
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$eq: ['$_id', '$$id'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
$lookup: {
|
||||
from: 'ACollection',
|
||||
let: { id: '_id' },
|
||||
as: 'results',
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$eq: ['$_id', '$$id'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('support the use of `_id` in stages', () => {
|
||||
const body = [
|
||||
{ match: { _id: 'randomId' } },
|
||||
{ sort: { _id: -1 } },
|
||||
{ addFields: { _id: 1 } },
|
||||
{ group: { _id: {} } },
|
||||
{ project: { _id: 0 } },
|
||||
];
|
||||
const expected = [
|
||||
{ $match: { _id: 'randomId' } },
|
||||
{ $sort: { _id: -1 } },
|
||||
{ $addFields: { _id: 1 } },
|
||||
{ $group: { _id: {} } },
|
||||
{ $project: { _id: 0 } },
|
||||
];
|
||||
const result = AggregateRouter.getPipeline(body);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
+15
-14
@@ -326,22 +326,24 @@ describe('AudiencesRouter', () => {
|
||||
{ name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }) },
|
||||
{ useMasterKey: true }
|
||||
).then(audience => {
|
||||
database.collection('test__Audience').updateOne(
|
||||
{ _id: audience.objectId },
|
||||
{
|
||||
$set: {
|
||||
times_used: 1,
|
||||
_last_used: now,
|
||||
},
|
||||
},
|
||||
{},
|
||||
error => {
|
||||
expect(error).toEqual(null);
|
||||
database
|
||||
.collection('test__Audience')
|
||||
.updateOne(
|
||||
{ _id: audience.objectId },
|
||||
{
|
||||
$set: {
|
||||
times_used: 1,
|
||||
_last_used: now,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
expect(result).toBeTruthy();
|
||||
database
|
||||
.collection('test__Audience')
|
||||
.find({ _id: audience.objectId })
|
||||
.toArray((error, rows) => {
|
||||
expect(error).toEqual(null);
|
||||
expect(error).toEqual(undefined);
|
||||
expect(rows[0]['times_used']).toEqual(1);
|
||||
expect(rows[0]['_last_used']).toEqual(now);
|
||||
Parse._request(
|
||||
@@ -361,8 +363,7 @@ describe('AudiencesRouter', () => {
|
||||
done.fail(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+10
-9
@@ -1,3 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
describe('Auth', () => {
|
||||
const { Auth, getAuthForSessionToken } = require('../lib/Auth.js');
|
||||
const Config = require('../lib/Config');
|
||||
@@ -151,7 +153,7 @@ describe('Auth', () => {
|
||||
});
|
||||
|
||||
describe('getRolesForUser', () => {
|
||||
const rolesNumber = 300;
|
||||
const rolesNumber = 100;
|
||||
|
||||
it('should load all roles without config', async () => {
|
||||
const user = new Parse.User();
|
||||
@@ -168,9 +170,9 @@ describe('Auth', () => {
|
||||
const acl = new Parse.ACL();
|
||||
const role = new Parse.Role('roleloadtest' + i, acl);
|
||||
role.getUsers().add([user]);
|
||||
roles.push(role.save());
|
||||
roles.push(role);
|
||||
}
|
||||
const savedRoles = await Promise.all(roles);
|
||||
const savedRoles = await Parse.Object.saveAll(roles);
|
||||
expect(savedRoles.length).toBe(rolesNumber);
|
||||
const cloudRoles = await userAuth.getRolesForUser();
|
||||
expect(cloudRoles.length).toBe(rolesNumber);
|
||||
@@ -192,16 +194,15 @@ describe('Auth', () => {
|
||||
const acl = new Parse.ACL();
|
||||
const role = new Parse.Role('roleloadtest' + i, acl);
|
||||
role.getUsers().add([user]);
|
||||
roles.push(role.save());
|
||||
roles.push(role);
|
||||
}
|
||||
const savedRoles = await Promise.all(roles);
|
||||
const savedRoles = await Parse.Object.saveAll(roles);
|
||||
expect(savedRoles.length).toBe(rolesNumber);
|
||||
const cloudRoles = await userAuth.getRolesForUser();
|
||||
expect(cloudRoles.length).toBe(rolesNumber);
|
||||
});
|
||||
|
||||
it('should load all roles for different users with config', async () => {
|
||||
const rolesNumber = 100;
|
||||
const user = new Parse.User();
|
||||
await user.signUp({
|
||||
username: 'hello',
|
||||
@@ -229,10 +230,10 @@ describe('Auth', () => {
|
||||
const role2 = new Parse.Role('role2loadtest' + i, acl2);
|
||||
role.getUsers().add([user]);
|
||||
role2.getUsers().add([user2]);
|
||||
roles.push(role.save());
|
||||
roles.push(role2.save());
|
||||
roles.push(role);
|
||||
roles.push(role2);
|
||||
}
|
||||
const savedRoles = await Promise.all(roles);
|
||||
const savedRoles = await Parse.Object.saveAll(roles);
|
||||
expect(savedRoles.length).toBe(rolesNumber * 2);
|
||||
const cloudRoles = await userAuth.getRolesForUser();
|
||||
const cloudRoles2 = await user2Auth.getRolesForUser();
|
||||
|
||||
@@ -5,7 +5,7 @@ const authenticationLoader = require('../lib/Adapters/Auth');
|
||||
const path = require('path');
|
||||
const responses = {
|
||||
gpgames: { playerId: 'userId' },
|
||||
instagram: { data: { id: 'userId' } },
|
||||
instagram: { id: 'userId' },
|
||||
janrainengage: { stat: 'ok', profile: { identifier: 'userId' } },
|
||||
janraincapture: { stat: 'ok', result: 'userId' },
|
||||
line: { userId: 'userId' },
|
||||
@@ -492,7 +492,15 @@ describe('instagram auth adapter', () => {
|
||||
'https://graph.instagram.com/me?fields=id&access_token=the_token'
|
||||
);
|
||||
});
|
||||
|
||||
it('response object without data child', async () => {
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
return Promise.resolve({ id: 'userId' });
|
||||
});
|
||||
await instagram.validateAuthData({ id: 'userId', access_token: 'the_token' }, {});
|
||||
expect(httpsRequest.get).toHaveBeenCalledWith(
|
||||
'https://graph.instagram.com/me?fields=id&access_token=the_token'
|
||||
);
|
||||
});
|
||||
it('should pass in api url', async () => {
|
||||
spyOn(httpsRequest, 'get').and.callFake(() => {
|
||||
return Promise.resolve({ data: { id: 'userId' } });
|
||||
@@ -1644,8 +1652,41 @@ describe('apple signin auth adapter', () => {
|
||||
|
||||
describe('Apple Game Center Auth adapter', () => {
|
||||
const gcenter = require('../lib/Adapters/Auth/gcenter');
|
||||
|
||||
const fs = require('fs');
|
||||
const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
|
||||
it('can load adapter', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl:
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
});
|
||||
it('validateAuthData should validate', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl:
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
// real token is used
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
@@ -1656,48 +1697,147 @@ describe('Apple Game Center Auth adapter', () => {
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'cloud.xtralife.gamecenterauth',
|
||||
};
|
||||
|
||||
try {
|
||||
await gcenter.validateAuthData(authData);
|
||||
} catch (e) {
|
||||
fail();
|
||||
}
|
||||
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
|
||||
await gcenter.validateAuthData(authData);
|
||||
});
|
||||
|
||||
it('validateAuthData invalid signature id', async () => {
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
{}
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature: '1234',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'cloud.xtralife.gamecenterauth',
|
||||
bundleId: 'com.example.com',
|
||||
};
|
||||
|
||||
try {
|
||||
await gcenter.validateAuthData(authData);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Apple Game Center - invalid signature');
|
||||
}
|
||||
await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature')
|
||||
);
|
||||
});
|
||||
|
||||
it('validateAuthData invalid public key url', async () => {
|
||||
const authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'invalid.com',
|
||||
timestamp: 1565257031287,
|
||||
signature: '1234',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'cloud.xtralife.gamecenterauth',
|
||||
it('validateAuthData invalid public key http url', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl:
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
const publicKeyUrls = [
|
||||
'example.com',
|
||||
'http://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
'https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg',
|
||||
'https://example.com/ \\.apple.com/public_key.cer',
|
||||
'https://example.com/ &.apple.com/public_key.cer',
|
||||
];
|
||||
await Promise.all(
|
||||
publicKeyUrls.map(publicKeyUrl =>
|
||||
expectAsync(
|
||||
gcenter.validateAuthData({
|
||||
id: 'G:1965586982',
|
||||
timestamp: 1565257031287,
|
||||
publicKeyUrl,
|
||||
signature: '1234',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'com.example.com',
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.SCRIPT_FAILED,
|
||||
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await gcenter.validateAuthData(authData);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: invalid.com');
|
||||
}
|
||||
it('should not validate Symantec Cert', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl:
|
||||
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
expect(() =>
|
||||
gcenter.verifyPublicKeyIssuer(
|
||||
testCert,
|
||||
'https://static.gc.apple.com/public-key/gc-prod-4.cer'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('adapter should load default cert', async () => {
|
||||
const options = {
|
||||
gcenter: {},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
const previous = new Date();
|
||||
await adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
);
|
||||
|
||||
const duration = new Date().getTime() - previous.getTime();
|
||||
expect(duration).toEqual(0);
|
||||
});
|
||||
|
||||
it('adapter should throw', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
rootCertificateUrl: 'https://example.com',
|
||||
},
|
||||
};
|
||||
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
|
||||
'gcenter',
|
||||
options
|
||||
);
|
||||
await expectAsync(
|
||||
adapter.validateAppId(
|
||||
appIds,
|
||||
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
|
||||
providerOptions
|
||||
)
|
||||
).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1742,9 +1882,393 @@ describe('microsoft graph auth adapter', () => {
|
||||
access_token: 'very.long.bad.token',
|
||||
};
|
||||
microsoft.validateAuthData(authData).then(done.fail, err => {
|
||||
expect(err.code).toBe(101);
|
||||
expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
|
||||
expect(err.message).toBe('Microsoft Graph auth is invalid for this user.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('facebook limited auth adapter', () => {
|
||||
const facebook = require('../lib/Adapters/Auth/facebook');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const util = require('util');
|
||||
|
||||
// TODO: figure out a way to run this test alongside facebook classic tests
|
||||
xit('(using client id as string) should throw error with missing id_token', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData({}, { clientId: 'secret' });
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Facebook auth is not configured.');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: figure out a way to run this test alongside facebook classic tests
|
||||
xit('(using client id as array) should throw error with missing id_token', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData({}, { clientId: ['secret'] });
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Facebook auth is not configured.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should not decode invalid id_token', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('provided token does not decode as JWT');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error if public key used to encode token is not available', async () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '789', alg: 'RS256' },
|
||||
};
|
||||
try {
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
`Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should use algorithm from key header to verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
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' },
|
||||
{ clientId: 'secret' }
|
||||
);
|
||||
expect(result).toEqual(fakeClaim);
|
||||
expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg);
|
||||
});
|
||||
|
||||
it('should not verify invalid id_token', async () => {
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('jwt malformed');
|
||||
}
|
||||
});
|
||||
|
||||
it('(using client id as array) should not verify invalid id_token', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: ['secret'] }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('provided token does not decode as JWT');
|
||||
}
|
||||
});
|
||||
|
||||
it('(using client id as string) should verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
);
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it('(using client id as array) should verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: ['secret'] }
|
||||
);
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it('(using client id as array with multiple items) should verify id_token', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
aud: 'secret',
|
||||
exp: Date.now(),
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
const result = await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: ['secret', 'secret 123'] }
|
||||
);
|
||||
expect(result).toEqual(fakeClaim);
|
||||
});
|
||||
|
||||
it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
|
||||
// and a private key
|
||||
xit('(using client id as array) should throw error with with invalid jwt issuer', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{
|
||||
id: 'INSERT ID HERE',
|
||||
token: 'INSERT FACEBOOK TOKEN HERE WITH INVALID JWT ISSUER',
|
||||
},
|
||||
{ clientId: ['INSERT CLIENT ID HERE'] }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('(using client id as string) should throw error with with invalid jwt issuer', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://not.facebook.com',
|
||||
sub: 'the_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{
|
||||
id: 'INSERT ID HERE',
|
||||
token: 'INSERT FACEBOOK TOKEN HERE WITH INVALID JWT ISSUER',
|
||||
},
|
||||
{ clientId: 'INSERT CLIENT ID HERE' }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'id token not issued by correct OpenID provider - expected: https://facebook.com | from: https://not.facebook.com'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
|
||||
// and a private key
|
||||
xit('(using client id as string) should throw error with invalid jwt clientId', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{
|
||||
id: 'INSERT ID HERE',
|
||||
token: 'INSERT FACEBOOK TOKEN HERE',
|
||||
},
|
||||
{ clientId: 'secret' }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('jwt audience invalid. expected: secret');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
|
||||
// and a private key
|
||||
xit('(using client id as array) should throw error with invalid jwt clientId', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{
|
||||
id: 'INSERT ID HERE',
|
||||
token: 'INSERT FACEBOOK TOKEN HERE',
|
||||
},
|
||||
{ clientId: ['secret'] }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('jwt audience invalid. expected: secret');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account
|
||||
// and a private key
|
||||
xit('should throw error with invalid user id', async () => {
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{
|
||||
id: 'invalid user',
|
||||
token: 'INSERT FACEBOOK TOKEN HERE',
|
||||
},
|
||||
{ clientId: 'INSERT CLIENT ID HERE' }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('auth data is invalid for this user.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error with with invalid user id', async () => {
|
||||
const fakeClaim = {
|
||||
iss: 'https://facebook.com',
|
||||
aud: 'invalid_client_id',
|
||||
sub: 'a_different_user_id',
|
||||
};
|
||||
const fakeDecodedToken = {
|
||||
header: { kid: '123', alg: 'RS256' },
|
||||
};
|
||||
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
|
||||
const fakeGetSigningKeyAsyncFunction = () => {
|
||||
return {
|
||||
kid: '123',
|
||||
rsaPublicKey: 'the_rsa_public_key',
|
||||
};
|
||||
};
|
||||
spyOn(util, 'promisify').and.callFake(() => fakeGetSigningKeyAsyncFunction);
|
||||
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
|
||||
|
||||
try {
|
||||
await facebook.validateAuthData(
|
||||
{ id: 'the_user_id', token: 'the_token' },
|
||||
{ clientId: 'secret' }
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('auth data is invalid for this user.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+11
-1
@@ -209,8 +209,12 @@ describe('execution', () => {
|
||||
const binPath = path.resolve(__dirname, '../bin/parse-server');
|
||||
let childProcess;
|
||||
|
||||
afterEach(async () => {
|
||||
afterEach(done => {
|
||||
if (childProcess) {
|
||||
childProcess.on('close', () => {
|
||||
childProcess = undefined;
|
||||
done();
|
||||
});
|
||||
childProcess.kill();
|
||||
}
|
||||
});
|
||||
@@ -223,6 +227,8 @@ describe('execution', () => {
|
||||
'test',
|
||||
'--databaseURI',
|
||||
'mongodb://localhost/test',
|
||||
'--port',
|
||||
'1339',
|
||||
]);
|
||||
childProcess.stdout.on('data', data => {
|
||||
data = data.toString();
|
||||
@@ -243,6 +249,8 @@ describe('execution', () => {
|
||||
'test',
|
||||
'--databaseURI',
|
||||
'mongodb://localhost/test',
|
||||
'--port',
|
||||
'1340',
|
||||
'--mountGraphQL',
|
||||
]);
|
||||
let output = '';
|
||||
@@ -267,6 +275,8 @@ describe('execution', () => {
|
||||
'test',
|
||||
'--databaseURI',
|
||||
'mongodb://localhost/test',
|
||||
'--port',
|
||||
'1341',
|
||||
'--mountGraphQL',
|
||||
'--mountPlayground',
|
||||
]);
|
||||
|
||||
@@ -92,9 +92,7 @@ describe('cloud validator', () => {
|
||||
},
|
||||
async () => {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
throw 'async error';
|
||||
}
|
||||
@@ -132,7 +130,7 @@ describe('cloud validator', () => {
|
||||
await Parse.Cloud.run('myFunction');
|
||||
});
|
||||
|
||||
it('require user on cloud functions', done => {
|
||||
it('require user on cloud functions', async done => {
|
||||
Parse.Cloud.define(
|
||||
'hello1',
|
||||
() => {
|
||||
@@ -142,16 +140,14 @@ describe('cloud validator', () => {
|
||||
requireUser: true,
|
||||
}
|
||||
);
|
||||
|
||||
Parse.Cloud.run('hello1', {})
|
||||
.then(() => {
|
||||
fail('function should have failed.');
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR);
|
||||
expect(error.message).toEqual('Validation failed. Please login to continue.');
|
||||
done();
|
||||
});
|
||||
try {
|
||||
await Parse.Cloud.run('hello1', {});
|
||||
fail('function should have failed.');
|
||||
} catch (error) {
|
||||
expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR);
|
||||
expect(error.message).toEqual('Validation failed. Please login to continue.');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('require master on cloud functions', done => {
|
||||
@@ -264,6 +260,24 @@ describe('cloud validator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('set params type allow array', async () => {
|
||||
Parse.Cloud.define(
|
||||
'hello',
|
||||
() => {
|
||||
return 'Hello world!';
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
data: {
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
const result = await Parse.Cloud.run('hello', { data: [{ foo: 'bar' }] });
|
||||
expect(result).toBe('Hello world!');
|
||||
});
|
||||
|
||||
it('set params type', done => {
|
||||
Parse.Cloud.define(
|
||||
'hello',
|
||||
@@ -341,6 +355,117 @@ describe('cloud validator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('set params not-required options data', done => {
|
||||
Parse.Cloud.define(
|
||||
'hello',
|
||||
req => {
|
||||
expect(req.params.data).toBe('abc');
|
||||
return 'Hello world!';
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
data: {
|
||||
type: String,
|
||||
required: false,
|
||||
options: s => {
|
||||
return s.length >= 4 && s.length <= 50;
|
||||
},
|
||||
error: 'Validation failed. Expected length of data to be between 4 and 50.',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
Parse.Cloud.run('hello', { data: 'abc' })
|
||||
.then(() => {
|
||||
fail('function should have failed.');
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR);
|
||||
expect(error.message).toEqual(
|
||||
'Validation failed. Expected length of data to be between 4 and 50.'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set params not-required type', done => {
|
||||
Parse.Cloud.define(
|
||||
'hello',
|
||||
req => {
|
||||
expect(req.params.data).toBe(null);
|
||||
return 'Hello world!';
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
data: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
Parse.Cloud.run('hello', { data: null })
|
||||
.then(() => {
|
||||
fail('function should have failed.');
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR);
|
||||
expect(error.message).toEqual('Validation failed. Invalid type for data. Expected: string');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set params not-required options', done => {
|
||||
Parse.Cloud.define(
|
||||
'hello',
|
||||
() => {
|
||||
return 'Hello world!';
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
data: {
|
||||
type: String,
|
||||
required: false,
|
||||
options: s => {
|
||||
return s.length >= 4 && s.length <= 50;
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
Parse.Cloud.run('hello', {})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(() => {
|
||||
fail('function should not have failed.');
|
||||
});
|
||||
});
|
||||
|
||||
it('set params not-required no-options', done => {
|
||||
Parse.Cloud.define(
|
||||
'hello',
|
||||
() => {
|
||||
return 'Hello world!';
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
data: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
Parse.Cloud.run('hello', {})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(() => {
|
||||
fail('function should not have failed.');
|
||||
});
|
||||
});
|
||||
|
||||
it('set params option', done => {
|
||||
Parse.Cloud.define(
|
||||
'hello',
|
||||
@@ -587,16 +712,10 @@ describe('cloud validator', () => {
|
||||
expect(obj.get('foo')).toBe('bar');
|
||||
|
||||
const query = new Parse.Query('beforeFind');
|
||||
try {
|
||||
const first = await query.first({ useMasterKey: true });
|
||||
expect(first).toBeDefined();
|
||||
expect(first.id).toBe(obj.id);
|
||||
done();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e.code);
|
||||
throw e;
|
||||
}
|
||||
const first = await query.first({ useMasterKey: true });
|
||||
expect(first).toBeDefined();
|
||||
expect(first.id).toBe(obj.id);
|
||||
done();
|
||||
});
|
||||
|
||||
it('basic beforeDelete skipWithMasterKey', async function (done) {
|
||||
@@ -860,6 +979,152 @@ describe('cloud validator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('basic validator requireAnyUserRoles', async function (done) {
|
||||
Parse.Cloud.define(
|
||||
'cloudFunction',
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
{
|
||||
requireUser: true,
|
||||
requireAnyUserRoles: ['Admin'],
|
||||
}
|
||||
);
|
||||
const user = await Parse.User.signUp('testuser', 'p@ssword');
|
||||
try {
|
||||
await Parse.Cloud.run('cloudFunction');
|
||||
fail('cloud validator should have failed.');
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Validation failed. User does not match the required roles.');
|
||||
}
|
||||
const roleACL = new Parse.ACL();
|
||||
roleACL.setPublicReadAccess(true);
|
||||
const role = new Parse.Role('Admin', roleACL);
|
||||
role.getUsers().add(user);
|
||||
await role.save({ useMasterKey: true });
|
||||
await Parse.Cloud.run('cloudFunction');
|
||||
done();
|
||||
});
|
||||
|
||||
it('basic validator requireAllUserRoles', async function (done) {
|
||||
Parse.Cloud.define(
|
||||
'cloudFunction',
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
{
|
||||
requireUser: true,
|
||||
requireAllUserRoles: ['Admin', 'Admin2'],
|
||||
}
|
||||
);
|
||||
const user = await Parse.User.signUp('testuser', 'p@ssword');
|
||||
try {
|
||||
await Parse.Cloud.run('cloudFunction');
|
||||
fail('cloud validator should have failed.');
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Validation failed. User does not match all the required roles.');
|
||||
}
|
||||
const roleACL = new Parse.ACL();
|
||||
roleACL.setPublicReadAccess(true);
|
||||
const role = new Parse.Role('Admin', roleACL);
|
||||
role.getUsers().add(user);
|
||||
|
||||
const role2 = new Parse.Role('Admin2', roleACL);
|
||||
role2.getUsers().add(user);
|
||||
await role.save({ useMasterKey: true });
|
||||
await role2.save({ useMasterKey: true });
|
||||
await Parse.Cloud.run('cloudFunction');
|
||||
done();
|
||||
});
|
||||
|
||||
it('allow requireAnyUserRoles to be a function', async function (done) {
|
||||
Parse.Cloud.define(
|
||||
'cloudFunction',
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
{
|
||||
requireUser: true,
|
||||
requireAnyUserRoles: () => {
|
||||
return ['Admin Func'];
|
||||
},
|
||||
}
|
||||
);
|
||||
const user = await Parse.User.signUp('testuser', 'p@ssword');
|
||||
try {
|
||||
await Parse.Cloud.run('cloudFunction');
|
||||
fail('cloud validator should have failed.');
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Validation failed. User does not match the required roles.');
|
||||
}
|
||||
const roleACL = new Parse.ACL();
|
||||
roleACL.setPublicReadAccess(true);
|
||||
const role = new Parse.Role('Admin Func', roleACL);
|
||||
role.getUsers().add(user);
|
||||
await role.save({ useMasterKey: true });
|
||||
await Parse.Cloud.run('cloudFunction');
|
||||
done();
|
||||
});
|
||||
|
||||
it('allow requireAllUserRoles to be a function', async function (done) {
|
||||
Parse.Cloud.define(
|
||||
'cloudFunction',
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
{
|
||||
requireUser: true,
|
||||
requireAllUserRoles: () => {
|
||||
return ['AdminA', 'AdminB'];
|
||||
},
|
||||
}
|
||||
);
|
||||
const user = await Parse.User.signUp('testuser', 'p@ssword');
|
||||
try {
|
||||
await Parse.Cloud.run('cloudFunction');
|
||||
fail('cloud validator should have failed.');
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Validation failed. User does not match all the required roles.');
|
||||
}
|
||||
const roleACL = new Parse.ACL();
|
||||
roleACL.setPublicReadAccess(true);
|
||||
const role = new Parse.Role('AdminA', roleACL);
|
||||
role.getUsers().add(user);
|
||||
|
||||
const role2 = new Parse.Role('AdminB', roleACL);
|
||||
role2.getUsers().add(user);
|
||||
await role.save({ useMasterKey: true });
|
||||
await role2.save({ useMasterKey: true });
|
||||
await Parse.Cloud.run('cloudFunction');
|
||||
done();
|
||||
});
|
||||
|
||||
it('basic requireAllUserRoles but no user', async function (done) {
|
||||
Parse.Cloud.define(
|
||||
'cloudFunction',
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
{
|
||||
requireAllUserRoles: ['Admin'],
|
||||
}
|
||||
);
|
||||
try {
|
||||
await Parse.Cloud.run('cloudFunction');
|
||||
fail('cloud validator should have failed.');
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Validation failed. Please login to continue.');
|
||||
}
|
||||
const user = await Parse.User.signUp('testuser', 'p@ssword');
|
||||
const roleACL = new Parse.ACL();
|
||||
roleACL.setPublicReadAccess(true);
|
||||
const role = new Parse.Role('Admin', roleACL);
|
||||
role.getUsers().add(user);
|
||||
await role.save({ useMasterKey: true });
|
||||
await Parse.Cloud.run('cloudFunction');
|
||||
done();
|
||||
});
|
||||
|
||||
it('basic beforeSave requireMaster', function (done) {
|
||||
Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, {
|
||||
requireMaster: true,
|
||||
@@ -1266,4 +1531,230 @@ describe('cloud validator', () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not log on valid config', () => {
|
||||
Parse.Cloud.define('myFunction', () => {}, {
|
||||
requireUser: true,
|
||||
requireMaster: true,
|
||||
validateMasterKey: false,
|
||||
skipWithMasterKey: true,
|
||||
requireUserKeys: {
|
||||
Acc: {
|
||||
constant: true,
|
||||
options: ['A', 'B'],
|
||||
required: true,
|
||||
default: 'f',
|
||||
error: 'a',
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
Acc: {
|
||||
constant: true,
|
||||
options: ['A', 'B'],
|
||||
required: true,
|
||||
default: 'f',
|
||||
error: 'a',
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('Logs on invalid config', () => {
|
||||
const fields = [
|
||||
{
|
||||
field: 'requiredUser',
|
||||
value: true,
|
||||
error: 'requiredUser is not a supported parameter for Cloud Function validations.',
|
||||
},
|
||||
{
|
||||
field: 'requireUser',
|
||||
value: [],
|
||||
error:
|
||||
'Invalid type for Cloud Function validation key requireUser. Expected boolean, actual array',
|
||||
},
|
||||
{
|
||||
field: 'requireMaster',
|
||||
value: [],
|
||||
error:
|
||||
'Invalid type for Cloud Function validation key requireMaster. Expected boolean, actual array',
|
||||
},
|
||||
{
|
||||
field: 'validateMasterKey',
|
||||
value: [],
|
||||
error:
|
||||
'Invalid type for Cloud Function validation key validateMasterKey. Expected boolean, actual array',
|
||||
},
|
||||
{
|
||||
field: 'skipWithMasterKey',
|
||||
value: [],
|
||||
error:
|
||||
'Invalid type for Cloud Function validation key skipWithMasterKey. Expected boolean, actual array',
|
||||
},
|
||||
{
|
||||
field: 'requireAllUserRoles',
|
||||
value: true,
|
||||
error:
|
||||
'Invalid type for Cloud Function validation key requireAllUserRoles. Expected array|function, actual boolean',
|
||||
},
|
||||
{
|
||||
field: 'requireAnyUserRoles',
|
||||
value: true,
|
||||
error:
|
||||
'Invalid type for Cloud Function validation key requireAnyUserRoles. Expected array|function, actual boolean',
|
||||
},
|
||||
{
|
||||
field: 'fields',
|
||||
value: true,
|
||||
error:
|
||||
'Invalid type for Cloud Function validation key fields. Expected array|object, actual boolean',
|
||||
},
|
||||
{
|
||||
field: 'requireUserKeys',
|
||||
value: true,
|
||||
error:
|
||||
'Invalid type for Cloud Function validation key requireUserKeys. Expected array|object, actual boolean',
|
||||
},
|
||||
];
|
||||
for (const field of fields) {
|
||||
try {
|
||||
Parse.Cloud.define('myFunction', () => {}, {
|
||||
[field.field]: field.value,
|
||||
});
|
||||
fail(`Expected error registering invalid Cloud Function validation ${field.field}.`);
|
||||
} catch (e) {
|
||||
expect(e).toBe(field.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('Logs on invalid config', () => {
|
||||
const fields = [
|
||||
{
|
||||
field: 'otherKey',
|
||||
value: true,
|
||||
error: 'otherKey is not a supported parameter for Cloud Function validations.',
|
||||
},
|
||||
{
|
||||
field: 'constant',
|
||||
value: [],
|
||||
error:
|
||||
'Invalid type for Cloud Function validation key constant. Expected boolean, actual array',
|
||||
},
|
||||
{
|
||||
field: 'required',
|
||||
value: [],
|
||||
error:
|
||||
'Invalid type for Cloud Function validation key required. Expected boolean, actual array',
|
||||
},
|
||||
{
|
||||
field: 'error',
|
||||
value: [],
|
||||
error:
|
||||
'Invalid type for Cloud Function validation key error. Expected string, actual array',
|
||||
},
|
||||
];
|
||||
for (const field of fields) {
|
||||
try {
|
||||
Parse.Cloud.define('myFunction', () => {}, {
|
||||
fields: {
|
||||
name: {
|
||||
[field.field]: field.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
fail(`Expected error registering invalid Cloud Function validation ${field.field}.`);
|
||||
} catch (e) {
|
||||
expect(e).toBe(field.error);
|
||||
}
|
||||
try {
|
||||
Parse.Cloud.define('myFunction', () => {}, {
|
||||
requireUserKeys: {
|
||||
name: {
|
||||
[field.field]: field.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
fail(`Expected error registering invalid Cloud Function validation ${field.field}.`);
|
||||
} catch (e) {
|
||||
expect(e).toBe(field.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('set params options function async', async () => {
|
||||
Parse.Cloud.define(
|
||||
'hello',
|
||||
() => {
|
||||
return 'Hello world!';
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
data: {
|
||||
type: String,
|
||||
required: true,
|
||||
options: async val => {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
return val === 'f';
|
||||
},
|
||||
error: 'Validation failed.',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
try {
|
||||
await Parse.Cloud.run('hello', { data: 'd' });
|
||||
fail('validation should have failed');
|
||||
} catch (error) {
|
||||
expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR);
|
||||
expect(error.message).toEqual('Validation failed.');
|
||||
}
|
||||
const result = await Parse.Cloud.run('hello', { data: 'f' });
|
||||
expect(result).toBe('Hello world!');
|
||||
});
|
||||
|
||||
it('basic beforeSave requireUserKey as custom async function', async () => {
|
||||
Parse.Cloud.beforeSave(Parse.User, () => {}, {
|
||||
fields: {
|
||||
accType: {
|
||||
default: 'normal',
|
||||
constant: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
Parse.Cloud.define(
|
||||
'secureFunction',
|
||||
() => {
|
||||
return "Here's all the secure data!";
|
||||
},
|
||||
{
|
||||
requireUserKeys: {
|
||||
accType: {
|
||||
options: async val => {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
return ['admin', 'admin2'].includes(val);
|
||||
},
|
||||
error: 'Unauthorized.',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
const user = new Parse.User();
|
||||
user.set('username', 'testuser');
|
||||
user.set('password', 'p@ssword');
|
||||
user.set('accType', 'admin');
|
||||
await user.signUp();
|
||||
expect(user.get('accType')).toBe('normal');
|
||||
try {
|
||||
await Parse.Cloud.run('secureFunction');
|
||||
fail('function should only be available to admin users');
|
||||
} catch (error) {
|
||||
expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR);
|
||||
expect(error.message).toEqual('Unauthorized.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+662
-269
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ describe('Cloud Code Logger', () => {
|
||||
return reconfigureServer({
|
||||
// useful to flip to false for fine tuning :).
|
||||
silent: true,
|
||||
logLevel: undefined,
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.User.signUp('tester', 'abc')
|
||||
@@ -160,7 +161,7 @@ describe('Cloud Code Logger', () => {
|
||||
expect(log[0]).toEqual('error');
|
||||
const error = log[2].error;
|
||||
expect(error instanceof Parse.Error).toBeTruthy();
|
||||
expect(error.code).toBe(141);
|
||||
expect(error.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||
expect(error.message).toBe('uh oh!');
|
||||
done();
|
||||
});
|
||||
@@ -198,7 +199,9 @@ describe('Cloud Code Logger', () => {
|
||||
expect(log[1]).toMatch(
|
||||
/Failed running cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Error:/
|
||||
);
|
||||
const errorString = JSON.stringify(new Parse.Error(141, 'it failed!'));
|
||||
const errorString = JSON.stringify(
|
||||
new Parse.Error(Parse.Error.SCRIPT_FAILED, 'it failed!')
|
||||
);
|
||||
expect(log[1].indexOf(errorString)).toBeGreaterThan(0);
|
||||
done();
|
||||
})
|
||||
|
||||
@@ -236,6 +236,57 @@ describe('DatabaseController', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not return a $or operation if the query involves one of the two fields also used as array/pointer permissions', done => {
|
||||
const clp = buildCLP(['users', 'user']);
|
||||
const query = { a: 'b', user: createUserPointer(USER_ID) };
|
||||
schemaController.testPermissionsForClassName
|
||||
.withArgs(CLASS_NAME, ACL_GROUP, OPERATION)
|
||||
.and.returnValue(false);
|
||||
schemaController.getClassLevelPermissions.withArgs(CLASS_NAME).and.returnValue(clp);
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'user')
|
||||
.and.returnValue({ type: 'Pointer' });
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'users')
|
||||
.and.returnValue({ type: 'Array' });
|
||||
const output = databaseController.addPointerPermissions(
|
||||
schemaController,
|
||||
CLASS_NAME,
|
||||
OPERATION,
|
||||
query,
|
||||
ACL_GROUP
|
||||
);
|
||||
expect(output).toEqual({ ...query, user: createUserPointer(USER_ID) });
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not return a $or operation if the query involves one of the fields also used as array/pointer permissions', done => {
|
||||
const clp = buildCLP(['user', 'users', 'userObject']);
|
||||
const query = { a: 'b', user: createUserPointer(USER_ID) };
|
||||
schemaController.testPermissionsForClassName
|
||||
.withArgs(CLASS_NAME, ACL_GROUP, OPERATION)
|
||||
.and.returnValue(false);
|
||||
schemaController.getClassLevelPermissions.withArgs(CLASS_NAME).and.returnValue(clp);
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'user')
|
||||
.and.returnValue({ type: 'Pointer' });
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'users')
|
||||
.and.returnValue({ type: 'Array' });
|
||||
schemaController.getExpectedType
|
||||
.withArgs(CLASS_NAME, 'userObject')
|
||||
.and.returnValue({ type: 'Object' });
|
||||
const output = databaseController.addPointerPermissions(
|
||||
schemaController,
|
||||
CLASS_NAME,
|
||||
OPERATION,
|
||||
query,
|
||||
ACL_GROUP
|
||||
);
|
||||
expect(output).toEqual({ ...query, user: createUserPointer(USER_ID) });
|
||||
done();
|
||||
});
|
||||
|
||||
it('should throw an error if for some unexpected reason the property specified in the CLP is neither a pointer nor an array', done => {
|
||||
const clp = buildCLP(['user']);
|
||||
const query = { a: 'b' };
|
||||
@@ -265,6 +316,51 @@ describe('DatabaseController', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('reduceOperations', function () {
|
||||
const databaseController = new DatabaseController();
|
||||
|
||||
it('objectToEntriesStrings', done => {
|
||||
const output = databaseController.objectToEntriesStrings({ a: 1, b: 2, c: 3 });
|
||||
expect(output).toEqual(['"a":1', '"b":2', '"c":3']);
|
||||
done();
|
||||
});
|
||||
|
||||
it('reduceOrOperation', done => {
|
||||
expect(databaseController.reduceOrOperation({ a: 1 })).toEqual({ a: 1 });
|
||||
expect(databaseController.reduceOrOperation({ $or: [{ a: 1 }, { b: 2 }] })).toEqual({
|
||||
$or: [{ a: 1 }, { b: 2 }],
|
||||
});
|
||||
expect(databaseController.reduceOrOperation({ $or: [{ a: 1 }, { a: 2 }] })).toEqual({
|
||||
$or: [{ a: 1 }, { a: 2 }],
|
||||
});
|
||||
expect(databaseController.reduceOrOperation({ $or: [{ a: 1 }, { a: 1 }] })).toEqual({ a: 1 });
|
||||
expect(
|
||||
databaseController.reduceOrOperation({ $or: [{ a: 1, b: 2, c: 3 }, { a: 1 }] })
|
||||
).toEqual({ a: 1 });
|
||||
expect(
|
||||
databaseController.reduceOrOperation({ $or: [{ b: 2 }, { a: 1, b: 2, c: 3 }] })
|
||||
).toEqual({ b: 2 });
|
||||
done();
|
||||
});
|
||||
|
||||
it('reduceAndOperation', done => {
|
||||
expect(databaseController.reduceAndOperation({ a: 1 })).toEqual({ a: 1 });
|
||||
expect(databaseController.reduceAndOperation({ $and: [{ a: 1 }, { b: 2 }] })).toEqual({
|
||||
$and: [{ a: 1 }, { b: 2 }],
|
||||
});
|
||||
expect(databaseController.reduceAndOperation({ $and: [{ a: 1 }, { a: 2 }] })).toEqual({
|
||||
$and: [{ a: 1 }, { a: 2 }],
|
||||
});
|
||||
expect(databaseController.reduceAndOperation({ $and: [{ a: 1 }, { a: 1 }] })).toEqual({
|
||||
a: 1,
|
||||
});
|
||||
expect(
|
||||
databaseController.reduceAndOperation({ $and: [{ a: 1, b: 2, c: 3 }, { b: 2 }] })
|
||||
).toEqual({ a: 1, b: 2, c: 3 });
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildCLP(pointerNames) {
|
||||
|
||||
@@ -0,0 +1,644 @@
|
||||
const { DefinedSchemas } = require('../lib/SchemaMigrations/DefinedSchemas');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
const cleanUpIndexes = schema => {
|
||||
if (schema.indexes) {
|
||||
delete schema.indexes._id_;
|
||||
if (!Object.keys(schema.indexes).length) {
|
||||
delete schema.indexes;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('DefinedSchemas', () => {
|
||||
let config;
|
||||
afterEach(async () => {
|
||||
config = Config.get('test');
|
||||
if (config) {
|
||||
await config.database.adapter.deleteAllClasses();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Fields', () => {
|
||||
it('should keep default fields if not provided', async () => {
|
||||
const server = await reconfigureServer();
|
||||
// Will perform create
|
||||
await new DefinedSchemas({ definitions: [{ className: 'Test' }] }, server.config).execute();
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
const expectedFields = {
|
||||
objectId: { type: 'String' },
|
||||
createdAt: { type: 'Date' },
|
||||
updatedAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
};
|
||||
expect(schema.fields).toEqual(expectedFields);
|
||||
|
||||
await server.config.schemaCache.clear();
|
||||
// Will perform update
|
||||
await new DefinedSchemas({ definitions: [{ className: 'Test' }] }, server.config).execute();
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields).toEqual(expectedFields);
|
||||
});
|
||||
it('should protect default fields', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
const schemas = {
|
||||
definitions: [
|
||||
{
|
||||
className: '_User',
|
||||
fields: {
|
||||
email: 'Object',
|
||||
},
|
||||
},
|
||||
{
|
||||
className: '_Role',
|
||||
fields: {
|
||||
users: 'Object',
|
||||
},
|
||||
},
|
||||
{
|
||||
className: '_Installation',
|
||||
fields: {
|
||||
installationId: 'Object',
|
||||
},
|
||||
},
|
||||
{
|
||||
className: 'Test',
|
||||
fields: {
|
||||
createdAt: { type: 'Object' },
|
||||
objectId: { type: 'Number' },
|
||||
updatedAt: { type: 'String' },
|
||||
ACL: { type: 'String' },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const expectedFields = {
|
||||
objectId: { type: 'String' },
|
||||
createdAt: { type: 'Date' },
|
||||
updatedAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
};
|
||||
|
||||
const expectedUserFields = {
|
||||
objectId: { type: 'String' },
|
||||
createdAt: { type: 'Date' },
|
||||
updatedAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
username: { type: 'String' },
|
||||
password: { type: 'String' },
|
||||
email: { type: 'String' },
|
||||
emailVerified: { type: 'Boolean' },
|
||||
authData: { type: 'Object' },
|
||||
};
|
||||
|
||||
const expectedRoleFields = {
|
||||
objectId: { type: 'String' },
|
||||
createdAt: { type: 'Date' },
|
||||
updatedAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
name: { type: 'String' },
|
||||
users: { type: 'Relation', targetClass: '_User' },
|
||||
roles: { type: 'Relation', targetClass: '_Role' },
|
||||
};
|
||||
|
||||
const expectedInstallationFields = {
|
||||
objectId: { type: 'String' },
|
||||
createdAt: { type: 'Date' },
|
||||
updatedAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
installationId: { type: 'String' },
|
||||
deviceToken: { type: 'String' },
|
||||
channels: { type: 'Array' },
|
||||
deviceType: { type: 'String' },
|
||||
pushType: { type: 'String' },
|
||||
GCMSenderId: { type: 'String' },
|
||||
timeZone: { type: 'String' },
|
||||
localeIdentifier: { type: 'String' },
|
||||
badge: { type: 'Number' },
|
||||
appVersion: { type: 'String' },
|
||||
appName: { type: 'String' },
|
||||
appIdentifier: { type: 'String' },
|
||||
parseVersion: { type: 'String' },
|
||||
};
|
||||
|
||||
// Perform create
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields).toEqual(expectedFields);
|
||||
|
||||
let userSchema = await new Parse.Schema('_User').get();
|
||||
expect(userSchema.fields).toEqual(expectedUserFields);
|
||||
|
||||
let roleSchema = await new Parse.Schema('_Role').get();
|
||||
expect(roleSchema.fields).toEqual(expectedRoleFields);
|
||||
|
||||
let installationSchema = await new Parse.Schema('_Installation').get();
|
||||
expect(installationSchema.fields).toEqual(expectedInstallationFields);
|
||||
|
||||
await server.config.schemaCache.clear();
|
||||
// Perform update
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields).toEqual(expectedFields);
|
||||
|
||||
userSchema = await new Parse.Schema('_User').get();
|
||||
expect(userSchema.fields).toEqual(expectedUserFields);
|
||||
|
||||
roleSchema = await new Parse.Schema('_Role').get();
|
||||
expect(roleSchema.fields).toEqual(expectedRoleFields);
|
||||
|
||||
installationSchema = await new Parse.Schema('_Installation').get();
|
||||
expect(installationSchema.fields).toEqual(expectedInstallationFields);
|
||||
});
|
||||
it('should create new fields', async () => {
|
||||
const server = await reconfigureServer();
|
||||
const fields = {
|
||||
objectId: { type: 'String' },
|
||||
createdAt: { type: 'Date' },
|
||||
updatedAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
aString: { type: 'String' },
|
||||
aStringWithDefault: { type: 'String', defaultValue: 'Test' },
|
||||
aStringWithRequired: { type: 'String', required: true },
|
||||
aStringWithRequiredAndDefault: { type: 'String', required: true, defaultValue: 'Test' },
|
||||
aBoolean: { type: 'Boolean' },
|
||||
aFile: { type: 'File' },
|
||||
aNumber: { type: 'Number' },
|
||||
aRelation: { type: 'Relation', targetClass: '_User' },
|
||||
aPointer: { type: 'Pointer', targetClass: '_Role' },
|
||||
aDate: { type: 'Date' },
|
||||
aGeoPoint: { type: 'GeoPoint' },
|
||||
aPolygon: { type: 'Polygon' },
|
||||
aArray: { type: 'Array' },
|
||||
aObject: { type: 'Object' },
|
||||
};
|
||||
const schemas = {
|
||||
definitions: [
|
||||
{
|
||||
className: 'Test',
|
||||
fields,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Create
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields).toEqual(fields);
|
||||
|
||||
fields.anotherObject = { type: 'Object' };
|
||||
// Update
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields).toEqual(fields);
|
||||
});
|
||||
it('should not delete removed fields when "deleteExtraFields" is false', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
await new DefinedSchemas(
|
||||
{ definitions: [{ className: 'Test', fields: { aField: { type: 'String' } } }] },
|
||||
server.config
|
||||
).execute();
|
||||
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields.aField).toBeDefined();
|
||||
|
||||
await new DefinedSchemas({ definitions: [{ className: 'Test' }] }, server.config).execute();
|
||||
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields).toEqual({
|
||||
objectId: { type: 'String' },
|
||||
createdAt: { type: 'Date' },
|
||||
updatedAt: { type: 'Date' },
|
||||
aField: { type: 'String' },
|
||||
ACL: { type: 'ACL' },
|
||||
});
|
||||
});
|
||||
it('should delete removed fields when "deleteExtraFields" is true', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
await new DefinedSchemas(
|
||||
{
|
||||
definitions: [{ className: 'Test', fields: { aField: { type: 'String' } } }],
|
||||
},
|
||||
server.config
|
||||
).execute();
|
||||
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields.aField).toBeDefined();
|
||||
|
||||
await new DefinedSchemas(
|
||||
{ deleteExtraFields: true, definitions: [{ className: 'Test' }] },
|
||||
server.config
|
||||
).execute();
|
||||
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields).toEqual({
|
||||
objectId: { type: 'String' },
|
||||
createdAt: { type: 'Date' },
|
||||
updatedAt: { type: 'Date' },
|
||||
ACL: { type: 'ACL' },
|
||||
});
|
||||
});
|
||||
it('should re create fields with changed type when "recreateModifiedFields" is true', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
await new DefinedSchemas(
|
||||
{ definitions: [{ className: 'Test', fields: { aField: { type: 'String' } } }] },
|
||||
server.config
|
||||
).execute();
|
||||
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields.aField).toEqual({ type: 'String' });
|
||||
|
||||
const object = new Parse.Object('Test');
|
||||
await object.save({ aField: 'Hello' }, { useMasterKey: true });
|
||||
|
||||
await new DefinedSchemas(
|
||||
{
|
||||
recreateModifiedFields: true,
|
||||
definitions: [{ className: 'Test', fields: { aField: { type: 'Number' } } }],
|
||||
},
|
||||
server.config
|
||||
).execute();
|
||||
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields.aField).toEqual({ type: 'Number' });
|
||||
|
||||
await object.fetch({ useMasterKey: true });
|
||||
expect(object.get('aField')).toBeUndefined();
|
||||
});
|
||||
it('should not re create fields with changed type when "recreateModifiedFields" is not true', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
await new DefinedSchemas(
|
||||
{ definitions: [{ className: 'Test', fields: { aField: { type: 'String' } } }] },
|
||||
server.config
|
||||
).execute();
|
||||
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields.aField).toEqual({ type: 'String' });
|
||||
|
||||
const object = new Parse.Object('Test');
|
||||
await object.save({ aField: 'Hello' }, { useMasterKey: true });
|
||||
|
||||
await new DefinedSchemas(
|
||||
{ definitions: [{ className: 'Test', fields: { aField: { type: 'Number' } } }] },
|
||||
server.config
|
||||
).execute();
|
||||
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields.aField).toEqual({ type: 'String' });
|
||||
|
||||
await object.fetch({ useMasterKey: true });
|
||||
expect(object.get('aField')).toBeDefined();
|
||||
});
|
||||
it('should just update classic fields with changed params', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
await new DefinedSchemas(
|
||||
{ definitions: [{ className: 'Test', fields: { aField: { type: 'String' } } }] },
|
||||
server.config
|
||||
).execute();
|
||||
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields.aField).toEqual({ type: 'String' });
|
||||
|
||||
const object = new Parse.Object('Test');
|
||||
await object.save({ aField: 'Hello' }, { useMasterKey: true });
|
||||
|
||||
await new DefinedSchemas(
|
||||
{
|
||||
definitions: [
|
||||
{ className: 'Test', fields: { aField: { type: 'String', required: true } } },
|
||||
],
|
||||
},
|
||||
server.config
|
||||
).execute();
|
||||
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.fields.aField).toEqual({ type: 'String', required: true });
|
||||
|
||||
await object.fetch({ useMasterKey: true });
|
||||
expect(object.get('aField')).toEqual('Hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Indexes', () => {
|
||||
it('should create new indexes', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
const indexes = { complex: { createdAt: 1, updatedAt: 1 } };
|
||||
|
||||
const schemas = {
|
||||
definitions: [{ className: 'Test', fields: { aField: { type: 'String' } }, indexes }],
|
||||
};
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toEqual(indexes);
|
||||
|
||||
indexes.complex2 = { createdAt: 1, aField: 1 };
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toEqual(indexes);
|
||||
});
|
||||
it('should re create changed indexes', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
let indexes = { complex: { createdAt: 1, updatedAt: 1 } };
|
||||
|
||||
let schemas = { definitions: [{ className: 'Test', indexes }] };
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
|
||||
indexes = { complex: { createdAt: 1 } };
|
||||
schemas = { definitions: [{ className: 'Test', indexes }] };
|
||||
|
||||
// Change indexes
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toEqual(indexes);
|
||||
|
||||
// Update
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toEqual(indexes);
|
||||
});
|
||||
|
||||
it('should delete removed indexes', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
let indexes = { complex: { createdAt: 1, updatedAt: 1 } };
|
||||
|
||||
let schemas = { definitions: [{ className: 'Test', indexes }] };
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
|
||||
indexes = {};
|
||||
schemas = { definitions: [{ className: 'Test', indexes }] };
|
||||
// Change indexes
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
let schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toBeUndefined();
|
||||
|
||||
// Update
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
schema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(schema);
|
||||
expect(schema.indexes).toBeUndefined();
|
||||
});
|
||||
xit('should keep protected indexes', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
const expectedIndexes = {
|
||||
username_1: { username: 1 },
|
||||
case_insensitive_username: { username: 1 },
|
||||
email_1: { email: 1 },
|
||||
case_insensitive_email: { email: 1 },
|
||||
};
|
||||
const schemas = {
|
||||
definitions: [
|
||||
{
|
||||
className: '_User',
|
||||
indexes: {
|
||||
case_insensitive_username: { password: true },
|
||||
case_insensitive_email: { password: true },
|
||||
},
|
||||
},
|
||||
{ className: 'Test' },
|
||||
],
|
||||
};
|
||||
// Create
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
let userSchema = await new Parse.Schema('_User').get();
|
||||
let testSchema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(userSchema);
|
||||
cleanUpIndexes(testSchema);
|
||||
expect(testSchema.indexes).toBeUndefined();
|
||||
expect(userSchema.indexes).toEqual(expectedIndexes);
|
||||
|
||||
// Update
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
userSchema = await new Parse.Schema('_User').get();
|
||||
testSchema = await new Parse.Schema('Test').get();
|
||||
cleanUpIndexes(userSchema);
|
||||
cleanUpIndexes(testSchema);
|
||||
expect(testSchema.indexes).toBeUndefined();
|
||||
expect(userSchema.indexes).toEqual(expectedIndexes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ClassLevelPermissions', () => {
|
||||
it('should use default CLP', async () => {
|
||||
const server = await reconfigureServer();
|
||||
const schemas = { definitions: [{ className: 'Test' }] };
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
|
||||
const expectedTestCLP = {
|
||||
find: {},
|
||||
count: {},
|
||||
get: {},
|
||||
create: {},
|
||||
update: {},
|
||||
delete: {},
|
||||
addField: {},
|
||||
protectedFields: {},
|
||||
};
|
||||
let testSchema = await new Parse.Schema('Test').get();
|
||||
expect(testSchema.classLevelPermissions).toEqual(expectedTestCLP);
|
||||
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
testSchema = await new Parse.Schema('Test').get();
|
||||
expect(testSchema.classLevelPermissions).toEqual(expectedTestCLP);
|
||||
});
|
||||
it('should save CLP', async () => {
|
||||
const server = await reconfigureServer();
|
||||
|
||||
const expectedTestCLP = {
|
||||
find: {},
|
||||
count: { requiresAuthentication: true },
|
||||
get: { 'role:Admin': true },
|
||||
create: { 'role:ARole': true, requiresAuthentication: true },
|
||||
update: { requiresAuthentication: true },
|
||||
delete: { requiresAuthentication: true },
|
||||
addField: {},
|
||||
protectedFields: { '*': ['aField'], 'role:Admin': ['anotherField'] },
|
||||
};
|
||||
const schemas = {
|
||||
definitions: [
|
||||
{
|
||||
className: 'Test',
|
||||
fields: { aField: { type: 'String' }, anotherField: { type: 'Object' } },
|
||||
classLevelPermissions: expectedTestCLP,
|
||||
},
|
||||
],
|
||||
};
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
|
||||
let testSchema = await new Parse.Schema('Test').get();
|
||||
expect(testSchema.classLevelPermissions).toEqual(expectedTestCLP);
|
||||
|
||||
expectedTestCLP.update = {};
|
||||
expectedTestCLP.create = { requiresAuthentication: true };
|
||||
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
testSchema = await new Parse.Schema('Test').get();
|
||||
expect(testSchema.classLevelPermissions).toEqual(expectedTestCLP);
|
||||
});
|
||||
it('should force addField to empty', async () => {
|
||||
const server = await reconfigureServer();
|
||||
const schemas = {
|
||||
definitions: [{ className: 'Test', classLevelPermissions: { addField: { '*': true } } }],
|
||||
};
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
|
||||
const expectedTestCLP = {
|
||||
find: {},
|
||||
count: {},
|
||||
get: {},
|
||||
create: {},
|
||||
update: {},
|
||||
delete: {},
|
||||
addField: {},
|
||||
protectedFields: {},
|
||||
};
|
||||
|
||||
let testSchema = await new Parse.Schema('Test').get();
|
||||
expect(testSchema.classLevelPermissions).toEqual(expectedTestCLP);
|
||||
|
||||
await new DefinedSchemas(schemas, server.config).execute();
|
||||
testSchema = await new Parse.Schema('Test').get();
|
||||
expect(testSchema.classLevelPermissions).toEqual(expectedTestCLP);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not delete automatically classes', async () => {
|
||||
await reconfigureServer({
|
||||
schema: { definitions: [{ className: '_User' }, { className: 'Test' }] },
|
||||
});
|
||||
|
||||
await reconfigureServer({ schema: { definitions: [{ className: '_User' }] } });
|
||||
|
||||
const schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.className).toEqual('Test');
|
||||
});
|
||||
|
||||
it('should disable class PUT/POST endpoint when lockSchemas provided to avoid dual source of truth', async () => {
|
||||
await reconfigureServer({
|
||||
schema: {
|
||||
lockSchemas: true,
|
||||
definitions: [{ className: '_User' }, { className: 'Test' }],
|
||||
},
|
||||
});
|
||||
|
||||
const schema = await new Parse.Schema('Test').get();
|
||||
expect(schema.className).toEqual('Test');
|
||||
|
||||
const schemas = await Parse.Schema.all();
|
||||
// Role could be flaky since all system classes are not ensured
|
||||
// at start up by the DefinedSchema system
|
||||
expect(schemas.filter(({ className }) => className !== '_Role').length).toEqual(3);
|
||||
|
||||
await expectAsync(new Parse.Schema('TheNewTest').save()).toBeRejectedWithError(
|
||||
'Cannot perform this operation when schemas options is used.'
|
||||
);
|
||||
|
||||
await expectAsync(new Parse.Schema('_User').update()).toBeRejectedWithError(
|
||||
'Cannot perform this operation when schemas options is used.'
|
||||
);
|
||||
});
|
||||
it('should only enable delete class endpoint since', async () => {
|
||||
await reconfigureServer({
|
||||
schema: { definitions: [{ className: '_User' }, { className: 'Test' }] },
|
||||
});
|
||||
await reconfigureServer({ schema: { definitions: [{ className: '_User' }] } });
|
||||
|
||||
let schemas = await Parse.Schema.all();
|
||||
expect(schemas.length).toEqual(4);
|
||||
|
||||
await new Parse.Schema('_User').delete();
|
||||
schemas = await Parse.Schema.all();
|
||||
expect(schemas.length).toEqual(3);
|
||||
});
|
||||
it('should run beforeMigration before execution of DefinedSchemas', async () => {
|
||||
const config = {
|
||||
schema: {
|
||||
definitions: [{ className: '_User' }, { className: 'Test' }],
|
||||
beforeMigration: async () => {},
|
||||
},
|
||||
};
|
||||
const spy = spyOn(config.schema, 'beforeMigration');
|
||||
await reconfigureServer(config);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should run afterMigration after execution of DefinedSchemas', async () => {
|
||||
const config = {
|
||||
schema: {
|
||||
definitions: [{ className: '_User' }, { className: 'Test' }],
|
||||
afterMigration: async () => {},
|
||||
},
|
||||
};
|
||||
const spy = spyOn(config.schema, 'afterMigration');
|
||||
await reconfigureServer(config);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should use logger in case of error', async () => {
|
||||
const server = await reconfigureServer({ schema: { definitions: [{ className: '_User' }] } });
|
||||
const error = new Error('A test error');
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(DefinedSchemas.prototype, 'wait').and.resolveTo();
|
||||
spyOn(logger, 'error').and.callThrough();
|
||||
spyOn(Parse.Schema, 'all').and.callFake(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
await new DefinedSchemas(
|
||||
{ definitions: [{ className: 'Test', fields: { aField: { type: 'String' } } }] },
|
||||
server.config
|
||||
).execute();
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to run migrations: ${error.toString()}`);
|
||||
});
|
||||
it('should perform migration in parallel without failing', async () => {
|
||||
const server = await reconfigureServer();
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callThrough();
|
||||
const migrationOptions = {
|
||||
definitions: [
|
||||
{
|
||||
className: 'Test',
|
||||
fields: { aField: { type: 'String' } },
|
||||
indexes: { aField: { aField: 1 } },
|
||||
classLevelPermissions: {
|
||||
create: { requiresAuthentication: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Simulate parallel deployment
|
||||
await Promise.all([
|
||||
new DefinedSchemas(migrationOptions, server.config).execute(),
|
||||
new DefinedSchemas(migrationOptions, server.config).execute(),
|
||||
new DefinedSchemas(migrationOptions, server.config).execute(),
|
||||
new DefinedSchemas(migrationOptions, server.config).execute(),
|
||||
new DefinedSchemas(migrationOptions, server.config).execute(),
|
||||
]);
|
||||
|
||||
const testSchema = (await Parse.Schema.all()).find(
|
||||
({ className }) => className === migrationOptions.definitions[0].className
|
||||
);
|
||||
|
||||
expect(testSchema.indexes.aField).toEqual({ aField: 1 });
|
||||
expect(testSchema.fields.aField).toEqual({ type: 'String' });
|
||||
expect(testSchema.classLevelPermissions.create).toEqual({ requiresAuthentication: true });
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
const Deprecator = require('../lib/Deprecator/Deprecator');
|
||||
|
||||
describe('Deprecator', () => {
|
||||
let deprecations = [];
|
||||
|
||||
beforeEach(async () => {
|
||||
deprecations = [{ optionKey: 'exampleKey', changeNewDefault: 'exampleNewDefault' }];
|
||||
});
|
||||
|
||||
it('deprecations are an array', async () => {
|
||||
expect(Deprecator._getDeprecations()).toBeInstanceOf(Array);
|
||||
});
|
||||
|
||||
it('logs deprecation for new default', async () => {
|
||||
deprecations = [{ optionKey: 'exampleKey', changeNewDefault: 'exampleNewDefault' }];
|
||||
|
||||
spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations);
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
|
||||
await reconfigureServer();
|
||||
expect(logSpy.calls.all()[0].args[0]).toEqual(
|
||||
`DeprecationWarning: The Parse Server option '${deprecations[0].optionKey}' default will change to '${deprecations[0].changeNewDefault}' in a future version.`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not log deprecation for new default if option is set manually', async () => {
|
||||
deprecations = [{ optionKey: 'exampleKey', changeNewDefault: 'exampleNewDefault' }];
|
||||
|
||||
spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations);
|
||||
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});
|
||||
await reconfigureServer({ [deprecations[0].optionKey]: 'manuallySet' });
|
||||
expect(logSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs runtime deprecation', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
|
||||
const options = { usage: 'Doing this', solution: 'Do that instead.' };
|
||||
|
||||
Deprecator.logRuntimeDeprecation(options);
|
||||
expect(logSpy.calls.all()[0].args[0]).toEqual(
|
||||
`DeprecationWarning: ${options.usage} is deprecated and will be removed in a future version. ${options.solution}`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ const request = require('../lib/request');
|
||||
|
||||
describe('Enable express error handler', () => {
|
||||
it('should call the default handler in case of error, like updating a non existing object', async done => {
|
||||
spyOn(console, 'error');
|
||||
const parseServer = await reconfigureServer(
|
||||
Object.assign({}, defaultConfiguration, {
|
||||
enableExpressErrorHandler: true,
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
const auth = require('../lib/Auth');
|
||||
const Config = require('../lib/Config');
|
||||
const rest = require('../lib/rest');
|
||||
|
||||
describe('Enable single schema cache', () => {
|
||||
beforeEach(done => {
|
||||
reconfigureServer({
|
||||
enableSingleSchemaCache: true,
|
||||
schemaCacheTTL: 30000,
|
||||
}).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can perform multiple create and query operations', done => {
|
||||
let config = fakeRequestForConfig();
|
||||
let nobody = auth.nobody(config);
|
||||
rest
|
||||
.create(config, nobody, 'Foo', { type: 1 })
|
||||
.then(() => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
return rest.create(config, nobody, 'Foo', { type: 2 });
|
||||
})
|
||||
.then(() => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
return rest.create(config, nobody, 'Bar');
|
||||
})
|
||||
.then(() => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
return rest.find(config, nobody, 'Bar', { type: 1 });
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
fail('Should throw error');
|
||||
done();
|
||||
},
|
||||
error => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
expect(error).toBeDefined();
|
||||
return rest.find(config, nobody, 'Foo', { type: 1 });
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
config = fakeRequestForConfig();
|
||||
nobody = auth.nobody(config);
|
||||
expect(response.results.length).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const fakeRequestForConfig = function () {
|
||||
return Config.get('test');
|
||||
};
|
||||
@@ -3,7 +3,6 @@ const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapte
|
||||
.WinstonLoggerAdapter;
|
||||
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
|
||||
.GridFSBucketAdapter;
|
||||
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
|
||||
const Config = require('../lib/Config');
|
||||
const FilesController = require('../lib/Controllers/FilesController').default;
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
@@ -24,8 +23,8 @@ const mockAdapter = {
|
||||
describe('FilesController', () => {
|
||||
it('should properly expand objects', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
const filesController = new FilesController(gridStoreAdapter);
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
const result = filesController.expandFilesInObject(config, function () {});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
@@ -80,24 +79,27 @@ describe('FilesController', () => {
|
||||
expect(typeof error).toBe('object');
|
||||
expect(error.message.indexOf('biscuit')).toBe(13);
|
||||
expect(error.code).toBe(Parse.Error.INVALID_FILE_NAME);
|
||||
mockAdapter.validateFilename = () => {
|
||||
return null;
|
||||
};
|
||||
done();
|
||||
});
|
||||
|
||||
it('should add a unique hash to the file name when the preserveFileName option is false', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridStoreAdapter, 'createFile');
|
||||
gridStoreAdapter.createFile.and.returnValue(Promise.resolve());
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridFSAdapter, 'createFile');
|
||||
gridFSAdapter.createFile.and.returnValue(Promise.resolve());
|
||||
const fileName = 'randomFileName.pdf';
|
||||
const regexEscapedFileName = fileName.replace(/\./g, '\\$&');
|
||||
const filesController = new FilesController(gridStoreAdapter, null, {
|
||||
const filesController = new FilesController(gridFSAdapter, null, {
|
||||
preserveFileName: false,
|
||||
});
|
||||
|
||||
filesController.createFile(config, fileName);
|
||||
|
||||
expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toMatch(
|
||||
expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toMatch(
|
||||
`^.{32}_${regexEscapedFileName}$`
|
||||
);
|
||||
|
||||
@@ -106,42 +108,42 @@ describe('FilesController', () => {
|
||||
|
||||
it('should not add a unique hash to the file name when the preserveFileName option is true', done => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridStoreAdapter, 'createFile');
|
||||
gridStoreAdapter.createFile.and.returnValue(Promise.resolve());
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
spyOn(gridFSAdapter, 'createFile');
|
||||
gridFSAdapter.createFile.and.returnValue(Promise.resolve());
|
||||
const fileName = 'randomFileName.pdf';
|
||||
const filesController = new FilesController(gridStoreAdapter, null, {
|
||||
const filesController = new FilesController(gridFSAdapter, null, {
|
||||
preserveFileName: true,
|
||||
});
|
||||
|
||||
filesController.createFile(config, fileName);
|
||||
|
||||
expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName);
|
||||
expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1);
|
||||
expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should handle adapter without getMetadata', async () => {
|
||||
const gridStoreAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
gridStoreAdapter.getMetadata = null;
|
||||
const filesController = new FilesController(gridStoreAdapter);
|
||||
const gridFSAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
gridFSAdapter.getMetadata = null;
|
||||
const filesController = new FilesController(gridFSAdapter);
|
||||
|
||||
const result = await filesController.getMetadata();
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should reject slashes in file names', done => {
|
||||
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
const fileName = 'foo/randomFileName.pdf';
|
||||
expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null);
|
||||
expect(gridFSAdapter.validateFilename(fileName)).not.toBe(null);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should also reject slashes in file names', done => {
|
||||
const gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse');
|
||||
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
|
||||
const fileName = 'foo/randomFileName.pdf';
|
||||
expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null);
|
||||
expect(gridFSAdapter.validateFilename(fileName)).not.toBe(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
|
||||
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
|
||||
.GridFSBucketAdapter;
|
||||
const { randomString } = require('../lib/cryptoUtils');
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
const request = require('../lib/request');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
async function expectMissingFile(gfsAdapter, name) {
|
||||
try {
|
||||
@@ -15,25 +13,13 @@ async function expectMissingFile(gfsAdapter, name) {
|
||||
}
|
||||
}
|
||||
|
||||
describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
|
||||
describe_only_db('mongo')('GridFSBucket', () => {
|
||||
beforeEach(async () => {
|
||||
const gsAdapter = new GridStoreAdapter(databaseURI);
|
||||
const gsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
const db = await gsAdapter._connect();
|
||||
await db.dropDatabase();
|
||||
});
|
||||
|
||||
it('a file created in GridStore should be available in GridFS', async () => {
|
||||
const gsAdapter = new GridStoreAdapter(databaseURI);
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
await expectMissingFile(gfsAdapter, 'myFileName');
|
||||
const originalString = 'abcdefghi';
|
||||
await gsAdapter.createFile('myFileName', originalString);
|
||||
const gsResult = await gsAdapter.getFileData('myFileName');
|
||||
expect(gsResult.toString('utf8')).toBe(originalString);
|
||||
const gfsResult = await gfsAdapter.getFileData('myFileName');
|
||||
expect(gfsResult.toString('utf8')).toBe(originalString);
|
||||
});
|
||||
|
||||
it('should save an encrypted file that can only be decrypted by a GridFS adapter with the encryptionKey', async () => {
|
||||
const unencryptedAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
const encryptedAdapter = new GridFSBucketAdapter(
|
||||
@@ -395,8 +381,9 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
|
||||
});
|
||||
|
||||
it('should handle getMetadata error', async () => {
|
||||
const config = Config.get('test');
|
||||
config.filesController.getMetadata = () => Promise.reject();
|
||||
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
|
||||
await reconfigureServer({ filesAdapter: gfsAdapter });
|
||||
gfsAdapter.getMetadata = () => Promise.reject();
|
||||
|
||||
const headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
@@ -451,7 +438,7 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
|
||||
await db.admin().serverStatus();
|
||||
expect(false).toBe(true);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('topology was destroyed');
|
||||
expect(e.message).toEqual('MongoClient must be connected to perform this operation');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
const MongoClient = require('mongodb').MongoClient;
|
||||
const GridStore = require('mongodb').GridStore;
|
||||
|
||||
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
|
||||
const Config = require('../lib/Config');
|
||||
const FilesController = require('../lib/Controllers/FilesController').default;
|
||||
|
||||
// Small additional tests to improve overall coverage
|
||||
describe_only_db('mongo')('GridStoreAdapter', () => {
|
||||
it('should properly instanciate the GridStore when deleting a file', async done => {
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const gridStoreAdapter = new GridStoreAdapter(databaseURI);
|
||||
const db = await gridStoreAdapter._connect();
|
||||
await db.dropDatabase();
|
||||
const filesController = new FilesController(gridStoreAdapter, Parse.applicationId, {});
|
||||
|
||||
// save original unlink before redefinition
|
||||
const originalUnlink = GridStore.prototype.unlink;
|
||||
|
||||
let gridStoreMode;
|
||||
|
||||
// new unlink method that will capture the mode in which GridStore was opened
|
||||
GridStore.prototype.unlink = function () {
|
||||
// restore original unlink during first call
|
||||
GridStore.prototype.unlink = originalUnlink;
|
||||
|
||||
gridStoreMode = this.mode;
|
||||
|
||||
return originalUnlink.call(this);
|
||||
};
|
||||
|
||||
filesController
|
||||
.createFile(config, 'myFilename.txt', 'my file content', 'text/plain')
|
||||
.then(myFile => {
|
||||
return MongoClient.connect(databaseURI)
|
||||
.then(client => {
|
||||
const database = client.db(client.s.options.dbName);
|
||||
// Verify the existance of the fs.files document
|
||||
return database
|
||||
.collection('fs.files')
|
||||
.count()
|
||||
.then(count => {
|
||||
expect(count).toEqual(1);
|
||||
return { database, client };
|
||||
});
|
||||
})
|
||||
.then(({ database, client }) => {
|
||||
// Verify the existance of the fs.files document
|
||||
return database
|
||||
.collection('fs.chunks')
|
||||
.count()
|
||||
.then(count => {
|
||||
expect(count).toEqual(1);
|
||||
return client.close();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return filesController.deleteFile(config, myFile.name);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return MongoClient.connect(databaseURI)
|
||||
.then(client => {
|
||||
const database = client.db(client.s.options.dbName);
|
||||
// Verify the existance of the fs.files document
|
||||
return database
|
||||
.collection('fs.files')
|
||||
.count()
|
||||
.then(count => {
|
||||
expect(count).toEqual(0);
|
||||
return { database, client };
|
||||
});
|
||||
})
|
||||
.then(({ database, client }) => {
|
||||
// Verify the existance of the fs.files document
|
||||
return database
|
||||
.collection('fs.chunks')
|
||||
.count()
|
||||
.then(count => {
|
||||
expect(count).toEqual(0);
|
||||
return client.close();
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// Verify that gridStore was opened in read only mode
|
||||
expect(gridStoreMode).toEqual('r');
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(fail);
|
||||
});
|
||||
|
||||
it('handleShutdown, close connection', async () => {
|
||||
const databaseURI = 'mongodb://localhost:27017/parse';
|
||||
const gridStoreAdapter = new GridStoreAdapter(databaseURI);
|
||||
|
||||
const db = await gridStoreAdapter._connect();
|
||||
const status = await db.admin().serverStatus();
|
||||
expect(status.connections.current > 0).toEqual(true);
|
||||
|
||||
await gridStoreAdapter.handleShutdown();
|
||||
try {
|
||||
await db.admin().serverStatus();
|
||||
expect(false).toBe(true);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('topology was destroyed');
|
||||
}
|
||||
});
|
||||
});
|
||||
+91
-113
@@ -6,7 +6,7 @@ const httpRequest = require('../lib/cloud-code/httpRequest'),
|
||||
express = require('express');
|
||||
|
||||
const port = 13371;
|
||||
const httpRequestServer = 'http://localhost:' + port;
|
||||
const httpRequestServer = `http://localhost:${port}`;
|
||||
|
||||
function startServer(done) {
|
||||
const app = express();
|
||||
@@ -39,175 +39,148 @@ function startServer(done) {
|
||||
|
||||
describe('httpRequest', () => {
|
||||
let server;
|
||||
beforeAll(done => {
|
||||
server = startServer(done);
|
||||
beforeEach(done => {
|
||||
if (!server) {
|
||||
server = startServer(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(done => {
|
||||
server.close(done);
|
||||
});
|
||||
|
||||
it('should do /hello', done => {
|
||||
httpRequest({
|
||||
url: httpRequestServer + '/hello',
|
||||
}).then(function (httpResponse) {
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
|
||||
expect(httpResponse.text).toEqual('{"response":"OK"}');
|
||||
expect(httpResponse.data.response).toEqual('OK');
|
||||
done();
|
||||
}, done.fail);
|
||||
it('should do /hello', async () => {
|
||||
const httpResponse = await httpRequest({
|
||||
url: `${httpRequestServer}/hello`,
|
||||
});
|
||||
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.buffer).toEqual(Buffer.from('{"response":"OK"}'));
|
||||
expect(httpResponse.text).toEqual('{"response":"OK"}');
|
||||
expect(httpResponse.data.response).toEqual('OK');
|
||||
});
|
||||
|
||||
it('should do not follow redirects by default', done => {
|
||||
httpRequest({
|
||||
url: httpRequestServer + '/301',
|
||||
}).then(function (httpResponse) {
|
||||
expect(httpResponse.status).toBe(301);
|
||||
done();
|
||||
}, done.fail);
|
||||
it('should do not follow redirects by default', async () => {
|
||||
const httpResponse = await httpRequest({
|
||||
url: `${httpRequestServer}/301`,
|
||||
});
|
||||
|
||||
expect(httpResponse.status).toBe(301);
|
||||
});
|
||||
|
||||
it('should follow redirects when set', done => {
|
||||
httpRequest({
|
||||
url: httpRequestServer + '/301',
|
||||
it('should follow redirects when set', async () => {
|
||||
const httpResponse = await httpRequest({
|
||||
url: `${httpRequestServer}/301`,
|
||||
followRedirects: true,
|
||||
}).then(function (httpResponse) {
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
|
||||
expect(httpResponse.text).toEqual('{"response":"OK"}');
|
||||
expect(httpResponse.data.response).toEqual('OK');
|
||||
done();
|
||||
}, done.fail);
|
||||
});
|
||||
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.buffer).toEqual(Buffer.from('{"response":"OK"}'));
|
||||
expect(httpResponse.text).toEqual('{"response":"OK"}');
|
||||
expect(httpResponse.data.response).toEqual('OK');
|
||||
});
|
||||
|
||||
it('should fail on 404', done => {
|
||||
let calls = 0;
|
||||
httpRequest({
|
||||
url: httpRequestServer + '/404',
|
||||
}).then(
|
||||
function () {
|
||||
calls++;
|
||||
fail('should not succeed');
|
||||
done();
|
||||
},
|
||||
function (httpResponse) {
|
||||
calls++;
|
||||
expect(calls).toBe(1);
|
||||
expect(httpResponse.status).toBe(404);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('NO'));
|
||||
expect(httpResponse.text).toEqual('NO');
|
||||
expect(httpResponse.data).toBe(undefined);
|
||||
done();
|
||||
}
|
||||
it('should fail on 404', async () => {
|
||||
await expectAsync(
|
||||
httpRequest({
|
||||
url: `${httpRequestServer}/404`,
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
status: 404,
|
||||
buffer: Buffer.from('NO'),
|
||||
text: 'NO',
|
||||
data: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should post on echo', done => {
|
||||
httpRequest({
|
||||
it('should post on echo', async () => {
|
||||
const httpResponse = await httpRequest({
|
||||
method: 'POST',
|
||||
url: httpRequestServer + '/echo',
|
||||
url: `${httpRequestServer}/echo`,
|
||||
body: {
|
||||
foo: 'bar',
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(
|
||||
function (httpResponse) {
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({ foo: 'bar' });
|
||||
done();
|
||||
},
|
||||
function () {
|
||||
fail('should not fail');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('should encode a query string body by default', done => {
|
||||
it('should encode a query string body by default', () => {
|
||||
const options = {
|
||||
body: { foo: 'bar' },
|
||||
};
|
||||
const result = httpRequest.encodeBody(options);
|
||||
|
||||
expect(result.body).toEqual('foo=bar');
|
||||
expect(result.headers['Content-Type']).toEqual('application/x-www-form-urlencoded');
|
||||
done();
|
||||
});
|
||||
|
||||
it('should encode a JSON body', done => {
|
||||
it('should encode a JSON body', () => {
|
||||
const options = {
|
||||
body: { foo: 'bar' },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
const result = httpRequest.encodeBody(options);
|
||||
|
||||
expect(result.body).toEqual('{"foo":"bar"}');
|
||||
done();
|
||||
});
|
||||
it('should encode a www-form body', done => {
|
||||
|
||||
it('should encode a www-form body', () => {
|
||||
const options = {
|
||||
body: { foo: 'bar', bar: 'baz' },
|
||||
headers: { 'cOntent-tYpe': 'application/x-www-form-urlencoded' },
|
||||
};
|
||||
const result = httpRequest.encodeBody(options);
|
||||
|
||||
expect(result.body).toEqual('foo=bar&bar=baz');
|
||||
done();
|
||||
});
|
||||
it('should not encode a wrong content type', done => {
|
||||
|
||||
it('should not encode a wrong content type', () => {
|
||||
const options = {
|
||||
body: { foo: 'bar', bar: 'baz' },
|
||||
headers: { 'cOntent-tYpe': 'mime/jpeg' },
|
||||
};
|
||||
const result = httpRequest.encodeBody(options);
|
||||
|
||||
expect(result.body).toEqual({ foo: 'bar', bar: 'baz' });
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail gracefully', done => {
|
||||
httpRequest({
|
||||
url: 'http://not a good url',
|
||||
}).then(done.fail, function (error) {
|
||||
expect(error).not.toBeUndefined();
|
||||
expect(error).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
it('should fail gracefully', async () => {
|
||||
await expectAsync(
|
||||
httpRequest({
|
||||
url: 'http://not a good url',
|
||||
})
|
||||
).toBeRejected();
|
||||
});
|
||||
|
||||
it('should params object to query string', done => {
|
||||
httpRequest({
|
||||
url: httpRequestServer + '/qs',
|
||||
it('should params object to query string', async () => {
|
||||
const httpResponse = await httpRequest({
|
||||
url: `${httpRequestServer}/qs`,
|
||||
params: {
|
||||
foo: 'bar',
|
||||
},
|
||||
}).then(
|
||||
function (httpResponse) {
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({ foo: 'bar' });
|
||||
done();
|
||||
},
|
||||
function () {
|
||||
fail('should not fail');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('should params string to query string', done => {
|
||||
httpRequest({
|
||||
url: httpRequestServer + '/qs',
|
||||
it('should params string to query string', async () => {
|
||||
const httpResponse = await httpRequest({
|
||||
url: `${httpRequestServer}/qs`,
|
||||
params: 'foo=bar&foo2=bar2',
|
||||
}).then(
|
||||
function (httpResponse) {
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({ foo: 'bar', foo2: 'bar2' });
|
||||
done();
|
||||
},
|
||||
function () {
|
||||
fail('should not fail');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.data).toEqual({ foo: 'bar', foo2: 'bar2' });
|
||||
});
|
||||
|
||||
it('should not crash with undefined body', () => {
|
||||
@@ -226,6 +199,7 @@ describe('httpRequest', () => {
|
||||
|
||||
const serialized = JSON.stringify(httpResponse);
|
||||
const result = JSON.parse(serialized);
|
||||
|
||||
expect(result.text).toBe('hello');
|
||||
expect(result.data).toBe(undefined);
|
||||
expect(result.body).toBe(undefined);
|
||||
@@ -247,43 +221,47 @@ describe('httpRequest', () => {
|
||||
});
|
||||
|
||||
it('serialized httpResponse correctly with body buffer string', () => {
|
||||
const httpResponse = new HTTPResponse({}, new Buffer('hello'));
|
||||
const httpResponse = new HTTPResponse({}, Buffer.from('hello'));
|
||||
expect(httpResponse.text).toBe('hello');
|
||||
expect(httpResponse.data).toBe(undefined);
|
||||
|
||||
const serialized = JSON.stringify(httpResponse);
|
||||
const result = JSON.parse(serialized);
|
||||
|
||||
expect(result.text).toBe('hello');
|
||||
expect(result.data).toBe(undefined);
|
||||
});
|
||||
|
||||
it('serialized httpResponse correctly with body buffer JSON Object', () => {
|
||||
const json = '{"foo":"bar"}';
|
||||
const httpResponse = new HTTPResponse({}, new Buffer(json));
|
||||
const httpResponse = new HTTPResponse({}, Buffer.from(json));
|
||||
const serialized = JSON.stringify(httpResponse);
|
||||
const result = JSON.parse(serialized);
|
||||
|
||||
expect(result.text).toEqual('{"foo":"bar"}');
|
||||
expect(result.data).toEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('serialized httpResponse with Parse._encode should be allright', () => {
|
||||
const json = '{"foo":"bar"}';
|
||||
const httpResponse = new HTTPResponse({}, new Buffer(json));
|
||||
const httpResponse = new HTTPResponse({}, Buffer.from(json));
|
||||
const encoded = Parse._encode(httpResponse);
|
||||
let foundData,
|
||||
foundText,
|
||||
foundBody = false;
|
||||
|
||||
for (const key in encoded) {
|
||||
if (key == 'data') {
|
||||
if (key === 'data') {
|
||||
foundData = true;
|
||||
}
|
||||
if (key == 'text') {
|
||||
if (key === 'text') {
|
||||
foundText = true;
|
||||
}
|
||||
if (key == 'body') {
|
||||
if (key === 'body') {
|
||||
foundBody = true;
|
||||
}
|
||||
}
|
||||
|
||||
expect(foundData).toBe(true);
|
||||
expect(foundText).toBe(true);
|
||||
expect(foundBody).toBe(false);
|
||||
|
||||
@@ -6,11 +6,14 @@ const rest = require('../lib/rest');
|
||||
const auth = require('../lib/Auth');
|
||||
const uuid = require('uuid');
|
||||
|
||||
describe_only_db('mongo')('Idempotency', () => {
|
||||
describe('Idempotency', () => {
|
||||
// Parameters
|
||||
/** Enable TTL expiration simulated by removing entry instead of waiting for MongoDB TTL monitor which
|
||||
runs only every 60s, so it can take up to 119s until entry removal - ain't nobody got time for that */
|
||||
const SIMULATE_TTL = true;
|
||||
const ttl = 2;
|
||||
const maxTimeOut = 4000;
|
||||
|
||||
// Helpers
|
||||
async function deleteRequestEntry(reqId) {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
@@ -38,9 +41,10 @@ describe_only_db('mongo')('Idempotency', () => {
|
||||
}
|
||||
await setup({
|
||||
paths: ['functions/.*', 'jobs/.*', 'classes/.*', 'users', 'installations'],
|
||||
ttl: 30,
|
||||
ttl: ttl,
|
||||
});
|
||||
});
|
||||
|
||||
// Tests
|
||||
it('should enforce idempotency for cloud code function', async () => {
|
||||
let counter = 0;
|
||||
@@ -56,7 +60,7 @@ describe_only_db('mongo')('Idempotency', () => {
|
||||
'X-Parse-Request-Id': 'abc-123',
|
||||
},
|
||||
};
|
||||
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(30);
|
||||
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(ttl);
|
||||
await request(params);
|
||||
await request(params).then(fail, e => {
|
||||
expect(e.status).toEqual(400);
|
||||
@@ -83,12 +87,38 @@ describe_only_db('mongo')('Idempotency', () => {
|
||||
if (SIMULATE_TTL) {
|
||||
await deleteRequestEntry('abc-123');
|
||||
} else {
|
||||
await new Promise(resolve => setTimeout(resolve, 130000));
|
||||
await new Promise(resolve => setTimeout(resolve, maxTimeOut));
|
||||
}
|
||||
await expectAsync(request(params)).toBeResolved();
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
|
||||
it_only_db('postgres')(
|
||||
'should delete request entry when postgress ttl function is called',
|
||||
async () => {
|
||||
const client = Config.get(Parse.applicationId).database.adapter._client;
|
||||
let counter = 0;
|
||||
Parse.Cloud.define('myFunction', () => {
|
||||
counter++;
|
||||
});
|
||||
const params = {
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/functions/myFunction',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Request-Id': 'abc-123',
|
||||
},
|
||||
};
|
||||
await expectAsync(request(params)).toBeResolved();
|
||||
await expectAsync(request(params)).toBeRejected();
|
||||
await new Promise(resolve => setTimeout(resolve, maxTimeOut));
|
||||
await client.one('SELECT idempotency_delete_expired_records()');
|
||||
await expectAsync(request(params)).toBeResolved();
|
||||
expect(counter).toBe(2);
|
||||
}
|
||||
);
|
||||
|
||||
it('should enforce idempotency for cloud code jobs', async () => {
|
||||
let counter = 0;
|
||||
Parse.Cloud.job('myJob', () => {
|
||||
|
||||
+99
-146
@@ -1,56 +1,49 @@
|
||||
const ldap = require('../lib/Adapters/Auth/ldap');
|
||||
const mockLdapServer = require('./MockLdapServer');
|
||||
const mockLdapServer = require('./support/MockLdapServer');
|
||||
const fs = require('fs');
|
||||
const port = 12345;
|
||||
const sslport = 12346;
|
||||
|
||||
it('Should fail with missing options', done => {
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'testpw' })
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP auth configuration missing');
|
||||
done();
|
||||
});
|
||||
});
|
||||
describe('Ldap Auth', () => {
|
||||
it('Should fail with missing options', done => {
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'testpw' })
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP auth configuration missing');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should return a resolved promise when validating the app id', done => {
|
||||
ldap.validateAppId().then(done).catch(done.fail);
|
||||
});
|
||||
it('Should return a resolved promise when validating the app id', done => {
|
||||
ldap.validateAppId().then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it('Should succeed with right credentials', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example').then(server => {
|
||||
it('Should succeed with right credentials', async done => {
|
||||
const server = await mockLdapServer(port, 'uid=testuser, o=example');
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
};
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done)
|
||||
.catch(done.fail)
|
||||
.finally(() => server.close());
|
||||
await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should succeed with right credentials when LDAPS is used and certifcate is not checked', done => {
|
||||
mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => {
|
||||
it('Should succeed with right credentials when LDAPS is used and certifcate is not checked', async done => {
|
||||
const server = await mockLdapServer(sslport, 'uid=testuser, o=example', false, true);
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldaps://localhost:${sslport}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
tlsOptions: { rejectUnauthorized: false },
|
||||
};
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done)
|
||||
.catch(done.fail)
|
||||
.finally(() => server.close());
|
||||
await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should succeed when LDAPS is used and the presented certificate is the expected certificate', done => {
|
||||
mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => {
|
||||
it('Should succeed when LDAPS is used and the presented certificate is the expected certificate', async done => {
|
||||
const server = await mockLdapServer(sslport, 'uid=testuser, o=example', false, true);
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldaps://localhost:${sslport}`,
|
||||
@@ -60,16 +53,12 @@ it('Should succeed when LDAPS is used and the presented certificate is the expec
|
||||
rejectUnauthorized: true,
|
||||
},
|
||||
};
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done)
|
||||
.catch(done.fail)
|
||||
.finally(() => server.close());
|
||||
await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail when LDAPS is used and the presented certificate is not the expected certificate', done => {
|
||||
mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => {
|
||||
it('Should fail when LDAPS is used and the presented certificate is not the expected certificate', async done => {
|
||||
const server = await mockLdapServer(sslport, 'uid=testuser, o=example', false, true);
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldaps://localhost:${sslport}`,
|
||||
@@ -79,19 +68,17 @@ it('Should fail when LDAPS is used and the presented certificate is not the expe
|
||||
rejectUnauthorized: true,
|
||||
},
|
||||
};
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAPS: Certificate mismatch');
|
||||
done();
|
||||
})
|
||||
.finally(() => server.close());
|
||||
try {
|
||||
await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
|
||||
fail();
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('LDAPS: Certificate mismatch');
|
||||
}
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail when LDAPS is used certifcate matches but credentials are wrong', done => {
|
||||
mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => {
|
||||
it('Should fail when LDAPS is used certifcate matches but credentials are wrong', async done => {
|
||||
const server = await mockLdapServer(sslport, 'uid=testuser, o=example', false, true);
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldaps://localhost:${sslport}`,
|
||||
@@ -101,37 +88,33 @@ it('Should fail when LDAPS is used certifcate matches but credentials are wrong'
|
||||
rejectUnauthorized: true,
|
||||
},
|
||||
};
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'wrong!' }, options)
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP: Wrong username or password');
|
||||
done();
|
||||
})
|
||||
.finally(() => server.close());
|
||||
try {
|
||||
await ldap.validateAuthData({ id: 'testuser', password: 'wrong!' }, options);
|
||||
fail();
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('LDAP: Wrong username or password');
|
||||
}
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail with wrong credentials', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example').then(server => {
|
||||
it('Should fail with wrong credentials', async done => {
|
||||
const server = await mockLdapServer(port, 'uid=testuser, o=example');
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
};
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'wrong!' }, options)
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP: Wrong username or password');
|
||||
done();
|
||||
})
|
||||
.finally(() => server.close());
|
||||
try {
|
||||
await ldap.validateAuthData({ id: 'testuser', password: 'wrong!' }, options);
|
||||
fail();
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('LDAP: Wrong username or password');
|
||||
}
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should succeed if user is in given group', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example').then(server => {
|
||||
it('Should succeed if user is in given group', async done => {
|
||||
const server = await mockLdapServer(port, 'uid=testuser, o=example');
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
@@ -139,17 +122,12 @@ it('Should succeed if user is in given group', done => {
|
||||
groupCn: 'powerusers',
|
||||
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
|
||||
};
|
||||
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done)
|
||||
.catch(done.fail)
|
||||
.finally(() => server.close());
|
||||
await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail if user is not in given group', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example').then(server => {
|
||||
it('Should fail if user is not in given group', async done => {
|
||||
const server = await mockLdapServer(port, 'uid=testuser, o=example');
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
@@ -157,20 +135,17 @@ it('Should fail if user is not in given group', done => {
|
||||
groupCn: 'groupTheUserIsNotIn',
|
||||
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
|
||||
};
|
||||
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP: User not in group');
|
||||
done();
|
||||
})
|
||||
.finally(() => server.close());
|
||||
try {
|
||||
await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
|
||||
fail();
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('LDAP: User not in group');
|
||||
}
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail if the LDAP server does not allow searching inside the provided suffix', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example').then(server => {
|
||||
it('Should fail if the LDAP server does not allow searching inside the provided suffix', async done => {
|
||||
const server = await mockLdapServer(port, 'uid=testuser, o=example');
|
||||
const options = {
|
||||
suffix: 'o=invalid',
|
||||
url: `ldap://localhost:${port}`,
|
||||
@@ -178,20 +153,17 @@ it('Should fail if the LDAP server does not allow searching inside the provided
|
||||
groupCn: 'powerusers',
|
||||
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
|
||||
};
|
||||
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP group search failed');
|
||||
done();
|
||||
})
|
||||
.finally(() => server.close());
|
||||
try {
|
||||
await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
|
||||
fail();
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('LDAP group search failed');
|
||||
}
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail if the LDAP server encounters an error while searching', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example', true).then(server => {
|
||||
it('Should fail if the LDAP server encounters an error while searching', async done => {
|
||||
const server = await mockLdapServer(port, 'uid=testuser, o=example', true);
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
@@ -199,61 +171,42 @@ it('Should fail if the LDAP server encounters an error while searching', done =>
|
||||
groupCn: 'powerusers',
|
||||
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
|
||||
};
|
||||
|
||||
ldap
|
||||
.validateAuthData({ id: 'testuser', password: 'secret' }, options)
|
||||
.then(done.fail)
|
||||
.catch(err => {
|
||||
jequal(err.message, 'LDAP group search failed');
|
||||
done();
|
||||
})
|
||||
.finally(() => server.close());
|
||||
try {
|
||||
await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
|
||||
fail();
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('LDAP group search failed');
|
||||
}
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should delete the password from authData after validation', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example', true).then(server => {
|
||||
it('Should delete the password from authData after validation', async done => {
|
||||
const server = await mockLdapServer(port, 'uid=testuser, o=example', true);
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
};
|
||||
|
||||
const authData = { id: 'testuser', password: 'secret' };
|
||||
|
||||
ldap
|
||||
.validateAuthData(authData, options)
|
||||
.then(() => {
|
||||
expect(authData).toEqual({ id: 'testuser' });
|
||||
done();
|
||||
})
|
||||
.catch(done.fail)
|
||||
.finally(() => server.close());
|
||||
await ldap.validateAuthData(authData, options);
|
||||
expect(authData).toEqual({ id: 'testuser' });
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not save the password in the user record after authentication', done => {
|
||||
mockLdapServer(port, 'uid=testuser, o=example', true).then(server => {
|
||||
it('Should not save the password in the user record after authentication', async done => {
|
||||
const server = await mockLdapServer(port, 'uid=testuser, o=example', true);
|
||||
const options = {
|
||||
suffix: 'o=example',
|
||||
url: `ldap://localhost:${port}`,
|
||||
dn: 'uid={{id}}, o=example',
|
||||
};
|
||||
reconfigureServer({ auth: { ldap: options } }).then(() => {
|
||||
const authData = { authData: { id: 'testuser', password: 'secret' } };
|
||||
Parse.User.logInWith('ldap', authData).then(returnedUser => {
|
||||
const query = new Parse.Query('User');
|
||||
query
|
||||
.equalTo('objectId', returnedUser.id)
|
||||
.first({ useMasterKey: true })
|
||||
.then(user => {
|
||||
expect(user.get('authData')).toEqual({ ldap: { id: 'testuser' } });
|
||||
expect(user.get('authData').ldap.password).toBeUndefined();
|
||||
done();
|
||||
})
|
||||
.catch(done.fail)
|
||||
.finally(() => server.close());
|
||||
});
|
||||
});
|
||||
await reconfigureServer({ auth: { ldap: options } });
|
||||
const authData = { authData: { id: 'testuser', password: 'secret' } };
|
||||
const returnedUser = await Parse.User.logInWith('ldap', authData);
|
||||
const query = new Parse.Query('User');
|
||||
const user = await query.equalTo('objectId', returnedUser.id).first({ useMasterKey: true });
|
||||
expect(user.get('authData')).toEqual({ ldap: { id: 'testuser' } });
|
||||
expect(user.get('authData').ldap.password).toBeUndefined();
|
||||
server.close(done);
|
||||
});
|
||||
});
|
||||
|
||||
+7
-5
@@ -32,11 +32,13 @@ describe('WinstonLogger', () => {
|
||||
it('should disable files logs', done => {
|
||||
reconfigureServer({
|
||||
logsFolder: null,
|
||||
}).then(() => {
|
||||
const transports = logging.logger.transports;
|
||||
expect(transports.length).toBe(1);
|
||||
done();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
const transports = logging.logger.transports;
|
||||
expect(transports.length).toBe(1);
|
||||
return reconfigureServer();
|
||||
})
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('should have a timestamp', done => {
|
||||
|
||||
@@ -70,6 +70,7 @@ describe('LoggerController', () => {
|
||||
};
|
||||
|
||||
const loggerController = new LoggerController(new WinstonLoggerAdapter());
|
||||
loggerController.error('can process an ascending query without throwing');
|
||||
|
||||
expect(() => {
|
||||
loggerController
|
||||
@@ -115,6 +116,7 @@ describe('LoggerController', () => {
|
||||
};
|
||||
|
||||
const loggerController = new LoggerController(new WinstonLoggerAdapter());
|
||||
loggerController.error('can process a descending query without throwing');
|
||||
|
||||
expect(() => {
|
||||
loggerController
|
||||
|
||||
@@ -8,7 +8,9 @@ const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapte
|
||||
|
||||
const loggerController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
describe('LogsRouter', () => {
|
||||
describe_only(() => {
|
||||
return process.env.PARSE_SERVER_LOG_LEVEL !== 'debug';
|
||||
})('LogsRouter', () => {
|
||||
it('can check valid master key of request', done => {
|
||||
// Make mock request
|
||||
const request = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default;
|
||||
const { MongoClient } = require('mongodb');
|
||||
const { MongoClient, Collection } = require('mongodb');
|
||||
const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||
const request = require('../lib/request');
|
||||
const Config = require('../lib/Config');
|
||||
@@ -18,6 +18,7 @@ const fakeClient = {
|
||||
describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
beforeEach(done => {
|
||||
new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses().then(done, fail);
|
||||
Config.get(Parse.applicationId).schemaCache.clear();
|
||||
});
|
||||
|
||||
it('auto-escapes symbols in auth information', () => {
|
||||
@@ -100,7 +101,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
done.fail('Find succeeded despite taking too long!');
|
||||
},
|
||||
err => {
|
||||
expect(err.name).toEqual('MongoError');
|
||||
expect(err.name).toEqual('MongoServerError');
|
||||
expect(err.code).toEqual(50);
|
||||
expect(err.message).toMatch('operation exceeded time limit');
|
||||
done();
|
||||
@@ -282,7 +283,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
await adapter.database.admin().serverStatus();
|
||||
expect(false).toBe(true);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('topology was destroyed');
|
||||
expect(e.message).toEqual('MongoClient must be connected to perform this operation');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -314,6 +315,8 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
await user.signUp();
|
||||
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
await database.adapter.dropAllIndexes('_User');
|
||||
|
||||
const preIndexPlan = await database.find(
|
||||
'_User',
|
||||
{ username: 'bugs' },
|
||||
@@ -376,15 +379,12 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
await reconfigureServer({
|
||||
databaseAdapter: undefined,
|
||||
databaseURI:
|
||||
'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset',
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestUtils.destroyAllDataPermanently(true);
|
||||
});
|
||||
|
||||
@@ -392,8 +392,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
|
||||
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
await request({
|
||||
method: 'POST',
|
||||
@@ -412,9 +411,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
|
||||
let found = false;
|
||||
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
|
||||
Collection.prototype.findOneAndUpdate.calls.all().forEach(call => {
|
||||
found = true;
|
||||
expect(call.args[2].session.transaction.state).not.toBe('NO_TRANSACTION');
|
||||
expect(call.args[2].session.transaction.state).toBe('TRANSACTION_COMMITTED');
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
@@ -423,8 +422,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
|
||||
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
await request({
|
||||
method: 'POST',
|
||||
@@ -443,9 +441,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
|
||||
let found = false;
|
||||
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
|
||||
Collection.prototype.findOneAndUpdate.calls.all().forEach(call => {
|
||||
found = true;
|
||||
expect(call.args[2].session).toBe(undefined);
|
||||
expect(call.args[2].session).toBeFalsy();
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
@@ -454,8 +452,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
|
||||
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
await request({
|
||||
method: 'POST',
|
||||
@@ -473,9 +470,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
|
||||
let found = false;
|
||||
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
|
||||
Collection.prototype.findOneAndUpdate.calls.all().forEach(call => {
|
||||
found = true;
|
||||
expect(call.args[2].session).toBe(undefined);
|
||||
expect(call.args[2].session).toBeFalsy();
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
@@ -484,8 +481,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
|
||||
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
await request({
|
||||
method: 'PUT',
|
||||
@@ -495,30 +491,28 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
});
|
||||
|
||||
let found = false;
|
||||
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
|
||||
Collection.prototype.findOneAndUpdate.calls.all().forEach(call => {
|
||||
found = true;
|
||||
expect(call.args[2].session).toBe(undefined);
|
||||
expect(call.args[2].session).toBeFalsy();
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
it('should not use transactions when using SDK insert', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'insert').and.callThrough();
|
||||
spyOn(Collection.prototype, 'insertOne').and.callThrough();
|
||||
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
const calls = databaseAdapter.database.serverConfig.insert.calls.all();
|
||||
const calls = Collection.prototype.insertOne.calls.all();
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
calls.forEach(call => {
|
||||
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
|
||||
expect(call.args[1].session).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not use transactions when using SDK update', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'update').and.callThrough();
|
||||
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
|
||||
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
@@ -526,28 +520,55 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
||||
myObject.set('myAttribute', 'myValue');
|
||||
await myObject.save();
|
||||
|
||||
const calls = databaseAdapter.database.serverConfig.update.calls.all();
|
||||
const calls = Collection.prototype.findOneAndUpdate.calls.all();
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
calls.forEach(call => {
|
||||
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
|
||||
expect(call.args[2].session).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not use transactions when using SDK delete', async () => {
|
||||
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
|
||||
spyOn(databaseAdapter.database.serverConfig, 'remove').and.callThrough();
|
||||
spyOn(Collection.prototype, 'deleteMany').and.callThrough();
|
||||
|
||||
const myObject = new Parse.Object('MyObject');
|
||||
await myObject.save();
|
||||
|
||||
await myObject.destroy();
|
||||
|
||||
const calls = databaseAdapter.database.serverConfig.remove.calls.all();
|
||||
const calls = Collection.prototype.deleteMany.calls.all();
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
calls.forEach(call => {
|
||||
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
|
||||
expect(call.args[1].session).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('watch _SCHEMA', () => {
|
||||
it('should change', async done => {
|
||||
const adapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
collectionPrefix: '',
|
||||
mongoOptions: { enableSchemaHooks: true },
|
||||
});
|
||||
await reconfigureServer({ databaseAdapter: adapter });
|
||||
expect(adapter.enableSchemaHooks).toBe(true);
|
||||
spyOn(adapter, '_onchange');
|
||||
const schema = {
|
||||
fields: {
|
||||
array: { type: 'Array' },
|
||||
object: { type: 'Object' },
|
||||
date: { type: 'Date' },
|
||||
},
|
||||
};
|
||||
|
||||
await adapter.createClass('Stuff', schema);
|
||||
const myClassSchema = await adapter.getClass('Stuff');
|
||||
expect(myClassSchema).toBeDefined();
|
||||
setTimeout(() => {
|
||||
expect(adapter._onchange).toHaveBeenCalled();
|
||||
done();
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+12
-11
@@ -4,6 +4,7 @@
|
||||
const transform = require('../lib/Adapters/Storage/Mongo/MongoTransform');
|
||||
const dd = require('deep-diff');
|
||||
const mongodb = require('mongodb');
|
||||
const Utils = require('../lib/Utils');
|
||||
|
||||
describe('parseObjectToMongoObjectForCreate', () => {
|
||||
it('a basic number', done => {
|
||||
@@ -592,7 +593,7 @@ describe('relativeTimeToDate', () => {
|
||||
describe('In the future', () => {
|
||||
it('should parse valid natural time', () => {
|
||||
const text = 'in 1 year 2 weeks 12 days 10 hours 24 minutes 30 seconds';
|
||||
const { result, status, info } = transform.relativeTimeToDate(text, now);
|
||||
const { result, status, info } = Utils.relativeTimeToDate(text, now);
|
||||
expect(result.toISOString()).toBe('2018-10-22T23:52:46.617Z');
|
||||
expect(status).toBe('success');
|
||||
expect(info).toBe('future');
|
||||
@@ -602,7 +603,7 @@ describe('relativeTimeToDate', () => {
|
||||
describe('In the past', () => {
|
||||
it('should parse valid natural time', () => {
|
||||
const text = '2 days 12 hours 1 minute 12 seconds ago';
|
||||
const { result, status, info } = transform.relativeTimeToDate(text, now);
|
||||
const { result, status, info } = Utils.relativeTimeToDate(text, now);
|
||||
expect(result.toISOString()).toBe('2017-09-24T01:27:04.617Z');
|
||||
expect(status).toBe('success');
|
||||
expect(info).toBe('past');
|
||||
@@ -612,7 +613,7 @@ describe('relativeTimeToDate', () => {
|
||||
describe('From now', () => {
|
||||
it('should equal current time', () => {
|
||||
const text = 'now';
|
||||
const { result, status, info } = transform.relativeTimeToDate(text, now);
|
||||
const { result, status, info } = Utils.relativeTimeToDate(text, now);
|
||||
expect(result.toISOString()).toBe('2017-09-26T13:28:16.617Z');
|
||||
expect(status).toBe('success');
|
||||
expect(info).toBe('present');
|
||||
@@ -621,54 +622,54 @@ describe('relativeTimeToDate', () => {
|
||||
|
||||
describe('Error cases', () => {
|
||||
it('should error if string is completely gibberish', () => {
|
||||
expect(transform.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
|
||||
status: 'error',
|
||||
info: "Time should either start with 'in' or end with 'ago'",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if string contains neither `ago` nor `in`', () => {
|
||||
expect(transform.relativeTimeToDate('12 hours 1 minute')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('12 hours 1 minute')).toEqual({
|
||||
status: 'error',
|
||||
info: "Time should either start with 'in' or end with 'ago'",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if there are missing units or numbers', () => {
|
||||
expect(transform.relativeTimeToDate('in 12 hours 1')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('in 12 hours 1')).toEqual({
|
||||
status: 'error',
|
||||
info: 'Invalid time string. Dangling unit or number.',
|
||||
});
|
||||
|
||||
expect(transform.relativeTimeToDate('12 hours minute ago')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('12 hours minute ago')).toEqual({
|
||||
status: 'error',
|
||||
info: 'Invalid time string. Dangling unit or number.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error on floating point numbers', () => {
|
||||
expect(transform.relativeTimeToDate('in 12.3 hours')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('in 12.3 hours')).toEqual({
|
||||
status: 'error',
|
||||
info: "'12.3' is not an integer.",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if numbers are invalid', () => {
|
||||
expect(transform.relativeTimeToDate('12 hours 123a minute ago')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('12 hours 123a minute ago')).toEqual({
|
||||
status: 'error',
|
||||
info: "'123a' is not an integer.",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error on invalid interval units', () => {
|
||||
expect(transform.relativeTimeToDate('4 score 7 years ago')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('4 score 7 years ago')).toEqual({
|
||||
status: 'error',
|
||||
info: "Invalid interval: 'score'",
|
||||
});
|
||||
});
|
||||
|
||||
it("should error when string contains 'ago' and 'in'", () => {
|
||||
expect(transform.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
|
||||
expect(Utils.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
|
||||
status: 'error',
|
||||
info: "Time cannot have both 'in' and 'ago'",
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+252
-419
@@ -2,327 +2,235 @@
|
||||
|
||||
const request = require('../lib/request');
|
||||
|
||||
const delayPromise = delay => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, delay);
|
||||
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);
|
||||
result = await query.first({ useMasterKey: true });
|
||||
}
|
||||
};
|
||||
|
||||
const successfulAny = function (body, installations) {
|
||||
const promises = installations.map(device => {
|
||||
return Promise.resolve({
|
||||
transmitted: true,
|
||||
device: device,
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
const provideInstallations = function (num) {
|
||||
if (!num) {
|
||||
num = 2;
|
||||
}
|
||||
|
||||
const installations = [];
|
||||
while (installations.length !== num) {
|
||||
// add Android installations
|
||||
const installation = new Parse.Object('_Installation');
|
||||
installation.set('installationId', 'installation_' + installations.length);
|
||||
installation.set('deviceToken', 'device_token_' + installations.length);
|
||||
installation.set('deviceType', 'android');
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
return installations;
|
||||
};
|
||||
|
||||
const losingAdapter = {
|
||||
send: function (body, installations) {
|
||||
// simulate having lost an installation before this was called
|
||||
// thus invalidating our 'count' in _PushStatus
|
||||
installations.pop();
|
||||
|
||||
return successfulAny(body, installations);
|
||||
},
|
||||
getValidPushTypes: function () {
|
||||
return ['android'];
|
||||
},
|
||||
};
|
||||
|
||||
const setup = function () {
|
||||
const sendToInstallationSpy = jasmine.createSpy();
|
||||
|
||||
const pushAdapter = {
|
||||
send: function (body, installations) {
|
||||
const badge = body.data.badge;
|
||||
const promises = installations.map(installation => {
|
||||
sendToInstallationSpy(installation);
|
||||
|
||||
if (installation.deviceType == 'ios') {
|
||||
expect(installation.badge).toEqual(badge);
|
||||
expect(installation.originalBadge + 1).toEqual(installation.badge);
|
||||
} else {
|
||||
expect(installation.badge).toBeUndefined();
|
||||
}
|
||||
return Promise.resolve({
|
||||
err: null,
|
||||
device: installation,
|
||||
transmitted: true,
|
||||
});
|
||||
});
|
||||
return Promise.all(promises);
|
||||
},
|
||||
getValidPushTypes: function () {
|
||||
return ['ios', 'android'];
|
||||
},
|
||||
};
|
||||
|
||||
return reconfigureServer({
|
||||
appId: Parse.applicationId,
|
||||
masterKey: Parse.masterKey,
|
||||
serverURL: Parse.serverURL,
|
||||
push: {
|
||||
adapter: pushAdapter,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
const installations = [];
|
||||
while (installations.length != 10) {
|
||||
const installation = new Parse.Object('_Installation');
|
||||
installation.set('installationId', 'installation_' + installations.length);
|
||||
installation.set('deviceToken', 'device_token_' + installations.length);
|
||||
installation.set('badge', installations.length);
|
||||
installation.set('originalBadge', installations.length);
|
||||
installation.set('deviceType', 'ios');
|
||||
installations.push(installation);
|
||||
}
|
||||
return Parse.Object.saveAll(installations);
|
||||
})
|
||||
.then(() => {
|
||||
return {
|
||||
sendToInstallationSpy,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
describe('Parse.Push', () => {
|
||||
const setup = function () {
|
||||
const sendToInstallationSpy = jasmine.createSpy();
|
||||
|
||||
const pushAdapter = {
|
||||
send: function (body, installations) {
|
||||
const badge = body.data.badge;
|
||||
const promises = installations.map(installation => {
|
||||
sendToInstallationSpy(installation);
|
||||
|
||||
if (installation.deviceType == 'ios') {
|
||||
expect(installation.badge).toEqual(badge);
|
||||
expect(installation.originalBadge + 1).toEqual(installation.badge);
|
||||
} else {
|
||||
expect(installation.badge).toBeUndefined();
|
||||
}
|
||||
return Promise.resolve({
|
||||
err: null,
|
||||
device: installation,
|
||||
transmitted: true,
|
||||
});
|
||||
});
|
||||
return Promise.all(promises);
|
||||
it('should properly send push', async () => {
|
||||
const { sendToInstallationSpy } = await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
getValidPushTypes: function () {
|
||||
return ['ios', 'android'];
|
||||
data: {
|
||||
badge: 'Increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
};
|
||||
|
||||
return reconfigureServer({
|
||||
appId: Parse.applicationId,
|
||||
masterKey: Parse.masterKey,
|
||||
serverURL: Parse.serverURL,
|
||||
push: {
|
||||
adapter: pushAdapter,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
const installations = [];
|
||||
while (installations.length != 10) {
|
||||
const installation = new Parse.Object('_Installation');
|
||||
installation.set('installationId', 'installation_' + installations.length);
|
||||
installation.set('deviceToken', 'device_token_' + installations.length);
|
||||
installation.set('badge', installations.length);
|
||||
installation.set('originalBadge', installations.length);
|
||||
installation.set('deviceType', 'ios');
|
||||
installations.push(installation);
|
||||
}
|
||||
return Parse.Object.saveAll(installations);
|
||||
})
|
||||
.then(() => {
|
||||
return {
|
||||
sendToInstallationSpy,
|
||||
};
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
it('should properly send push', done => {
|
||||
return setup()
|
||||
.then(({ sendToInstallationSpy }) => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'Increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
)
|
||||
.then(() => {
|
||||
return delayPromise(500);
|
||||
})
|
||||
.then(() => {
|
||||
expect(sendToInstallationSpy.calls.count()).toEqual(10);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly send push with lowercaseIncrement', done => {
|
||||
return setup()
|
||||
.then(() => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
return delayPromise(500);
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow clients to query _PushStatus', done => {
|
||||
setup()
|
||||
.then(() =>
|
||||
Parse.Push.send(
|
||||
{
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
)
|
||||
)
|
||||
.then(() => delayPromise(500))
|
||||
.then(() => {
|
||||
request({
|
||||
url: 'http://localhost:8378/1/classes/_PushStatus',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
},
|
||||
}).then(fail, response => {
|
||||
expect(response.data.error).toEqual('unauthorized');
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow master key to query _PushStatus', done => {
|
||||
setup()
|
||||
.then(() =>
|
||||
Parse.Push.send(
|
||||
{
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
)
|
||||
)
|
||||
.then(() => delayPromise(500)) // put a delay as we keep writing
|
||||
.then(() => {
|
||||
request({
|
||||
url: 'http://localhost:8378/1/classes/_PushStatus',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
}).then(response => {
|
||||
const body = response.data;
|
||||
try {
|
||||
expect(body.results.length).toEqual(1);
|
||||
expect(body.results[0].query).toEqual('{"deviceType":"ios"}');
|
||||
expect(body.results[0].payload).toEqual('{"badge":"increment","alert":"Hello world!"}');
|
||||
} catch (e) {
|
||||
jfail(e);
|
||||
}
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error if missing push configuration', done => {
|
||||
reconfigureServer({ push: null })
|
||||
.then(() => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
);
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
fail('should not succeed');
|
||||
},
|
||||
err => {
|
||||
expect(err.code).toEqual(Parse.Error.PUSH_MISCONFIGURED);
|
||||
done();
|
||||
}
|
||||
)
|
||||
.catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
const successfulAny = function (body, installations) {
|
||||
const promises = installations.map(device => {
|
||||
return Promise.resolve({
|
||||
transmitted: true,
|
||||
device: device,
|
||||
});
|
||||
});
|
||||
await pushCompleted(pushStatusId);
|
||||
expect(sendToInstallationSpy.calls.count()).toEqual(10);
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
};
|
||||
it('should properly send push with lowercaseIncrement', async () => {
|
||||
await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
});
|
||||
await pushCompleted(pushStatusId);
|
||||
});
|
||||
|
||||
const provideInstallations = function (num) {
|
||||
if (!num) {
|
||||
num = 2;
|
||||
it('should not allow clients to query _PushStatus', async () => {
|
||||
await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
});
|
||||
await pushCompleted(pushStatusId);
|
||||
try {
|
||||
await request({
|
||||
url: 'http://localhost:8378/1/classes/_PushStatus',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
},
|
||||
});
|
||||
fail();
|
||||
} catch (response) {
|
||||
expect(response.data.error).toEqual('unauthorized');
|
||||
}
|
||||
});
|
||||
|
||||
const installations = [];
|
||||
while (installations.length !== num) {
|
||||
// add Android installations
|
||||
const installation = new Parse.Object('_Installation');
|
||||
installation.set('installationId', 'installation_' + installations.length);
|
||||
installation.set('deviceToken', 'device_token_' + installations.length);
|
||||
installation.set('deviceType', 'android');
|
||||
installations.push(installation);
|
||||
it('should allow master key to query _PushStatus', async () => {
|
||||
await setup();
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
});
|
||||
await pushCompleted(pushStatusId);
|
||||
const response = await request({
|
||||
url: 'http://localhost:8378/1/classes/_PushStatus',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
});
|
||||
const body = response.data;
|
||||
expect(body.results.length).toEqual(1);
|
||||
expect(body.results[0].query).toEqual('{"deviceType":"ios"}');
|
||||
expect(body.results[0].payload).toEqual('{"badge":"increment","alert":"Hello world!"}');
|
||||
});
|
||||
|
||||
it('should throw error if missing push configuration', async () => {
|
||||
await reconfigureServer({ push: null });
|
||||
try {
|
||||
await Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios',
|
||||
},
|
||||
data: {
|
||||
badge: 'increment',
|
||||
alert: 'Hello world!',
|
||||
},
|
||||
});
|
||||
fail();
|
||||
} catch (err) {
|
||||
expect(err.code).toEqual(Parse.Error.PUSH_MISCONFIGURED);
|
||||
}
|
||||
|
||||
return installations;
|
||||
};
|
||||
|
||||
const losingAdapter = {
|
||||
send: function (body, installations) {
|
||||
// simulate having lost an installation before this was called
|
||||
// thus invalidating our 'count' in _PushStatus
|
||||
installations.pop();
|
||||
|
||||
return successfulAny(body, installations);
|
||||
},
|
||||
getValidPushTypes: function () {
|
||||
return ['android'];
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies that _PushStatus cannot get stuck in a 'running' state
|
||||
* Simulates a simple push where 1 installation is removed between _PushStatus
|
||||
* count being set and the pushes being sent
|
||||
*/
|
||||
it("does not get stuck with _PushStatus 'running' on 1 installation lost", done => {
|
||||
reconfigureServer({
|
||||
it("does not get stuck with _PushStatus 'running' on 1 installation lost", async () => {
|
||||
await reconfigureServer({
|
||||
push: { adapter: losingAdapter },
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Object.saveAll(provideInstallations());
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
data: { alert: 'We fixed our status!' },
|
||||
where: { deviceType: 'android' },
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
// it is enqueued so it can take time
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// query for push status
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
// verify status is NOT broken
|
||||
expect(results.length).toBe(1);
|
||||
const result = results[0];
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
expect(result.get('numSent')).toEqual(1);
|
||||
expect(result.get('count')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
await Parse.Object.saveAll(provideInstallations());
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
data: { alert: 'We fixed our status!' },
|
||||
where: { deviceType: 'android' },
|
||||
});
|
||||
await pushCompleted(pushStatusId);
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
expect(result.get('numSent')).toEqual(1);
|
||||
expect(result.get('count')).toEqual(undefined);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -330,7 +238,7 @@ describe('Parse.Push', () => {
|
||||
* Simulates a simple push where 1 installation is added between _PushStatus
|
||||
* count being set and the pushes being sent
|
||||
*/
|
||||
it("does not get stuck with _PushStatus 'running' on 1 installation added", done => {
|
||||
it("does not get stuck with _PushStatus 'running' on 1 installation added", async () => {
|
||||
const installations = provideInstallations();
|
||||
|
||||
// add 1 iOS installation which we will omit & add later on
|
||||
@@ -340,14 +248,13 @@ describe('Parse.Push', () => {
|
||||
iOSInstallation.set('deviceType', 'ios');
|
||||
installations.push(iOSInstallation);
|
||||
|
||||
reconfigureServer({
|
||||
await reconfigureServer({
|
||||
push: {
|
||||
adapter: {
|
||||
send: function (body, installations) {
|
||||
// simulate having added an installation before this was called
|
||||
// thus invalidating our 'count' in _PushStatus
|
||||
installations.push(iOSInstallation);
|
||||
|
||||
return successfulAny(body, installations);
|
||||
},
|
||||
getValidPushTypes: function () {
|
||||
@@ -355,41 +262,17 @@ describe('Parse.Push', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Object.saveAll(installations);
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
data: { alert: 'We fixed our status!' },
|
||||
where: { deviceType: { $ne: 'random' } },
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
// it is enqueued so it can take time
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// query for push status
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
// verify status is NOT broken
|
||||
expect(results.length).toBe(1);
|
||||
const result = results[0];
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
expect(result.get('numSent')).toEqual(3);
|
||||
expect(result.get('count')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
data: { alert: 'We fixed our status!' },
|
||||
where: { deviceType: { $ne: 'random' } },
|
||||
});
|
||||
await pushCompleted(pushStatusId);
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
expect(result.get('numSent')).toEqual(3);
|
||||
expect(result.get('count')).toEqual(undefined);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -397,48 +280,24 @@ describe('Parse.Push', () => {
|
||||
* Simulates an extended push, where some installations may be removed,
|
||||
* resulting in a non-zero count
|
||||
*/
|
||||
it("does not get stuck with _PushStatus 'running' on many installations removed", done => {
|
||||
it("does not get stuck with _PushStatus 'running' on many installations removed", async () => {
|
||||
const devices = 1000;
|
||||
const installations = provideInstallations(devices);
|
||||
|
||||
reconfigureServer({
|
||||
await reconfigureServer({
|
||||
push: { adapter: losingAdapter },
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Object.saveAll(installations);
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
data: { alert: 'We fixed our status!' },
|
||||
where: { deviceType: 'android' },
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
// it is enqueued so it can take time
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// query for push status
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
// verify status is NOT broken
|
||||
expect(results.length).toBe(1);
|
||||
const result = results[0];
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
// expect # less than # of batches used, assuming each batch is 100 pushes
|
||||
expect(result.get('numSent')).toEqual(devices - devices / 100);
|
||||
expect(result.get('count')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
data: { alert: 'We fixed our status!' },
|
||||
where: { deviceType: 'android' },
|
||||
});
|
||||
await pushCompleted(pushStatusId);
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
// expect # less than # of batches used, assuming each batch is 100 pushes
|
||||
expect(result.get('numSent')).toEqual(devices - devices / 100);
|
||||
expect(result.get('count')).toEqual(undefined);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -446,13 +305,12 @@ describe('Parse.Push', () => {
|
||||
* Simulates an extended push, where some installations may be added,
|
||||
* resulting in a non-zero count
|
||||
*/
|
||||
it("does not get stuck with _PushStatus 'running' on many installations added", done => {
|
||||
it("does not get stuck with _PushStatus 'running' on many installations added", async () => {
|
||||
const devices = 1000;
|
||||
const installations = provideInstallations(devices);
|
||||
|
||||
// add 1 iOS installation which we will omit & add later on
|
||||
const iOSInstallations = [];
|
||||
|
||||
while (iOSInstallations.length !== devices / 100) {
|
||||
const iOSInstallation = new Parse.Object('_Installation');
|
||||
iOSInstallation.set('installationId', 'installation_' + installations.length);
|
||||
@@ -461,15 +319,13 @@ describe('Parse.Push', () => {
|
||||
installations.push(iOSInstallation);
|
||||
iOSInstallations.push(iOSInstallation);
|
||||
}
|
||||
|
||||
reconfigureServer({
|
||||
await reconfigureServer({
|
||||
push: {
|
||||
adapter: {
|
||||
send: function (body, installations) {
|
||||
// simulate having added an installation before this was called
|
||||
// thus invalidating our 'count' in _PushStatus
|
||||
installations.push(iOSInstallations.pop());
|
||||
|
||||
return successfulAny(body, installations);
|
||||
},
|
||||
getValidPushTypes: function () {
|
||||
@@ -477,41 +333,18 @@ describe('Parse.Push', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Object.saveAll(installations);
|
||||
})
|
||||
.then(() => {
|
||||
return Parse.Push.send(
|
||||
{
|
||||
data: { alert: 'We fixed our status!' },
|
||||
where: { deviceType: { $ne: 'random' } },
|
||||
},
|
||||
{ useMasterKey: true }
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
// it is enqueued so it can take time
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// query for push status
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
// verify status is NOT broken
|
||||
expect(results.length).toBe(1);
|
||||
const result = results[0];
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
// expect # less than # of batches used, assuming each batch is 100 pushes
|
||||
expect(result.get('numSent')).toEqual(devices + devices / 100);
|
||||
expect(result.get('count')).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
await Parse.Object.saveAll(installations);
|
||||
|
||||
const pushStatusId = await Parse.Push.send({
|
||||
data: { alert: 'We fixed our status!' },
|
||||
where: { deviceType: { $ne: 'random' } },
|
||||
});
|
||||
await pushCompleted(pushStatusId);
|
||||
const result = await Parse.Push.getPushStatus(pushStatusId);
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
// expect # less than # of batches used, assuming each batch is 100 pushes
|
||||
expect(result.get('numSent')).toEqual(devices + devices / 100);
|
||||
expect(result.get('count')).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
+38
-28
@@ -23,32 +23,15 @@ const headers = {
|
||||
'X-Parse-Installation-Id': 'yolo',
|
||||
};
|
||||
|
||||
describe_only_db('mongo')('miscellaneous', () => {
|
||||
it('test rest_create_app', function (done) {
|
||||
let appId;
|
||||
Parse._request('POST', 'rest_create_app')
|
||||
.then(res => {
|
||||
expect(typeof res.application_id).toEqual('string');
|
||||
expect(res.master_key).toEqual('master');
|
||||
appId = res.application_id;
|
||||
Parse.initialize(appId, 'unused');
|
||||
const obj = new Parse.Object('TestObject');
|
||||
obj.set('foo', 'bar');
|
||||
return obj.save();
|
||||
})
|
||||
.then(() => {
|
||||
const config = Config.get(appId);
|
||||
return config.database.adapter.find('TestObject', { fields: {} }, {}, {});
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]['foo']).toEqual('bar');
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
fail(JSON.stringify(error));
|
||||
done();
|
||||
});
|
||||
describe('miscellaneous', () => {
|
||||
it('db contains document after successful save', async () => {
|
||||
const obj = new Parse.Object('TestObject');
|
||||
obj.set('foo', 'bar');
|
||||
await obj.save();
|
||||
const config = Config.get(defaultConfiguration.appId);
|
||||
const results = await config.database.adapter.find('TestObject', { fields: {} }, {}, {});
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]['foo']).toEqual('bar');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,6 +67,7 @@ describe('miscellaneous', function () {
|
||||
});
|
||||
|
||||
it('fail to create a duplicate username', async () => {
|
||||
await reconfigureServer();
|
||||
let numFailed = 0;
|
||||
let numCreated = 0;
|
||||
const p1 = request({
|
||||
@@ -131,6 +115,7 @@ describe('miscellaneous', function () {
|
||||
});
|
||||
|
||||
it('ensure that email is uniquely indexed', async () => {
|
||||
await reconfigureServer();
|
||||
let numFailed = 0;
|
||||
let numCreated = 0;
|
||||
const p1 = request({
|
||||
@@ -187,6 +172,7 @@ describe('miscellaneous', function () {
|
||||
const config = Config.get('test');
|
||||
// Remove existing data to clear out unique index
|
||||
TestUtils.destroyAllDataPermanently()
|
||||
.then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] }))
|
||||
.then(() => config.database.adapter.createClass('_User', userSchema))
|
||||
.then(() =>
|
||||
config.database.adapter
|
||||
@@ -227,6 +213,7 @@ describe('miscellaneous', function () {
|
||||
const config = Config.get('test');
|
||||
// Remove existing data to clear out unique index
|
||||
TestUtils.destroyAllDataPermanently()
|
||||
.then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] }))
|
||||
.then(() => config.database.adapter.createClass('_User', userSchema))
|
||||
.then(() =>
|
||||
config.database.adapter.createObject('_User', userSchema, {
|
||||
@@ -261,7 +248,8 @@ describe('miscellaneous', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('ensure that if you try to sign up a user with a unique username and email, but duplicates in some other field that has a uniqueness constraint, you get a regular duplicate value error', done => {
|
||||
it('ensure that if you try to sign up a user with a unique username and email, but duplicates in some other field that has a uniqueness constraint, you get a regular duplicate value error', async done => {
|
||||
await reconfigureServer();
|
||||
const config = Config.get('test');
|
||||
config.database.adapter
|
||||
.addFieldIfNotExists('_User', 'randomField', { type: 'String' })
|
||||
@@ -658,6 +646,28 @@ describe('miscellaneous', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it_only_db('mongo')('pointer reassign on nested fields is working properly (#7391)', async () => {
|
||||
const obj = new Parse.Object('GameScore'); // This object will include nested pointers
|
||||
const ptr1 = new Parse.Object('GameScore');
|
||||
await ptr1.save(); // Obtain a unique id
|
||||
const ptr2 = new Parse.Object('GameScore');
|
||||
await ptr2.save(); // Obtain a unique id
|
||||
obj.set('data', { ptr: ptr1 });
|
||||
await obj.save();
|
||||
|
||||
obj.set('data.ptr', ptr2);
|
||||
await obj.save();
|
||||
|
||||
const obj2 = await new Parse.Query('GameScore').get(obj.id);
|
||||
expect(obj2.get('data').ptr.id).toBe(ptr2.id);
|
||||
|
||||
const query = new Parse.Query('GameScore');
|
||||
query.equalTo('data.ptr', ptr2);
|
||||
const res = await query.find();
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].get('data').ptr.id).toBe(ptr2.id);
|
||||
});
|
||||
|
||||
it('test afterSave get full object on create and update', function (done) {
|
||||
let triggerTime = 0;
|
||||
// Register a mock beforeSave hook
|
||||
@@ -1005,7 +1015,7 @@ describe('miscellaneous', function () {
|
||||
done();
|
||||
},
|
||||
e => {
|
||||
expect(e.code).toEqual(141);
|
||||
expect(e.code).toEqual(Parse.Error.SCRIPT_FAILED);
|
||||
expect(e.message).toEqual('noway');
|
||||
done();
|
||||
}
|
||||
|
||||
+717
-504
File diff suppressed because it is too large
Load Diff
@@ -28,42 +28,39 @@ describe('ParseGraphQLController', () => {
|
||||
return graphQLConfigRecord;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
schemaCacheTTL: 100,
|
||||
});
|
||||
databaseController = parseServer.config.databaseController;
|
||||
cacheController = parseServer.config.cacheController;
|
||||
beforeEach(async () => {
|
||||
if (!parseServer) {
|
||||
parseServer = await global.reconfigureServer();
|
||||
databaseController = parseServer.config.databaseController;
|
||||
cacheController = parseServer.config.cacheController;
|
||||
|
||||
const defaultFind = databaseController.find.bind(databaseController);
|
||||
databaseController.find = async (className, query, ...args) => {
|
||||
if (className === GraphQLConfigClassName && isEqual(query, { objectId: GraphQLConfigId })) {
|
||||
const graphQLConfigRecord = getConfigFromDb();
|
||||
return graphQLConfigRecord ? [graphQLConfigRecord] : [];
|
||||
} else {
|
||||
return defaultFind(className, query, ...args);
|
||||
}
|
||||
};
|
||||
const defaultFind = databaseController.find.bind(databaseController);
|
||||
databaseController.find = async (className, query, ...args) => {
|
||||
if (className === GraphQLConfigClassName && isEqual(query, { objectId: GraphQLConfigId })) {
|
||||
const graphQLConfigRecord = getConfigFromDb();
|
||||
return graphQLConfigRecord ? [graphQLConfigRecord] : [];
|
||||
} else {
|
||||
return defaultFind(className, query, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
const defaultUpdate = databaseController.update.bind(databaseController);
|
||||
databaseController.update = async (className, query, update, fullQueryOptions) => {
|
||||
databaseUpdateArgs = [className, query, update, fullQueryOptions];
|
||||
if (
|
||||
className === GraphQLConfigClassName &&
|
||||
isEqual(query, { objectId: GraphQLConfigId }) &&
|
||||
update &&
|
||||
!!update[GraphQLConfigKey] &&
|
||||
fullQueryOptions &&
|
||||
isEqual(fullQueryOptions, { upsert: true })
|
||||
) {
|
||||
setConfigOnDb(update[GraphQLConfigKey]);
|
||||
} else {
|
||||
return defaultUpdate(...databaseUpdateArgs);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const defaultUpdate = databaseController.update.bind(databaseController);
|
||||
databaseController.update = async (className, query, update, fullQueryOptions) => {
|
||||
databaseUpdateArgs = [className, query, update, fullQueryOptions];
|
||||
if (
|
||||
className === GraphQLConfigClassName &&
|
||||
isEqual(query, { objectId: GraphQLConfigId }) &&
|
||||
update &&
|
||||
!!update[GraphQLConfigKey] &&
|
||||
fullQueryOptions &&
|
||||
isEqual(fullQueryOptions, { upsert: true })
|
||||
) {
|
||||
setConfigOnDb(update[GraphQLConfigKey]);
|
||||
} else {
|
||||
return defaultUpdate(...databaseUpdateArgs);
|
||||
}
|
||||
};
|
||||
}
|
||||
databaseUpdateArgs = null;
|
||||
});
|
||||
|
||||
|
||||
@@ -9,10 +9,8 @@ describe('ParseGraphQLSchema', () => {
|
||||
let parseGraphQLSchema;
|
||||
const appId = 'test';
|
||||
|
||||
beforeAll(async () => {
|
||||
parseServer = await global.reconfigureServer({
|
||||
schemaCacheTTL: 100,
|
||||
});
|
||||
beforeEach(async () => {
|
||||
parseServer = await global.reconfigureServer();
|
||||
databaseController = parseServer.config.databaseController;
|
||||
parseGraphQLController = parseServer.config.parseGraphQLController;
|
||||
parseGraphQLSchema = new ParseGraphQLSchema({
|
||||
@@ -54,14 +52,12 @@ describe('ParseGraphQLSchema', () => {
|
||||
const graphQLSchema = await parseGraphQLSchema.load();
|
||||
const updatedGraphQLSchema = await parseGraphQLSchema.load();
|
||||
expect(graphQLSchema).toBe(updatedGraphQLSchema);
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
expect(graphQLSchema).toBe(await parseGraphQLSchema.load());
|
||||
});
|
||||
|
||||
it('should load a brand new GraphQL Schema if Parse Schema changes', async () => {
|
||||
await parseGraphQLSchema.load();
|
||||
const parseClasses = parseGraphQLSchema.parseClasses;
|
||||
const parseClassesString = parseGraphQLSchema.parseClassesString;
|
||||
const parseCachedClasses = parseGraphQLSchema.parseCachedClasses;
|
||||
const parseClassTypes = parseGraphQLSchema.parseClassTypes;
|
||||
const graphQLSchema = parseGraphQLSchema.graphQLSchema;
|
||||
const graphQLTypes = parseGraphQLSchema.graphQLTypes;
|
||||
@@ -70,11 +66,11 @@ describe('ParseGraphQLSchema', () => {
|
||||
const graphQLSubscriptions = parseGraphQLSchema.graphQLSubscriptions;
|
||||
const newClassObject = new Parse.Object('NewClass');
|
||||
await newClassObject.save();
|
||||
await databaseController.schemaCache.clear();
|
||||
await parseServer.config.schemaCache.clear();
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
await parseGraphQLSchema.load();
|
||||
expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses);
|
||||
expect(parseClassesString).not.toBe(parseGraphQLSchema.parseClassesString);
|
||||
expect(parseCachedClasses).not.toBe(parseGraphQLSchema.parseCachedClasses);
|
||||
expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes);
|
||||
expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema);
|
||||
expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes);
|
||||
@@ -98,7 +94,7 @@ describe('ParseGraphQLSchema', () => {
|
||||
});
|
||||
await parseGraphQLSchema.load();
|
||||
const parseClasses = parseGraphQLSchema.parseClasses;
|
||||
const parseClassesString = parseGraphQLSchema.parseClassesString;
|
||||
const parseCachedClasses = parseGraphQLSchema.parseCachedClasses;
|
||||
const parseClassTypes = parseGraphQLSchema.parseClassTypes;
|
||||
const graphQLSchema = parseGraphQLSchema.graphQLSchema;
|
||||
const graphQLTypes = parseGraphQLSchema.graphQLTypes;
|
||||
@@ -113,7 +109,7 @@ describe('ParseGraphQLSchema', () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
await parseGraphQLSchema.load();
|
||||
expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses);
|
||||
expect(parseClassesString).not.toBe(parseGraphQLSchema.parseClassesString);
|
||||
expect(parseCachedClasses).not.toBe(parseGraphQLSchema.parseCachedClasses);
|
||||
expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes);
|
||||
expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema);
|
||||
expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes);
|
||||
@@ -428,14 +424,14 @@ describe('ParseGraphQLSchema', () => {
|
||||
log: defaultLogger,
|
||||
appId,
|
||||
});
|
||||
await parseGraphQLSchema.databaseController.schemaCache.clear();
|
||||
await parseGraphQLSchema.schemaCache.clear();
|
||||
const schema1 = await parseGraphQLSchema.load();
|
||||
const types1 = parseGraphQLSchema.graphQLTypes;
|
||||
const queries1 = parseGraphQLSchema.graphQLQueries;
|
||||
const mutations1 = parseGraphQLSchema.graphQLMutations;
|
||||
const user = new Parse.Object('User');
|
||||
await user.save();
|
||||
await parseGraphQLSchema.databaseController.schemaCache.clear();
|
||||
await parseGraphQLSchema.schemaCache.clear();
|
||||
const schema2 = await parseGraphQLSchema.load();
|
||||
const types2 = parseGraphQLSchema.graphQLTypes;
|
||||
const queries2 = parseGraphQLSchema.graphQLQueries;
|
||||
@@ -458,14 +454,14 @@ describe('ParseGraphQLSchema', () => {
|
||||
});
|
||||
const car1 = new Parse.Object('Car');
|
||||
await car1.save();
|
||||
await parseGraphQLSchema.databaseController.schemaCache.clear();
|
||||
await parseGraphQLSchema.schemaCache.clear();
|
||||
const schema1 = await parseGraphQLSchema.load();
|
||||
const types1 = parseGraphQLSchema.graphQLTypes;
|
||||
const queries1 = parseGraphQLSchema.graphQLQueries;
|
||||
const mutations1 = parseGraphQLSchema.graphQLMutations;
|
||||
const car2 = new Parse.Object('car');
|
||||
await car2.save();
|
||||
await parseGraphQLSchema.databaseController.schemaCache.clear();
|
||||
await parseGraphQLSchema.schemaCache.clear();
|
||||
const schema2 = await parseGraphQLSchema.load();
|
||||
const types2 = parseGraphQLSchema.graphQLTypes;
|
||||
const queries2 = parseGraphQLSchema.graphQLQueries;
|
||||
@@ -488,13 +484,13 @@ describe('ParseGraphQLSchema', () => {
|
||||
});
|
||||
const car = new Parse.Object('Car');
|
||||
await car.save();
|
||||
await parseGraphQLSchema.databaseController.schemaCache.clear();
|
||||
await parseGraphQLSchema.schemaCache.clear();
|
||||
const schema1 = await parseGraphQLSchema.load();
|
||||
const queries1 = parseGraphQLSchema.graphQLQueries;
|
||||
const mutations1 = parseGraphQLSchema.graphQLMutations;
|
||||
const cars = new Parse.Object('cars');
|
||||
await cars.save();
|
||||
await parseGraphQLSchema.databaseController.schemaCache.clear();
|
||||
await parseGraphQLSchema.schemaCache.clear();
|
||||
const schema2 = await parseGraphQLSchema.load();
|
||||
const queries2 = parseGraphQLSchema.graphQLQueries;
|
||||
const mutations2 = parseGraphQLSchema.graphQLMutations;
|
||||
@@ -534,7 +530,7 @@ describe('ParseGraphQLSchema', () => {
|
||||
|
||||
await data.save();
|
||||
|
||||
await parseGraphQLSchema.databaseController.schemaCache.clear();
|
||||
await parseGraphQLSchema.schemaCache.clear();
|
||||
await parseGraphQLSchema.load();
|
||||
|
||||
const queries1 = parseGraphQLSchema.graphQLQueries;
|
||||
@@ -571,7 +567,7 @@ describe('ParseGraphQLSchema', () => {
|
||||
|
||||
await data.save();
|
||||
|
||||
await parseGraphQLSchema.databaseController.schemaCache.clear();
|
||||
await parseGraphQLSchema.schemaCache.clear();
|
||||
await parseGraphQLSchema.load();
|
||||
|
||||
const mutations = parseGraphQLSchema.graphQLMutations;
|
||||
|
||||
+725
-321
File diff suppressed because it is too large
Load Diff
+12
-9
@@ -8,17 +8,20 @@ const bodyParser = require('body-parser');
|
||||
const auth = require('../lib/Auth');
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
const port = 12345;
|
||||
const port = 34567;
|
||||
const hookServerURL = 'http://localhost:' + port;
|
||||
const AppCache = require('../lib/cache').AppCache;
|
||||
|
||||
describe('Hooks', () => {
|
||||
let server;
|
||||
let app;
|
||||
beforeAll(done => {
|
||||
app = express();
|
||||
app.use(bodyParser.json({ type: '*/*' }));
|
||||
server = app.listen(12345, undefined, done);
|
||||
beforeEach(done => {
|
||||
if (!app) {
|
||||
app = express();
|
||||
app.use(bodyParser.json({ type: '*/*' }));
|
||||
server = app.listen(port, undefined, done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(done => {
|
||||
@@ -379,7 +382,7 @@ describe('Hooks', () => {
|
||||
}
|
||||
const hooksController = new HooksController(
|
||||
Parse.applicationId,
|
||||
AppCache.get('test').databaseController
|
||||
Config.get('test').database
|
||||
);
|
||||
return hooksController.load();
|
||||
},
|
||||
@@ -461,7 +464,7 @@ describe('Hooks', () => {
|
||||
expect(err).not.toBe(undefined);
|
||||
expect(err).not.toBe(null);
|
||||
if (err) {
|
||||
expect(err.code).toBe(141);
|
||||
expect(err.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||
expect(err.message.code).toEqual(1337);
|
||||
expect(err.message.error).toEqual('hacking that one!');
|
||||
}
|
||||
@@ -533,7 +536,7 @@ describe('Hooks', () => {
|
||||
expect(err).not.toBe(undefined);
|
||||
expect(err).not.toBe(null);
|
||||
if (err) {
|
||||
expect(err.code).toBe(141);
|
||||
expect(err.code).toBe(Parse.Error.SCRIPT_FAILED);
|
||||
expect(err.message).toEqual('incorrect key provided');
|
||||
}
|
||||
done();
|
||||
|
||||
+248
-156
@@ -15,12 +15,14 @@ describe('ParseLiveQuery', function () {
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
|
||||
const requestedUser = new Parse.User();
|
||||
requestedUser.setUsername('username');
|
||||
requestedUser.setPassword('password');
|
||||
Parse.Cloud.onLiveQueryEvent(req => {
|
||||
const { event, sessionToken } = req;
|
||||
if (event === 'ws_disconnect') {
|
||||
Parse.Cloud._removeAllHooks();
|
||||
expect(sessionToken).toBeDefined();
|
||||
expect(sessionToken).toBe(requestedUser.getSessionToken());
|
||||
done();
|
||||
@@ -56,6 +58,29 @@ describe('ParseLiveQuery', function () {
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('can use patterns in className', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['Test.*'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('update', object => {
|
||||
expect(object.get('foo')).toBe('bar');
|
||||
done();
|
||||
});
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('expect afterEvent create', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
@@ -294,6 +319,41 @@ describe('ParseLiveQuery', function () {
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('can log on afterLiveQueryEvent throw', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
|
||||
let session = undefined;
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', ({ sessionToken }) => {
|
||||
session = sessionToken;
|
||||
/* eslint-disable no-undef */
|
||||
foo.bar();
|
||||
/* eslint-enable no-undef */
|
||||
});
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
const subscription = await query.subscribe();
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
await new Promise(resolve => subscription.on('error', resolve));
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Failed running afterLiveQueryEvent on class TestObject for event update with session ${session} with:\n Error: {"message":"foo is not defined","code":141}`
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle afterEvent sendEvent to false', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
@@ -333,7 +393,7 @@ describe('ParseLiveQuery', function () {
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('expect afterEvent create', async done => {
|
||||
it('can handle afterEvent set pointers', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
@@ -342,172 +402,31 @@ describe('ParseLiveQuery', function () {
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
expect(req.event).toBe('create');
|
||||
expect(req.user).toBeUndefined();
|
||||
expect(req.object.get('foo')).toBe('bar');
|
||||
});
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('create', object => {
|
||||
expect(object.get('foo')).toBe('bar');
|
||||
done();
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
object.set('foo', 'bar');
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('expect afterEvent payload', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
expect(req.event).toBe('update');
|
||||
expect(req.user).toBeUndefined();
|
||||
expect(req.object.get('foo')).toBe('bar');
|
||||
expect(req.original.get('foo')).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
await query.subscribe();
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
});
|
||||
|
||||
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();
|
||||
expect(req.object.get('foo')).toBe('bar');
|
||||
expect(req.original.get('foo')).toBeUndefined();
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('foo', 'bar');
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('enter', object => {
|
||||
expect(object.get('foo')).toBe('bar');
|
||||
done();
|
||||
});
|
||||
const secondObject = new Parse.Object('Test2');
|
||||
secondObject.set('foo', 'bar');
|
||||
await secondObject.save();
|
||||
|
||||
object.set('foo', 'bar');
|
||||
await object.save();
|
||||
});
|
||||
|
||||
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();
|
||||
expect(req.object.get('foo')).toBeUndefined();
|
||||
expect(req.original.get('foo')).toBe('bar');
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
object.set('foo', 'bar');
|
||||
await object.save();
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('foo', 'bar');
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('leave', object => {
|
||||
expect(object.get('foo')).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
|
||||
object.unset('foo');
|
||||
await object.save();
|
||||
});
|
||||
|
||||
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();
|
||||
req.object.set('foo', 'bar');
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('delete', object => {
|
||||
expect(object.get('foo')).toBe('bar');
|
||||
done();
|
||||
});
|
||||
|
||||
await object.destroy();
|
||||
});
|
||||
|
||||
it('can handle afterEvent modification', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', req => {
|
||||
const current = req.object;
|
||||
current.set('foo', 'yolo');
|
||||
|
||||
const original = req.original;
|
||||
original.set('yolo', 'foo');
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', async ({ object }) => {
|
||||
const query = new Parse.Query('Test2');
|
||||
const obj = await query.first();
|
||||
object.set('obj', obj);
|
||||
});
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('objectId', object.id);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('update', (object, original) => {
|
||||
expect(object.get('foo')).toBe('yolo');
|
||||
expect(original.get('yolo')).toBe('foo');
|
||||
subscription.on('update', object => {
|
||||
expect(object.get('obj')).toBeDefined();
|
||||
expect(object.get('obj').get('foo')).toBe('bar');
|
||||
done();
|
||||
});
|
||||
subscription.on('error', () => {
|
||||
fail('error should not have been called.');
|
||||
});
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
});
|
||||
@@ -599,6 +518,7 @@ describe('ParseLiveQuery', function () {
|
||||
Parse.Cloud.beforeConnect(() => {}, validatorFail);
|
||||
let complete = false;
|
||||
Parse.LiveQuery.on('error', error => {
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
if (complete) {
|
||||
return;
|
||||
}
|
||||
@@ -672,6 +592,7 @@ describe('ParseLiveQuery', function () {
|
||||
throw new Error('You shall not pass!');
|
||||
});
|
||||
Parse.LiveQuery.on('error', error => {
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
expect(error).toBe('You shall not pass!');
|
||||
done();
|
||||
});
|
||||
@@ -680,6 +601,33 @@ describe('ParseLiveQuery', function () {
|
||||
await query.subscribe();
|
||||
});
|
||||
|
||||
it('can log on beforeConnect throw', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
let token = undefined;
|
||||
Parse.Cloud.beforeConnect(({ sessionToken }) => {
|
||||
token = sessionToken;
|
||||
/* eslint-disable no-undef */
|
||||
foo.bar();
|
||||
/* eslint-enable no-undef */
|
||||
});
|
||||
new Parse.Query(TestObject).subscribe();
|
||||
await new Promise(resolve => Parse.LiveQuery.on('error', resolve));
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Failed running beforeConnect for session ${token} with:\n Error: {"message":"foo is not defined","code":141}`
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle beforeSubscribe error', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
@@ -702,11 +650,40 @@ describe('ParseLiveQuery', function () {
|
||||
query.equalTo('objectId', object.id);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('error', error => {
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
expect(error).toBe('You shall not subscribe!');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can log on beforeSubscribe error', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const logger = require('../lib/logger').logger;
|
||||
spyOn(logger, 'error').and.callFake(() => {});
|
||||
|
||||
Parse.Cloud.beforeSubscribe(TestObject, () => {
|
||||
/* eslint-disable no-undef */
|
||||
foo.bar();
|
||||
/* eslint-enable no-undef */
|
||||
});
|
||||
|
||||
const query = new Parse.Query(TestObject);
|
||||
const subscription = await query.subscribe();
|
||||
await new Promise(resolve => subscription.on('error', resolve));
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`Failed running beforeSubscribe on TestObject for session undefined with:\n Error: {"message":"foo is not defined","code":141}`
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle mutate beforeSubscribe query', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
@@ -796,6 +773,121 @@ describe('ParseLiveQuery', function () {
|
||||
await object.save();
|
||||
});
|
||||
|
||||
it('LiveQuery with ACL', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['Chat'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username');
|
||||
user.setPassword('password');
|
||||
await user.signUp();
|
||||
|
||||
const calls = {
|
||||
beforeConnect(req) {
|
||||
expect(req.event).toBe('connect');
|
||||
expect(req.clients).toBe(0);
|
||||
expect(req.subscriptions).toBe(0);
|
||||
expect(req.useMasterKey).toBe(false);
|
||||
expect(req.installationId).toBeDefined();
|
||||
expect(req.client).toBeDefined();
|
||||
},
|
||||
beforeSubscribe(req) {
|
||||
expect(req.op).toBe('subscribe');
|
||||
expect(req.requestId).toBe(1);
|
||||
expect(req.query).toBeDefined();
|
||||
expect(req.user).toBeDefined();
|
||||
},
|
||||
afterLiveQueryEvent(req) {
|
||||
expect(req.user).toBeDefined();
|
||||
expect(req.object.get('foo')).toBe('bar');
|
||||
},
|
||||
create(object) {
|
||||
expect(object.get('foo')).toBe('bar');
|
||||
},
|
||||
delete(object) {
|
||||
expect(object.get('foo')).toBe('bar');
|
||||
},
|
||||
};
|
||||
for (const key in calls) {
|
||||
spyOn(calls, key).and.callThrough();
|
||||
}
|
||||
Parse.Cloud.beforeConnect(calls.beforeConnect);
|
||||
Parse.Cloud.beforeSubscribe('Chat', calls.beforeSubscribe);
|
||||
Parse.Cloud.afterLiveQueryEvent('Chat', calls.afterLiveQueryEvent);
|
||||
|
||||
const chatQuery = new Parse.Query('Chat');
|
||||
const subscription = await chatQuery.subscribe();
|
||||
subscription.on('create', calls.create);
|
||||
subscription.on('delete', calls.delete);
|
||||
const object = new Parse.Object('Chat');
|
||||
const acl = new Parse.ACL(user);
|
||||
object.setACL(acl);
|
||||
object.set({ foo: 'bar' });
|
||||
await object.save();
|
||||
await object.destroy();
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
for (const key in calls) {
|
||||
expect(calls[key]).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('liveQuery on Session class', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: { classNames: [Parse.Session] },
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('username');
|
||||
user.setPassword('password');
|
||||
await user.signUp();
|
||||
|
||||
const query = new Parse.Query(Parse.Session);
|
||||
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);
|
||||
expect(obj.get('installationId')).toBeDefined();
|
||||
expect(obj.get('createdAt')).toBeInstanceOf(Date);
|
||||
expect(obj.get('updatedAt')).toBeInstanceOf(Date);
|
||||
done();
|
||||
});
|
||||
|
||||
await Parse.User.logIn('username', 'password');
|
||||
});
|
||||
|
||||
it('prevent liveQuery on Session class when not logged in', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: [Parse.Session],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
Parse.LiveQuery.on('error', error => {
|
||||
expect(error).toBe('Invalid session token');
|
||||
});
|
||||
const query = new Parse.Query(Parse.Session);
|
||||
const subscription = await query.subscribe();
|
||||
subscription.on('error', error => {
|
||||
Parse.LiveQuery.removeAllListeners('error');
|
||||
expect(error).toBe('Invalid session token');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handle invalid websocket payload length', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
@@ -842,7 +934,7 @@ describe('ParseLiveQuery', function () {
|
||||
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['_User'],
|
||||
classNames: [Parse.User],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
|
||||
@@ -9,6 +9,8 @@ const queryHashValue = 'hash';
|
||||
const testUserId = 'userId';
|
||||
const testClassName = 'TestObject';
|
||||
|
||||
const timeout = () => jasmine.timeout(100);
|
||||
|
||||
describe('ParseLiveQueryServer', function () {
|
||||
beforeEach(function (done) {
|
||||
// Mock ParseWebSocketServer
|
||||
@@ -127,7 +129,10 @@ describe('ParseLiveQueryServer', function () {
|
||||
serverStartComplete: () => {
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).toBe(parseServer.server);
|
||||
parseServer.server.close(done);
|
||||
parseServer.server.close(async () => {
|
||||
await reconfigureServer();
|
||||
done();
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -149,7 +154,10 @@ describe('ParseLiveQueryServer', function () {
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
|
||||
parseServer.liveQueryServer.server.close(
|
||||
parseServer.server.close.bind(parseServer.server, done)
|
||||
parseServer.server.close.bind(parseServer.server, async () => {
|
||||
await reconfigureServer();
|
||||
done();
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -744,10 +752,10 @@ describe('ParseLiveQueryServer', function () {
|
||||
|
||||
// Make sure we send command to client, since _matchesACL is async, we have to
|
||||
// wait and check
|
||||
setTimeout(function () {
|
||||
expect(client.pushDelete).toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
await timeout();
|
||||
|
||||
expect(client.pushDelete).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('has no subscription and can handle object save command', async () => {
|
||||
@@ -779,14 +787,14 @@ describe('ParseLiveQueryServer', function () {
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we do not send command to client
|
||||
setTimeout(function () {
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
await timeout();
|
||||
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle object enter command which matches some subscriptions', async done => {
|
||||
@@ -816,14 +824,14 @@ describe('ParseLiveQueryServer', function () {
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send enter command to client
|
||||
setTimeout(function () {
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
await timeout();
|
||||
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle object update command which matches some subscriptions', async done => {
|
||||
@@ -849,14 +857,14 @@ describe('ParseLiveQueryServer', function () {
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send update command to client
|
||||
setTimeout(function () {
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
await timeout();
|
||||
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle object leave command which matches some subscriptions', async done => {
|
||||
@@ -886,14 +894,81 @@ describe('ParseLiveQueryServer', function () {
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send leave command to client
|
||||
setTimeout(function () {
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
await timeout();
|
||||
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('sends correct events for object with multiple subscriptions', async done => {
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
|
||||
Parse.Cloud.afterLiveQueryEvent('TestObject', () => {
|
||||
// Simulate delay due to trigger, auth, etc.
|
||||
return jasmine.timeout(10);
|
||||
});
|
||||
|
||||
// Make mock request message
|
||||
const message = generateMockMessage(true);
|
||||
// Add mock client
|
||||
const clientId = 1;
|
||||
const client = addMockClient(parseLiveQueryServer, clientId);
|
||||
client.sessionToken = 'sessionToken';
|
||||
|
||||
// Mock queryHash for this special test
|
||||
const mockQueryHash = jasmine.createSpy('matchesQuery').and.returnValue('hash1');
|
||||
jasmine.mockLibrary('../lib/LiveQuery/QueryTools', 'queryHash', mockQueryHash);
|
||||
// Add mock subscription 1
|
||||
const requestId2 = 2;
|
||||
await addMockSubscription(parseLiveQueryServer, clientId, requestId2, null, null, 'hash1');
|
||||
|
||||
// Mock queryHash for this special test
|
||||
const mockQueryHash2 = jasmine.createSpy('matchesQuery').and.returnValue('hash2');
|
||||
jasmine.mockLibrary('../lib/LiveQuery/QueryTools', 'queryHash', mockQueryHash2);
|
||||
// Add mock subscription 2
|
||||
const requestId3 = 3;
|
||||
await addMockSubscription(parseLiveQueryServer, clientId, requestId3, null, null, 'hash2');
|
||||
// Mock _matchesSubscription to return matching
|
||||
// In order to mimic a leave, then enter, we need original match return true
|
||||
// and the current match return false, then the other way around
|
||||
let counter = 0;
|
||||
parseLiveQueryServer._matchesSubscription = function (parseObject) {
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
counter += 1;
|
||||
// true, false, false, true
|
||||
return counter < 2 || counter > 3;
|
||||
};
|
||||
parseLiveQueryServer._matchesACL = function () {
|
||||
// Simulate call
|
||||
return jasmine.timeout(10).then(() => true);
|
||||
};
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send leave and enter command to client
|
||||
await timeout();
|
||||
|
||||
expect(client.pushCreate).not.toHaveBeenCalled();
|
||||
expect(client.pushEnter).toHaveBeenCalledTimes(1);
|
||||
expect(client.pushEnter).toHaveBeenCalledWith(
|
||||
requestId3,
|
||||
{ key: 'value', className: 'TestObject' },
|
||||
{ key: 'originalValue', className: 'TestObject' }
|
||||
);
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).toHaveBeenCalledTimes(1);
|
||||
expect(client.pushLeave).toHaveBeenCalledWith(
|
||||
requestId2,
|
||||
{ key: 'value', className: 'TestObject' },
|
||||
{ key: 'originalValue', className: 'TestObject' }
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle update command with original object', async done => {
|
||||
@@ -930,15 +1005,15 @@ describe('ParseLiveQueryServer', function () {
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send update command to client
|
||||
setTimeout(function () {
|
||||
expect(client.pushUpdate).toHaveBeenCalled();
|
||||
const args = parseWebSocket.send.calls.mostRecent().args;
|
||||
const toSend = JSON.parse(args[0]);
|
||||
await timeout();
|
||||
|
||||
expect(toSend.object).toBeDefined();
|
||||
expect(toSend.original).toBeDefined();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
expect(client.pushUpdate).toHaveBeenCalled();
|
||||
const args = parseWebSocket.send.calls.mostRecent().args;
|
||||
const toSend = JSON.parse(args[0]);
|
||||
|
||||
expect(toSend.object).toBeDefined();
|
||||
expect(toSend.original).toBeDefined();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle object create command which matches some subscriptions', async done => {
|
||||
@@ -964,14 +1039,14 @@ describe('ParseLiveQueryServer', function () {
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send create command to client
|
||||
setTimeout(function () {
|
||||
expect(client.pushCreate).toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
await timeout();
|
||||
|
||||
expect(client.pushCreate).toHaveBeenCalled();
|
||||
expect(client.pushEnter).not.toHaveBeenCalled();
|
||||
expect(client.pushUpdate).not.toHaveBeenCalled();
|
||||
expect(client.pushDelete).not.toHaveBeenCalled();
|
||||
expect(client.pushLeave).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can handle create command with fields', async done => {
|
||||
@@ -1014,14 +1089,14 @@ describe('ParseLiveQueryServer', function () {
|
||||
parseLiveQueryServer._onAfterSave(message);
|
||||
|
||||
// Make sure we send create command to client
|
||||
setTimeout(function () {
|
||||
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();
|
||||
done();
|
||||
}, jasmine.ASYNC_TEST_WAIT_TIME);
|
||||
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();
|
||||
done();
|
||||
});
|
||||
|
||||
it('can match subscription for null or undefined parse object', function () {
|
||||
@@ -1731,7 +1806,8 @@ describe('ParseLiveQueryServer', function () {
|
||||
clientId,
|
||||
requestId,
|
||||
parseWebSocket,
|
||||
query
|
||||
query,
|
||||
customQueryHashValue
|
||||
) {
|
||||
// If parseWebSocket is null, we use the default one
|
||||
if (!parseWebSocket) {
|
||||
@@ -1759,12 +1835,12 @@ describe('ParseLiveQueryServer', function () {
|
||||
// Make mock subscription
|
||||
const subscription = parseLiveQueryServer.subscriptions
|
||||
.get(query.className)
|
||||
.get(queryHashValue);
|
||||
.get(customQueryHashValue || queryHashValue);
|
||||
subscription.hasSubscribingClient = function () {
|
||||
return false;
|
||||
};
|
||||
subscription.className = query.className;
|
||||
subscription.hash = queryHashValue;
|
||||
subscription.hash = customQueryHashValue || queryHashValue;
|
||||
if (subscription.clientRequestIds && subscription.clientRequestIds.has(clientId)) {
|
||||
subscription.clientRequestIds.get(clientId).push(requestId);
|
||||
} else {
|
||||
|
||||
@@ -223,7 +223,7 @@ describe('Parse.Object testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('can set null', function (done) {
|
||||
it('can set null', function (done) {
|
||||
const obj = new Parse.Object('TestObject');
|
||||
obj.set('foo', null);
|
||||
obj.save().then(
|
||||
@@ -232,7 +232,7 @@ describe('Parse.Object testing', () => {
|
||||
equal(obj.get('foo'), null);
|
||||
});
|
||||
on_db('postgres', () => {
|
||||
fail('should not succeed');
|
||||
equal(obj.get('foo'), null);
|
||||
});
|
||||
done();
|
||||
},
|
||||
|
||||
+28
-21
@@ -1,7 +1,6 @@
|
||||
const TestObject = Parse.Object.extend('TestObject');
|
||||
const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default;
|
||||
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||
const request = require('../lib/request');
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
const defaultHeaders = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'rest',
|
||||
@@ -9,8 +8,6 @@ const defaultHeaders = {
|
||||
};
|
||||
|
||||
describe('Parse.Polygon testing', () => {
|
||||
beforeAll(() => require('../lib/TestUtils').destroyAllDataPermanently());
|
||||
|
||||
it('polygon save open path', done => {
|
||||
const coords = [
|
||||
[0, 0],
|
||||
@@ -211,7 +208,9 @@ describe('Parse.Polygon testing', () => {
|
||||
});
|
||||
|
||||
describe('with location', () => {
|
||||
beforeAll(() => require('../lib/TestUtils').destroyAllDataPermanently());
|
||||
if (process.env.PARSE_SERVER_TEST_DB !== 'postgres') {
|
||||
beforeEach(async () => await TestUtils.destroyAllDataPermanently());
|
||||
}
|
||||
|
||||
it('polygonContain query', done => {
|
||||
const points1 = [
|
||||
@@ -236,13 +235,13 @@ describe('Parse.Polygon testing', () => {
|
||||
const polygon1 = new Parse.Polygon(points1);
|
||||
const polygon2 = new Parse.Polygon(points2);
|
||||
const polygon3 = new Parse.Polygon(points3);
|
||||
const obj1 = new TestObject({ location: polygon1 });
|
||||
const obj2 = new TestObject({ location: polygon2 });
|
||||
const obj3 = new TestObject({ location: polygon3 });
|
||||
const obj1 = new TestObject({ boundary: polygon1 });
|
||||
const obj2 = new TestObject({ boundary: polygon2 });
|
||||
const obj3 = new TestObject({ boundary: polygon3 });
|
||||
Parse.Object.saveAll([obj1, obj2, obj3])
|
||||
.then(() => {
|
||||
const where = {
|
||||
location: {
|
||||
boundary: {
|
||||
$geoIntersects: {
|
||||
$point: { __type: 'GeoPoint', latitude: 0.5, longitude: 0.5 },
|
||||
},
|
||||
@@ -288,13 +287,13 @@ describe('Parse.Polygon testing', () => {
|
||||
const polygon1 = new Parse.Polygon(points1);
|
||||
const polygon2 = new Parse.Polygon(points2);
|
||||
const polygon3 = new Parse.Polygon(points3);
|
||||
const obj1 = new TestObject({ location: polygon1 });
|
||||
const obj2 = new TestObject({ location: polygon2 });
|
||||
const obj3 = new TestObject({ location: polygon3 });
|
||||
const obj1 = new TestObject({ boundary: polygon1 });
|
||||
const obj2 = new TestObject({ boundary: polygon2 });
|
||||
const obj3 = new TestObject({ boundary: polygon3 });
|
||||
Parse.Object.saveAll([obj1, obj2, obj3])
|
||||
.then(() => {
|
||||
const where = {
|
||||
location: {
|
||||
boundary: {
|
||||
$geoIntersects: {
|
||||
$point: { __type: 'GeoPoint', latitude: 0.5, longitude: 1.0 },
|
||||
},
|
||||
@@ -326,12 +325,12 @@ describe('Parse.Polygon testing', () => {
|
||||
[42.631655189280224, -83.78406753121705],
|
||||
];
|
||||
const polygon = new Parse.Polygon(detroit);
|
||||
const obj = new TestObject({ location: polygon });
|
||||
const obj = new TestObject({ boundary: polygon });
|
||||
obj
|
||||
.save()
|
||||
.then(() => {
|
||||
const where = {
|
||||
location: {
|
||||
boundary: {
|
||||
$geoIntersects: {
|
||||
$point: {
|
||||
__type: 'GeoPoint',
|
||||
@@ -366,12 +365,12 @@ describe('Parse.Polygon testing', () => {
|
||||
[1, 0],
|
||||
];
|
||||
const polygon = new Parse.Polygon(points);
|
||||
const obj = new TestObject({ location: polygon });
|
||||
const obj = new TestObject({ boundary: polygon });
|
||||
obj
|
||||
.save()
|
||||
.then(() => {
|
||||
const where = {
|
||||
location: {
|
||||
boundary: {
|
||||
$geoIntersects: {
|
||||
$point: { __type: 'GeoPoint', latitude: 181, longitude: 181 },
|
||||
},
|
||||
@@ -398,12 +397,12 @@ describe('Parse.Polygon testing', () => {
|
||||
[1, 0],
|
||||
];
|
||||
const polygon = new Parse.Polygon(points);
|
||||
const obj = new TestObject({ location: polygon });
|
||||
const obj = new TestObject({ boundary: polygon });
|
||||
obj
|
||||
.save()
|
||||
.then(() => {
|
||||
const where = {
|
||||
location: {
|
||||
boundary: {
|
||||
$geoIntersects: {
|
||||
$point: [],
|
||||
},
|
||||
@@ -425,7 +424,15 @@ describe('Parse.Polygon testing', () => {
|
||||
});
|
||||
|
||||
describe_only_db('mongo')('Parse.Polygon testing', () => {
|
||||
beforeEach(() => require('../lib/TestUtils').destroyAllDataPermanently());
|
||||
const Config = require('../lib/Config');
|
||||
let config;
|
||||
beforeEach(async () => {
|
||||
if (process.env.PARSE_SERVER_TEST_DB !== 'postgres') {
|
||||
await TestUtils.destroyAllDataPermanently();
|
||||
}
|
||||
config = Config.get('test');
|
||||
config.schemaCache.clear();
|
||||
});
|
||||
it('support 2d and 2dsphere', done => {
|
||||
const coords = [
|
||||
[0, 0],
|
||||
@@ -437,7 +444,7 @@ describe_only_db('mongo')('Parse.Polygon testing', () => {
|
||||
// testings against REST API, use raw formats
|
||||
const polygon = { __type: 'Polygon', coordinates: coords };
|
||||
const location = { __type: 'GeoPoint', latitude: 10, longitude: 10 };
|
||||
const databaseAdapter = new MongoStorageAdapter({ uri: mongoURI });
|
||||
const databaseAdapter = config.database.adapter;
|
||||
return reconfigureServer({
|
||||
appId: 'test',
|
||||
restAPIKey: 'rest',
|
||||
|
||||
@@ -23,28 +23,28 @@ const loadTestData = () => {
|
||||
const data1 = {
|
||||
score: 10,
|
||||
name: 'foo',
|
||||
sender: { group: 'A' },
|
||||
sender: { group: 'A' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
views: 900,
|
||||
size: ['S', 'M'],
|
||||
};
|
||||
const data2 = {
|
||||
score: 10,
|
||||
name: 'foo',
|
||||
sender: { group: 'A' },
|
||||
sender: { group: 'A' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
views: 800,
|
||||
size: ['M', 'L'],
|
||||
};
|
||||
const data3 = {
|
||||
score: 10,
|
||||
name: 'bar',
|
||||
sender: { group: 'B' },
|
||||
sender: { group: 'B' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
views: 700,
|
||||
size: ['S'],
|
||||
};
|
||||
const data4 = {
|
||||
score: 20,
|
||||
name: 'dpl',
|
||||
sender: { group: 'B' },
|
||||
sender: { group: 'B' }, // TODO: change to `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
views: 700,
|
||||
size: ['S'],
|
||||
};
|
||||
@@ -83,34 +83,10 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('invalid query invalid key', done => {
|
||||
it('invalid query group _id required', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
unknown: {},
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options).catch(error => {
|
||||
expect(error.error.code).toEqual(Parse.Error.INVALID_QUERY);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('invalid query group _id', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
group: { _id: null },
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options).catch(error => {
|
||||
expect(error.error.code).toEqual(Parse.Error.INVALID_QUERY);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('invalid query group objectId required', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
group: {},
|
||||
group: {}, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options).catch(error => {
|
||||
@@ -122,7 +98,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group by field', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
group: { objectId: '$name' },
|
||||
group: { objectId: '$name' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -143,7 +119,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
pipeline: {
|
||||
group: { objectId: '$name' },
|
||||
group: { objectId: '$name' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -161,7 +137,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
group: { objectId: {} },
|
||||
group: { objectId: {} }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -180,7 +156,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
group: { objectId: '' },
|
||||
group: { objectId: '' }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -199,7 +175,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
group: { objectId: [] },
|
||||
group: { objectId: [] }, // TODO: write as `$group`. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
];
|
||||
obj
|
||||
@@ -220,6 +196,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: {
|
||||
score: '$score',
|
||||
@@ -246,8 +223,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
group: {
|
||||
objectId: {
|
||||
$group: {
|
||||
_id: {
|
||||
day: { $dayOfMonth: '$_updated_at' },
|
||||
month: { $month: '$_created_at' },
|
||||
year: { $year: '$_created_at' },
|
||||
@@ -276,8 +253,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject();
|
||||
const pipeline = [
|
||||
{
|
||||
group: {
|
||||
objectId: {
|
||||
$group: {
|
||||
_id: {
|
||||
day: { $dayOfMonth: '$updatedAt' },
|
||||
month: { $month: '$createdAt' },
|
||||
year: { $year: '$createdAt' },
|
||||
@@ -303,7 +280,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group by number', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
group: { objectId: '$score' },
|
||||
group: { objectId: '$score' }, // TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
},
|
||||
});
|
||||
get(Parse.serverURL + '/aggregate/TestObject', options)
|
||||
@@ -325,6 +302,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 });
|
||||
const pipeline = [
|
||||
{
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: null,
|
||||
total: { $sum: { $multiply: ['$quantity', '$price'] } },
|
||||
@@ -384,7 +362,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
},
|
||||
{
|
||||
project: {
|
||||
objectId: 0,
|
||||
objectId: 0, // TODO: change to `_id`. See [#7339](https://bit.ly/3incnWx)
|
||||
total: { $multiply: ['$quantity', '$price'] },
|
||||
},
|
||||
},
|
||||
@@ -471,6 +449,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const obj3 = new TestObject({ dateField2019: new Date(1990, 11, 1) });
|
||||
const pipeline = [
|
||||
{
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: {
|
||||
objectId: {
|
||||
day: { $dayOfMonth: '$dateField2019' },
|
||||
@@ -520,6 +499,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group sum query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: '$score' } },
|
||||
},
|
||||
});
|
||||
@@ -536,6 +516,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group count query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: 1 } },
|
||||
},
|
||||
});
|
||||
@@ -552,6 +533,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group min query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, minScore: { $min: '$score' } },
|
||||
},
|
||||
});
|
||||
@@ -568,6 +550,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group max query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, maxScore: { $max: '$score' } },
|
||||
},
|
||||
});
|
||||
@@ -584,6 +567,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('group avg query', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, avgScore: { $avg: '$score' } },
|
||||
},
|
||||
});
|
||||
@@ -1029,6 +1013,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
project: { score: 1 },
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: '$score', score: { $sum: '$score' } },
|
||||
},
|
||||
});
|
||||
@@ -1056,6 +1041,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('class does not exist return empty', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: '$score' } },
|
||||
},
|
||||
});
|
||||
@@ -1070,6 +1056,7 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
it('field does not exist return empty', done => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
// TODO: update to new syntax. See [#7339](https://bit.ly/3incnWx)
|
||||
group: { objectId: null, total: { $sum: '$unknownfield' } },
|
||||
},
|
||||
});
|
||||
@@ -1312,7 +1299,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('aggregate allow multiple of same stage', done => {
|
||||
it_exclude_dbs(['postgres'])('aggregate allow multiple of same stage', async done => {
|
||||
await reconfigureServer();
|
||||
const pointer1 = new TestObject({ value: 1 });
|
||||
const pointer2 = new TestObject({ value: 2 });
|
||||
const pointer3 = new TestObject({ value: 3 });
|
||||
@@ -1415,20 +1403,16 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
expect(results.length).toEqual(2);
|
||||
expect(results[0].value).toEqual(2);
|
||||
expect(results[1].value).toEqual(3);
|
||||
await database.adapter.deleteAllClasses(false);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('aggregate geoNear with near GeoJSON point', async () => {
|
||||
// Create geo index which is required for `geoNear` query
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
const schema = await new Parse.Schema('GeoObject').save();
|
||||
await database.adapter.ensureIndex(
|
||||
'GeoObject',
|
||||
schema,
|
||||
['location'],
|
||||
undefined,
|
||||
false,
|
||||
'2dsphere'
|
||||
);
|
||||
await database.adapter.ensureIndex('GeoObject', schema, ['location'], undefined, false, {
|
||||
indexType: '2dsphere',
|
||||
});
|
||||
// Create objects
|
||||
const GeoObject = Parse.Object.extend('GeoObject');
|
||||
const obj1 = new GeoObject({
|
||||
@@ -1465,20 +1449,16 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const results = await query.aggregate(pipeline);
|
||||
// Check results
|
||||
expect(results.length).toEqual(3);
|
||||
await database.adapter.deleteAllClasses(false);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('aggregate geoNear with near legacy coordinate pair', async () => {
|
||||
// Create geo index which is required for `geoNear` query
|
||||
const database = Config.get(Parse.applicationId).database;
|
||||
const schema = await new Parse.Schema('GeoObject').save();
|
||||
await database.adapter.ensureIndex(
|
||||
'GeoObject',
|
||||
schema,
|
||||
['location'],
|
||||
undefined,
|
||||
false,
|
||||
'2dsphere'
|
||||
);
|
||||
await database.adapter.ensureIndex('GeoObject', schema, ['location'], undefined, false, {
|
||||
indexType: '2dsphere',
|
||||
});
|
||||
// Create objects
|
||||
const GeoObject = Parse.Object.extend('GeoObject');
|
||||
const obj1 = new GeoObject({
|
||||
@@ -1512,5 +1492,6 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
const results = await query.aggregate(pipeline);
|
||||
// Check results
|
||||
expect(results.length).toEqual(3);
|
||||
await database.adapter.deleteAllClasses(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,10 +5,9 @@ const Parse = require('parse/node');
|
||||
const request = require('../lib/request');
|
||||
let databaseAdapter;
|
||||
|
||||
const fullTextHelper = () => {
|
||||
const fullTextHelper = async () => {
|
||||
const config = Config.get('test');
|
||||
databaseAdapter = config.database.adapter;
|
||||
|
||||
const subjects = [
|
||||
'coffee',
|
||||
'Coffee Shopping',
|
||||
@@ -19,121 +18,84 @@ const fullTextHelper = () => {
|
||||
'coffee and cream',
|
||||
'Cafe con Leche',
|
||||
];
|
||||
const requests = [];
|
||||
for (const i in subjects) {
|
||||
const request = {
|
||||
method: 'POST',
|
||||
body: {
|
||||
subject: subjects[i],
|
||||
comment: subjects[i],
|
||||
},
|
||||
path: '/1/classes/TestObject',
|
||||
};
|
||||
requests.push(request);
|
||||
}
|
||||
return reconfigureServer({
|
||||
await reconfigureServer({
|
||||
appId: 'test',
|
||||
restAPIKey: 'test',
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
databaseAdapter,
|
||||
}).then(() => {
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/batch',
|
||||
body: {
|
||||
requests,
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
});
|
||||
await Parse.Object.saveAll(
|
||||
subjects.map(subject => new Parse.Object('TestObject').set({ subject, comment: subject }))
|
||||
);
|
||||
};
|
||||
|
||||
describe('Parse.Query Full Text Search testing', () => {
|
||||
it('fullTextSearch: $search', done => {
|
||||
fullTextHelper()
|
||||
.then(() => {
|
||||
const where = {
|
||||
subject: {
|
||||
$text: {
|
||||
$search: {
|
||||
$term: 'coffee',
|
||||
},
|
||||
},
|
||||
it('fullTextSearch: $search', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'coffee');
|
||||
const results = await query.find();
|
||||
expect(results.length).toBe(3);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $search, sort', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'coffee');
|
||||
query.select('$score');
|
||||
query.ascending('$score');
|
||||
const results = await query.find();
|
||||
expect(results.length).toBe(3);
|
||||
expect(results[0].get('score'));
|
||||
expect(results[1].get('score'));
|
||||
expect(results[2].get('score'));
|
||||
});
|
||||
|
||||
it('fulltext descending by $score', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'coffee');
|
||||
query.descending('$score');
|
||||
query.select('$score');
|
||||
const [first, second, third] = await query.find();
|
||||
expect(first).toBeDefined();
|
||||
expect(second).toBeDefined();
|
||||
expect(third).toBeDefined();
|
||||
expect(first.get('score'));
|
||||
expect(second.get('score'));
|
||||
expect(third.get('score'));
|
||||
expect(first.get('score') >= second.get('score')).toBeTrue();
|
||||
expect(second.get('score') >= third.get('score')).toBeTrue();
|
||||
});
|
||||
|
||||
it('fullTextSearch: $language', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'leche', { language: 'spanish' });
|
||||
const resp = await query.find();
|
||||
expect(resp.length).toBe(2);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $diacriticSensitive', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'CAFÉ', { diacriticSensitive: true });
|
||||
const resp = await query.find();
|
||||
expect(resp.length).toBe(1);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $search, invalid input', async () => {
|
||||
await fullTextHelper();
|
||||
const invalidQuery = async () => {
|
||||
const where = {
|
||||
subject: {
|
||||
$text: {
|
||||
$search: true,
|
||||
},
|
||||
};
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(
|
||||
resp => {
|
||||
expect(resp.data.results.length).toBe(3);
|
||||
done();
|
||||
},
|
||||
e => done.fail(e)
|
||||
);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $search, sort', done => {
|
||||
fullTextHelper()
|
||||
.then(() => {
|
||||
const where = {
|
||||
subject: {
|
||||
$text: {
|
||||
$search: {
|
||||
$term: 'coffee',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const order = '$score';
|
||||
const keys = '$score';
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
body: { where, order, keys, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(response => {
|
||||
const resp = response.data;
|
||||
expect(resp.results.length).toBe(3);
|
||||
expect(resp.results[0].score);
|
||||
expect(resp.results[1].score);
|
||||
expect(resp.results[2].score);
|
||||
done();
|
||||
}, done.fail);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $language', done => {
|
||||
fullTextHelper()
|
||||
.then(() => {
|
||||
const where = {
|
||||
subject: {
|
||||
$text: {
|
||||
$search: {
|
||||
$term: 'leche',
|
||||
$language: 'spanish',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return request({
|
||||
};
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
body: { where, _method: 'GET' },
|
||||
@@ -143,242 +105,73 @@ describe('Parse.Query Full Text Search testing', () => {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(resp => {
|
||||
expect(resp.data.results.length).toBe(2);
|
||||
done();
|
||||
}, done.fail);
|
||||
} catch (e) {
|
||||
throw new Parse.Error(e.data.code, e.data.error);
|
||||
}
|
||||
};
|
||||
await expectAsync(invalidQuery()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_JSON, 'bad $text: $search, should be object')
|
||||
);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $diacriticSensitive', done => {
|
||||
fullTextHelper()
|
||||
.then(() => {
|
||||
const where = {
|
||||
subject: {
|
||||
$text: {
|
||||
$search: {
|
||||
$term: 'CAFÉ',
|
||||
$diacriticSensitive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(resp => {
|
||||
expect(resp.data.results.length).toBe(1);
|
||||
done();
|
||||
}, done.fail);
|
||||
it('fullTextSearch: $language, invalid input', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'leche', { language: true });
|
||||
await expectAsync(query.find()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_JSON, 'bad $text: $language, should be string')
|
||||
);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $search, invalid input', done => {
|
||||
fullTextHelper()
|
||||
.then(() => {
|
||||
const where = {
|
||||
subject: {
|
||||
$text: {
|
||||
$search: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(resp => {
|
||||
fail(`no request should succeed: ${JSON.stringify(resp)}`);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
expect(err.data.code).toEqual(Parse.Error.INVALID_JSON);
|
||||
done();
|
||||
});
|
||||
it('fullTextSearch: $caseSensitive, invalid input', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'leche', { caseSensitive: 'string' });
|
||||
await expectAsync(query.find()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_JSON, 'bad $text: $caseSensitive, should be boolean')
|
||||
);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $language, invalid input', done => {
|
||||
fullTextHelper()
|
||||
.then(() => {
|
||||
const where = {
|
||||
subject: {
|
||||
$text: {
|
||||
$search: {
|
||||
$term: 'leche',
|
||||
$language: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(resp => {
|
||||
fail(`no request should succeed: ${JSON.stringify(resp)}`);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
expect(err.data.code).toEqual(Parse.Error.INVALID_JSON);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fullTextSearch: $caseSensitive, invalid input', done => {
|
||||
fullTextHelper()
|
||||
.then(() => {
|
||||
const where = {
|
||||
subject: {
|
||||
$text: {
|
||||
$search: {
|
||||
$term: 'Coffee',
|
||||
$caseSensitive: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(resp => {
|
||||
fail(`no request should succeed: ${JSON.stringify(resp)}`);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
expect(err.data.code).toEqual(Parse.Error.INVALID_JSON);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fullTextSearch: $diacriticSensitive, invalid input', done => {
|
||||
fullTextHelper()
|
||||
.then(() => {
|
||||
const where = {
|
||||
subject: {
|
||||
$text: {
|
||||
$search: {
|
||||
$term: 'CAFÉ',
|
||||
$diacriticSensitive: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(resp => {
|
||||
fail(`no request should succeed: ${JSON.stringify(resp)}`);
|
||||
done();
|
||||
})
|
||||
.catch(err => {
|
||||
expect(err.data.code).toEqual(Parse.Error.INVALID_JSON);
|
||||
done();
|
||||
});
|
||||
it('fullTextSearch: $diacriticSensitive, invalid input', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'leche', { diacriticSensitive: 'string' });
|
||||
await expectAsync(query.find()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_JSON, 'bad $text: $diacriticSensitive, should be boolean')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () => {
|
||||
it('fullTextSearch: does not create text index if compound index exist', done => {
|
||||
fullTextHelper()
|
||||
.then(() => {
|
||||
return databaseAdapter.dropAllIndexes('TestObject');
|
||||
})
|
||||
.then(() => {
|
||||
return databaseAdapter.getIndexes('TestObject');
|
||||
})
|
||||
.then(indexes => {
|
||||
expect(indexes.length).toEqual(1);
|
||||
return databaseAdapter.createIndex('TestObject', {
|
||||
subject: 'text',
|
||||
comment: 'text',
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return databaseAdapter.getIndexes('TestObject');
|
||||
})
|
||||
.then(indexes => {
|
||||
expect(indexes.length).toEqual(2);
|
||||
const where = {
|
||||
subject: {
|
||||
$text: {
|
||||
$search: {
|
||||
$term: 'coffee',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(resp => {
|
||||
expect(resp.data.results.length).toEqual(3);
|
||||
return databaseAdapter.getIndexes('TestObject');
|
||||
})
|
||||
.then(indexes => {
|
||||
expect(indexes.length).toEqual(2);
|
||||
request({
|
||||
url: 'http://localhost:8378/1/schemas/TestObject',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(response => {
|
||||
const body = response.data;
|
||||
expect(body.indexes._id_).toBeDefined();
|
||||
expect(body.indexes._id_._id).toEqual(1);
|
||||
expect(body.indexes.subject_text_comment_text).toBeDefined();
|
||||
expect(body.indexes.subject_text_comment_text.subject).toEqual('text');
|
||||
expect(body.indexes.subject_text_comment_text.comment).toEqual('text');
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(done.fail);
|
||||
it('fullTextSearch: does not create text index if compound index exist', async () => {
|
||||
await fullTextHelper();
|
||||
await databaseAdapter.dropAllIndexes('TestObject');
|
||||
let indexes = await databaseAdapter.getIndexes('TestObject');
|
||||
expect(indexes.length).toEqual(1);
|
||||
await databaseAdapter.createIndex('TestObject', {
|
||||
subject: 'text',
|
||||
comment: 'text',
|
||||
});
|
||||
indexes = await databaseAdapter.getIndexes('TestObject');
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'coffee');
|
||||
query.select('$score');
|
||||
query.ascending('$score');
|
||||
const results = await query.find();
|
||||
expect(results.length).toBe(3);
|
||||
expect(results[0].get('score'));
|
||||
expect(results[1].get('score'));
|
||||
expect(results[2].get('score'));
|
||||
|
||||
indexes = await databaseAdapter.getIndexes('TestObject');
|
||||
expect(indexes.length).toEqual(2);
|
||||
|
||||
const schemas = await new Parse.Schema('TestObject').get();
|
||||
expect(schemas.indexes._id_).toBeDefined();
|
||||
expect(schemas.indexes._id_._id).toEqual(1);
|
||||
expect(schemas.indexes.subject_text_comment_text).toBeDefined();
|
||||
expect(schemas.indexes.subject_text_comment_text.subject).toEqual('text');
|
||||
expect(schemas.indexes.subject_text_comment_text.comment).toEqual('text');
|
||||
});
|
||||
|
||||
it('fullTextSearch: does not create text index if schema compound index exist', done => {
|
||||
@@ -458,64 +251,20 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () =
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $diacriticSensitive - false', done => {
|
||||
fullTextHelper()
|
||||
.then(() => {
|
||||
const where = {
|
||||
subject: {
|
||||
$text: {
|
||||
$search: {
|
||||
$term: 'CAFÉ',
|
||||
$diacriticSensitive: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(resp => {
|
||||
expect(resp.data.results.length).toBe(2);
|
||||
done();
|
||||
}, done.fail);
|
||||
it('fullTextSearch: $diacriticSensitive - false', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'CAFÉ', { diacriticSensitive: false });
|
||||
const resp = await query.find();
|
||||
expect(resp.length).toBe(2);
|
||||
});
|
||||
|
||||
it('fullTextSearch: $caseSensitive', done => {
|
||||
fullTextHelper()
|
||||
.then(() => {
|
||||
const where = {
|
||||
subject: {
|
||||
$text: {
|
||||
$search: {
|
||||
$term: 'Coffee',
|
||||
$caseSensitive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/classes/TestObject',
|
||||
body: { where, _method: 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(resp => {
|
||||
expect(resp.data.results.length).toBe(1);
|
||||
done();
|
||||
}, done.fail);
|
||||
it('fullTextSearch: $caseSensitive', async () => {
|
||||
await fullTextHelper();
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.fullText('subject', 'Coffee', { caseSensitive: true });
|
||||
const results = await query.find();
|
||||
expect(results.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await config.database.schemaCache.clear();
|
||||
await TestUtils.destroyAllDataPermanently(false);
|
||||
});
|
||||
|
||||
@@ -54,7 +53,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('query aggregate with hint string', async () => {
|
||||
it_only_mongodb_version('<4.4')('query aggregate with hint string', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
@@ -74,7 +73,31 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it('query aggregate with hint object', async () => {
|
||||
it_only_mongodb_version('>=4.4')('query aggregate with hint string', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage).toBeUndefined();
|
||||
|
||||
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
hint: '_id_',
|
||||
explain: true,
|
||||
});
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.inputStage.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.stage).toBe('IXSCAN');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it_only_mongodb_version('<4.4')('query aggregate with hint object', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
@@ -94,6 +117,31 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=4.4')('query aggregate with hint object', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection('TestObject');
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage).toBeUndefined();
|
||||
|
||||
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
hint: { _id: 1 },
|
||||
explain: true,
|
||||
});
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.inputStage.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.stage).toBe('IXSCAN');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it('query find with hint (rest)', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
@@ -119,7 +167,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
expect(explain.queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it('query aggregate with hint (rest)', async () => {
|
||||
it_only_mongodb_version('<4.4')('query aggregate with hint (rest)', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
@@ -145,4 +193,37 @@ describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it_only_mongodb_version('>=4.4')('query aggregate with hint (rest)', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
let { queryPlanner } = response.data.results[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage).toBeUndefined();
|
||||
|
||||
options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('PROJECTION_SIMPLE');
|
||||
expect(queryPlanner.winningPlan.inputStage.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.stage).toBe('IXSCAN');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
|
||||
expect(queryPlanner.winningPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
+614
-186
@@ -18,6 +18,10 @@ const masterKeyOptions = {
|
||||
headers: masterKeyHeaders,
|
||||
};
|
||||
|
||||
const BoxedNumber = Parse.Object.extend({
|
||||
className: 'BoxedNumber',
|
||||
});
|
||||
|
||||
describe('Parse.Query testing', () => {
|
||||
it('basic query', function (done) {
|
||||
const baz = new TestObject({ foo: 'baz' });
|
||||
@@ -323,32 +327,26 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('query with limit equal to maxlimit', function (done) {
|
||||
it('query with limit equal to maxlimit', async () => {
|
||||
const baz = new TestObject({ foo: 'baz' });
|
||||
const qux = new TestObject({ foo: 'qux' });
|
||||
reconfigureServer({ maxLimit: 1 });
|
||||
Parse.Object.saveAll([baz, qux]).then(function () {
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.limit(1);
|
||||
query.find().then(function (results) {
|
||||
equal(results.length, 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
await reconfigureServer({ maxLimit: 1 });
|
||||
await Parse.Object.saveAll([baz, qux]);
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.limit(1);
|
||||
const results = await query.find();
|
||||
equal(results.length, 1);
|
||||
});
|
||||
|
||||
it('query with limit exceeding maxlimit', function (done) {
|
||||
it('query with limit exceeding maxlimit', async () => {
|
||||
const baz = new TestObject({ foo: 'baz' });
|
||||
const qux = new TestObject({ foo: 'qux' });
|
||||
reconfigureServer({ maxLimit: 1 });
|
||||
Parse.Object.saveAll([baz, qux]).then(function () {
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.limit(2);
|
||||
query.find().then(function (results) {
|
||||
equal(results.length, 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
await reconfigureServer({ maxLimit: 1 });
|
||||
await Parse.Object.saveAll([baz, qux]);
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.limit(2);
|
||||
const results = await query.find();
|
||||
equal(results.length, 1);
|
||||
});
|
||||
|
||||
it('containedIn object array queries', function (done) {
|
||||
@@ -987,10 +985,6 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const BoxedNumber = Parse.Object.extend({
|
||||
className: 'BoxedNumber',
|
||||
});
|
||||
|
||||
it('equalTo queries', function (done) {
|
||||
const makeBoxedNumber = function (i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
@@ -2096,9 +2090,9 @@ describe('Parse.Query testing', () => {
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.matches(
|
||||
'myString',
|
||||
"parse # First fragment. We'll write this in one case but match " +
|
||||
'insensitively\n.com # Second fragment. This can be separated by any ' +
|
||||
'character, including newline',
|
||||
"parse # First fragment. We'll write this in one case but match insensitively\n" +
|
||||
'.com # Second fragment. This can be separated by any character, including newline;' +
|
||||
'however, this comment must end with a newline to recognize it as a comment\n',
|
||||
'mixs'
|
||||
);
|
||||
query.find().then(
|
||||
@@ -2981,10 +2975,10 @@ describe('Parse.Query testing', () => {
|
||||
const saves = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function (x) {
|
||||
const obj = new Parse.Object('TestObject');
|
||||
obj.set('x', x + 1);
|
||||
return obj.save();
|
||||
return obj;
|
||||
});
|
||||
|
||||
Promise.all(saves)
|
||||
Parse.Object.saveAll(saves)
|
||||
.then(function () {
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.ascending('x');
|
||||
@@ -3187,77 +3181,394 @@ describe('Parse.Query testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('select keys query', function (done) {
|
||||
const obj = new TestObject({ foo: 'baz', bar: 1 });
|
||||
it('select keys query JS SDK', async () => {
|
||||
const obj = new TestObject({ foo: 'baz', bar: 1, qux: 2 });
|
||||
await obj.save();
|
||||
obj._clearServerData();
|
||||
const query1 = new Parse.Query(TestObject);
|
||||
query1.select('foo');
|
||||
const result1 = await query1.first();
|
||||
ok(result1.id, 'expected object id to be set');
|
||||
ok(result1.createdAt, 'expected object createdAt to be set');
|
||||
ok(result1.updatedAt, 'expected object updatedAt to be set');
|
||||
ok(!result1.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result1.get('foo'), 'baz');
|
||||
strictEqual(result1.get('bar'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result1.get('qux'), undefined, "expected 'qux' field to be unset");
|
||||
|
||||
obj
|
||||
.save()
|
||||
.then(function () {
|
||||
obj._clearServerData();
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.select('foo');
|
||||
return query.first();
|
||||
})
|
||||
.then(function (result) {
|
||||
ok(result.id, 'expected object id to be set');
|
||||
ok(result.createdAt, 'expected object createdAt to be set');
|
||||
ok(result.updatedAt, 'expected object updatedAt to be set');
|
||||
ok(!result.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result.get('foo'), 'baz');
|
||||
strictEqual(result.get('bar'), undefined, "expected 'bar' field to be unset");
|
||||
return result.fetch();
|
||||
})
|
||||
.then(function (result) {
|
||||
strictEqual(result.get('foo'), 'baz');
|
||||
strictEqual(result.get('bar'), 1);
|
||||
})
|
||||
.then(function () {
|
||||
obj._clearServerData();
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.select([]);
|
||||
return query.first();
|
||||
})
|
||||
.then(function (result) {
|
||||
ok(result.id, 'expected object id to be set');
|
||||
ok(!result.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result.get('foo'), undefined, "expected 'foo' field to be unset");
|
||||
strictEqual(result.get('bar'), undefined, "expected 'bar' field to be unset");
|
||||
})
|
||||
.then(function () {
|
||||
obj._clearServerData();
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.select(['foo', 'bar']);
|
||||
return query.first();
|
||||
})
|
||||
.then(function (result) {
|
||||
ok(result.id, 'expected object id to be set');
|
||||
ok(!result.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result.get('foo'), 'baz');
|
||||
strictEqual(result.get('bar'), 1);
|
||||
})
|
||||
.then(function () {
|
||||
obj._clearServerData();
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.select('foo', 'bar');
|
||||
return query.first();
|
||||
})
|
||||
.then(function (result) {
|
||||
ok(result.id, 'expected object id to be set');
|
||||
ok(!result.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result.get('foo'), 'baz');
|
||||
strictEqual(result.get('bar'), 1);
|
||||
})
|
||||
.then(
|
||||
function () {
|
||||
done();
|
||||
},
|
||||
function (err) {
|
||||
ok(false, 'other error: ' + JSON.stringify(err));
|
||||
done();
|
||||
}
|
||||
);
|
||||
const result2 = await result1.fetch();
|
||||
strictEqual(result2.get('foo'), 'baz');
|
||||
strictEqual(result2.get('bar'), 1);
|
||||
strictEqual(result2.get('qux'), 2);
|
||||
|
||||
obj._clearServerData();
|
||||
const query2 = new Parse.Query(TestObject);
|
||||
query2.select();
|
||||
const result3 = await query2.first();
|
||||
ok(result3.id, 'expected object id to be set');
|
||||
ok(result3.createdAt, 'expected object createdAt to be set');
|
||||
ok(result3.updatedAt, 'expected object updatedAt to be set');
|
||||
ok(!result3.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result3.get('foo'), undefined, "expected 'foo' field to be unset");
|
||||
strictEqual(result3.get('bar'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result3.get('qux'), undefined, "expected 'qux' field to be unset");
|
||||
|
||||
obj._clearServerData();
|
||||
const query3 = new Parse.Query(TestObject);
|
||||
query3.select([]);
|
||||
const result4 = await query3.first();
|
||||
ok(result4.id, 'expected object id to be set');
|
||||
ok(result4.createdAt, 'expected object createdAt to be set');
|
||||
ok(result4.updatedAt, 'expected object updatedAt to be set');
|
||||
ok(!result4.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result4.get('foo'), undefined, "expected 'foo' field to be unset");
|
||||
strictEqual(result4.get('bar'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result4.get('qux'), undefined, "expected 'qux' field to be unset");
|
||||
|
||||
obj._clearServerData();
|
||||
const query4 = new Parse.Query(TestObject);
|
||||
query4.select(['foo']);
|
||||
const result5 = await query4.first();
|
||||
ok(result5.id, 'expected object id to be set');
|
||||
ok(result5.createdAt, 'expected object createdAt to be set');
|
||||
ok(result5.updatedAt, 'expected object updatedAt to be set');
|
||||
ok(!result5.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result5.get('foo'), 'baz');
|
||||
strictEqual(result5.get('bar'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result5.get('qux'), undefined, "expected 'qux' field to be unset");
|
||||
|
||||
obj._clearServerData();
|
||||
const query5 = new Parse.Query(TestObject);
|
||||
query5.select(['foo', 'bar']);
|
||||
const result6 = await query5.first();
|
||||
ok(result6.id, 'expected object id to be set');
|
||||
ok(!result6.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result6.get('foo'), 'baz');
|
||||
strictEqual(result6.get('bar'), 1);
|
||||
strictEqual(result6.get('qux'), undefined, "expected 'qux' field to be unset");
|
||||
|
||||
obj._clearServerData();
|
||||
const query6 = new Parse.Query(TestObject);
|
||||
query6.select(['foo', 'bar', 'qux']);
|
||||
const result7 = await query6.first();
|
||||
ok(result7.id, 'expected object id to be set');
|
||||
ok(!result7.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result7.get('foo'), 'baz');
|
||||
strictEqual(result7.get('bar'), 1);
|
||||
strictEqual(result7.get('qux'), 2);
|
||||
|
||||
obj._clearServerData();
|
||||
const query7 = new Parse.Query(TestObject);
|
||||
query7.select('foo', 'bar');
|
||||
const result8 = await query7.first();
|
||||
ok(result8.id, 'expected object id to be set');
|
||||
ok(!result8.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result8.get('foo'), 'baz');
|
||||
strictEqual(result8.get('bar'), 1);
|
||||
strictEqual(result8.get('qux'), undefined, "expected 'qux' field to be unset");
|
||||
|
||||
obj._clearServerData();
|
||||
const query8 = new Parse.Query(TestObject);
|
||||
query8.select('foo', 'bar', 'qux');
|
||||
const result9 = await query8.first();
|
||||
ok(result9.id, 'expected object id to be set');
|
||||
ok(!result9.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result9.get('foo'), 'baz');
|
||||
strictEqual(result9.get('bar'), 1);
|
||||
strictEqual(result9.get('qux'), 2);
|
||||
});
|
||||
it('exclude keys', async () => {
|
||||
|
||||
it('select keys (arrays)', async () => {
|
||||
const obj = new TestObject({ foo: 'baz', bar: 1, hello: 'world' });
|
||||
await obj.save();
|
||||
|
||||
const response = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
keys: 'hello',
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
expect(response.data.results[0].foo).toBeUndefined();
|
||||
expect(response.data.results[0].bar).toBeUndefined();
|
||||
expect(response.data.results[0].hello).toBe('world');
|
||||
|
||||
const response2 = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
keys: ['foo', 'hello'],
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
expect(response2.data.results[0].foo).toBe('baz');
|
||||
expect(response2.data.results[0].bar).toBeUndefined();
|
||||
expect(response2.data.results[0].hello).toBe('world');
|
||||
|
||||
const response3 = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
keys: ['foo', 'bar', 'hello'],
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
expect(response3.data.results[0].foo).toBe('baz');
|
||||
expect(response3.data.results[0].bar).toBe(1);
|
||||
expect(response3.data.results[0].hello).toBe('world');
|
||||
|
||||
const response4 = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
keys: [''],
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response4.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response4.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response4.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response4.data.results[0].foo).toBeUndefined();
|
||||
expect(response4.data.results[0].bar).toBeUndefined();
|
||||
expect(response4.data.results[0].hello).toBeUndefined();
|
||||
|
||||
const response5 = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
keys: [],
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response5.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response5.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response5.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response5.data.results[0].foo).toBe('baz');
|
||||
expect(response5.data.results[0].bar).toBe(1);
|
||||
expect(response5.data.results[0].hello).toBe('world');
|
||||
});
|
||||
|
||||
it('select keys (strings)', async () => {
|
||||
const obj = new TestObject({ foo: 'baz', bar: 1, hello: 'world' });
|
||||
await obj.save();
|
||||
|
||||
const response = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
keys: '',
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response.data.results[0].foo).toBeUndefined();
|
||||
expect(response.data.results[0].bar).toBeUndefined();
|
||||
expect(response.data.results[0].hello).toBeUndefined();
|
||||
|
||||
const response2 = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
keys: '["foo", "hello"]',
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response2.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response2.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response2.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response2.data.results[0].foo).toBe('baz');
|
||||
expect(response2.data.results[0].bar).toBeUndefined();
|
||||
expect(response2.data.results[0].hello).toBe('world');
|
||||
|
||||
const response3 = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
keys: '["foo", "bar", "hello"]',
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response3.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response3.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response3.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response3.data.results[0].foo).toBe('baz');
|
||||
expect(response3.data.results[0].bar).toBe(1);
|
||||
expect(response3.data.results[0].hello).toBe('world');
|
||||
});
|
||||
|
||||
it('exclude keys query JS SDK', async () => {
|
||||
const obj = new TestObject({ foo: 'baz', bar: 1, qux: 2 });
|
||||
|
||||
await obj.save();
|
||||
obj._clearServerData();
|
||||
const query1 = new Parse.Query(TestObject);
|
||||
query1.exclude('foo');
|
||||
const result1 = await query1.first();
|
||||
ok(result1.id, 'expected object id to be set');
|
||||
ok(result1.createdAt, 'expected object createdAt to be set');
|
||||
ok(result1.updatedAt, 'expected object updatedAt to be set');
|
||||
ok(!result1.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result1.get('foo'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result1.get('bar'), 1);
|
||||
strictEqual(result1.get('qux'), 2);
|
||||
|
||||
const result2 = await result1.fetch();
|
||||
strictEqual(result2.get('foo'), 'baz');
|
||||
strictEqual(result2.get('bar'), 1);
|
||||
strictEqual(result2.get('qux'), 2);
|
||||
|
||||
obj._clearServerData();
|
||||
const query2 = new Parse.Query(TestObject);
|
||||
query2.exclude();
|
||||
const result3 = await query2.first();
|
||||
ok(result3.id, 'expected object id to be set');
|
||||
ok(result3.createdAt, 'expected object createdAt to be set');
|
||||
ok(result3.updatedAt, 'expected object updatedAt to be set');
|
||||
ok(!result3.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result3.get('foo'), 'baz');
|
||||
strictEqual(result3.get('bar'), 1);
|
||||
strictEqual(result3.get('qux'), 2);
|
||||
|
||||
obj._clearServerData();
|
||||
const query3 = new Parse.Query(TestObject);
|
||||
query3.exclude([]);
|
||||
const result4 = await query3.first();
|
||||
ok(result4.id, 'expected object id to be set');
|
||||
ok(result4.createdAt, 'expected object createdAt to be set');
|
||||
ok(result4.updatedAt, 'expected object updatedAt to be set');
|
||||
ok(!result4.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result4.get('foo'), 'baz');
|
||||
strictEqual(result4.get('bar'), 1);
|
||||
strictEqual(result4.get('qux'), 2);
|
||||
|
||||
obj._clearServerData();
|
||||
const query4 = new Parse.Query(TestObject);
|
||||
query4.exclude(['foo']);
|
||||
const result5 = await query4.first();
|
||||
ok(result5.id, 'expected object id to be set');
|
||||
ok(result5.createdAt, 'expected object createdAt to be set');
|
||||
ok(result5.updatedAt, 'expected object updatedAt to be set');
|
||||
ok(!result5.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result5.get('foo'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result5.get('bar'), 1);
|
||||
strictEqual(result5.get('qux'), 2);
|
||||
|
||||
obj._clearServerData();
|
||||
const query5 = new Parse.Query(TestObject);
|
||||
query5.exclude(['foo', 'bar']);
|
||||
const result6 = await query5.first();
|
||||
ok(result6.id, 'expected object id to be set');
|
||||
ok(!result6.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result6.get('foo'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result6.get('bar'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result6.get('qux'), 2);
|
||||
|
||||
obj._clearServerData();
|
||||
const query6 = new Parse.Query(TestObject);
|
||||
query6.exclude(['foo', 'bar', 'qux']);
|
||||
const result7 = await query6.first();
|
||||
ok(result7.id, 'expected object id to be set');
|
||||
ok(!result7.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result7.get('foo'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result7.get('bar'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result7.get('qux'), undefined, "expected 'bar' field to be unset");
|
||||
|
||||
obj._clearServerData();
|
||||
const query7 = new Parse.Query(TestObject);
|
||||
query7.exclude('foo');
|
||||
const result8 = await query7.first();
|
||||
ok(result8.id, 'expected object id to be set');
|
||||
ok(!result8.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result8.get('foo'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result8.get('bar'), 1);
|
||||
strictEqual(result8.get('qux'), 2);
|
||||
|
||||
obj._clearServerData();
|
||||
const query8 = new Parse.Query(TestObject);
|
||||
query8.exclude('foo', 'bar');
|
||||
const result9 = await query8.first();
|
||||
ok(result9.id, 'expected object id to be set');
|
||||
ok(!result9.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result9.get('foo'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result9.get('bar'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result9.get('qux'), 2);
|
||||
|
||||
obj._clearServerData();
|
||||
const query9 = new Parse.Query(TestObject);
|
||||
query9.exclude('foo', 'bar', 'qux');
|
||||
const result10 = await query9.first();
|
||||
ok(result10.id, 'expected object id to be set');
|
||||
ok(!result10.dirty(), 'expected result not to be dirty');
|
||||
strictEqual(result10.get('foo'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result10.get('bar'), undefined, "expected 'bar' field to be unset");
|
||||
strictEqual(result10.get('qux'), undefined, "expected 'bar' field to be unset");
|
||||
});
|
||||
|
||||
it('exclude keys (arrays)', async () => {
|
||||
const obj = new TestObject({ foo: 'baz', hello: 'world' });
|
||||
await obj.save();
|
||||
|
||||
const response = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
excludeKeys: ['foo'],
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response.data.results[0].foo).toBeUndefined();
|
||||
expect(response.data.results[0].hello).toBe('world');
|
||||
|
||||
const response2 = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
excludeKeys: ['foo', 'hello'],
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response2.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response2.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response2.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response2.data.results[0].foo).toBeUndefined();
|
||||
expect(response2.data.results[0].hello).toBeUndefined();
|
||||
|
||||
const response3 = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
excludeKeys: [],
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response3.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response3.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response3.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response3.data.results[0].foo).toBe('baz');
|
||||
expect(response3.data.results[0].hello).toBe('world');
|
||||
|
||||
const response4 = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
excludeKeys: [''],
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response4.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response4.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response4.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response4.data.results[0].foo).toBe('baz');
|
||||
expect(response4.data.results[0].hello).toBe('world');
|
||||
});
|
||||
|
||||
it('exclude keys (strings)', async () => {
|
||||
const obj = new TestObject({ foo: 'baz', hello: 'world' });
|
||||
await obj.save();
|
||||
|
||||
@@ -3269,8 +3580,53 @@ describe('Parse.Query testing', () => {
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response.data.results[0].foo).toBeUndefined();
|
||||
expect(response.data.results[0].hello).toBe('world');
|
||||
|
||||
const response2 = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
excludeKeys: '',
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response2.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response2.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response2.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response2.data.results[0].foo).toBe('baz');
|
||||
expect(response2.data.results[0].hello).toBe('world');
|
||||
|
||||
const response3 = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
excludeKeys: '["hello"]',
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response3.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response3.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response3.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response3.data.results[0].foo).toBe('baz');
|
||||
expect(response3.data.results[0].hello).toBeUndefined();
|
||||
|
||||
const response4 = await request({
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
excludeKeys: '["foo", "hello"]',
|
||||
where: JSON.stringify({ objectId: obj.id }),
|
||||
},
|
||||
headers: masterKeyHeaders,
|
||||
});
|
||||
ok(response4.data.results[0].objectId, 'expected objectId to be set');
|
||||
ok(response4.data.results[0].createdAt, 'expected object createdAt to be set');
|
||||
ok(response4.data.results[0].updatedAt, 'expected object updatedAt to be set');
|
||||
expect(response4.data.results[0].foo).toBeUndefined();
|
||||
expect(response4.data.results[0].hello).toBeUndefined();
|
||||
});
|
||||
|
||||
it('exclude keys with select same key', async () => {
|
||||
@@ -3897,7 +4253,6 @@ describe('Parse.Query testing', () => {
|
||||
})
|
||||
.then(function (savedFoobar) {
|
||||
const foobarQuery = new Parse.Query('Foobar');
|
||||
foobarQuery.include('barBaz');
|
||||
foobarQuery.select(['fizz', 'barBaz.key']);
|
||||
foobarQuery.get(savedFoobar.id).then(function (foobarObj) {
|
||||
equal(foobarObj.get('fizz'), 'buzz');
|
||||
@@ -3935,8 +4290,6 @@ describe('Parse.Query testing', () => {
|
||||
})
|
||||
.then(function (savedFoobar) {
|
||||
const foobarQuery = new Parse.Query('Foobar');
|
||||
foobarQuery.include('barBaz');
|
||||
foobarQuery.include('barBaz.bazoo');
|
||||
foobarQuery.select(['fizz', 'barBaz.key', 'barBaz.bazoo.some']);
|
||||
foobarQuery.get(savedFoobar.id).then(function (foobarObj) {
|
||||
equal(foobarObj.get('fizz'), 'buzz');
|
||||
@@ -3954,6 +4307,65 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('exclude nested keys', async () => {
|
||||
const Foobar = new Parse.Object('Foobar');
|
||||
const BarBaz = new Parse.Object('Barbaz');
|
||||
BarBaz.set('key', 'value');
|
||||
BarBaz.set('otherKey', 'value');
|
||||
await BarBaz.save();
|
||||
|
||||
Foobar.set('foo', 'bar');
|
||||
Foobar.set('fizz', 'buzz');
|
||||
Foobar.set('barBaz', BarBaz);
|
||||
const savedFoobar = await Foobar.save();
|
||||
|
||||
const foobarQuery = new Parse.Query('Foobar');
|
||||
foobarQuery.exclude(['foo', 'barBaz.otherKey']);
|
||||
const foobarObj = await foobarQuery.get(savedFoobar.id);
|
||||
equal(foobarObj.get('fizz'), 'buzz');
|
||||
equal(foobarObj.get('foo'), undefined);
|
||||
if (foobarObj.has('barBaz')) {
|
||||
equal(foobarObj.get('barBaz').get('key'), 'value');
|
||||
equal(foobarObj.get('barBaz').get('otherKey'), undefined);
|
||||
} else {
|
||||
fail('barBaz should be set');
|
||||
}
|
||||
});
|
||||
|
||||
it('exclude nested keys 2 level', async () => {
|
||||
const Foobar = new Parse.Object('Foobar');
|
||||
const BarBaz = new Parse.Object('Barbaz');
|
||||
const Bazoo = new Parse.Object('Bazoo');
|
||||
|
||||
Bazoo.set('some', 'thing');
|
||||
Bazoo.set('otherSome', 'value');
|
||||
await Bazoo.save();
|
||||
|
||||
BarBaz.set('key', 'value');
|
||||
BarBaz.set('otherKey', 'value');
|
||||
BarBaz.set('bazoo', Bazoo);
|
||||
await BarBaz.save();
|
||||
|
||||
Foobar.set('foo', 'bar');
|
||||
Foobar.set('fizz', 'buzz');
|
||||
Foobar.set('barBaz', BarBaz);
|
||||
const savedFoobar = await Foobar.save();
|
||||
|
||||
const foobarQuery = new Parse.Query('Foobar');
|
||||
foobarQuery.exclude(['foo', 'barBaz.otherKey', 'barBaz.bazoo.otherSome']);
|
||||
const foobarObj = await foobarQuery.get(savedFoobar.id);
|
||||
equal(foobarObj.get('fizz'), 'buzz');
|
||||
equal(foobarObj.get('foo'), undefined);
|
||||
if (foobarObj.has('barBaz')) {
|
||||
equal(foobarObj.get('barBaz').get('key'), 'value');
|
||||
equal(foobarObj.get('barBaz').get('otherKey'), undefined);
|
||||
equal(foobarObj.get('barBaz').get('bazoo').get('some'), 'thing');
|
||||
equal(foobarObj.get('barBaz').get('bazoo').get('otherSome'), undefined);
|
||||
} else {
|
||||
fail('barBaz should be set');
|
||||
}
|
||||
});
|
||||
|
||||
it('include with *', async () => {
|
||||
const child1 = new TestObject({ foo: 'bar', name: 'ac' });
|
||||
const child2 = new TestObject({ foo: 'baz', name: 'flo' });
|
||||
@@ -3978,6 +4390,30 @@ describe('Parse.Query testing', () => {
|
||||
equal(result.child3.name, 'mo');
|
||||
});
|
||||
|
||||
it('include with ["*"]', async () => {
|
||||
const child1 = new TestObject({ foo: 'bar', name: 'ac' });
|
||||
const child2 = new TestObject({ foo: 'baz', name: 'flo' });
|
||||
const child3 = new TestObject({ foo: 'bad', name: 'mo' });
|
||||
const parent = new Container({ child1, child2, child3 });
|
||||
await Parse.Object.saveAll([parent, child1, child2, child3]);
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
qs: {
|
||||
where: JSON.stringify({ objectId: parent.id }),
|
||||
include: '["*"]',
|
||||
},
|
||||
});
|
||||
const resp = await request(
|
||||
Object.assign({ url: Parse.serverURL + '/classes/Container' }, options)
|
||||
);
|
||||
const result = resp.data.results[0];
|
||||
equal(result.child1.foo, 'bar');
|
||||
equal(result.child2.foo, 'baz');
|
||||
equal(result.child3.foo, 'bad');
|
||||
equal(result.child1.name, 'ac');
|
||||
equal(result.child2.name, 'flo');
|
||||
equal(result.child3.name, 'mo');
|
||||
});
|
||||
|
||||
it('include with * overrides', async () => {
|
||||
const child1 = new TestObject({ foo: 'bar', name: 'ac' });
|
||||
const child2 = new TestObject({ foo: 'baz', name: 'flo' });
|
||||
@@ -4002,6 +4438,30 @@ describe('Parse.Query testing', () => {
|
||||
equal(result.child3.name, 'mo');
|
||||
});
|
||||
|
||||
it('include with ["*"] overrides', async () => {
|
||||
const child1 = new TestObject({ foo: 'bar', name: 'ac' });
|
||||
const child2 = new TestObject({ foo: 'baz', name: 'flo' });
|
||||
const child3 = new TestObject({ foo: 'bad', name: 'mo' });
|
||||
const parent = new Container({ child1, child2, child3 });
|
||||
await Parse.Object.saveAll([parent, child1, child2, child3]);
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
qs: {
|
||||
where: JSON.stringify({ objectId: parent.id }),
|
||||
include: '["child2","*"]',
|
||||
},
|
||||
});
|
||||
const resp = await request(
|
||||
Object.assign({ url: Parse.serverURL + '/classes/Container' }, options)
|
||||
);
|
||||
const result = resp.data.results[0];
|
||||
equal(result.child1.foo, 'bar');
|
||||
equal(result.child2.foo, 'baz');
|
||||
equal(result.child3.foo, 'bad');
|
||||
equal(result.child1.name, 'ac');
|
||||
equal(result.child2.name, 'flo');
|
||||
equal(result.child3.name, 'mo');
|
||||
});
|
||||
|
||||
it('includeAll', done => {
|
||||
const child1 = new TestObject({ foo: 'bar', name: 'ac' });
|
||||
const child2 = new TestObject({ foo: 'baz', name: 'flo' });
|
||||
@@ -4306,7 +4766,7 @@ describe('Parse.Query testing', () => {
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('should handle relative times correctly', function (done) {
|
||||
it('should handle relative times correctly', async () => {
|
||||
const now = Date.now();
|
||||
const obj1 = new Parse.Object('MyCustomObject', {
|
||||
name: 'obj1',
|
||||
@@ -4317,94 +4777,75 @@ describe('Parse.Query testing', () => {
|
||||
ttl: new Date(now - 2 * 24 * 60 * 60 * 1000), // 2 days ago
|
||||
});
|
||||
|
||||
Parse.Object.saveAll([obj1, obj2])
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: 'in 1 day' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(1);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: '1 day ago' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(1);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.lessThan('ttl', { $relativeTime: '5 days ago' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(0);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: '3 days ago' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(2);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: 'now' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(1);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: 'now' });
|
||||
q.lessThan('ttl', { $relativeTime: 'in 1 day' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(0);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: '1 year 3 weeks ago' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then(results => {
|
||||
expect(results.length).toBe(2);
|
||||
})
|
||||
.then(done, done.fail);
|
||||
await Parse.Object.saveAll([obj1, obj2]);
|
||||
const q1 = new Parse.Query('MyCustomObject');
|
||||
q1.greaterThan('ttl', { $relativeTime: 'in 1 day' });
|
||||
const results1 = await q1.find({ useMasterKey: true });
|
||||
expect(results1.length).toBe(1);
|
||||
|
||||
const q2 = new Parse.Query('MyCustomObject');
|
||||
q2.greaterThan('ttl', { $relativeTime: '1 day ago' });
|
||||
const results2 = await q2.find({ useMasterKey: true });
|
||||
expect(results2.length).toBe(1);
|
||||
|
||||
const q3 = new Parse.Query('MyCustomObject');
|
||||
q3.lessThan('ttl', { $relativeTime: '5 days ago' });
|
||||
const results3 = await q3.find({ useMasterKey: true });
|
||||
expect(results3.length).toBe(0);
|
||||
|
||||
const q4 = new Parse.Query('MyCustomObject');
|
||||
q4.greaterThan('ttl', { $relativeTime: '3 days ago' });
|
||||
const results4 = await q4.find({ useMasterKey: true });
|
||||
expect(results4.length).toBe(2);
|
||||
|
||||
const q5 = new Parse.Query('MyCustomObject');
|
||||
q5.greaterThan('ttl', { $relativeTime: 'now' });
|
||||
const results5 = await q5.find({ useMasterKey: true });
|
||||
expect(results5.length).toBe(1);
|
||||
|
||||
const q6 = new Parse.Query('MyCustomObject');
|
||||
q6.greaterThan('ttl', { $relativeTime: 'now' });
|
||||
q6.lessThan('ttl', { $relativeTime: 'in 1 day' });
|
||||
const results6 = await q6.find({ useMasterKey: true });
|
||||
expect(results6.length).toBe(0);
|
||||
|
||||
const q7 = new Parse.Query('MyCustomObject');
|
||||
q7.greaterThan('ttl', { $relativeTime: '1 year 3 weeks ago' });
|
||||
const results7 = await q7.find({ useMasterKey: true });
|
||||
expect(results7.length).toBe(2);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('should error on invalid relative time', function (done) {
|
||||
it('should error on invalid relative time', async () => {
|
||||
const obj1 = new Parse.Object('MyCustomObject', {
|
||||
name: 'obj1',
|
||||
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
|
||||
});
|
||||
|
||||
await obj1.save({ useMasterKey: true });
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: '-12 bananas ago' });
|
||||
obj1
|
||||
.save({ useMasterKey: true })
|
||||
.then(() => q.find({ useMasterKey: true }))
|
||||
.then(done.fail, () => done());
|
||||
try {
|
||||
await q.find({ useMasterKey: true });
|
||||
fail('Should have thrown error');
|
||||
} catch (error) {
|
||||
expect(error.code).toBe(Parse.Error.INVALID_JSON);
|
||||
}
|
||||
});
|
||||
|
||||
it_only_db('mongo')('should error when using $relativeTime on non-Date field', function (done) {
|
||||
it('should error when using $relativeTime on non-Date field', async () => {
|
||||
const obj1 = new Parse.Object('MyCustomObject', {
|
||||
name: 'obj1',
|
||||
nonDateField: 'abcd',
|
||||
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
|
||||
});
|
||||
|
||||
await obj1.save({ useMasterKey: true });
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('nonDateField', { $relativeTime: '1 day ago' });
|
||||
obj1
|
||||
.save({ useMasterKey: true })
|
||||
.then(() => q.find({ useMasterKey: true }))
|
||||
.then(done.fail, () => done());
|
||||
try {
|
||||
await q.find({ useMasterKey: true });
|
||||
fail('Should have thrown error');
|
||||
} catch (error) {
|
||||
expect(error.code).toBe(Parse.Error.INVALID_JSON);
|
||||
}
|
||||
});
|
||||
|
||||
it('should match complex structure with dot notation when using matchesKeyInQuery', function (done) {
|
||||
@@ -4773,19 +5214,6 @@ describe('Parse.Query testing', () => {
|
||||
equal(results[0].get('array').length, 105);
|
||||
});
|
||||
|
||||
it('exclude keys (sdk query)', async done => {
|
||||
const obj = new TestObject({ foo: 'baz', hello: 'world' });
|
||||
await obj.save();
|
||||
|
||||
const query = new Parse.Query('TestObject');
|
||||
query.exclude('foo');
|
||||
|
||||
const object = await query.get(obj.id);
|
||||
expect(object.get('foo')).toBeUndefined();
|
||||
expect(object.get('hello')).toBe('world');
|
||||
done();
|
||||
});
|
||||
|
||||
xit('todo: exclude keys with select key (sdk query get)', async done => {
|
||||
// there is some problem with js sdk caching
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ describe('Parse.Relation testing', () => {
|
||||
.then(done, done.fail);
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('queries with relations', async () => {
|
||||
it('queries with relations', async () => {
|
||||
const ChildObject = Parse.Object.extend('ChildObject');
|
||||
const childObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
@@ -283,7 +283,7 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('query on pointer and relation fields with equal', done => {
|
||||
it('query on pointer and relation fields with equal', done => {
|
||||
const ChildObject = Parse.Object.extend('ChildObject');
|
||||
const childObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
@@ -366,7 +366,7 @@ describe('Parse.Relation testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it_exclude_dbs(['postgres'])('or queries on pointer and relation fields', done => {
|
||||
it('or queries on pointer and relation fields', done => {
|
||||
const ChildObject = Parse.Object.extend('ChildObject');
|
||||
const childObjects = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
|
||||
+55
-54
@@ -6,6 +6,58 @@ const RestQuery = require('../lib/RestQuery');
|
||||
const Auth = require('../lib/Auth').Auth;
|
||||
const Config = require('../lib/Config');
|
||||
|
||||
function testLoadRoles(config, done) {
|
||||
const rolesNames = ['FooRole', 'BarRole', 'BazRole'];
|
||||
const roleIds = {};
|
||||
createTestUser()
|
||||
.then(user => {
|
||||
// Put the user on the 1st role
|
||||
return createRole(rolesNames[0], null, user)
|
||||
.then(aRole => {
|
||||
roleIds[aRole.get('name')] = aRole.id;
|
||||
// set the 1st role as a sibling of the second
|
||||
// user will should have 2 role now
|
||||
return createRole(rolesNames[1], aRole, null);
|
||||
})
|
||||
.then(anotherRole => {
|
||||
roleIds[anotherRole.get('name')] = anotherRole.id;
|
||||
// set this role as a sibling of the last
|
||||
// the user should now have 3 roles
|
||||
return createRole(rolesNames[2], anotherRole, null);
|
||||
})
|
||||
.then(lastRole => {
|
||||
roleIds[lastRole.get('name')] = lastRole.id;
|
||||
const auth = new Auth({ config, isMaster: true, user: user });
|
||||
return auth._loadRoles();
|
||||
});
|
||||
})
|
||||
.then(
|
||||
roles => {
|
||||
expect(roles.length).toEqual(3);
|
||||
rolesNames.forEach(name => {
|
||||
expect(roles.indexOf('role:' + name)).not.toBe(-1);
|
||||
});
|
||||
done();
|
||||
},
|
||||
function () {
|
||||
fail('should succeed');
|
||||
done();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const createRole = function (name, sibling, user) {
|
||||
const role = new Parse.Role(name, new Parse.ACL());
|
||||
if (user) {
|
||||
const users = role.relation('users');
|
||||
users.add(user);
|
||||
}
|
||||
if (sibling) {
|
||||
role.relation('roles').add(sibling);
|
||||
}
|
||||
return role.save({}, { useMasterKey: true });
|
||||
};
|
||||
|
||||
describe('Parse Role testing', () => {
|
||||
it('Do a bunch of basic role testing', done => {
|
||||
let user;
|
||||
@@ -74,18 +126,6 @@ describe('Parse Role testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
const createRole = function (name, sibling, user) {
|
||||
const role = new Parse.Role(name, new Parse.ACL());
|
||||
if (user) {
|
||||
const users = role.relation('users');
|
||||
users.add(user);
|
||||
}
|
||||
if (sibling) {
|
||||
role.relation('roles').add(sibling);
|
||||
}
|
||||
return role.save({}, { useMasterKey: true });
|
||||
};
|
||||
|
||||
it('should not recursively load the same role multiple times', done => {
|
||||
const rootRole = 'RootRole';
|
||||
const roleNames = ['FooRole', 'BarRole', 'BazRole'];
|
||||
@@ -157,46 +197,6 @@ describe('Parse Role testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function testLoadRoles(config, done) {
|
||||
const rolesNames = ['FooRole', 'BarRole', 'BazRole'];
|
||||
const roleIds = {};
|
||||
createTestUser()
|
||||
.then(user => {
|
||||
// Put the user on the 1st role
|
||||
return createRole(rolesNames[0], null, user)
|
||||
.then(aRole => {
|
||||
roleIds[aRole.get('name')] = aRole.id;
|
||||
// set the 1st role as a sibling of the second
|
||||
// user will should have 2 role now
|
||||
return createRole(rolesNames[1], aRole, null);
|
||||
})
|
||||
.then(anotherRole => {
|
||||
roleIds[anotherRole.get('name')] = anotherRole.id;
|
||||
// set this role as a sibling of the last
|
||||
// the user should now have 3 roles
|
||||
return createRole(rolesNames[2], anotherRole, null);
|
||||
})
|
||||
.then(lastRole => {
|
||||
roleIds[lastRole.get('name')] = lastRole.id;
|
||||
const auth = new Auth({ config, isMaster: true, user: user });
|
||||
return auth._loadRoles();
|
||||
});
|
||||
})
|
||||
.then(
|
||||
roles => {
|
||||
expect(roles.length).toEqual(3);
|
||||
rolesNames.forEach(name => {
|
||||
expect(roles.indexOf('role:' + name)).not.toBe(-1);
|
||||
});
|
||||
done();
|
||||
},
|
||||
function () {
|
||||
fail('should succeed');
|
||||
done();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
it('should recursively load roles', done => {
|
||||
testLoadRoles(Config.get('test'), done);
|
||||
});
|
||||
@@ -227,7 +227,8 @@ describe('Parse Role testing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Different _Role objects cannot have the same name.', done => {
|
||||
it('Different _Role objects cannot have the same name.', async done => {
|
||||
await reconfigureServer();
|
||||
const roleName = 'MyRole';
|
||||
let aUser;
|
||||
createTestUser()
|
||||
@@ -427,7 +428,7 @@ describe('Parse Role testing', () => {
|
||||
},
|
||||
e => {
|
||||
if (e) {
|
||||
expect(e.code).toEqual(101);
|
||||
expect(e.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
|
||||
} else {
|
||||
fail('should return an error');
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user