Compare commits

..
370 Commits
Author SHA1 Message Date
semantic-release-bot b905137278 chore(release): 5.4.2 [skip ci]
## [5.4.2](https://github.com/parse-community/parse-server/compare/5.4.1...5.4.2) (2023-02-16)

### Bug Fixes

* Security upgrade jsonwebtoken to 9.0.0 ([#8431](https://github.com/parse-community/parse-server/issues/8431)) ([2c19c2e](https://github.com/parse-community/parse-server/commit/2c19c2e4d4cfdeefe31e0ec11d2dc0e8ee508a91))
2023-02-16 19:00:29 +00:00
Daniel 2c19c2e4d4 fix: Security upgrade jsonwebtoken to 9.0.0 (#8431) 2023-02-16 19:52:48 +01:00
semantic-release-bot 30576f1091 chore(release): 5.4.1 [skip ci]
## [5.4.1](https://github.com/parse-community/parse-server/compare/5.4.0...5.4.1) (2023-01-31)

### Bug Fixes

* The client IP address may be determined incorrectly in some cases; it is now required to set the Parse Server option `trustProxy` accordingly if Parse Server runs behind a proxy server, see the express framework's [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting; this fixes a security vulnerability in which the Parse Server option `masterKeyIps` may be circumvented, see [GHSA-vm5r-c87r-pf6x](https://github.com/parse-community/parse-server/security/advisories/GHSA-vm5r-c87r-pf6x) ([#8369](https://github.com/parse-community/parse-server/issues/8369)) ([e016d81](https://github.com/parse-community/parse-server/commit/e016d813e083ce6828f9abce245d15b681a224d8))
2023-01-31 03:57:58 +00:00
Manuel e016d813e0 fix: The client IP address may be determined incorrectly in some cases; it is now required to set the Parse Server option trustProxy accordingly if Parse Server runs behind a proxy server, see the express framework's [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting; this fixes a security vulnerability in which the Parse Server option masterKeyIps may be circumvented, see [GHSA-vm5r-c87r-pf6x](https://github.com/parse-community/parse-server/security/advisories/GHSA-vm5r-c87r-pf6x) (#8369) 2023-01-05 14:20:40 +01:00
Manuel c8bc200729 ci: Add LTS branches to CI workflow 2023-01-05 13:20:43 +01:00
Manuel Trezza 09d04b0aad ci: update auto-release workflow 2022-11-19 12:35:00 +01:00
Manuel Trezza 38f64be596 ci: update auto-release for LTS 2022-11-19 12:31:43 +01:00
semantic-release-bot 9b34b0263b chore(release): 5.4.0 [skip ci]
# [5.4.0](https://github.com/parse-community/parse-server/compare/5.3.3...5.4.0) (2022-11-19)

### Bug Fixes

* authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) [skip release] ([#8187](https://github.com/parse-community/parse-server/issues/8187)) ([8c8ec71](https://github.com/parse-community/parse-server/commit/8c8ec715739e0f851338cfed794409ebac66c51b))
* brute force guessing of user sensitive data via search patterns (GHSA-2m6g-crv8-p3c6) ([#8146](https://github.com/parse-community/parse-server/issues/8146)) [skip release] ([4c0c7c7](https://github.com/parse-community/parse-server/commit/4c0c7c77b76257878b9bcb05ff9de01c9d790262))
* certificate in Apple Game Center auth adapter not validated [skip release] ([#8058](https://github.com/parse-community/parse-server/issues/8058)) ([75af9a2](https://github.com/parse-community/parse-server/commit/75af9a26cc8e9e88a33d1e452c93a0ee6e509f17))
* graphQL query ignores condition `equalTo` with value `false` ([#8032](https://github.com/parse-community/parse-server/issues/8032)) ([7f5a15d](https://github.com/parse-community/parse-server/commit/7f5a15d5df0dfa3515e9f73709d6a49663545f9b))
* internal indices for classes `_Idempotency` and `_Role` are not protected in defined schema ([#8121](https://github.com/parse-community/parse-server/issues/8121)) ([c16f529](https://github.com/parse-community/parse-server/commit/c16f529f74f92154401bf662f634b3c5fa45e18e))
* invalid file request not properly handled [skip release] ([#8062](https://github.com/parse-community/parse-server/issues/8062)) ([4c9e956](https://github.com/parse-community/parse-server/commit/4c9e95674ad081f13062e8cd30b77b1962d5df57))
* liveQuery with `containedIn` not working when object field is an array ([#8128](https://github.com/parse-community/parse-server/issues/8128)) ([1d9605b](https://github.com/parse-community/parse-server/commit/1d9605bc93009263d3811df4d4249034ba6eb8c4))
* protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [skip release] ([#8076](https://github.com/parse-community/parse-server/issues/8076)) ([9fd4516](https://github.com/parse-community/parse-server/commit/9fd4516cde5c742f9f29dd05468b4a43a85639a6))
* push notifications `badge` doesn't update with Installation beforeSave trigger ([#8162](https://github.com/parse-community/parse-server/issues/8162)) ([3c75c2b](https://github.com/parse-community/parse-server/commit/3c75c2ba4851fae96a8c19b11a3efde03816c9a1))
* query aggregation pipeline cannot handle value of type `Date` when `directAccess: true` ([#8167](https://github.com/parse-community/parse-server/issues/8167)) ([e424137](https://github.com/parse-community/parse-server/commit/e4241374061caef66538de15112fb6bbafb1f5bb))
* relation constraints in compound queries `Parse.Query.or`, `Parse.Query.and` not working ([#8203](https://github.com/parse-community/parse-server/issues/8203)) ([28f0d26](https://github.com/parse-community/parse-server/commit/28f0d2667787d2ac68726607b811d6f0ef62b9f1))
* security upgrade undici from 5.6.0 to 5.8.0 ([#8108](https://github.com/parse-community/parse-server/issues/8108)) ([4aa016b](https://github.com/parse-community/parse-server/commit/4aa016b7322467422b9fdf05d8e29b9ecf910da7))
* server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) [skip release] ([#8238](https://github.com/parse-community/parse-server/issues/8238)) ([c03908f](https://github.com/parse-community/parse-server/commit/c03908f74e5c9eed834874a89df6c89c1a1e849f))
* session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) [skip release] ([#8180](https://github.com/parse-community/parse-server/issues/8180)) ([37fed30](https://github.com/parse-community/parse-server/commit/37fed3062ccc3ef1dfd49a9fc53318e72b3e4aff))
* sorting by non-existing value throws `INVALID_SERVER_ERROR` on Postgres ([#8157](https://github.com/parse-community/parse-server/issues/8157)) ([3b775a1](https://github.com/parse-community/parse-server/commit/3b775a1fb8a1878714e3451191438963d688f1b0))
* updating object includes unchanged keys in client response for certain key types ([#8159](https://github.com/parse-community/parse-server/issues/8159)) ([37af1d7](https://github.com/parse-community/parse-server/commit/37af1d78fce5a15039ffe3af7b323c1f1e8582fc))

### Features

* add convenience access to Parse Server configuration in Cloud Code via `Parse.Server` ([#8244](https://github.com/parse-community/parse-server/issues/8244)) ([9f11115](https://github.com/parse-community/parse-server/commit/9f111158edf7fd57a65db0c4f9244b37e58cf293))
* add option to change the default value of the `Parse.Query.limit()` constraint ([#8152](https://github.com/parse-community/parse-server/issues/8152)) ([0388956](https://github.com/parse-community/parse-server/commit/038895680894984e569dff54bf5c7b31094f3891))
* add support for MongoDB 6 ([#8242](https://github.com/parse-community/parse-server/issues/8242)) ([aba0081](https://github.com/parse-community/parse-server/commit/aba0081ce1a166a93de57f3928c19a05562b5cc1))
* add support for Postgres 15 ([#8215](https://github.com/parse-community/parse-server/issues/8215)) ([2feb6c4](https://github.com/parse-community/parse-server/commit/2feb6c46080946c984daa351187fa07cd582355d))
* liveQuery support for unsorted distance queries ([#8221](https://github.com/parse-community/parse-server/issues/8221)) ([0f763da](https://github.com/parse-community/parse-server/commit/0f763da17d646b2fec2cd980d3857e46072a8a07))
2022-11-19 03:37:26 +00:00
Manuel e373f09040 build: Release (#8324) 2022-11-19 04:30:10 +01:00
Manuel a9a9772b05 Merge branch 'release' into beta 2022-11-19 04:12:00 +01:00
Manuel 735669a86a refactor: Prototype pollution via Cloud Code Webhooks; fixes security vulnerability [GHSA-93vw-8fm5-p2jf](https://github.com/parse-community/parse-server/security/advisories/GHSA-93vw-8fm5-p2jf) (#8307) 2022-11-10 00:24:26 +01:00
semantic-release-bot fd8a11bc27 chore(release): 5.3.3 [skip ci]
## [5.3.3](https://github.com/parse-community/parse-server/compare/5.3.2...5.3.3) (2022-11-09)

### Bug Fixes

* Prototype pollution via Cloud Code Webhooks; fixes security vulnerability [GHSA-93vw-8fm5-p2jf](https://github.com/parse-community/parse-server/security/advisories/GHSA-93vw-8fm5-p2jf) ([#8305](https://github.com/parse-community/parse-server/issues/8305)) ([60c5a73](https://github.com/parse-community/parse-server/commit/60c5a73d257e0d536056b38bdafef8b7130524d8))
2022-11-09 20:56:00 +00:00
Manuel 60c5a73d25 fix: Prototype pollution via Cloud Code Webhooks; fixes security vulnerability [GHSA-93vw-8fm5-p2jf](https://github.com/parse-community/parse-server/security/advisories/GHSA-93vw-8fm5-p2jf) (#8305) 2022-11-09 21:32:02 +01:00
semantic-release-bot 3e983c41ff chore(release): 5.3.2 [skip ci]
## [5.3.2](https://github.com/parse-community/parse-server/compare/5.3.1...5.3.2) (2022-11-09)

### Bug Fixes

* Parse Server option `requestKeywordDenylist` can be bypassed via Cloud Code Webhooks or Triggers; fixes security vulnerability [GHSA-xprv-wvh7-qqqx](https://github.com/parse-community/parse-server/security/advisories/GHSA-xprv-wvh7-qqqx) ([#8302](https://github.com/parse-community/parse-server/issues/8302)) ([6728da1](https://github.com/parse-community/parse-server/commit/6728da1e3591db1e27031d335d64d8f25546a06f))
2022-11-09 19:12:19 +00:00
Manuel d9c3c02e7d refactor: Parse Server option requestKeywordDenylist can be bypassed via Cloud Code Webhooks or Triggers; fixes security vulnerability [GHSA-xprv-wvh7-qqqx](https://github.com/parse-community/parse-server/security/advisories/GHSA-xprv-wvh7-qqqx) (#8303) 2022-11-09 20:01:39 +01:00
Manuel 6728da1e35 fix: Parse Server option requestKeywordDenylist can be bypassed via Cloud Code Webhooks or Triggers; fixes security vulnerability [GHSA-xprv-wvh7-qqqx](https://github.com/parse-community/parse-server/security/advisories/GHSA-xprv-wvh7-qqqx) (#8302) 2022-11-09 20:00:29 +01:00
Manuel 46dbecdec1 refactor: Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-prm5-8g2m-24gg](https://github.com/parse-community/parse-server/security/advisories/GHSA-prm5-8g2m-24gg) (#8298) 2022-11-07 23:49:41 +01:00
semantic-release-bot 2458a8c58d chore(release): 5.3.1 [skip ci]
## [5.3.1](https://github.com/parse-community/parse-server/compare/5.3.0...5.3.1) (2022-11-07)

### Bug Fixes

* Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-prm5-8g2m-24gg](https://github.com/parse-community/parse-server/security/advisories/GHSA-prm5-8g2m-24gg) ([#8295](https://github.com/parse-community/parse-server/issues/8295)) ([50eed3c](https://github.com/parse-community/parse-server/commit/50eed3cffe80fadfb4bdac52b2783a18da2cfc4f))
2022-11-07 22:10:47 +00:00
Manuel 50eed3cffe fix: Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-prm5-8g2m-24gg](https://github.com/parse-community/parse-server/security/advisories/GHSA-prm5-8g2m-24gg) (#8295) 2022-11-07 23:03:24 +01:00
semantic-release-bot 0e30c76e39 chore(release): 5.4.0-beta.1 [skip ci]
# [5.4.0-beta.1](https://github.com/parse-community/parse-server/compare/5.3.0...5.4.0-beta.1) (2022-10-29)

### Bug Fixes

* authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) [skip release] ([#8187](https://github.com/parse-community/parse-server/issues/8187)) ([8c8ec71](https://github.com/parse-community/parse-server/commit/8c8ec715739e0f851338cfed794409ebac66c51b))
* brute force guessing of user sensitive data via search patterns (GHSA-2m6g-crv8-p3c6) ([#8146](https://github.com/parse-community/parse-server/issues/8146)) [skip release] ([4c0c7c7](https://github.com/parse-community/parse-server/commit/4c0c7c77b76257878b9bcb05ff9de01c9d790262))
* certificate in Apple Game Center auth adapter not validated [skip release] ([#8058](https://github.com/parse-community/parse-server/issues/8058)) ([75af9a2](https://github.com/parse-community/parse-server/commit/75af9a26cc8e9e88a33d1e452c93a0ee6e509f17))
* graphQL query ignores condition `equalTo` with value `false` ([#8032](https://github.com/parse-community/parse-server/issues/8032)) ([7f5a15d](https://github.com/parse-community/parse-server/commit/7f5a15d5df0dfa3515e9f73709d6a49663545f9b))
* internal indices for classes `_Idempotency` and `_Role` are not protected in defined schema ([#8121](https://github.com/parse-community/parse-server/issues/8121)) ([c16f529](https://github.com/parse-community/parse-server/commit/c16f529f74f92154401bf662f634b3c5fa45e18e))
* invalid file request not properly handled [skip release] ([#8062](https://github.com/parse-community/parse-server/issues/8062)) ([4c9e956](https://github.com/parse-community/parse-server/commit/4c9e95674ad081f13062e8cd30b77b1962d5df57))
* liveQuery with `containedIn` not working when object field is an array ([#8128](https://github.com/parse-community/parse-server/issues/8128)) ([1d9605b](https://github.com/parse-community/parse-server/commit/1d9605bc93009263d3811df4d4249034ba6eb8c4))
* protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [skip release] ([#8076](https://github.com/parse-community/parse-server/issues/8076)) ([9fd4516](https://github.com/parse-community/parse-server/commit/9fd4516cde5c742f9f29dd05468b4a43a85639a6))
* push notifications `badge` doesn't update with Installation beforeSave trigger ([#8162](https://github.com/parse-community/parse-server/issues/8162)) ([3c75c2b](https://github.com/parse-community/parse-server/commit/3c75c2ba4851fae96a8c19b11a3efde03816c9a1))
* query aggregation pipeline cannot handle value of type `Date` when `directAccess: true` ([#8167](https://github.com/parse-community/parse-server/issues/8167)) ([e424137](https://github.com/parse-community/parse-server/commit/e4241374061caef66538de15112fb6bbafb1f5bb))
* relation constraints in compound queries `Parse.Query.or`, `Parse.Query.and` not working ([#8203](https://github.com/parse-community/parse-server/issues/8203)) ([28f0d26](https://github.com/parse-community/parse-server/commit/28f0d2667787d2ac68726607b811d6f0ef62b9f1))
* security upgrade undici from 5.6.0 to 5.8.0 ([#8108](https://github.com/parse-community/parse-server/issues/8108)) ([4aa016b](https://github.com/parse-community/parse-server/commit/4aa016b7322467422b9fdf05d8e29b9ecf910da7))
* server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) [skip release] ([#8238](https://github.com/parse-community/parse-server/issues/8238)) ([c03908f](https://github.com/parse-community/parse-server/commit/c03908f74e5c9eed834874a89df6c89c1a1e849f))
* session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) [skip release] ([#8180](https://github.com/parse-community/parse-server/issues/8180)) ([37fed30](https://github.com/parse-community/parse-server/commit/37fed3062ccc3ef1dfd49a9fc53318e72b3e4aff))
* sorting by non-existing value throws `INVALID_SERVER_ERROR` on Postgres ([#8157](https://github.com/parse-community/parse-server/issues/8157)) ([3b775a1](https://github.com/parse-community/parse-server/commit/3b775a1fb8a1878714e3451191438963d688f1b0))
* updating object includes unchanged keys in client response for certain key types ([#8159](https://github.com/parse-community/parse-server/issues/8159)) ([37af1d7](https://github.com/parse-community/parse-server/commit/37af1d78fce5a15039ffe3af7b323c1f1e8582fc))

### Features

* add convenience access to Parse Server configuration in Cloud Code via `Parse.Server` ([#8244](https://github.com/parse-community/parse-server/issues/8244)) ([9f11115](https://github.com/parse-community/parse-server/commit/9f111158edf7fd57a65db0c4f9244b37e58cf293))
* add option to change the default value of the `Parse.Query.limit()` constraint ([#8152](https://github.com/parse-community/parse-server/issues/8152)) ([0388956](https://github.com/parse-community/parse-server/commit/038895680894984e569dff54bf5c7b31094f3891))
* add support for MongoDB 6 ([#8242](https://github.com/parse-community/parse-server/issues/8242)) ([aba0081](https://github.com/parse-community/parse-server/commit/aba0081ce1a166a93de57f3928c19a05562b5cc1))
* add support for Postgres 15 ([#8215](https://github.com/parse-community/parse-server/issues/8215)) ([2feb6c4](https://github.com/parse-community/parse-server/commit/2feb6c46080946c984daa351187fa07cd582355d))
* liveQuery support for unsorted distance queries ([#8221](https://github.com/parse-community/parse-server/issues/8221)) ([0f763da](https://github.com/parse-community/parse-server/commit/0f763da17d646b2fec2cd980d3857e46072a8a07))
2022-10-29 20:01:12 +00:00
Manuel 1d277db467 build: beta release (#8264) 2022-10-29 21:53:56 +02:00
Manuel 5e9d494979 Merge branch 'beta' into build-beta 2022-10-29 21:31:22 +02:00
semantic-release-bot 12e174bcb6 chore(release): 5.3.0 [skip ci]
# [5.3.0](https://github.com/parse-community/parse-server/compare/5.2.8...5.3.0) (2022-10-29)

### Bug Fixes

* afterSave trigger removes pointer in Parse object ([#7913](https://github.com/parse-community/parse-server/issues/7913)) ([47d796e](https://github.com/parse-community/parse-server/commit/47d796ea58f65e71612ce37149be692abc9ea97f))
* authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) [skip release] ([#8188](https://github.com/parse-community/parse-server/issues/8188)) ([1a2b1b9](https://github.com/parse-community/parse-server/commit/1a2b1b9bc18012acc019ec2b5ee16fddfa41de41))
* auto-release process may fail if optional back-merging task fails ([#8051](https://github.com/parse-community/parse-server/issues/8051)) ([cf925e7](https://github.com/parse-community/parse-server/commit/cf925e75e87a6989f41e2e2abb2aba4332b1e79f))
* brute force guessing of user sensitive data via search patterns (GHSA-2m6g-crv8-p3c6) ([#8145](https://github.com/parse-community/parse-server/issues/8145)) [skip release] ([f0db4ca](https://github.com/parse-community/parse-server/commit/f0db4ca4a45208e6c86e21ce563105977c2b1c1f))
* certificate in Apple Game Center auth adapter not validated [skip release] ([#8055](https://github.com/parse-community/parse-server/issues/8055)) ([4c2aa63](https://github.com/parse-community/parse-server/commit/4c2aa63fd2f4ee6cca48b051b7d345ca7c975591))
* custom database options are not passed to MongoDB GridFS ([#7911](https://github.com/parse-community/parse-server/issues/7911)) ([b1e5565](https://github.com/parse-community/parse-server/commit/b1e5565b22f2eff229571fe9a9500314bd30965b))
* depreciate allowClientClassCreation defaulting to true ([#7925](https://github.com/parse-community/parse-server/issues/7925)) ([38ed96a](https://github.com/parse-community/parse-server/commit/38ed96ace534d639db007aa7dd5387b2da8f03ae))
* errors in GraphQL do not show the original error but a general `Unexpected Error` ([#8045](https://github.com/parse-community/parse-server/issues/8045)) ([0d81887](https://github.com/parse-community/parse-server/commit/0d818879c217f9c56100a5f59868fa37e6d24b71))
* interrupted WebSocket connection not closed by LiveQuery server ([#8012](https://github.com/parse-community/parse-server/issues/8012)) ([2d5221e](https://github.com/parse-community/parse-server/commit/2d5221e48012fb7781c0406d543a922d313075ea))
* invalid file request not properly handled [skip release] ([#8061](https://github.com/parse-community/parse-server/issues/8061)) ([1a04a34](https://github.com/parse-community/parse-server/commit/1a04a347cf5e015069fd133bfc7d2566374c99d7))
* live query role cache does not clear when a user is added to a role ([#8026](https://github.com/parse-community/parse-server/issues/8026)) ([199dfc1](https://github.com/parse-community/parse-server/commit/199dfc17226d85a78ab85f24362cce740f4ada39))
* peer dependency mismatch for GraphQL dependencies ([#7934](https://github.com/parse-community/parse-server/issues/7934)) ([0a6faa8](https://github.com/parse-community/parse-server/commit/0a6faa81fa97f8620e7fd05e8c7bbdb4b7da9578))
* protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [skip release] ([#8075](https://github.com/parse-community/parse-server/issues/8075)) ([636d16e](https://github.com/parse-community/parse-server/commit/636d16e0f9f40bbb68ae2b32bcb4d016c1cd749c))
* return correct response when revert is used in beforeSave ([#7839](https://github.com/parse-community/parse-server/issues/7839)) ([19900fc](https://github.com/parse-community/parse-server/commit/19900fcdf8c9f29a674fb62cf6e4b3341d796891))
* security upgrade @parse/fs-files-adapter from 1.2.1 to 1.2.2 ([#7948](https://github.com/parse-community/parse-server/issues/7948)) ([3a70fda](https://github.com/parse-community/parse-server/commit/3a70fda6798d4143f21046439b5eaf232a31bdb6))
* security upgrade moment from 2.29.1 to 2.29.2 ([#7931](https://github.com/parse-community/parse-server/issues/7931)) ([731c550](https://github.com/parse-community/parse-server/commit/731c5507144bbacff236097e7a2a03bfe54f6e10))
* security upgrade parse push adapter from 4.1.0 to 4.1.2 ([#7893](https://github.com/parse-community/parse-server/issues/7893)) ([93667b4](https://github.com/parse-community/parse-server/commit/93667b4e8402bf13b46c4d3ef12cec6532fd9da7))
* server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) [skip release] ([#8237](https://github.com/parse-community/parse-server/issues/8237)) ([4c1befa](https://github.com/parse-community/parse-server/commit/4c1befabf2e40bf3cf41b8b3db257435684f7a62))
* session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) [skip release] ([#8181](https://github.com/parse-community/parse-server/issues/8181)) ([83cdc89](https://github.com/parse-community/parse-server/commit/83cdc89be994416f74533030b90e8d1dd82fec57))
* websocket connection of LiveQuery interrupts frequently ([#8048](https://github.com/parse-community/parse-server/issues/8048)) ([03caae1](https://github.com/parse-community/parse-server/commit/03caae1e611f28079cdddbbe433daaf69e3f595c))

### Features

* add MongoDB 5.1 compatibility ([#7682](https://github.com/parse-community/parse-server/issues/7682)) ([022a856](https://github.com/parse-community/parse-server/commit/022a85619d8a2c57a2f2938e245e4d8a47c15276))
* add MongoDB 5.2 support ([#7894](https://github.com/parse-community/parse-server/issues/7894)) ([5bfa716](https://github.com/parse-community/parse-server/commit/5bfa7160d9e35b237cbae1016ed86724aa99f8d7))
* add support for Node 17 and 18 ([#7896](https://github.com/parse-community/parse-server/issues/7896)) ([3e9f292](https://github.com/parse-community/parse-server/commit/3e9f292d840334244934cee9a34545ac86313549))
* align file trigger syntax with class trigger; use the new syntax `Parse.Cloud.beforeSave(Parse.File, (request) => {})`, the old syntax `Parse.Cloud.beforeSaveFile((request) => {})` has been deprecated ([#7966](https://github.com/parse-community/parse-server/issues/7966)) ([c6dcad8](https://github.com/parse-community/parse-server/commit/c6dcad8d167d44912dbd416d328519314c0809bd))
* replace GraphQL Apollo with GraphQL Yoga ([#7967](https://github.com/parse-community/parse-server/issues/7967)) ([1aa2204](https://github.com/parse-community/parse-server/commit/1aa2204aebfdbe273d54d6d56c6029f7c34aab14))
* selectively enable / disable default authentication adapters ([#7953](https://github.com/parse-community/parse-server/issues/7953)) ([c1e808f](https://github.com/parse-community/parse-server/commit/c1e808f9e807fc49508acbde0d8b3f2b901a1638))
* upgrade mongodb from 4.4.1 to 4.5.0 ([#7991](https://github.com/parse-community/parse-server/issues/7991)) ([e692b5d](https://github.com/parse-community/parse-server/commit/e692b5dd8214cdb0ce79bedd30d9aa3cf4de76a5))

### Performance Improvements

* reduce database operations when using the constant parameter in Cloud Function validation ([#7892](https://github.com/parse-community/parse-server/issues/7892)) ([041197f](https://github.com/parse-community/parse-server/commit/041197fb4ca1cd7cf18dc426ce38647267823668))
2022-10-29 19:11:33 +00:00
Manuel 254954064f build: release (#8263) 2022-10-29 21:04:12 +02:00
Manuel 50409aa866 Merge branch 'release' into build-release 2022-10-29 19:58:37 +02:00
semantic-release-bot 9053e79151 chore(release): 5.3.0-alpha.32 [skip ci]
# [5.3.0-alpha.32](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.31...5.3.0-alpha.32) (2022-10-29)

### Features

* add convenience access to Parse Server configuration in Cloud Code via `Parse.Server` ([#8244](https://github.com/parse-community/parse-server/issues/8244)) ([9f11115](https://github.com/parse-community/parse-server/commit/9f111158edf7fd57a65db0c4f9244b37e58cf293))
2022-10-29 17:10:47 +00:00
dblythy 9f111158ed feat: add convenience access to Parse Server configuration in Cloud Code via Parse.Server (#8244) 2022-10-29 19:03:31 +02:00
Snyk bot eab9cdd749 refactor: upgrade body-parser from 1.20.0 to 1.20.1 (#8262) 2022-10-29 17:39:56 +02:00
Parse Platform 4918665dd2 refactor: upgrade semver from 7.3.7 to 7.3.8 (#8261) 2022-10-27 14:04:09 +02:00
dblythy c41e5fca95 ci: remove running CI for superseded commits (#8213) 2022-10-27 10:36:52 +02:00
semantic-release-bot b54af6b701 chore(release): 5.3.0-alpha.31 [skip ci]
# [5.3.0-alpha.31](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.30...5.3.0-alpha.31) (2022-10-24)

### Bug Fixes

* relation constraints in compound queries `Parse.Query.or`, `Parse.Query.and` not working ([#8203](https://github.com/parse-community/parse-server/issues/8203)) ([28f0d26](https://github.com/parse-community/parse-server/commit/28f0d2667787d2ac68726607b811d6f0ef62b9f1))
2022-10-24 10:54:30 +00:00
dblythy 28f0d26677 fix: relation constraints in compound queries Parse.Query.or, Parse.Query.and not working (#8203) 2022-10-24 12:45:17 +02:00
dependabot[bot] fb503320c3 refactor: bump async from 3.2.1 to 3.2.4 (#8254) 2022-10-20 22:43:34 +02:00
Parse Platform a0499521cc refactor: upgrade pg-monitor from 1.4.1 to 1.5.0 (#8248) 2022-10-20 22:03:20 +02:00
Parse Platform b2491234e8 refactor: upgrade ws from 8.8.1 to 8.9.0 (#8247) 2022-10-19 10:04:34 +02:00
Manuel 8863ad2309 docs: remove differentiation between MongoDB 5.x versions (#8246) 2022-10-17 23:04:45 +02:00
semantic-release-bot 3cde50f82a chore(release): 5.3.0-alpha.30 [skip ci]
# [5.3.0-alpha.30](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.29...5.3.0-alpha.30) (2022-10-17)

### Features

* add support for MongoDB 6 ([#8242](https://github.com/parse-community/parse-server/issues/8242)) ([aba0081](https://github.com/parse-community/parse-server/commit/aba0081ce1a166a93de57f3928c19a05562b5cc1))
2022-10-17 17:29:15 +00:00
Manuel aba0081ce1 feat: add support for MongoDB 6 (#8242) 2022-10-17 19:21:32 +02:00
dblythy 4af13af991 ci: reduce timeout after idempotency tests (#8227) 2022-10-17 01:53:10 +02:00
Manuel 2b00c5b762 docs: change backmerge instructions in contribution docs 2022-10-16 00:53:24 +02:00
semantic-release-bot c1a70fe820 chore(release): 5.3.0-alpha.29 [skip ci]
# [5.3.0-alpha.29](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.28...5.3.0-alpha.29) (2022-10-15)

### Bug Fixes

* server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) [skip release] ([#8238](https://github.com/parse-community/parse-server/issues/8238)) ([c03908f](https://github.com/parse-community/parse-server/commit/c03908f74e5c9eed834874a89df6c89c1a1e849f))

### Features

* add support for Postgres 15 ([#8215](https://github.com/parse-community/parse-server/issues/8215)) ([2feb6c4](https://github.com/parse-community/parse-server/commit/2feb6c46080946c984daa351187fa07cd582355d))
2022-10-15 22:26:12 +00:00
Corey 2feb6c4608 feat: add support for Postgres 15 (#8215) 2022-10-16 00:16:29 +02:00
Snyk bot 6d0bc1eff0 refactor: upgrade follow-redirects from 1.15.1 to 1.15.2 (#8233) 2022-10-15 17:12:27 +02:00
Manuel c03908f74e fix: server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) [skip release] (#8238) 2022-10-15 01:06:45 +02:00
semantic-release-bot 8011b2fdac chore(release): 5.2.8 [skip ci]
## [5.2.8](https://github.com/parse-community/parse-server/compare/5.2.7...5.2.8) (2022-10-14)

### Bug Fixes

* server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) ([#8235](https://github.com/parse-community/parse-server/issues/8235)) ([066f296](https://github.com/parse-community/parse-server/commit/066f29673ab4030b6b5b90c0c0326f7d3fe7612a))
2022-10-14 22:55:38 +00:00
Manuel 4c1befabf2 fix: server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) [skip release] (#8237) 2022-10-15 00:54:08 +02:00
Manuel 066f29673a fix: server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) (#8235) 2022-10-15 00:48:22 +02:00
Snyk bot 89fad24bae refactor: upgrade mongodb from 4.9.1 to 4.10.0 (#8234) 2022-10-14 19:17:30 +02:00
semantic-release-bot 8c30c81da6 chore(release): 5.3.0-alpha.28 [skip ci]
# [5.3.0-alpha.28](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.27...5.3.0-alpha.28) (2022-10-11)

### Features

* liveQuery support for unsorted distance queries ([#8221](https://github.com/parse-community/parse-server/issues/8221)) ([0f763da](https://github.com/parse-community/parse-server/commit/0f763da17d646b2fec2cd980d3857e46072a8a07))
2022-10-11 22:36:58 +00:00
Diamond Lewis 0f763da17d feat: liveQuery support for unsorted distance queries (#8221) 2022-10-12 00:27:29 +02:00
dblythy 2a82d19dbd refactor: code style fixes with prettier and lint (#8208) 2022-10-03 13:55:05 +02:00
Parse Platform 63d51fa6c8 refactor: upgrade @graphql-tools/merge from 8.3.5 to 8.3.6 (#8206) 2022-10-01 21:41:10 +02:00
Parse Platform a0956b5496 refactor: upgrade @graphql-tools/schema from 9.0.3 to 9.0.4 (#8207) 2022-10-01 16:13:21 +02:00
Parse Platform 777b4a9961 refactor: upgrade @graphql-tools/utils from 8.11.0 to 8.12.0 (#8205) 2022-10-01 14:26:53 +02:00
Manuel 566592e443 docs: add FAQs to contribution guide 2022-10-01 13:58:47 +02:00
Manuel 65330e022f docs: update contribution guide 2022-10-01 13:47:27 +02:00
dblythy eb649f226f test: fix flaky Apple Game Center tests (#8204) 2022-10-01 12:14:59 +02:00
semantic-release-bot 5376c3b145 chore(release): 5.3.0-alpha.27 [skip ci]
# [5.3.0-alpha.27](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.26...5.3.0-alpha.27) (2022-09-29)

### Bug Fixes

* authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) [skip release] ([#8187](https://github.com/parse-community/parse-server/issues/8187)) ([8c8ec71](https://github.com/parse-community/parse-server/commit/8c8ec715739e0f851338cfed794409ebac66c51b))
* session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) [skip release] ([#8180](https://github.com/parse-community/parse-server/issues/8180)) ([37fed30](https://github.com/parse-community/parse-server/commit/37fed3062ccc3ef1dfd49a9fc53318e72b3e4aff))

### Features

* add option to change the default value of the `Parse.Query.limit()` constraint ([#8152](https://github.com/parse-community/parse-server/issues/8152)) ([0388956](https://github.com/parse-community/parse-server/commit/038895680894984e569dff54bf5c7b31094f3891))
2022-09-29 23:02:45 +00:00
vzukanov 0388956808 feat: add option to change the default value of the Parse.Query.limit() constraint (#8152) 2022-09-30 00:38:57 +02:00
Manuel b96a4cbdc5 docs: minor rephrasing in contribution docs 2022-09-29 12:34:29 +02:00
Manuel 5908a40d2f docs: minor rephrasing in CONTRIBUTING docs 2022-09-28 15:44:20 +02:00
Manuel b2dd1d9c0f docs: add contribution FAQs (#8201) 2022-09-28 15:22:21 +02:00
Parse Platform c9bd87e2f3 refactor: upgrade @graphql-tools/merge from 8.3.4 to 8.3.5 (#8199) 2022-09-28 02:04:47 +02:00
Parse Platform c1dbd007c5 refactor: upgrade @graphql-tools/utils from 8.10.1 to 8.11.0 (#8198) 2022-09-28 00:06:25 +02:00
Parse Platform 9872051e3b refactor: upgrade @graphql-tools/schema from 9.0.2 to 9.0.3 (#8197) 2022-09-27 22:27:25 +02:00
Parse Platform 643d49199a refactor: upgrade uuid from 8.3.2 to 9.0.0 (#8200) 2022-09-27 13:01:54 +02:00
Manuel ae8d08294b ci: adapt to new MongoDB lifecycle schedule (#8195) 2022-09-26 15:08:12 +02:00
Corey 377ce67239 ci: upgrade Postgres versions (#8191) 2022-09-24 01:06:22 +02:00
Manuel eb20e92f8f docs: update Parse Server support for Postgres table 2022-09-22 22:44:36 +02:00
Snyk bot e8242423e4 refactor: upgrade mongodb from 4.9.0 to 4.9.1 (#8190) 2022-09-22 11:27:47 +02:00
Manuel d9afeb86a5 docs: remove diamond sponsors from README 2022-09-20 23:22:19 +02:00
Manuel 8c8ec71573 fix: authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration appIds is set as a string (e.g. abc) instead of an array of strings (e.g. ["abc"]) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) [skip release] (#8187) 2022-09-20 23:05:44 +02:00
Manuel 1a2b1b9bc1 fix: authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration appIds is set as a string (e.g. abc) instead of an array of strings (e.g. ["abc"]) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) [skip release] (#8188) 2022-09-20 23:03:21 +02:00
semantic-release-bot e6dc487963 chore(release): 5.2.7 [skip ci]
## [5.2.7](https://github.com/parse-community/parse-server/compare/5.2.6...5.2.7) (2022-09-20)

### Bug Fixes

* authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) ([#8185](https://github.com/parse-community/parse-server/issues/8185)) ([ecf0814](https://github.com/parse-community/parse-server/commit/ecf0814499bde31ab6082b6e42854aa65ad2e03e))
2022-09-20 20:43:51 +00:00
Manuel ecf0814499 fix: authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration appIds is set as a string (e.g. abc) instead of an array of strings (e.g. ["abc"]) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) (#8185) 2022-09-20 22:31:19 +02:00
Manuel 83cdc89be9 fix: session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) [skip release] (#8181) 2022-09-20 02:36:54 +02:00
semantic-release-bot 7aac70cca6 chore(release): 5.2.6 [skip ci]
## [5.2.6](https://github.com/parse-community/parse-server/compare/5.2.5...5.2.6) (2022-09-20)

### Bug Fixes

* session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) ([#8182](https://github.com/parse-community/parse-server/issues/8182)) ([6d0b2f5](https://github.com/parse-community/parse-server/commit/6d0b2f534603301bb630d9c8e497af3bc7ff1d09))
2022-09-20 00:27:18 +00:00
Manuel 37fed3062c fix: session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) [skip release] (#8180) 2022-09-20 02:23:49 +02:00
Manuel 6d0b2f5346 fix: session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) (#8182) 2022-09-20 02:18:07 +02:00
Snyk bot 004faf41e4 refactor: upgrade @graphql-tools/merge from 8.3.3 to 8.3.4 (#8175) 2022-09-19 22:07:14 +02:00
Snyk bot bf7d5ba605 refactor: upgrade @graphql-tools/schema from 9.0.1 to 9.0.2 (#8176) 2022-09-19 20:07:26 +02:00
Snyk bot 6fb4d68f4c refactor: upgrade pg-promise from 10.11.1 to 10.12.0 (#8178) 2022-09-19 16:35:20 +02:00
Snyk bot 9fe16738b6 refactor: upgrade @graphql-tools/utils from 8.10.0 to 8.10.1 (#8177) 2022-09-19 15:45:00 +02:00
dblythy df12ba3ba2 docs: regenerate API docs (#8179) 2022-09-19 12:40:15 +02:00
dblythy a5ba5da36d docs: describe additional database options (#8173) 2022-09-18 18:44:31 +02:00
semantic-release-bot b2fe087a02 chore(release): 5.3.0-alpha.26 [skip ci]
# [5.3.0-alpha.26](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.25...5.3.0-alpha.26) (2022-09-17)

### Bug Fixes

* sorting by non-existing value throws `INVALID_SERVER_ERROR` on Postgres ([#8157](https://github.com/parse-community/parse-server/issues/8157)) ([3b775a1](https://github.com/parse-community/parse-server/commit/3b775a1fb8a1878714e3451191438963d688f1b0))
2022-09-17 18:49:17 +00:00
dblythy 3b775a1fb8 fix: sorting by non-existing value throws INVALID_SERVER_ERROR on Postgres (#8157) 2022-09-17 20:41:45 +02:00
semantic-release-bot 73e1763a63 chore(release): 5.3.0-alpha.25 [skip ci]
# [5.3.0-alpha.25](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.24...5.3.0-alpha.25) (2022-09-17)

### Bug Fixes

* updating object includes unchanged keys in client response for certain key types ([#8159](https://github.com/parse-community/parse-server/issues/8159)) ([37af1d7](https://github.com/parse-community/parse-server/commit/37af1d78fce5a15039ffe3af7b323c1f1e8582fc))
2022-09-17 16:30:24 +00:00
dblythy 37af1d78fc fix: updating object includes unchanged keys in client response for certain key types (#8159) 2022-09-17 18:20:50 +02:00
semantic-release-bot 41e44302b4 chore(release): 5.3.0-alpha.24 [skip ci]
# [5.3.0-alpha.24](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.23...5.3.0-alpha.24) (2022-09-17)

### Bug Fixes

* query aggregation pipeline cannot handle value of type `Date` when `directAccess: true` ([#8167](https://github.com/parse-community/parse-server/issues/8167)) ([e424137](https://github.com/parse-community/parse-server/commit/e4241374061caef66538de15112fb6bbafb1f5bb))
2022-09-17 14:26:53 +00:00
dblythy e424137406 fix: query aggregation pipeline cannot handle value of type Date when directAccess: true (#8167) 2022-09-17 16:19:28 +02:00
semantic-release-bot cec3071170 chore(release): 5.3.0-alpha.23 [skip ci]
# [5.3.0-alpha.23](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.22...5.3.0-alpha.23) (2022-09-17)

### Bug Fixes

* liveQuery with `containedIn` not working when object field is an array ([#8128](https://github.com/parse-community/parse-server/issues/8128)) ([1d9605b](https://github.com/parse-community/parse-server/commit/1d9605bc93009263d3811df4d4249034ba6eb8c4))
2022-09-17 12:10:06 +00:00
Stew 1d9605bc93 fix: liveQuery with containedIn not working when object field is an array (#8128) 2022-09-17 13:59:45 +02:00
semantic-release-bot 4a45cc467c chore(release): 5.3.0-alpha.22 [skip ci]
# [5.3.0-alpha.22](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.21...5.3.0-alpha.22) (2022-09-16)

### Bug Fixes

* brute force guessing of user sensitive data via search patterns (GHSA-2m6g-crv8-p3c6) ([#8146](https://github.com/parse-community/parse-server/issues/8146)) [skip release] ([4c0c7c7](https://github.com/parse-community/parse-server/commit/4c0c7c77b76257878b9bcb05ff9de01c9d790262))
* push notifications `badge` doesn't update with Installation beforeSave trigger ([#8162](https://github.com/parse-community/parse-server/issues/8162)) ([3c75c2b](https://github.com/parse-community/parse-server/commit/3c75c2ba4851fae96a8c19b11a3efde03816c9a1))
2022-09-16 19:50:56 +00:00
dblythy 3c75c2ba48 fix: push notifications badge doesn't update with Installation beforeSave trigger (#8162) 2022-09-16 21:43:03 +02:00
dependabot[bot] 5250c07a1c refactor: bump jose from 2.0.5 to 2.0.6 (#8171) 2022-09-16 21:03:05 +02:00
Manuel 1109d0ca23 docs: fix link of official parse email adapter 2022-09-16 11:35:41 +02:00
Manuel 9cd4a35120 ci: add code scanning (#8169) 2022-09-14 23:38:37 +02:00
dblythy c85bc016e2 ci: fix flaky Apple Game Center tests (#8163) 2022-09-14 16:33:55 +02:00
Parse Platform 7c32bfe95f refactor: upgrade mongodb from 4.8.1 to 4.9.0 (#8158) 2022-09-10 10:55:03 +02:00
Snyk bot 07acecdc77 refactor: upgrade winston from 3.8.0 to 3.8.1 (#8155) 2022-09-09 01:20:44 +02:00
Parse Platform dfe9168512 refactor: upgrade graphql from 16.5.0 to 16.6.0 (#8154) 2022-09-08 17:32:39 +02:00
dependabot[bot] 780ee0203a refactor: bump node-fetch from 3.2.4 to 3.2.10 (#8150) 2022-09-04 17:32:27 +02:00
Snyk bot 6a2651c325 refactor: upgrade @graphql-tools/schema from 9.0.0 to 9.0.1 (#8147) 2022-09-04 14:01:16 +02:00
Snyk bot a2d0de7656 refactor: upgrade mongodb from 4.7.0 to 4.8.1 (#8148) 2022-09-04 11:46:36 +02:00
Snyk bot 149884fe3e refactor: upgrade mongodb from 4.6.0 to 4.7.0 (#8083) 2022-09-03 11:22:42 +02:00
Snyk bot f821dfd02a refactor: upgrade @graphql-tools/utils from 8.9.1 to 8.10.0 (#8142) 2022-09-03 02:20:43 +02:00
Manuel 4c0c7c77b7 fix: brute force guessing of user sensitive data via search patterns (GHSA-2m6g-crv8-p3c6) (#8146) [skip release] 2022-09-02 21:43:31 +02:00
Manuel f0db4ca4a4 fix: brute force guessing of user sensitive data via search patterns (GHSA-2m6g-crv8-p3c6) (#8145) [skip release] 2022-09-02 21:43:09 +02:00
semantic-release-bot 83fd16c1b9 chore(release): 5.2.5 [skip ci]
## [5.2.5](https://github.com/parse-community/parse-server/compare/5.2.4...5.2.5) (2022-09-02)

### Bug Fixes

* brute force guessing of user sensitive data via search patterns; this fixes a security vulnerability in which internal and protected fields may be used as query constraints to guess the value of these fields and obtain sensitive data (GHSA-2m6g-crv8-p3c6) ([#8144](https://github.com/parse-community/parse-server/issues/8144)) ([e39d51b](https://github.com/parse-community/parse-server/commit/e39d51bd329cd978589983bd659db46e1d45aad4))
2022-09-02 19:20:39 +00:00
Manuel e39d51bd32 fix: brute force guessing of user sensitive data via search patterns; this fixes a security vulnerability in which internal and protected fields may be used as query constraints to guess the value of these fields and obtain sensitive data (GHSA-2m6g-crv8-p3c6) (#8144) 2022-09-02 21:13:18 +02:00
Snyk bot 5432082d82 refactor: upgrade @graphql-tools/merge from 8.3.2 to 8.3.3 (#8141) 2022-09-02 18:49:04 +02:00
Snyk bot 3de466ba9b refactor: upgrade @graphql-tools/schema from 8.5.1 to 9.0.0 (#8138) 2022-09-02 17:37:40 +02:00
Snyk bot 82eb4613d9 refactor: upgrade @graphql-tools/utils from 8.9.0 to 8.9.1 (#8140) 2022-08-31 19:19:10 +02:00
Snyk bot 276c32a0dd refactor: upgrade @graphql-tools/merge from 8.3.1 to 8.3.2 (#8139) 2022-08-31 13:24:47 +02:00
Snyk bot 4de1c9bdd5 refactor: upgrade @graphql-tools/schema from 8.5.0 to 8.5.1 (#8130) 2022-08-25 14:03:01 +02:00
Snyk bot 0287098ef5 refactor: upgrade @graphql-tools/merge from 8.3.0 to 8.3.1 (#8131) 2022-08-22 19:55:17 +02:00
Snyk bot ef5d59d784 refactor: upgrade @graphql-tools/utils from 8.6.13 to 8.9.0 (#8129) 2022-08-19 12:42:11 +02:00
dependabot[bot] 1db432db51 refactor: bump semver-regex and husky (#8134) 2022-08-19 00:31:14 +02:00
dependabot[bot] 54649eca67 refactor: upgrade @actions/core from 1.2.6 to 1.9.1 (#8132) 2022-08-18 23:57:20 +02:00
dependabot[bot] f693b55b1c refactor: upgrade undici from 5.8.0 to 5.9.1 (#8133) 2022-08-18 22:25:22 +02:00
Manuel 5c2d2c5193 refactor: upgrade ws from 8.8.0 to 8.8.1 (#8123) 2022-08-06 16:15:13 +02:00
semantic-release-bot eef750aa3e chore(release): 5.3.0-alpha.21 [skip ci]
# [5.3.0-alpha.21](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.20...5.3.0-alpha.21) (2022-08-05)

### Bug Fixes

* internal indices for classes `_Idempotency` and `_Role` are not protected in defined schema ([#8121](https://github.com/parse-community/parse-server/issues/8121)) ([c16f529](https://github.com/parse-community/parse-server/commit/c16f529f74f92154401bf662f634b3c5fa45e18e))
2022-08-05 09:34:45 +00:00
Antoine Cormouls c16f529f74 fix: internal indices for classes _Idempotency and _Role are not protected in defined schema (#8121) 2022-08-05 11:25:02 +02:00
Snyk bot 3351ca7cec refactor: upgrade lru-cache from 7.10.2 to 7.12.0 (#8114) 2022-07-27 01:37:32 +02:00
Antonio Davi Macedo Coelho de Castro 2ea4e37a37 refactor: upgrade ldapjs from 2.3.2 to 2.3.3 (#8091) 2022-07-25 23:59:31 +02:00
Snyk bot 1246551be3 refactor: upgrade lru-cache from 7.10.1 to 7.10.2 (#8102) 2022-07-25 14:09:27 +02:00
semantic-release-bot 145008c7d1 chore(release): 5.3.0-alpha.20 [skip ci]
# [5.3.0-alpha.20](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.19...5.3.0-alpha.20) (2022-07-22)

### Bug Fixes

* security upgrade undici from 5.6.0 to 5.8.0 ([#8108](https://github.com/parse-community/parse-server/issues/8108)) ([4aa016b](https://github.com/parse-community/parse-server/commit/4aa016b7322467422b9fdf05d8e29b9ecf910da7))
2022-07-22 18:18:50 +00:00
dependabot[bot] 4aa016b732 fix: security upgrade undici from 5.6.0 to 5.8.0 (#8108) 2022-07-22 20:11:21 +02:00
Snyk bot a96e15b3f7 refactor: upgrade @graphql-tools/merge from 8.2.15 to 8.3.0 (#8106) 2022-07-21 00:27:35 +02:00
Snyk bot 861fb211c7 refactor: upgrade @graphql-tools/schema from 8.3.14 to 8.5.0 (#8104) 2022-07-19 18:34:00 +02:00
Snyk bot 266011c5a5 refactor: upgrade winston from 3.7.2 to 3.8.0 (#8103) 2022-07-17 13:37:15 +02:00
dependabot[bot] e93a0aab57 refactor: bump moment from 2.29.3 to 2.29.4 (#8101) 2022-07-15 22:39:48 +02:00
Snyk bot 24fe6dc939 refactor: upgrade @graphql-tools/merge from 8.2.14 to 8.2.15 (#8100) 2022-07-15 12:55:10 +02:00
dependabot[bot] 38ba9b4f47 refactor: bump undici from 5.2.0 to 5.6.0 (#8094) 2022-07-03 13:45:01 +02:00
semantic-release-bot e3f634e740 chore(release): 5.3.0-alpha.19 [skip ci]
# [5.3.0-alpha.19](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.18...5.3.0-alpha.19) (2022-07-03)

### Bug Fixes

* certificate in Apple Game Center auth adapter not validated [skip release] ([#8058](https://github.com/parse-community/parse-server/issues/8058)) ([75af9a2](https://github.com/parse-community/parse-server/commit/75af9a26cc8e9e88a33d1e452c93a0ee6e509f17))
* graphQL query ignores condition `equalTo` with value `false` ([#8032](https://github.com/parse-community/parse-server/issues/8032)) ([7f5a15d](https://github.com/parse-community/parse-server/commit/7f5a15d5df0dfa3515e9f73709d6a49663545f9b))
* invalid file request not properly handled [skip release] ([#8062](https://github.com/parse-community/parse-server/issues/8062)) ([4c9e956](https://github.com/parse-community/parse-server/commit/4c9e95674ad081f13062e8cd30b77b1962d5df57))
* protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [skip release] ([#8076](https://github.com/parse-community/parse-server/issues/8076)) ([9fd4516](https://github.com/parse-community/parse-server/commit/9fd4516cde5c742f9f29dd05468b4a43a85639a6))
2022-07-03 10:30:00 +00:00
Jong Eun Lee 7f5a15d5df fix: graphQL query ignores condition equalTo with value false (#8032) 2022-07-03 12:13:10 +02:00
Diamond Lewis 6e68656629 refactor: upgrade @graphql-tools/merge from 8.2.13 to 8.2.14 (#8085) 2022-07-03 11:33:30 +02:00
Antonio Davi Macedo Coelho de Castro 0d16a64eea refactor: upgrade ws from 8.7.0 to 8.8.0 (#8092) 2022-07-02 11:36:03 +02:00
Diamond Lewis abd8536f48 refactor: upgrade @graphql-tools/utils from 8.6.12 to 8.6.13 (#8086) 2022-06-30 23:50:12 +02:00
Diamond Lewis 65ce27440a refactor: upgrade @graphql-tools/schema from 8.3.13 to 8.3.14 (#8087) 2022-06-30 16:41:34 +02:00
Manuel 9fd4516cde fix: protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [skip release] (#8076) 2022-06-30 13:01:40 +02:00
Manuel 636d16e0f9 fix: protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [skip release] (#8075) 2022-06-30 12:53:31 +02:00
semantic-release-bot e42be5c526 chore(release): 5.2.4 [skip ci]
## [5.2.4](https://github.com/parse-community/parse-server/compare/5.2.3...5.2.4) (2022-06-30)

### Bug Fixes

* protected fields exposed via LiveQuery; this removes protected fields from the client response; this may be a breaking change if your app is currently expecting to receive these protected fields ([GHSA-crrq-vr9j-fxxh](https://github.com/parse-community/parse-server/security/advisories/GHSA-crrq-vr9j-fxxh)) (https://github.com/parse-community/parse-server/pull/8074) ([#8073](https://github.com/parse-community/parse-server/issues/8073)) ([309f64c](https://github.com/parse-community/parse-server/commit/309f64ced8700321df056fb3cc97f15007a00df1))
2022-06-30 10:46:12 +00:00
Manuel 309f64ced8 fix: protected fields exposed via LiveQuery; this removes protected fields from the client response; this may be a breaking change if your app is currently expecting to receive these protected fields ([GHSA-crrq-vr9j-fxxh](https://github.com/parse-community/parse-server/security/advisories/GHSA-crrq-vr9j-fxxh)) (https://github.com/parse-community/parse-server/pull/8074) (#8073) 2022-06-30 12:26:39 +02:00
Diamond Lewis e8eb546c90 refactor: upgrade jwks-rsa from 2.1.3 to 2.1.4 (#8088) 2022-06-30 11:28:33 +02:00
Manuel 0fd600cf24 docs: add template and issue link requirements (#8080) 2022-06-28 11:18:02 +02:00
Manuel 35cd6910de docs: add LTS explanation and open vulnerabilities to README (#8077) 2022-06-28 10:19:48 +02:00
Manuel 7844442840 docs: add missing heading to commit message section (#8079) 2022-06-28 10:03:40 +02:00
Antonio Davi Macedo Coelho de Castro 42c9543189 refactor: upgrade winston-daily-rotate-file from 4.6.1 to 4.7.1 (#8066) 2022-06-22 22:14:03 +02:00
Snyk bot 86832b9b95 refactor: upgrade follow-redirects from 1.15.0 to 1.15.1 (#8063) 2022-06-19 15:48:12 +02:00
Snyk bot e26beb1f5c refactor: upgrade ws from 8.6.0 to 8.7.0 (#8064) 2022-06-19 10:12:20 +02:00
Manuel 4c9e95674a fix: invalid file request not properly handled [skip release] (#8062) 2022-06-18 02:38:04 +02:00
Manuel 1a04a347cf fix: invalid file request not properly handled [skip release] (#8061) 2022-06-18 02:15:08 +02:00
semantic-release-bot eb2952fff7 chore(release): 5.2.3 [skip ci]
## [5.2.3](https://github.com/parse-community/parse-server/compare/5.2.2...5.2.3) (2022-06-17)

### Bug Fixes

* invalid file request not properly handled; this fixes a security vulnerability in which an invalid file request can crash the server ([GHSA-xw6g-jjvf-wwf9](https://github.com/parse-community/parse-server/security/advisories/GHSA-xw6g-jjvf-wwf9)) ([#8060](https://github.com/parse-community/parse-server/issues/8060)) ([5be375d](https://github.com/parse-community/parse-server/commit/5be375dec2fa35425c1003ae81c55995ac72af92))
2022-06-17 23:40:39 +00:00
Manuel 5be375dec2 fix: invalid file request not properly handled; this fixes a security vulnerability in which an invalid file request can crash the server ([GHSA-xw6g-jjvf-wwf9](https://github.com/parse-community/parse-server/security/advisories/GHSA-xw6g-jjvf-wwf9)) (#8060) 2022-06-18 01:33:19 +02:00
Manuel 75af9a26cc fix: certificate in Apple Game Center auth adapter not validated [skip release] (#8058) 2022-06-17 20:22:35 +02:00
Manuel 4c2aa63fd2 fix: certificate in Apple Game Center auth adapter not validated [skip release] (#8055) 2022-06-17 19:32:30 +02:00
Manuel 4a1039679c docs: add release instructions (#8056) 2022-06-17 19:31:49 +02:00
semantic-release-bot ed0baa87af chore(release): 5.2.2 [skip ci]
## [5.2.2](https://github.com/parse-community/parse-server/compare/5.2.1...5.2.2) (2022-06-17)

### Bug Fixes

* certificate in Apple Game Center auth adapter not validated; this fixes a security vulnerability in which authentication could be bypassed using a fake certificate; if you are using the Apple Gamer Center auth adapter it is your responsibility to keep its root certificate up-to-date and we advice you read the security advisory ([GHSA-rh9j-f5f8-rvgc](https://github.com/parse-community/parse-server/security/advisories/GHSA-rh9j-f5f8-rvgc)) ([ba2b0a9](https://github.com/parse-community/parse-server/commit/ba2b0a9cb9a568817a114b132a4c2e0911d76df1))
2022-06-17 16:36:47 +00:00
Manuel ba2b0a9cb9 fix: certificate in Apple Game Center auth adapter not validated; this fixes a security vulnerability in which authentication could be bypassed using a fake certificate; if you are using the Apple Gamer Center auth adapter it is your responsibility to keep its root certificate up-to-date and we advice you read the security advisory ([GHSA-rh9j-f5f8-rvgc](https://github.com/parse-community/parse-server/security/advisories/GHSA-rh9j-f5f8-rvgc)) 2022-06-17 18:29:26 +02:00
semantic-release-bot ed7a25d5be chore(release): 5.3.0-beta.1 [skip ci]
# [5.3.0-beta.1](https://github.com/parse-community/parse-server/compare/5.2.1...5.3.0-beta.1) (2022-06-17)

### Bug Fixes

* afterSave trigger removes pointer in Parse object ([#7913](https://github.com/parse-community/parse-server/issues/7913)) ([47d796e](https://github.com/parse-community/parse-server/commit/47d796ea58f65e71612ce37149be692abc9ea97f))
* auto-release process may fail if optional back-merging task fails ([#8051](https://github.com/parse-community/parse-server/issues/8051)) ([cf925e7](https://github.com/parse-community/parse-server/commit/cf925e75e87a6989f41e2e2abb2aba4332b1e79f))
* custom database options are not passed to MongoDB GridFS ([#7911](https://github.com/parse-community/parse-server/issues/7911)) ([b1e5565](https://github.com/parse-community/parse-server/commit/b1e5565b22f2eff229571fe9a9500314bd30965b))
* depreciate allowClientClassCreation defaulting to true ([#7925](https://github.com/parse-community/parse-server/issues/7925)) ([38ed96a](https://github.com/parse-community/parse-server/commit/38ed96ace534d639db007aa7dd5387b2da8f03ae))
* errors in GraphQL do not show the original error but a general `Unexpected Error` ([#8045](https://github.com/parse-community/parse-server/issues/8045)) ([0d81887](https://github.com/parse-community/parse-server/commit/0d818879c217f9c56100a5f59868fa37e6d24b71))
* interrupted WebSocket connection not closed by LiveQuery server ([#8012](https://github.com/parse-community/parse-server/issues/8012)) ([2d5221e](https://github.com/parse-community/parse-server/commit/2d5221e48012fb7781c0406d543a922d313075ea))
* live query role cache does not clear when a user is added to a role ([#8026](https://github.com/parse-community/parse-server/issues/8026)) ([199dfc1](https://github.com/parse-community/parse-server/commit/199dfc17226d85a78ab85f24362cce740f4ada39))
* peer dependency mismatch for GraphQL dependencies ([#7934](https://github.com/parse-community/parse-server/issues/7934)) ([0a6faa8](https://github.com/parse-community/parse-server/commit/0a6faa81fa97f8620e7fd05e8c7bbdb4b7da9578))
* return correct response when revert is used in beforeSave ([#7839](https://github.com/parse-community/parse-server/issues/7839)) ([19900fc](https://github.com/parse-community/parse-server/commit/19900fcdf8c9f29a674fb62cf6e4b3341d796891))
* security upgrade @parse/fs-files-adapter from 1.2.1 to 1.2.2 ([#7948](https://github.com/parse-community/parse-server/issues/7948)) ([3a70fda](https://github.com/parse-community/parse-server/commit/3a70fda6798d4143f21046439b5eaf232a31bdb6))
* security upgrade moment from 2.29.1 to 2.29.2 ([#7931](https://github.com/parse-community/parse-server/issues/7931)) ([731c550](https://github.com/parse-community/parse-server/commit/731c5507144bbacff236097e7a2a03bfe54f6e10))
* security upgrade parse push adapter from 4.1.0 to 4.1.2 ([#7893](https://github.com/parse-community/parse-server/issues/7893)) ([93667b4](https://github.com/parse-community/parse-server/commit/93667b4e8402bf13b46c4d3ef12cec6532fd9da7))
* websocket connection of LiveQuery interrupts frequently ([#8048](https://github.com/parse-community/parse-server/issues/8048)) ([03caae1](https://github.com/parse-community/parse-server/commit/03caae1e611f28079cdddbbe433daaf69e3f595c))

### Features

* add MongoDB 5.1 compatibility ([#7682](https://github.com/parse-community/parse-server/issues/7682)) ([022a856](https://github.com/parse-community/parse-server/commit/022a85619d8a2c57a2f2938e245e4d8a47c15276))
* add MongoDB 5.2 support ([#7894](https://github.com/parse-community/parse-server/issues/7894)) ([5bfa716](https://github.com/parse-community/parse-server/commit/5bfa7160d9e35b237cbae1016ed86724aa99f8d7))
* add support for Node 17 and 18 ([#7896](https://github.com/parse-community/parse-server/issues/7896)) ([3e9f292](https://github.com/parse-community/parse-server/commit/3e9f292d840334244934cee9a34545ac86313549))
* align file trigger syntax with class trigger; use the new syntax `Parse.Cloud.beforeSave(Parse.File, (request) => {})`, the old syntax `Parse.Cloud.beforeSaveFile((request) => {})` has been deprecated ([#7966](https://github.com/parse-community/parse-server/issues/7966)) ([c6dcad8](https://github.com/parse-community/parse-server/commit/c6dcad8d167d44912dbd416d328519314c0809bd))
* replace GraphQL Apollo with GraphQL Yoga ([#7967](https://github.com/parse-community/parse-server/issues/7967)) ([1aa2204](https://github.com/parse-community/parse-server/commit/1aa2204aebfdbe273d54d6d56c6029f7c34aab14))
* selectively enable / disable default authentication adapters ([#7953](https://github.com/parse-community/parse-server/issues/7953)) ([c1e808f](https://github.com/parse-community/parse-server/commit/c1e808f9e807fc49508acbde0d8b3f2b901a1638))
* upgrade mongodb from 4.4.1 to 4.5.0 ([#7991](https://github.com/parse-community/parse-server/issues/7991)) ([e692b5d](https://github.com/parse-community/parse-server/commit/e692b5dd8214cdb0ce79bedd30d9aa3cf4de76a5))

### Performance Improvements

* reduce database operations when using the constant parameter in Cloud Function validation ([#7892](https://github.com/parse-community/parse-server/issues/7892)) ([041197f](https://github.com/parse-community/parse-server/commit/041197fb4ca1cd7cf18dc426ce38647267823668))
2022-06-17 12:36:46 +00:00
Manuel b9a69381d2 build: release 2022-06-17 14:26:47 +02:00
semantic-release-bot 2235678de3 chore(release): 5.3.0-alpha.18 [skip ci]
# [5.3.0-alpha.18](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.17...5.3.0-alpha.18) (2022-06-17)

### Bug Fixes

* auto-release process may fail if optional back-merging task fails ([#8051](https://github.com/parse-community/parse-server/issues/8051)) ([cf925e7](https://github.com/parse-community/parse-server/commit/cf925e75e87a6989f41e2e2abb2aba4332b1e79f))
2022-06-17 12:14:50 +00:00
Manuel cf925e75e8 fix: auto-release process may fail if optional back-merging task fails (#8051) 2022-06-17 14:02:25 +02:00
semantic-release-bot 16c7656188 chore(release): 5.3.0-alpha.17 [skip ci]
# [5.3.0-alpha.17](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.16...5.3.0-alpha.17) (2022-06-17)

### Bug Fixes

* errors in GraphQL do not show the original error but a general `Unexpected Error` ([#8045](https://github.com/parse-community/parse-server/issues/8045)) ([0d81887](https://github.com/parse-community/parse-server/commit/0d818879c217f9c56100a5f59868fa37e6d24b71))
* websocket connection of LiveQuery interrupts frequently ([#8048](https://github.com/parse-community/parse-server/issues/8048)) ([03caae1](https://github.com/parse-community/parse-server/commit/03caae1e611f28079cdddbbe433daaf69e3f595c))
2022-06-17 11:51:14 +00:00
Antoine Cormouls 0d818879c2 fix: errors in GraphQL do not show the original error but a general Unexpected Error (#8045) 2022-06-17 13:40:31 +02:00
Layne Bernardo 03caae1e61 fix: websocket connection of LiveQuery interrupts frequently (#8048) 2022-06-17 13:20:48 +02:00
Antoine Cormouls 72fac8a5fc refactor: lru-cache maxAge to ttl (#8039) 2022-06-13 15:29:50 +02:00
Manuel 5f7d392a06 ci: fix slow install (#8040) 2022-06-13 14:33:47 +02:00
Antonio Davi Macedo Coelho de Castro 528690b7b6 refactor: upgrade @graphql-tools/utils from 8.6.10 to 8.6.12 (#8037) 2022-06-12 14:59:18 +02:00
Antonio Davi Macedo Coelho de Castro f67cc99948 refactor: upgrade @graphql-tools/merge from 8.2.11 to 8.2.13 (#8036) 2022-06-12 14:26:31 +02:00
Antonio Davi Macedo Coelho de Castro 61ba1b2913 refactor: upgrade @graphql-tools/schema from 8.3.11 to 8.3.13 (#8035) 2022-06-12 01:52:14 +02:00
Antonio Davi Macedo Coelho de Castro c484094759 refactor: upgrade jwks-rsa from 2.1.2 to 2.1.3 (#8038) 2022-06-12 00:34:34 +02:00
semantic-release-bot 5de4c4723f chore(release): 5.3.0-alpha.16 [skip ci]
# [5.3.0-alpha.16](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.15...5.3.0-alpha.16) (2022-06-11)

### Bug Fixes

* live query role cache does not clear when a user is added to a role ([#8026](https://github.com/parse-community/parse-server/issues/8026)) ([199dfc1](https://github.com/parse-community/parse-server/commit/199dfc17226d85a78ab85f24362cce740f4ada39))
2022-06-11 08:29:17 +00:00
dblythy 199dfc1722 fix: live query role cache does not clear when a user is added to a role (#8026) 2022-06-11 10:21:55 +02:00
Antoine Cormouls 0cd902b8c2 refactor: upgrade GraphQL dependencies (#7970) 2022-06-10 14:01:45 +02:00
semantic-release-bot 0dc2843503 chore(release): 5.3.0-alpha.15 [skip ci]
# [5.3.0-alpha.15](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.14...5.3.0-alpha.15) (2022-06-05)

### Bug Fixes

* interrupted WebSocket connection not closed by LiveQuery server ([#8012](https://github.com/parse-community/parse-server/issues/8012)) ([2d5221e](https://github.com/parse-community/parse-server/commit/2d5221e48012fb7781c0406d543a922d313075ea))
2022-06-05 14:09:29 +00:00
Javad 2d5221e480 fix: interrupted WebSocket connection not closed by LiveQuery server (#8012) 2022-06-05 16:01:48 +02:00
Diamond Lewis 468e98785f refactor: upgrade mongodb from 4.5.0 to 4.6.0 (#8022) 2022-06-04 02:41:43 +02:00
Diamond Lewis 9e506969c3 refactor: upgrade jwks-rsa from 2.1.1 to 2.1.2 (#8023) 2022-06-04 01:52:55 +02:00
Jeff Gu Kang a2491ff036 docs: correct README following markdown rules (#8024) 2022-06-03 13:26:42 +02:00
semantic-release-bot 5e15864dc9 chore(release): 5.3.0-alpha.14 [skip ci]
# [5.3.0-alpha.14](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.13...5.3.0-alpha.14) (2022-05-29)

### Features

* align file trigger syntax with class trigger; use the new syntax `Parse.Cloud.beforeSave(Parse.File, (request) => {})`, the old syntax `Parse.Cloud.beforeSaveFile((request) => {})` has been deprecated ([#7966](https://github.com/parse-community/parse-server/issues/7966)) ([c6dcad8](https://github.com/parse-community/parse-server/commit/c6dcad8d167d44912dbd416d328519314c0809bd))
2022-05-29 19:03:36 +00:00
dblythy c6dcad8d16 feat: align file trigger syntax with class trigger; use the new syntax Parse.Cloud.beforeSave(Parse.File, (request) => {}), the old syntax Parse.Cloud.beforeSaveFile((request) => {}) has been deprecated (#7966) 2022-05-29 20:48:55 +02:00
semantic-release-bot ac283d3cc0 chore(release): 5.3.0-alpha.13 [skip ci]
# [5.3.0-alpha.13](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.12...5.3.0-alpha.13) (2022-05-28)

### Features

* selectively enable / disable default authentication adapters ([#7953](https://github.com/parse-community/parse-server/issues/7953)) ([c1e808f](https://github.com/parse-community/parse-server/commit/c1e808f9e807fc49508acbde0d8b3f2b901a1638))
2022-05-28 23:58:04 +00:00
dblythy c1e808f9e8 feat: selectively enable / disable default authentication adapters (#7953) 2022-05-29 01:50:43 +02:00
Diamond Lewis 88b4d9dda1 refactor: upgrade jwks-rsa from 2.1.0 to 2.1.1 (#8018) 2022-05-29 00:59:44 +02:00
Snyk bot 34a4cca240 refactor: upgrade follow-redirects from 1.14.9 to 1.15.0 (#8014) 2022-05-27 12:15:30 +02:00
Snyk bot 28901aa35f refactor: upgrade parse from 3.4.1 to 3.4.2 (#8013) 2022-05-25 10:28:36 +02:00
Snyk bot 91bb235bc7 refactor: upgrade ws from 8.5.0 to 8.6.0 (#8011) 2022-05-23 19:36:46 +02:00
Antonio Davi Macedo Coelho de Castro 51fb14be3f refactor: upgrade express from 4.18.0 to 4.18.1 (#8009) 2022-05-21 01:52:28 +02:00
semantic-release-bot 8fb9a4a8bc chore(release): 5.3.0-alpha.12 [skip ci]
# [5.3.0-alpha.12](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.11...5.3.0-alpha.12) (2022-05-20)

### Bug Fixes

* afterSave trigger removes pointer in Parse object ([#7913](https://github.com/parse-community/parse-server/issues/7913)) ([47d796e](https://github.com/parse-community/parse-server/commit/47d796ea58f65e71612ce37149be692abc9ea97f))
2022-05-20 08:57:02 +00:00
dblythy 47d796ea58 fix: afterSave trigger removes pointer in Parse object (#7913) 2022-05-20 10:47:38 +02:00
Diamond Lewis 2e750b78cc refactor: upgrade jwks-rsa from 2.0.5 to 2.1.0 (#8007) 2022-05-20 00:42:32 +02:00
Manuel a176de94d2 docs: add chat badge to README (#8006) 2022-05-19 15:00:38 +02:00
semantic-release-bot 0081e1c81c chore(release): 5.3.0-alpha.11 [skip ci]
# [5.3.0-alpha.11](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.10...5.3.0-alpha.11) (2022-05-18)

### Features

* replace GraphQL Apollo with GraphQL Yoga ([#7967](https://github.com/parse-community/parse-server/issues/7967)) ([1aa2204](https://github.com/parse-community/parse-server/commit/1aa2204aebfdbe273d54d6d56c6029f7c34aab14))
2022-05-18 18:03:09 +00:00
Antoine Cormouls 1aa2204aeb feat: replace GraphQL Apollo with GraphQL Yoga (#7967) 2022-05-18 19:55:43 +02:00
Antonio Davi Macedo Coelho de Castro b2ae2e1db4 refactor: upgrade express from 4.17.3 to 4.18.0 (#8003) 2022-05-17 15:32:10 +02:00
Diamond Lewis b10182f35e refactor: upgrade winston-daily-rotate-file from 4.6.0 to 4.6.1 (#7995) 2022-05-11 11:16:07 +02:00
semantic-release-bot 2b1dc5c46a chore(release): 5.3.0-alpha.10 [skip ci]
# [5.3.0-alpha.10](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.9...5.3.0-alpha.10) (2022-05-09)

### Features

* upgrade mongodb from 4.4.1 to 4.5.0 ([#7991](https://github.com/parse-community/parse-server/issues/7991)) ([e692b5d](https://github.com/parse-community/parse-server/commit/e692b5dd8214cdb0ce79bedd30d9aa3cf4de76a5))
2022-05-09 21:59:49 +00:00
Diamond Lewis e692b5dd82 feat: upgrade mongodb from 4.4.1 to 4.5.0 (#7991) 2022-05-09 23:57:08 +02:00
Diamond Lewis 364e346548 refactor: upgrade ws from 8.2.3 to 8.5.0 (#7988) 2022-05-09 14:20:09 +02:00
dblythy 4462b39d48 docs: fix incorrect deprecation date of allowClientClassCreation option (#7992) 2022-05-09 12:28:06 +02:00
Diamond Lewis c35d74d176 refactor: upgrade ldapjs from 2.3.1 to 2.3.2 (#7990) 2022-05-09 03:54:49 +02:00
Antoine Cormouls 330286d22b ci: fix Node version specific tests not running properly in local environment (#7984) 2022-05-08 13:36:58 +02:00
semantic-release-bot ad5e82f2b7 chore(release): 5.3.0-alpha.9 [skip ci]
# [5.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.8...5.3.0-alpha.9) (2022-05-07)

### Bug Fixes

* depreciate allowClientClassCreation defaulting to true ([#7925](https://github.com/parse-community/parse-server/issues/7925)) ([38ed96a](https://github.com/parse-community/parse-server/commit/38ed96ace534d639db007aa7dd5387b2da8f03ae))
2022-05-07 19:09:58 +00:00
dblythy 38ed96ace5 fix: depreciate allowClientClassCreation defaulting to true (#7925) 2022-05-07 21:08:59 +02:00
Manuel d691591630 test: enable GraphQL file upload tests (#7980) 2022-05-06 22:31:30 +02:00
semantic-release-bot 2eb56039e2 chore(release): 5.3.0-alpha.8 [skip ci]
# [5.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.7...5.3.0-alpha.8) (2022-05-06)

### Features

* add support for Node 17 and 18 ([#7896](https://github.com/parse-community/parse-server/issues/7896)) ([3e9f292](https://github.com/parse-community/parse-server/commit/3e9f292d840334244934cee9a34545ac86313549))
2022-05-06 17:15:30 +00:00
Manuel 3e9f292d84 feat: add support for Node 17 and 18 (#7896) 2022-05-06 19:12:19 +02:00
Antoine Cormouls 68b15c298e refactor: replace internal GraphQL array classes to object style (#7788) 2022-05-06 02:09:09 +02:00
Snyk bot 39fbcde612 refactor: upgrade semver from 7.3.5 to 7.3.7 (#7976) 2022-05-06 01:17:50 +02:00
Snyk bot 8f7393f847 refactor: upgrade express from 4.17.2 to 4.17.3 (#7977) 2022-05-06 00:38:04 +02:00
Snyk bot 0294d45078 refactor: upgrade @graphql-tools/links from 8.2.9 to 8.2.11 (#7971) 2022-05-05 10:42:01 +02:00
Snyk bot b84acd578a refactor: upgrade follow-redirects from 1.14.8 to 1.14.9 (#7968) 2022-05-03 21:39:31 +02:00
Diamond Lewis 261ce010dd refactor: upgrade @graphql-tools/links from 8.2.8 to 8.2.9 (#7964) 2022-05-02 03:03:53 +02:00
Manuel e6d7d8f0a2 refactor: upgrade body-parser from 1.19.2 to 1.20.0 (#7961) 2022-05-01 04:26:08 +02:00
dblythy 3fb6b2b4ab ci: fix flaky tests for Apple Game Center authentication (#7958) 2022-05-01 04:26:08 +02:00
Manuel e849cf52d7 refactor: upgrade @apollo/client from 3.5.9 to 3.5.10 (#7957) 2022-05-01 04:26:08 +02:00
Manuel 3c60d6f299 refactor: upgrade winston from 3.5.1 to 3.7.2 (#7954) 2022-05-01 04:26:08 +02:00
semantic-release-bot 19717b8f84 chore(release): 5.3.0-alpha.7 [skip ci]
# [5.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.6...5.3.0-alpha.7) (2022-04-25)

### Bug Fixes

* security upgrade @parse/fs-files-adapter from 1.2.1 to 1.2.2 ([#7948](https://github.com/parse-community/parse-server/issues/7948)) ([20fc4e2](https://github.com/parse-community/parse-server/commit/20fc4e23b53c91aac657f894bd70d049b7525c37))
2022-05-01 04:25:21 +02:00
Snyk bot 3a70fda679 fix: security upgrade @parse/fs-files-adapter from 1.2.1 to 1.2.2 (#7948) 2022-05-01 04:25:20 +02:00
Diamond Lewis e0b588351a refactor: upgrade @graphql-tools/links from 8.2.7 to 8.2.8 (#7945) 2022-05-01 04:25:20 +02:00
Diamond Lewis a31a1ff777 refactor: upgrade body-parser from 1.19.1 to 1.19.2 (#7944) 2022-05-01 04:25:20 +02:00
Diamond Lewis a68d0502bd refactor: upgrade @graphql-tools/links from 8.2.6 to 8.2.7 (#7941) 2022-05-01 04:24:47 +02:00
Snyk bot 89b412caff refactor: upgrade @graphql-tools/links from 8.2.4 to 8.2.6 (#7935) 2022-05-01 04:24:47 +02:00
Snyk bot da99abeaec refactor: upgrade @graphql-tools/links from 8.2.2 to 8.2.4 (#7932) 2022-05-01 04:24:04 +02:00
semantic-release-bot dd40dfa4d9 chore(release): 5.3.0-alpha.6 [skip ci]
# [5.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.5...5.3.0-alpha.6) (2022-04-11)

### Bug Fixes

* peer dependency mismatch for GraphQL dependencies ([#7934](https://github.com/parse-community/parse-server/issues/7934)) ([b7a1d76](https://github.com/parse-community/parse-server/commit/b7a1d7617b4bcac677cecedfeb6ac4a27447083b))
2022-05-01 04:23:07 +02:00
Marvin ROGER 0a6faa81fa fix: peer dependency mismatch for GraphQL dependencies (#7934) 2022-05-01 04:23:00 +02:00
semantic-release-bot f47d58471e chore(release): 5.3.0-alpha.5 [skip ci]
# [5.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.4...5.3.0-alpha.5) (2022-04-09)

### Bug Fixes

* security upgrade moment from 2.29.1 to 2.29.2 ([#7931](https://github.com/parse-community/parse-server/issues/7931)) ([6b68593](https://github.com/parse-community/parse-server/commit/6b68593eaec17e8b183899d2b92699c9ede7625b))
2022-05-01 04:22:38 +02:00
dependabot[bot] 731c550714 fix: security upgrade moment from 2.29.1 to 2.29.2 (#7931) 2022-05-01 04:22:31 +02:00
Snyk bot f51d638246 refactor: upgrade @apollo/client from 3.5.8 to 3.5.9 (#7921) 2022-05-01 04:22:24 +02:00
semantic-release-bot 519b6bac68 chore(release): 5.3.0-alpha.4 [skip ci]
# [5.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.3...5.3.0-alpha.4) (2022-04-04)

### Bug Fixes

* custom database options are not passed to MongoDB GridFS ([#7911](https://github.com/parse-community/parse-server/issues/7911)) ([a72b384](https://github.com/parse-community/parse-server/commit/a72b384f76137a3d83ffb69f65cb25aff1bbab4f))
2022-05-01 04:21:51 +02:00
dblythy b1e5565b22 fix: custom database options are not passed to MongoDB GridFS (#7911) 2022-05-01 04:21:40 +02:00
Antoine Cormouls a169663304 refactor: add missing schema definitions (#7917) 2022-05-01 04:21:33 +02:00
github-actions[bot] e47b61de46 ci: bump environment (#7907) 2022-05-01 04:21:25 +02:00
Snyk bot 341f9f71e3 refactor: upgrade mongodb from 4.3.1 to 4.4.1 (#7906) 2022-05-01 04:21:13 +02:00
semantic-release-bot 91eca2e45f chore(release): 5.3.0-alpha.3 [skip ci]
# [5.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.2...5.3.0-alpha.3) (2022-03-27)

### Features

* add MongoDB 5.2 support ([#7894](https://github.com/parse-community/parse-server/issues/7894)) ([6b4b358](https://github.com/parse-community/parse-server/commit/6b4b358f0842ae920e45652f5e8b2afebc6caf3a))
2022-05-01 04:20:33 +02:00
Manuel 5bfa7160d9 feat: add MongoDB 5.2 support (#7894) 2022-05-01 04:20:25 +02:00
semantic-release-bot 38eb5ace37 chore(release): 5.3.0-alpha.2 [skip ci]
# [5.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.1...5.3.0-alpha.2) (2022-03-27)

### Bug Fixes

* security upgrade parse push adapter from 4.1.0 to 4.1.2 ([#7893](https://github.com/parse-community/parse-server/issues/7893)) ([ef56e98](https://github.com/parse-community/parse-server/commit/ef56e98ef65041b4d3b7b82cce3473269c27f6fd))
2022-05-01 04:19:24 +02:00
Manuel 93667b4e84 fix: security upgrade parse push adapter from 4.1.0 to 4.1.2 (#7893) 2022-05-01 04:19:14 +02:00
semantic-release-bot 58cd870a1e chore(release): 5.3.0-alpha.1 [skip ci]
# [5.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/5.2.1-alpha.2...5.3.0-alpha.1) (2022-03-27)

### Features

* add MongoDB 5.1 compatibility ([#7682](https://github.com/parse-community/parse-server/issues/7682)) ([90155cf](https://github.com/parse-community/parse-server/commit/90155cf1680e5e0499b0000e071c6cb0ce3aef96))
2022-05-01 04:18:40 +02:00
github-actions[bot] 022a85619d feat: add MongoDB 5.1 compatibility (#7682) 2022-05-01 04:18:23 +02:00
semantic-release-bot 94e27ef850 chore(release): 5.2.1-alpha.2 [skip ci]
## [5.2.1-alpha.2](https://github.com/parse-community/parse-server/compare/5.2.1-alpha.1...5.2.1-alpha.2) (2022-03-26)

### Performance Improvements

* reduce database operations when using the constant parameter in Cloud Function validation ([#7892](https://github.com/parse-community/parse-server/issues/7892)) ([48bd512](https://github.com/parse-community/parse-server/commit/48bd512eeb47666967dff8c5e723ddc5b7801daa))
2022-05-01 04:17:49 +02:00
dblythy 041197fb4c perf: reduce database operations when using the constant parameter in Cloud Function validation (#7892) 2022-05-01 04:17:14 +02:00
semantic-release-bot 884f4c0ecd chore(release): 5.2.1-alpha.1 [skip ci]
* return correct response when revert is used in beforeSave ([#7839](https://github.com/parse-community/parse-server/issues/7839)) ([f63fb2b](https://github.com/parse-community/parse-server/commit/f63fb2b338c908f0e7a648d338c26b9daa50c8f2))
2022-05-01 02:40:46 +02:00
dblythy 19900fcdf8 fix: return correct response when revert is used in beforeSave (#7839) 2022-05-01 02:39:56 +02:00
Manuel d609c72783 ci: fix node engine check (#7891) 2022-05-01 02:39:56 +02:00
semantic-release-bot a8aef820af chore(release): 5.2.1 [skip ci]
## [5.2.1](https://github.com/parse-community/parse-server/compare/5.2.0...5.2.1) (2022-05-01)

### Bug Fixes

* authentication bypass and denial of service (DoS) vulnerabilities in Apple Game Center auth adapter (GHSA-qf8x-vqjv-92gr) ([#7962](https://github.com/parse-community/parse-server/issues/7962)) ([af4a041](https://github.com/parse-community/parse-server/commit/af4a0417a9f3c1e99b3793806b4b18e04d9fa999))
2022-05-01 00:31:48 +00:00
Manuel af4a0417a9 fix: authentication bypass and denial of service (DoS) vulnerabilities in Apple Game Center auth adapter (GHSA-qf8x-vqjv-92gr) (#7962) 2022-05-01 02:28:16 +02:00
semantic-release-bot 852bb4782a chore(release): 5.2.0 [skip ci]
# [5.2.0](https://github.com/parse-community/parse-server/compare/5.1.1...5.2.0) (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))

### 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))
2022-03-24 18:23:57 +00:00
Manuel 9c414804ac build: release 2022-03-24 19:18:30 +01:00
mtrezza ff5b39123b ci: release commit 2022-03-24 18:16:15 +00:00
semantic-release-bot 2aeae76d80 chore(release): 5.2.0-beta.2 [skip ci]
# [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))
2022-03-24 18:13:50 +00:00
Manuel 065141f00e build: release beta 2022-03-24 19:12:47 +01:00
mtrezza 0575952509 ci: release commit 2022-03-24 18:12:13 +00:00
semantic-release-bot bc07a3ff3a chore(release): 5.2.0-alpha.3 [skip ci]
# [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))
2022-03-24 13:42:07 +00:00
dependabot[bot] c5cf282d11 fix: security bump minimist from 1.2.5 to 1.2.6 (#7884) 2022-03-24 14:41:04 +01:00
Manuel e2333ba262 docs: add release instructions to contribution guide (#7885) 2022-03-24 03:33:31 +01:00
semantic-release-bot 892c6f94d5 chore(release): 5.2.0-alpha.2 [skip ci]
# [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))
2022-03-24 01:55:04 +00:00
Manuel 0d6f9e951d fix: sensitive keyword detection may produce false positives (#7881) 2022-03-24 02:54:07 +01:00
semantic-release-bot 765cfd02dc chore(release): 5.2.0-beta.1 [skip ci]
# [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))
2022-03-23 01:22:38 +00:00
Manuel ccd3d86137 build: release beta 2022-03-23 02:19:37 +01:00
mtrezza 693d72060d ci: release commit 2022-03-23 01:15:29 +00:00
semantic-release-bot 2b7f772a1b chore(release): 5.2.0-alpha.1 [skip ci]
# [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))
2022-03-23 01:12:46 +00:00
dblythy 443a509905 feat: improved LiveQuery error logging with additional information (#7837) 2022-03-23 02:11:39 +01:00
Manuel 2830021990 build: release 2022-03-23 00:45:30 +01:00
Manuel d2d12d3d69 build: release 2022-03-23 00:39:13 +01:00
Manuel 1099ebbcd8 ci: fix backmerge credentials (#7876) 2022-03-23 00:26:36 +01:00
Manuel 4c50c4d11c ci: add backmerge branches (#7874) 2022-03-23 00:09:49 +01:00
Manuel e9d23830a3 docs: improve reverting in CONTRIBUTION guide (#7866) 2022-03-22 23:21:14 +01:00
semantic-release-bot 01096b360a chore(release): 5.1.1 [skip ci]
## [5.1.1](https://github.com/parse-community/parse-server/compare/5.1.0...5.1.1) (2022-03-18)

### Reverts

* ci: temporarily disable breaking change detection ([#7861](https://github.com/parse-community/parse-server/issues/7861)) ([effed92](https://github.com/parse-community/parse-server/commit/effed92cabd88676fdf9eca2e079a4d8be017f1b))
2022-03-18 15:14:57 +00:00
Manuel effed92cab revert: ci: temporarily disable breaking change detection (#7861)
This reverts commit ef9ee66640.
2022-03-18 16:13:29 +01:00
Manuel 9cdc6ca5b3 docs: update changelog (#7864) 2022-03-18 16:06:49 +01:00
semantic-release-bot 158a974c89 chore(release): 5.1.0 [skip ci]
# [5.1.0](https://github.com/parse-community/parse-server/compare/5.0.0...5.1.0) (2022-03-18)

### 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))
2022-03-18 14:48:03 +00:00
Manuel Trezza 1593575a87 build: release 2022-03-18 15:17:12 +01:00
Manuel ef9ee66640 ci: temporarily disable breaking change detection (#7861) 2022-03-18 15:02:41 +01:00
Manuel d5db318db8 ci: temporarily disable braking change detection (#7860) 2022-03-18 14:38:52 +01:00
semantic-release-bot 9fa80eb4ed chore(release): 5.0.0-beta.10 [skip ci]
# [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))
2022-03-15 00:13:57 +00:00
Manuel ff16839450 build: release beta 2022-03-15 01:12:57 +01:00
Manuel 0c1b75fcbe Merge branch 'beta' into build-release-beta-19837863611 2022-03-15 00:56:54 +01:00
mtrezza 69781ce0cc ci: release commit 2022-03-14 23:51:23 +00:00
Manuel 2eebc68f21 docs: fix release link in CHANGELOG (#7856) 2022-03-14 18:20:12 +01:00
Manuel 79e00e1dee docs: update changelog (#7855) 2022-03-14 13:57:39 +01:00
Manuel 1f6e19fcce docs: add major release instructions to CONTRIBUTING guide (#7854) 2022-03-13 13:44:24 +01:00
Manuel 2dceec7359 docs: improve explanation for commit type usage in CONTRIBUTING guide (#7849) 2022-03-12 16:23:25 +01:00
semantic-release-bot 3e68491df0 chore(release): 5.0.0-alpha.29 [skip ci]
# [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))
2022-03-12 14:12:38 +00:00
Manuel 5ace99d542 feat: bump required node engine to >=12.22.10 (#7846)
BREAKING CHANGE: This requires Node.js version >=12.22.10.
2022-03-12 15:10:44 +01:00
semantic-release-bot 9b344da9d0 chore(release): 5.0.0-alpha.28 [skip ci]
# [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))
2022-03-12 13:48:37 +00:00
Manuel e569f402b1 fix: security vulnerability that allows remote code execution (GHSA-p6h4-93qp-jhcm) (#7844) 2022-03-12 14:47:23 +01:00
Manuel 972b800ae4 docs: add revert commit instructions to CONTRIBUTING guide (#7845) 2022-03-12 14:30:59 +01:00
semantic-release-bot 6bc021ee14 chore(release): 5.0.0-alpha.27 [skip ci]
# [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))
2022-03-12 13:14:36 +00:00
Manuel f235412c1b revert: update node engine to 2.22.0 (#7827)
This reverts commit 8a126fc1a2.
2022-03-12 14:13:38 +01:00
Antonio Davi Macedo Coelho de Castro 8a126fc1a2 Update node engine to 2.22.0 (#7827) 2022-02-25 15:01:15 -08:00
semantic-release-bot ad5d17953b chore(release): 5.0.0-alpha.26 [skip ci]
# [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))
2022-02-25 16:57:59 +00:00
Snyk bot 5ca2288823 fix: package.json & package-lock.json to reduce vulnerabilities (#7823)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-NODEFETCH-2342118
2022-02-25 08:56:29 -08:00
semantic-release-bot 9806912045 chore(release): 5.0.0-alpha.25 [skip ci]
# [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))
2022-02-23 00:56:33 +00:00
Snyk bot 4af253d1f8 fix: upgrade winston from 3.5.0 to 3.5.1 (#7820) 2022-02-23 01:55:33 +01:00
Snyk bot 2a6cec173e refactor: upgrade winston from 3.4.0 to 3.5.0 (#7817) 2022-02-20 18:26:13 +01:00
Snyk bot c7012012aa refactor: upgrade winston-daily-rotate-file from 4.5.5 to 4.6.0 (#7816) 2022-02-18 21:44:52 +01:00
Snyk bot 8cece4a2a8 refactor: upgrade @apollo/client from 3.5.7 to 3.5.8 (#7814) 2022-02-16 23:48:05 +01:00
Snyk bot 7a30d7c885 refactor: upgrade lru-cache from 5.1.1 to 6.0.0 (#7813) 2022-02-15 12:42:13 +01:00
Snyk bot b6a20d9e9f refactor: upgrade winston from 3.3.3 to 3.4.0 (#7812) 2022-02-14 10:13:35 +01:00
Snyk bot 82c3015dc0 refactor: upgrade @parse/push-adapter from 3.4.1 to 4.1.0 (#7810) 2022-02-13 11:14:57 +01:00
semantic-release-bot d678a43696 chore(release): 5.0.0-alpha.24 [skip ci]
# [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))
2022-02-10 11:11:17 +00:00
Snyk bot 70088a95a7 fix: security upgrade follow-redirects from 1.14.7 to 1.14.8 (#7801) 2022-02-10 12:10:14 +01:00
Snyk bot fc9d77f1d4 refactor: upgrade jwks-rsa from 1.12.3 to 2.0.5 (#7800) 2022-02-09 19:07:53 +01:00
Snyk bot c6b2032039 refactor: upgrade @apollo/client from 3.4.17 to 3.5.7 (#7797) 2022-02-08 11:02:45 +01:00
semantic-release-bot 43cc97d99f chore(release): 5.0.0-alpha.23 [skip ci]
# [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))
2022-02-06 23:10:52 +00:00
Antoine Cormouls 08089d6fcb fix: server crash using GraphQL due to missing @apollo/client peer dependency (#7787) 2022-02-07 00:09:41 +01:00
semantic-release-bot 1e05309e79 chore(release): 5.0.0-alpha.22 [skip ci]
# [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))
2022-02-06 17:31:41 +00:00
Antoine Cormouls f88aa2a62a feat: upgrade to MongoDB Node.js driver 4.x for MongoDB 5.0 support (#7794)
BREAKING CHANGE: 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.
2022-02-06 18:30:36 +01:00
semantic-release-bot 1299f0697c chore(release): 5.0.0-alpha.21 [skip ci]
# [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))
2022-01-25 11:41:15 +00:00
yog27ray 315290d161 feat: add Cloud Code context to ParseObject.fetch (#7779) 2022-01-25 12:40:22 +01:00
Snyk bot 433e82f1df refactor: upgrade parse from 3.4.0 to 3.4.1 (#7783) 2022-01-25 01:42:55 +01:00
semantic-release-bot 350b59ac5f chore(release): 5.0.0-alpha.20 [skip ci]
# [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))
2022-01-22 13:32:51 +00:00
dependabot[bot] 9082351411 fix: bump node-fetch from 2.6.1 to 3.1.1 (#7782) 2022-01-22 14:31:45 +01:00
semantic-release-bot f105d7aef1 chore(release): 5.0.0-alpha.19 [skip ci]
# [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))
2022-01-22 12:59:57 +00:00
dependabot[bot] f5f63bfc64 fix: bump nanoid from 3.1.25 to 3.2.0 (#7781) 2022-01-22 13:54:53 +01:00
Snyk bot 826aa79b9f refactor: upgrade express from 4.17.1 to 4.17.2 (#7776) 2022-01-14 14:22:30 +01:00
semantic-release-bot c720fbba5a chore(release): 5.0.0-alpha.18 [skip ci]
# [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))
2022-01-13 02:14:07 +00:00
Snyk bot 8f5a8618cf fix: security upgrade follow-redirects from 1.14.6 to 1.14.7 (#7769) 2022-01-13 03:05:15 +01:00
semantic-release-bot 98abd7112c chore(release): 5.0.0-alpha.17 [skip ci]
# [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))
2022-01-13 01:04:40 +00:00
ThornWu 5af6e5dfaa fix: schema cache not cleared in some cases (#7678) 2022-01-13 02:03:33 +01:00
Corey a5ffb95022 refactor: remove deprecated url.parse() method (#7751) 2022-01-06 15:26:00 +01:00
Corey a43638f300 test: improve transaction tests to use async/await (#7759) 2022-01-04 00:49:43 +01:00
Snyk bot d05fb9f2fa refactor: upgrade graphql from 15.7.0 to 15.7.1 (#7706) 2022-01-03 15:56:55 +01:00
semantic-release-bot 410a1c74fb chore(release): 5.0.0-alpha.16 [skip ci]
# [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))
2022-01-02 18:26:55 +00:00
Corey 0c3feaaa17 feat: add Idempotency to Postgres (#7750) 2022-01-02 19:25:53 +01:00
Corey 5e363eae44 refactor: remove unnecessary error checking in PostgresAdapter (#7761) 2022-01-02 18:43:12 +01:00
Corey 7af5de4b98 test: improve PushController tests (#7760) 2022-01-02 15:51:49 +01:00
semantic-release-bot 75b9a56e0a chore(release): 5.0.0-alpha.15 [skip ci]
# [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))
2022-01-02 14:26:36 +00:00
Corey caf4a2341f feat: support postgresql protocol in database URI (#7757) 2022-01-02 15:25:43 +01:00
Corey 912edacb53 test: make GraphQL server test more reliable (#7758) 2022-01-02 14:59:00 +01:00
Corey b106ffc994 ci: test server with PostGIS 3.2 (#7752) 2022-01-02 01:45:00 +01:00
semantic-release-bot acfdc54a06 chore(release): 5.0.0-alpha.14 [skip ci]
# [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))
2022-01-02 00:11:48 +00:00
Corey 16b1b2a197 feat: support relativeTime query constraint on Postgres (#7747) 2022-01-02 01:10:54 +01:00
Snyk bot 7448521cb9 refactor: upgrade body-parser from 1.19.0 to 1.19.1 (#7756) 2022-01-01 21:22:03 +01:00
Snyk bot 9e477591fd refactor: upgrade follow-redirects from 1.14.5 to 1.14.6 (#7755) 2021-12-31 00:38:26 +01:00
semantic-release-bot 191d80b667 chore(release): 5.0.0-alpha.13 [skip ci]
# [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))
2021-12-08 10:20:38 +00:00
Manuel ea7c01400f fix: node engine compatibility did not include node 16 (#7739) 2021-12-08 11:19:38 +01:00
semantic-release-bot ac789c8537 chore(release): 5.0.0-alpha.12 [skip ci]
# [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))
2021-12-06 23:54:00 +00:00
Ben Devore 6a6248b6cb fix: adding or modifying a nested property requires addField permissions (#7679) 2021-12-07 00:52:59 +01:00
semantic-release-bot e7c7f44265 chore(release): 5.0.0-alpha.11 [skip ci]
# [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))
2021-11-29 23:51:26 +00:00
Snyk bot f5ef98bde3 fix: upgrade mime from 2.5.2 to 3.0.0 (#7725) 2021-11-30 00:50:25 +01:00
semantic-release-bot 37ac29d798 chore(release): 5.0.0-alpha.10 [skip ci]
# [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))
2021-11-29 17:20:43 +00:00
Snyk bot d4c1f47307 fix: upgrade parse from 3.3.1 to 3.4.0 (#7723) 2021-11-29 18:19:48 +01:00
Snyk bot 7dad413a17 refactor: upgrade graphql-tag from 2.12.5 to 2.12.6 (#7726) 2021-11-29 17:58:02 +01:00
Snyk bot 35ac4c3e68 refactor: upgrade subscriptions-transport-ws from 0.10.0 to 0.11.0 (#7727) 2021-11-28 15:44:33 +01:00
semantic-release-bot 1b4d9324de chore(release): 5.0.0-alpha.9 [skip ci]
# [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))
2021-11-27 11:28:15 +00:00
Antoine Cormouls ed86c80772 fix: unable to use objectId size higher than 19 on GraphQL API (#7627) 2021-11-27 12:27:08 +01:00
Corey c789f6c979 refactor: test moved to correct test group (#7717) 2021-11-25 19:16:46 +01:00
Snyk bot 260290409e refactor: upgrade follow-redirects from 1.14.4 to 1.14.5 (#7712) 2021-11-21 14:49:37 +01:00
Manuel 899da8d765 docs: remove node 15 from badge; add node 17 to compatibility table (#7709) 2021-11-18 23:44:10 +01:00
semantic-release-bot fea308a01a chore(release): 5.0.0-alpha.8 [skip ci]
# [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))
2021-11-18 22:38:42 +00:00
Marvin ROGER 45cc58c7e5 feat: add support for Node 16 (#7707)
BREAKING CHANGE: Removes official Node 15 support which has reached it end-of-life date.
2021-11-18 23:37:47 +01:00
Snyk bot ae99b92809 refactor: upgrade graphql from 15.6.1 to 15.7.0 (#7704) 2021-11-17 13:28:34 +01:00
Snyk bot 251ff0ef71 refactor: upgrade graphql from 15.6.0 to 15.6.1 (#7696) 2021-11-14 04:14:21 +01:00
Snyk bot 426ccdc8f3 refactor: upgrade ws from 8.2.2 to 8.2.3 (#7698) 2021-11-14 03:31:05 +01:00
Snyk bot a2fefde4ba refactor: upgrade pg-promise from 10.11.0 to 10.11.1 (#7697) 2021-11-14 03:14:01 +01:00
Snyk bot 3d7ed8dfac refactor: upgrade @parse/fs-files-adapter from 1.2.0 to 1.2.1 (#7695) 2021-11-13 20:11:52 +01:00
Snyk bot e4aa1cb0a0 refactor: upgrade follow-redirects from 1.14.2 to 1.14.4 (#7694) 2021-11-13 00:47:02 +01:00
semantic-release-bot d72717d23a chore(release): 5.0.0-alpha.7 [skip ci]
# [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))
2021-11-12 17:38:15 +00:00
Manuel 573558d3ad fix: node engine range has no upper limit to exclude incompatible node versions (#7692) 2021-11-12 18:37:03 +01:00
semantic-release-bot d35eeb825e chore(release): 5.0.0-alpha.6 [skip ci]
# [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))
2021-11-10 15:27:17 +00:00
Manuel b64640c570 revert: refactor: allow ES import for cloud string if package type is module
This reverts commit 0225340ccb.
2021-11-10 16:26:20 +01:00
Corey 611bd9baaa docs: add FS storage to docs (#7681) 2021-11-08 01:30:15 +01:00
Manuel 31960b59b0 docs: fix sponsor links in readme 2021-11-03 23:44:24 +01:00
Manuel 69fd494ab2 docs: add bronze sponsor and add sponsor widgets to README 2021-11-03 23:03:13 +01:00
Manuel Trezza b6a66761f0 docs: enable badge and changelog for beta release 2021-11-01 17:28:37 +01:00
112 changed files with 7270 additions and 6365 deletions
+101 -52
View File
@@ -1,18 +1,38 @@
name: ci
on:
push:
branches: [ release, alpha, beta, next-major ]
branches: [ release*, alpha, beta ]
pull_request:
branches:
- '**'
branches: [ release*, alpha, beta ]
env:
NODE_VERSION: 14.18.1
NODE_VERSION: 18.1.0
PARSE_SERVER_TEST_TIMEOUT: 20000
jobs:
check-code-analysis:
name: Code Analysis
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
source-root: src
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
check-ci:
name: Node Engine Check
timeout-minutes: 15
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
@@ -33,7 +53,7 @@ jobs:
check-lint:
name: Lint
timeout-minutes: 15
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
@@ -53,7 +73,7 @@ jobs:
check-circular:
name: Circular Dependencies
timeout-minutes: 5
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
@@ -73,7 +93,7 @@ jobs:
check-docker:
name: Docker Build
timeout-minutes: 15
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- name: Checkout repository
uses: actions/checkout@v2
@@ -90,7 +110,7 @@ jobs:
check-lock-file-version:
name: NPM Lock File Version
timeout-minutes: 5
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Check NPM lock file version
@@ -101,63 +121,80 @@ jobs:
strategy:
matrix:
include:
- name: MongoDB 5.0, ReplicaSet, WiredTiger
MONGODB_VERSION: 5.0.3
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 14.18.1
- name: MongoDB 4.4, ReplicaSet, WiredTiger
MONGODB_VERSION: 4.4.10
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 14.18.1
- name: MongoDB 4.2, ReplicaSet, WiredTiger
MONGODB_VERSION: 4.2.17
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 14.18.1
- name: MongoDB 4.0, ReplicaSet, WiredTiger
MONGODB_VERSION: 4.0.27
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 14.18.1
- name: MongoDB 4.0, Standalone, MMAPv1
MONGODB_VERSION: 4.0.27
MONGODB_VERSION: 4.0.28
MONGODB_TOPOLOGY: standalone
MONGODB_STORAGE_ENGINE: mmapv1
NODE_VERSION: 14.18.1
NODE_VERSION: 18.1.0
- name: MongoDB 4.0, ReplicaSet, WiredTiger
MONGODB_VERSION: 4.0.28
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 18.1.0
- name: MongoDB 4.2, ReplicaSet, WiredTiger
MONGODB_VERSION: 4.2.19
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 18.1.0
- name: MongoDB 4.4, ReplicaSet, WiredTiger
MONGODB_VERSION: 4.4.13
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 18.1.0
- name: MongoDB 5, ReplicaSet, WiredTiger
MONGODB_VERSION: 5.3.2
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 18.1.0
- name: MongoDB 6, ReplicaSet, WiredTiger
MONGODB_VERSION: 6.0.2
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 18.1.0
- name: Redis Cache
PARSE_SERVER_TEST_CACHE: redis
MONGODB_VERSION: 4.4.10
MONGODB_VERSION: 4.4.13
MONGODB_TOPOLOGY: standalone
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 14.18.1
NODE_VERSION: 18.1.0
- name: Node 12
MONGODB_VERSION: 4.4.10
MONGODB_VERSION: 4.4.13
MONGODB_TOPOLOGY: standalone
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 12.22.7
- name: Node 15
MONGODB_VERSION: 4.4.10
NODE_VERSION: 12.22.11
- name: Node 14
MONGODB_VERSION: 4.4.13
MONGODB_TOPOLOGY: standalone
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 15.14.0
NODE_VERSION: 14.19.1
- name: Node 16
MONGODB_VERSION: 4.4.13
MONGODB_TOPOLOGY: standalone
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 16.14.2
- name: Node 17
MONGODB_VERSION: 4.4.13
MONGODB_TOPOLOGY: standalone
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 17.9.0
fail-fast: false
name: ${{ matrix.name }}
timeout-minutes: 15
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
services:
redis:
image: redis
ports:
- 6379:6379
env:
env:
MONGODB_VERSION: ${{ matrix.MONGODB_VERSION }}
MONGODB_TOPOLOGY: ${{ matrix.MONGODB_TOPOLOGY }}
MONGODB_STORAGE_ENGINE: ${{ matrix.MONGODB_STORAGE_ENGINE }}
PARSE_SERVER_TEST_CACHE: ${{ matrix.PARSE_SERVER_TEST_CACHE }}
NODE_VERSION: ${{ matrix.NODE_VERSION }}
steps:
- name: Fix usage of insecure GitHub protocol
run: sudo git config --system url."https://github".insteadOf "git://github"
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
uses: actions/setup-node@v2
@@ -183,23 +220,32 @@ jobs:
include:
- name: PostgreSQL 11, PostGIS 3.0
POSTGRES_IMAGE: postgis/postgis:11-3.0
NODE_VERSION: 14.18.1
NODE_VERSION: 18.1.0
- name: PostgreSQL 11, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:11-3.1
NODE_VERSION: 14.18.1
- name: PostgreSQL 12, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:12-3.1
NODE_VERSION: 14.18.1
- name: PostgreSQL 13, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:13-3.1
NODE_VERSION: 14.18.1
- name: PostgreSQL 14, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:14-3.1
NODE_VERSION: 14.18.1
NODE_VERSION: 18.1.0
- name: PostgreSQL 11, PostGIS 3.2
POSTGRES_IMAGE: postgis/postgis:11-3.2
NODE_VERSION: 18.1.0
- name: PostgreSQL 11, PostGIS 3.3
POSTGRES_IMAGE: postgis/postgis:11-3.3
NODE_VERSION: 18.1.0
- name: PostgreSQL 12, PostGIS 3.3
POSTGRES_IMAGE: postgis/postgis:12-3.3
NODE_VERSION: 18.1.0
- name: PostgreSQL 13, PostGIS 3.3
POSTGRES_IMAGE: postgis/postgis:13-3.3
NODE_VERSION: 18.1.0
- name: PostgreSQL 14, PostGIS 3.3
POSTGRES_IMAGE: postgis/postgis:14-3.3
NODE_VERSION: 18.1.0
- name: PostgreSQL 15, PostGIS 3.3
POSTGRES_IMAGE: postgis/postgis:15-3.3
NODE_VERSION: 18.1.0
fail-fast: false
name: ${{ matrix.name }}
timeout-minutes: 15
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
services:
redis:
image: redis
@@ -242,3 +288,6 @@ jobs:
env:
CI: true
- run: bash <(curl -s https://codecov.io/bash)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+2 -2
View File
@@ -1,7 +1,7 @@
name: release-automated
on:
push:
branches: [ release, alpha, beta, next-major ]
branches: [ release, alpha, beta, next-major, release-4.x.x, release-5.x.x ]
jobs:
release:
runs-on: ubuntu-latest
@@ -30,7 +30,7 @@ jobs:
- run: npx semantic-release
env:
GH_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Determine tag on current commit
id: tag
+1 -2
View File
@@ -42,8 +42,7 @@ Details:
- Suitable environment: experimental
[log_release]: https://github.com/parse-community/parse-server/blob/release/CHANGELOG.md
<!--[log_release]: https://github.com/parse-community/parse-server/blob/release/changelogs/CHANGELOG_release.md -->
[log_release]: https://github.com/parse-community/parse-server/blob/release/changelogs/CHANGELOG_release.md
[log_beta]: https://github.com/parse-community/parse-server/blob/beta/changelogs/CHANGELOG_beta.md
[log_alpha]: https://github.com/parse-community/parse-server/blob/alpha/changelogs/CHANGELOG_alpha.md
[branch_release]: https://github.com/parse-community/parse-server/tree/release
+180 -7
View File
@@ -2,7 +2,14 @@
## Table of Contents <!-- omit in toc -->
- [Contributing](#contributing)
- [Issue vs. Pull Request](#issue-vs-pull-request)
- [Templates](#templates)
- [Why Contributing?](#why-contributing)
- [Contribution FAQs](#contribution-faqs)
- [Reviewer Role](#reviewer-role)
- [Review Feedback](#review-feedback)
- [Merge Readiness](#merge-readiness)
- [Review Validity](#review-validity)
- [Environment Setup](#environment-setup)
- [Recommended Tools](#recommended-tools)
- [Setting up your local machine](#setting-up-your-local-machine)
@@ -20,8 +27,18 @@
- [Parse Error](#parse-error)
- [Parse Server Configuration](#parse-server-configuration)
- [Pull Request](#pull-request)
- [Commit Message](#commit-message)
- [Breaking Change](#breaking-change)
- [Merging](#merging)
- [Breaking Change](#breaking-change-1)
- [Reverting](#reverting)
- [Releasing](#releasing)
- [General Considerations](#general-considerations)
- [Major Release / Long-Term-Support](#major-release--long-term-support)
- [Preparing Release](#preparing-release)
- [Publishing Release (forward-merge):](#publishing-release-forward-merge)
- [Publishing Hotfix (back-merge):](#publishing-hotfix-back-merge)
- [Publishing Major Release (Yearly Release)](#publishing-major-release-yearly-release)
- [Versioning](#versioning)
- [Code of Conduct](#code-of-conduct)
@@ -39,6 +56,22 @@ When you are ready to code, you can find more information about opening a pull r
Whether this is your first contribution or you are already an experienced contributor, the Parse Community has your back – don't hesitate to ask for help!
### Issue vs. Pull Request
An issue is required to be linked in every pull request. We understand that no-one likes to create an issue for something that appears to be a simple pull request, but here is why this is beneficial for everyone:
- An issue get more visibility than a pull request as issues can be pinned, receive bounties and it is primarily the issue list that people browse through rather than the more technical pull request list. Visibility is a key aspect so others can weigh in on issues and contribute their opinion.
- The discussion in the issue is different from the discussion in the pull request. The issue discussion is focused on the issue and how to address it, whereas the discussion in the pull request is focused on a specific implemention. An issue may even have multiple pull requests because either the issue requires multiple implementations or multiple pull requests are opened to compare and test different approaches to later decide for one.
- High-level conceptual discussions about the issue should be still available, even if a pull request is closed because its appraoch was discarded. If these discussions are in the pull request instead, they can easily become fragmented over multiple pull requests and issues, which can make it very hard to make sense of all aspects of an issue.
### Templates
You are required to use and completely fill out the templates for new issues and pull requests. We understand that no-one enjoys filling out forms, but here is why this is beneficial for everyone:
- It may take you 30 seconds longer, but will save even more time for everyone else trying to understand your issue.
- It helps to fix issues and merge pull requests faster as reviewers spend less time trying to understand your issue.
- It makes investigations easier when others try to understand your issue and code changes made even years later.
## Why Contributing?
Buy cheap, buy twice. What? No, this is not the Economics 101 class, but the same is true for contributing.
@@ -60,6 +93,46 @@ Consider the benefits you get:
Most importantly, with every contribution you improve your skills so that future contributions take even less time and you get all the benefits above for free — easy choice, right?
## Contribution FAQs
### Reviewer Role
> *Instead of writing review comments back-and-forth, why doesn't the reviewer just write the code themselves?*
A reviewer is already helping you to make a code contribution through their review. A reviewer *may* even help you to write code by actually writing it for you, but is not obliged to do so.
GitHub allows reviewers to suggest and write code changes as part of the review feedback. These code suggestions are likely to contain mistakes due to the lack of code syntax checks when writing code directly on GitHub. You should therefore always review these suggestions before accepting them, ideally in an IDE. If you merge a code suggestion and the CI then fails, take another look at the code change before asking the reviewer for help.
### Review Feedback
> *It takes too much effort to incorporate the review feedback, why why can't you just merge my pull request?*
If you are a new contributor, it's naturally a learning experience for you and therefore takes longer. We welcome contributors of any experience levels and gladly support you in getting familiar with the code base and our quality standards and contribution requirements. In return we expect you to be open to and appreciative of the reviewers' feedback.
In a large pull request, it can be a significant effort to bring it over the finish line. Luckily this is a collaborative environment and others are free to jump in to contribute to the pull request to share the effort. You can either give others access to your fork or they can open their own pull request based on your previous work.
If you are out of resources stay calm, explain your personal constraints (expertise or time) and ask for help. Wasting time by complaining about the amount of review comments will neither use your own time in a meaningful way, nor the time of others who read your complaint.
This is a collaborative enviroment in which everyone works on a common goal - to get a pull request ready for merging. Reviewers are working *with* you to get your pull request ready, *not against you*.
**❗️ Always be mindful that the reviewers' efforts are an integral part of code contribution. Their review is as important as your written code and their review time is a valuable as your coding time.**
### Merge Readiness
> *The feature already works, why do you request more changes instead of just merging my pull request?*
A feature may work for your own use case or in your own environment, but that doesn't necessarily mean that it's ready for merging. Aside from code quality and code style requirements, reviewers also review based on strategic and architectural considerations. It's often easy to just get a feature to work, but it needs to be also maintained in the future, robust therefore well tested and validated, intuitive for other developers to use, well documented, and not cause a forseeable breaking change in the near future.
### Review Validity
> *The reviewer has never worked on the issue and was never part of any previous discussion, why would I care about their opinion?*
It's contrary to an open, collaborative environment to expect others to be involved in an issue or discussion since its beginning. Such a mindset would close out any new views, which are important for a differentiated discussion.
> *The reviewer doesn't have any expertise in that matter, why would I care about their opinion?*
Your arguments must focus on the issue, not on your assumption of someone else's personal experience. We will take immediate and appropriate action in case of personal attacks, regardless of your previous contributions. Personal attacks are not permissible. If you became a victim of personal attacks, you can privately [report](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam) the GitHub comment to the Parse Platform PMC.
## Environment Setup
### Recommended Tools
@@ -154,7 +227,7 @@ If your pull request introduces a change that may affect the storage or retrieva
[PostGIS images (select one with v2.2 or higher) on docker dashboard](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://registry.hub.docker.com/_/postgres/) image and will work out-of-the-box (as long as you create a user with the necessary extensions for each of your Parse databases; see below). To launch the compatible Postgres instance, copy and paste the following line into your shell:
```
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:13-3.1-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:13-3.2-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
```
To stop the Postgres instance:
@@ -162,7 +235,7 @@ To stop the Postgres instance:
docker stop parse-postgres
```
You can also use the [postgis/postgis:13-3.1-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
You can also use the [postgis/postgis:13-3.2-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
```
#Install additional scripts. These are run in abc order during initial start
@@ -296,6 +369,8 @@ Introducing new [Parse Server configuration][config] parameters requires the fol
## Pull Request
### Commit Message
For release automation, the title of pull requests needs to be written in a defined syntax. We loosely follow the [Conventional Commits](https://www.conventionalcommits.org) specification, which defines this syntax:
```
@@ -303,7 +378,7 @@ For release automation, the title of pull requests needs to be written in a defi
```
The _type_ is the category of change that is made, possible types are:
- `feat` - add a new feature
- `feat` - add a new feature or improve an existing feature
- `fix` - fix a bug
- `refactor` - refactor code without impact on features or performance
- `docs` - add or edit code comments, documentation, GitHub pages
@@ -335,8 +410,13 @@ If a pull request contains a braking change, the description of the pull request
The following guide is for anyone who merges a contributor pull request into the working branch, the working branch into a release branch, a release branch into another release branch, or any other direct commits such as hotfixes into release branches or the working branch.
- For changelog generation, only the commit message set when merging the pull request is relevant. The title and description of the GitHub pull request as authored by the contributor have no influence on the changelog generation. However, the title of the GitHub pull request should be used as the commit message.
- If the pull request contains a breaking change, the commit message must contain the phrase `BREAKING CHANGE`, capitalized and without any formatting, followed by a short description of the breaking change and ideally how the developer should address it, all in a single line. This line should contain more details focusing on the "breaking” aspect of the change and is intended to assist the developer in adapting. Keep it concise, as it will become part of the changelog entry, for example:
- A contributor pull request must be merged into the working branch using `Squash and Merge`, to create a single commit message that describes the change.
- A release branch or the default branch must be merged into another release branch using `Merge Commit`, to preserve each individual commit message that describes its respective change.
- For changelog generation, only the commit message set when merging the pull request is relevant. The title and description of the GitHub pull request as authored by the contributor have no influence on the changelog generation. However, the title of the GitHub pull request should be used as the commit message. See the following chapters for considerations in special scenarios, e.g. merging a breaking change or reverting a commit.
### Breaking Change
If the pull request contains a breaking change, the commit message must contain the phrase `BREAKING CHANGE`, capitalized and without any formatting, followed by a short description of the breaking change and ideally how the developer should address it, all in a single line. This line should contain more details focusing on the "breaking” aspect of the change and is intended to assist the developer in adapting. Keep it concise, as it will become part of the changelog entry, for example:
```
fix: remove handle from door
@@ -344,8 +424,101 @@ The following guide is for anyone who merges a contributor pull request into the
BREAKING CHANGE: You cannot open the door anymore by using a handle. See the [#migration guide](http://example.com) for more details.
```
Keep in mind that in a repository with release automation, merging such a commit message will trigger a release with a major version increment.
- A contributor pull request must be merged into the working branch using `Squash and Merge`, to create a single commit message that describes the change.
- A release branch or the default branch must be merged into another release branch using `Merge Commit`, to preserve each individual commit message that describes its respective change.
### Reverting
If the commit reverts a previous commit, use the prefix `revert:`, followed by the header of the reverted commit. In the body of the commit message add `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted. For example:
```
revert: fix: remove handle from door
This reverts commit 1234567890abcdef.
```
⚠️ A `revert` prefix will *always* trigger a release. Generally, a commit that did not trigger a release when it was initially merged should also not trigger a release when it is reverted. For example, do not use the `revert` prefix when reverting a commit that has a `ci` prefix:
```
ci: add something
```
is reverted with:
```
ci: remove something
```
instead of:
```
revert: ci: add something
This reverts commit 1234567890abcdef.
```
## Releasing
### General Considerations
- The `package-lock.json` file has to be deleted and recreated by npm from scratch in regular intervals using the `npm i` command. It is not enough to only update the file via automated security pull requests (e.g. dependabot, snyk), that can create inconsistencies between sub-dependencies of a dependency and increase the chances of vulnerabilities. The file should be recreated once every release cycle which is usually monthly.
### Major Release / Long-Term-Support
While the current major version is published on branch `release`, a Long-Term-Support (LTS) version is published on branch `release-#.x.x`, for example `release-4.x.x` for the Parse Server 4.x LTS branch.
### Preparing Release
The following changes are done in the `alpha` branch, before publishing the last `beta` version that will eventually become the major release. This way the changes trickle naturally through all branches and code consistency is ensured among branches.
- Make sure all [deprecations](https://github.com/parse-community/parse-server/blob/alpha/DEPRECATIONS.md) are reflected in code, old code is removed and the deprecations table is updated.
- Add the future LTS branch `release-#.x.x` to the branch list in [release.config.js](https://github.com/parse-community/parse-server/blob/alpha/release.config.js) so that the branch will later be recognized for release automation.
### Publishing Release (forward-merge):
1. Create new temporary branch `build` on branch `beta`.
2. Create PR to merge `build` into `release`:
- PR title: `build: release`
- PR description: (leave empty)
3. Resolve any conflicts:
- For conflicts regarding the package version in `package.json` and `package-lock.json` it doesn't matter which version is chosen, as the version will be set by auto-release in a commit after merging. However, for both files the same version should be chosen when resolving the conflict.
4. Merge PR with a "merge commit", do not "squash and merge":
- Commit message: (use PR title)
- Description: (leave empty)
5. Wait for GitHub Action `release-automated` to finish:
- If GitHub Action fails, investigate why; manual correction may be needed.
6. Pull all remote branches into local branches.
7. Delete temporary branch `build`.
8. Create new temporary branch `build` on branch `alpha`.
9. Create PR to merge `build` into `beta`:
- PR title: `build: release`
- PR description: (leave empty)
8. Repeat steps 3-7 for PR from step 9.
### Publishing Hotfix (back-merge):
1. Create PR to merge hotfix PR into `release`:
- Merge PR following the same rules as any PR would be merged into the working branch `alpha`.
2. Wait for GitHub Action `release-automated` to finish:
- GitHub Action will fail with error `! [rejected] HEAD -> beta (non-fast-forward)`; this is expected as auto-release currently cannot fully handle back-merging; docker will not publish the new release, so this has to be done manually using the GitHub workflow `release-manual-docker` and entering the version tag that has been created by auto-release.
3. Pull all remote branches into local branches.
4. Create a new temporary branch `backmerge` on branch `release`.
5. Create PR to merge `backmerge` into `beta`:
- PR title: `refactor: <commit-summary>` where `<commit-summary>` is the commit summary of step 1. The commit type needs to be `refactor`, otherwise the commit will show in the changelog of the `release` branch, once the `beta` branch is merged into release; this would a duplicate entry because the same changelog entry has already been generated when the PR was merged into the `release` branch in step 1.
- PR description: (leave empty)
6. Resolve any conflicts:
- During back-merging, usually all changes are preserved; current changes come from the hotfix in the `release` branch, the incoming changes come from the `beta` branch usually being ahead of the `release` branch. This makes back-merging so complex and bug-prone and is the main reason why it should be avoided if possible.
7. Merge PR with "squash and merge", do not do a "merge commit":
- Commit message: (use PR title)
- Description: (leave empty)
ℹ️ Merging this PR will not trigger a release; the back-merge will not appear in changelogs of the `beta`, `alpha` branches; the back-merged fix will be an undocumented change of these branches' next releases; if necessary, the change needs to be added manually to the pre-release changelogs *after* the next pre-releases.
8. Delete temporary branch `backmerge`.
10. Create a new temporary branch `backmerge` on branch `beta`.
11. Repeat steps 4-8 to merge PR into `alpha`.
⚠️ Long-term-support branches are excluded from the processes above and handled individually as they do not have pre-releases branches and are not considered part of the current codebase anymore. It may be necessary to significantly adapt a PR for a LTS branch due to the differences in codebase and CI tests. This adaption should be done in advance before merging any related PR, especially for security fixes, as to not publish a vulnerability while it may still take significant time to adapt the fix for the older codebase of a LTS branch.
### Publishing Major Release (Yearly Release)
1. Create LTS branch `release-#.x.x` off the latest version tag on `release` branch.
2. Create temporary branch `build-release` off branch `beta` and create a pull request with `release` as the base branch.
3. Merge branch `build-release` into `release`. Given that there will be breaking changes, a new major release will be created. In the unlikely case that there have been no breaking changes between the previous major release and the upcoming release, a major version increment has to be triggered manually. See the docs of the release automation framework for how to do that.
## Versioning
+3
View File
@@ -8,6 +8,9 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
| DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
| DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - |
| DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
| DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
| DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - |
[i_deprecation]: ## "The version and date of the deprecation."
[i_removal]: ## "The version and date of the planned removal."
+92 -66
View File
@@ -5,22 +5,22 @@
[![Build Status](https://github.com/parse-community/parse-server/workflows/ci/badge.svg?branch=alpha)](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Aalpha)
[![Snyk Badge](https://snyk.io/test/github/parse-community/parse-server/badge.svg)](https://snyk.io/test/github/parse-community/parse-server)
[![Coverage](https://img.shields.io/codecov/c/github/parse-community/parse-server/alpha.svg)](https://codecov.io/github/parse-community/parse-server?branch=alpha)
[![Node Version](https://img.shields.io/badge/nodejs-12,_14,_15-green.svg?logo=node.js&style=flat)](https://nodejs.org)
[![MongoDB Version](https://img.shields.io/badge/mongodb-4.0,_4.2,_4.4,_5.0-green.svg?logo=mongodb&style=flat)](https://www.mongodb.com)
[![Postgres Version](https://img.shields.io/badge/postgresql-11,_12,_13,_14-green.svg?logo=postgresql&style=flat)](https://www.postgresql.org)
[![auto-release](https://img.shields.io/badge/%F0%9F%9A%80-auto--release-9e34eb.svg)](https://github.com/parse-community/parse-dashboard/releases)
[![Node Version](https://img.shields.io/badge/nodejs-12,_14,_16,_17,_18-green.svg?logo=node.js&style=flat)](https://nodejs.org)
[![MongoDB Version](https://img.shields.io/badge/mongodb-4.0,_4.2,_4.4,_5,_6-green.svg?logo=mongodb&style=flat)](https://www.mongodb.com)
[![Postgres Version](https://img.shields.io/badge/postgresql-11,_12,_13,_14,_15-green.svg?logo=postgresql&style=flat)](https://www.postgresql.org)
[![npm latest version](https://img.shields.io/npm/v/parse-server/latest.svg)](https://www.npmjs.com/package/parse-server)
[![npm beta version](https://img.shields.io/npm/v/parse-server/beta.svg)](https://www.npmjs.com/package/parse-server)
[![npm alpha version](https://img.shields.io/npm/v/parse-server/alpha.svg)](https://www.npmjs.com/package/parse-server)
[![Backers on Open Collective](https://opencollective.com/parse-server/backers/badge.svg)][open-collective-link]
[![Sponsors on Open Collective](https://opencollective.com/parse-server/sponsors/badge.svg)][open-collective-link]
[![License][license-svg]][license-link]
[![Forum](https://img.shields.io/discourse/https/community.parseplatform.org/topics.svg)](https://community.parseplatform.org/c/parse-server)
[![Twitter](https://img.shields.io/twitter/follow/ParsePlatform.svg?label=Follow&style=social)](https://twitter.com/intent/follow?screen_name=ParsePlatform)
[![Chat](https://img.shields.io/badge/Chat-Join!-%23fff?style=social&logo=slack)](https://chat.parseplatform.org)
---
@@ -30,14 +30,16 @@ The full documentation for Parse Server is available in the [wiki](https://githu
---
A big *thank you* to all our backers and sponsors who support the development of Parse Platform!
A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who support the development of Parse Platform!
### 💎 Diamond Sponsors
[![Sponsor](https://opencollective.com/parse-server/sponsor/0/avatar.svg)](https://opencollective.com/parse-server/sponsor/0/website)
#### Bronze Sponsors
[![Bronze Sponsors](https://opencollective.com/parse-server/tiers/bronze-sponsor.svg?avatarHeight=36&button=false)](https://opencollective.com/parse-server/contribute/bronze-sponsor-10559)
---
- [Flavors & Branches](#flavors--branches)
- [Long Term Support](#long-term-support)
- [Getting Started](#getting-started)
- [Running Parse Server](#running-parse-server)
- [Compatibility](#compatibility)
@@ -85,13 +87,25 @@ A big *thank you* to all our backers and sponsors who support the development of
- [Using automatically generated operations](#using-automatically-generated-operations)
- [Customizing your GraphQL Schema](#customizing-your-graphql-schema)
- [Learning more](#learning-more)
- [Upgrading to 3.0.0](#upgrading-to-300)
- [Want to ride the bleeding edge?](#want-to-ride-the-bleeding-edge)
- [Upgrading to Parse Server 3.0](#upgrading-to-parse-server-30)
- [Contributing](#contributing)
- [Contributors](#contributors)
- [Sponsors](#sponsors)
- [Backers](#backers)
# Flavors & Branches
Parse Server is available in different flavors on different branches:
- The main branches are [release][log_release], [beta][log_beta] and [alpha][log_alpha]. See the [changelog overview](CHANGELOG.md) for details.
- The long-term-support (LTS) branches are named `release-<version>.x.x`, for example `release-4.x.x`. LTS branches do not have pre-release branches.
## Long Term Support
Long-Term-Support (LTS) is provided for the previous Parse Server major version. For example, Parse Server 4.x will receive security updates until Parse Server 5.x is superseded by Parse Server 6.x and becomes the new LTS version. While the current major version is published on branch `release`, a LTS version is published on branch `release-#.x.x`, for example `release-4.x.x` for the Parse Server 4.x LTS branch.
⚠️ LTS versions are provided to help you transition as soon as possible to the current major version. While we aim to fix security vulnerabilities in the LTS version, our main focus is on developing the current major version and preparing the next major release. Therefore we may leave certain vulnerabilities up to the community to fix. Search for [pull requests with the specific LTS base branch](https://github.com/parse-community/parse-server/pulls?q=is%3Aopen+is%3Apr+base%3Arelease-4.x.x) to see the current open vulnerabilities for that LTS branch.
# Getting Started
The fastest and easiest way to get started is to run MongoDB and Parse Server locally.
@@ -107,36 +121,43 @@ Before you start make sure you have installed:
### Compatibility
#### Node.js
Parse Server is continuously tested with the most recent releases of Node.js to ensure compatibility. We follow the [Node.js Long Term Support plan](https://github.com/nodejs/Release) and only test against versions that are officially supported and have not reached their end-of-life date.
| Version | Latest Version | End-of-Life | Compatible |
|------------|----------------|-------------|---------------|
| Node.js 12 | 12.22.7 | April 2022 | ✅ Yes |
| Node.js 14 | 14.18.1 | April 2023 | ✅ Yes |
| Node.js 15 | 15.14.0 | June 2021 | ✅ Yes |
| Node.js 16 | 16.x.x | April 2024 | ❌ Not tested |
| Version | Latest Version | End-of-Life | Compatible |
|------------|----------------|-------------|------------|
| Node.js 12 | 12.22.11 | April 2022 | ✅ Yes |
| Node.js 14 | 14.19.1 | April 2023 | ✅ Yes |
| Node.js 16 | 16.14.2 | April 2024 | ✅ Yes |
| Node.js 17 | 17.9.0 | June 2022 | ✅ Yes |
| Node.js 18 | 18.1.0 | April 2025 | ✅ Yes |
#### MongoDB
Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and only test against versions that are officially supported and have not reached their end-of-life date.
| Version | Latest Version | End-of-Life | Compatible |
|-------------|----------------|--------------|------------|
| MongoDB 4.0 | 4.0.27 | April 2022 | ✅ Yes |
| MongoDB 4.2 | 4.2.17 | TBD | ✅ Yes |
| MongoDB 4.4 | 4.4.10 | TBD | ✅ Yes |
| MongoDB 5.0 | 5.0.3 | January 2024 | ✅ Yes |
Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and [MongoDB lifecycle schedule](https://www.mongodb.com/support-policy/lifecycles) and only test against versions that are officially supported and have not reached their end-of-life date. We consider the end-of-life date of a MongoDB "rapid release" to be the same as its major version release.
| Version | Latest Version | End-of-Life | Compatible |
|-------------|----------------|---------------|--------------|
| MongoDB 4.0 | 4.0.28 | April 2022 | ✅ Yes |
| MongoDB 4.2 | 4.2.19 | April 2023 | ✅ Yes |
| MongoDB 4.4 | 4.4.13 | February 2024 | ✅ Yes |
| MongoDB 5 | 5.3.2 | October 2024 | ✅ Yes |
| MongoDB 6 | 6.0.2 | July 2025 | ✅ Yes |
#### PostgreSQL
Parse Server is continuously tested with the most recent releases of PostgreSQL and PostGIS to ensure compatibility, using [PostGIS docker images](https://registry.hub.docker.com/r/postgis/postgis/tags?page=1&ordering=last_updated). We follow the [PostgreSQL support schedule](https://www.postgresql.org/support/versioning) and [PostGIS support schedule](https://www.postgis.net/eol_policy/) and only test against versions that are officially supported and have not reached their end-of-life date. Due to the extensive PostgreSQL support duration of 5 years, Parse Server drops support if a version is older than 3.5 years and a newer version has been available for at least 2.5 years.
| Version | PostGIS Version | End-of-Life | Parse Server Support End | Compatible |
|-------------|-----------------|---------------|--------------------------|------------|
| Postgres 11 | 3.0, 3.1 | November 2023 | April 2022 | ✅ Yes |
| Postgres 12 | 3.1 | November 2024 | April 2023 | ✅ Yes |
| Postgres 13 | 3.1 | November 2025 | April 2024 | ✅ Yes |
| Postgres 14 | 3.1 | November 2026 | April 2025 | ✅ Yes |
Parse Server is continuously tested with the most recent releases of PostgreSQL and PostGIS to ensure compatibility, using [PostGIS docker images](https://registry.hub.docker.com/r/postgis/postgis/tags?page=1&ordering=last_updated). We follow the [PostgreSQL support schedule](https://www.postgresql.org/support/versioning) and [PostGIS support schedule](https://www.postgis.net/eol_policy/) and only test against versions that are officially supported and have not reached their end-of-life date. Due to the extensive PostgreSQL support duration of 5 years, Parse Server drops support about 2 years before the official end-of-life date.
| Version | PostGIS Version | End-of-Life | Parse Server Support | Compatible |
|-------------|--------------------|---------------|----------------------|------------|
| Postgres 11 | 3.0, 3.1, 3.2, 3.3 | November 2023 | <= 5.x (2022) | ✅ Yes |
| Postgres 12 | 3.3 | November 2024 | <= 5.x (2022) | ✅ Yes |
| Postgres 13 | 3.3 | November 2025 | <= 6.x (2023) | ✅ Yes |
| Postgres 14 | 3.3 | November 2026 | <= 7.x (2024) | ✅ Yes |
| Postgres 15 | 3.3 | November 2027 | <= 8.x (2025) | ✅ Yes |
### Locally
```bash
$ npm install -g parse-server mongodb-runner
$ mongodb-runner start
@@ -229,7 +250,6 @@ $ curl -X GET \
}
]
}
```
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](http://docs.parseplatform.org).
@@ -335,8 +355,10 @@ const server = ParseServer({
});
```
Offical email adapters maintained by Parse Platform:
- [parse-server-api-mail-adapter](https://github.com/parse-community/parse-server-api-mail-adapter) (localization, templates, universally supports any email provider)
Email adapters contributed by the community:
- [parse-server-api-mail-adapter](https://www.npmjs.com/package/parse-server-api-mail-adapter) (localization, templates, universally supports any email provider)
- [parse-smtp-template](https://www.npmjs.com/package/parse-smtp-template) (localization, templates)
- [parse-server-postmark-adapter](https://www.npmjs.com/package/parse-server-postmark-adapter)
- [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter)
@@ -379,6 +401,7 @@ const server = ParseServer({
```
## Custom Routes
**Caution, this is an experimental feature that may not be appropriate for production.**
Custom routes allow to build user flows with webpages, similar to the existing password reset and email verification features. Custom routes are defined with the `pages` option in the Parse Server configuration:
@@ -411,6 +434,7 @@ The above route can be invoked by sending a `GET` request to:
The `handler` receives the `request` and returns a `custom_page.html` webpage from the `pages.pagesPath` directory as response. The advantage of building a custom route this way is that it automatically makes use of Parse Server's built-in capabilities, such as [page localization](#pages) and [dynamic placeholders](#dynamic-placeholders).
### Reserved Paths
The following paths are already used by Parse Server's built-in features and are therefore not available for custom routes. Custom routes with an identical combination of `path` and `method` are ignored.
| Path | HTTP Method | Feature |
@@ -486,11 +510,12 @@ You can also find more adapters maintained by the community by searching on [npm
Parse Server allows developers to choose from several options when hosting files:
* `GridFSBucketAdapter`, which is backed by MongoDB;
* `S3Adapter`, which is backed by [Amazon S3](https://aws.amazon.com/s3/); or
* `GCSAdapter`, which is backed by [Google Cloud Storage](https://cloud.google.com/storage/)
* `GridFSBucketAdapter` - which is backed by MongoDB
* `S3Adapter` - which is backed by [Amazon S3](https://aws.amazon.com/s3/)
* `GCSAdapter` - which is backed by [Google Cloud Storage](https://cloud.google.com/storage/)
* `FSAdapter` - local file storage
`GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters).
`GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using Amazon S3, Google Cloud Storage, or local file storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters).
## Idempotency Enforcement
@@ -505,6 +530,7 @@ Identical requests are identified by their request header `X-Parse-Request-Id`.
Deduplication is only done for object creation and update (`POST` and `PUT` requests). Deduplication is not done for object finding and deletion (`GET` and `DELETE` requests), as these operations are already idempotent by definition.
### Configuration example <!-- omit in toc -->
```
let api = new ParseServer({
idempotencyOptions: {
@@ -513,6 +539,7 @@ let api = new ParseServer({
}
}
```
### Parameters <!-- omit in toc -->
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
@@ -521,13 +548,31 @@ let api = new ParseServer({
| `idempotencyOptions.paths` | yes | `Array<String>` | `[]` | `.*` (all paths, includes the examples below), <br>`functions/.*` (all functions), <br>`jobs/.*` (all jobs), <br>`classes/.*` (all classes), <br>`functions/.*` (all functions), <br>`users` (user creation / update), <br>`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specify the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. |
| `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. |
### Notes <!-- omit in toc -->
### Postgres <!-- omit in toc -->
- This feature is currently only available for MongoDB and not for Postgres.
To use this feature in Postgres, you will need to create a cron job using [pgAdmin](https://www.pgadmin.org/docs/pgadmin4/development/pgagent_jobs.html) or similar to call the Postgres function `idempotency_delete_expired_records()` that deletes expired idempotency records. You can find an example script below. Make sure the script has the same privileges to log into Postgres as Parse Server.
```bash
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
SELECT idempotency_delete_expired_records();
EOSQL
exec "$@"
```
Assuming the script above is named, `parse_idempotency_delete_expired_records.sh`, a cron job that runs the script every 2 minutes may look like:
```bash
2 * * * * /root/parse_idempotency_delete_expired_records.sh >/dev/null 2>&1
```
## Localization
### Pages
**Caution, this is an experimental feature that may not be appropriate for production.**
Custom pages as well as feature pages (e.g. password reset, email verification) can be localized with the `pages` option in the Parse Server configuration:
@@ -1071,37 +1116,15 @@ You also have a very powerful tool inside your GraphQL Playground. Please look a
Additionally, the [GraphQL Learn Section](https://graphql.org/learn/) is a very good source to learn more about the power of the GraphQL language.
# Upgrading to 3.0.0
# Upgrading to Parse Server 3.0
Starting 3.0.0, parse-server uses the JS SDK version 2.0.
In short, parse SDK v2.0 removes the backbone style callbacks as well as the Parse.Promise object in favor of native promises.
All the Cloud Code interfaces also have been updated to reflect those changes, and all backbone style response objects are removed and replaced by Promise style resolution.
Starting Parse Server 3.0, Parse Server uses the Parse JavaScript SDK 2.0. In short, the Parse JavaScript SDK 2.0 removes the backbone style callbacks as well as the `Parse.Promise` object in favor of native promises. All the Cloud Code interfaces also have been updated to reflect those changes, and all backbone style response objects are removed and replaced by promise style resolution.
We have written up a [migration guide](3.0.0.md), hoping this will help you transition to the next major release.
# Want to ride the bleeding edge?
It is recommend to use builds deployed npm for many reasons, but if you want to use
the latest not-yet-released version of parse-server, you can do so by depending
directly on this branch:
```
npm install parse-community/parse-server.git#master
```
## Experimenting <!-- omit in toc -->
You can also use your own forks, and work in progress branches by specifying them:
```
npm install github:myUsername/parse-server#my-awesome-feature
```
And don't forget, if you plan to deploy it remotely, you should run `npm install` with the `--save` option.
We have written up a [migration guide](3.0.0.md) to help you transition to the next major release.
# Contributing
We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md).
Please see the [Contributing Guide](CONTRIBUTING.md).
# Contributors
@@ -1157,3 +1180,6 @@ As of April 5, 2017, Parse, LLC has transferred this code to the parse-community
[license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg
[license-link]: LICENSE
[open-collective-link]: https://opencollective.com/parse-server
[log_release]: https://github.com/parse-community/parse-server/blob/release/changelogs/CHANGELOG_release.md
[log_beta]: https://github.com/parse-community/parse-server/blob/beta/changelogs/CHANGELOG_beta.md
[log_alpha]: https://github.com/parse-community/parse-server/blob/alpha/changelogs/CHANGELOG_alpha.md
+456
View File
@@ -1,3 +1,459 @@
# [5.3.0-alpha.32](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.31...5.3.0-alpha.32) (2022-10-29)
### Features
* add convenience access to Parse Server configuration in Cloud Code via `Parse.Server` ([#8244](https://github.com/parse-community/parse-server/issues/8244)) ([9f11115](https://github.com/parse-community/parse-server/commit/9f111158edf7fd57a65db0c4f9244b37e58cf293))
# [5.3.0-alpha.31](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.30...5.3.0-alpha.31) (2022-10-24)
### Bug Fixes
* relation constraints in compound queries `Parse.Query.or`, `Parse.Query.and` not working ([#8203](https://github.com/parse-community/parse-server/issues/8203)) ([28f0d26](https://github.com/parse-community/parse-server/commit/28f0d2667787d2ac68726607b811d6f0ef62b9f1))
# [5.3.0-alpha.30](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.29...5.3.0-alpha.30) (2022-10-17)
### Features
* add support for MongoDB 6 ([#8242](https://github.com/parse-community/parse-server/issues/8242)) ([aba0081](https://github.com/parse-community/parse-server/commit/aba0081ce1a166a93de57f3928c19a05562b5cc1))
# [5.3.0-alpha.29](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.28...5.3.0-alpha.29) (2022-10-15)
### Bug Fixes
* server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) [skip release] ([#8238](https://github.com/parse-community/parse-server/issues/8238)) ([c03908f](https://github.com/parse-community/parse-server/commit/c03908f74e5c9eed834874a89df6c89c1a1e849f))
### Features
* add support for Postgres 15 ([#8215](https://github.com/parse-community/parse-server/issues/8215)) ([2feb6c4](https://github.com/parse-community/parse-server/commit/2feb6c46080946c984daa351187fa07cd582355d))
# [5.3.0-alpha.28](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.27...5.3.0-alpha.28) (2022-10-11)
### Features
* liveQuery support for unsorted distance queries ([#8221](https://github.com/parse-community/parse-server/issues/8221)) ([0f763da](https://github.com/parse-community/parse-server/commit/0f763da17d646b2fec2cd980d3857e46072a8a07))
# [5.3.0-alpha.27](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.26...5.3.0-alpha.27) (2022-09-29)
### Bug Fixes
* authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) [skip release] ([#8187](https://github.com/parse-community/parse-server/issues/8187)) ([8c8ec71](https://github.com/parse-community/parse-server/commit/8c8ec715739e0f851338cfed794409ebac66c51b))
* session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) [skip release] ([#8180](https://github.com/parse-community/parse-server/issues/8180)) ([37fed30](https://github.com/parse-community/parse-server/commit/37fed3062ccc3ef1dfd49a9fc53318e72b3e4aff))
### Features
* add option to change the default value of the `Parse.Query.limit()` constraint ([#8152](https://github.com/parse-community/parse-server/issues/8152)) ([0388956](https://github.com/parse-community/parse-server/commit/038895680894984e569dff54bf5c7b31094f3891))
# [5.3.0-alpha.26](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.25...5.3.0-alpha.26) (2022-09-17)
### Bug Fixes
* sorting by non-existing value throws `INVALID_SERVER_ERROR` on Postgres ([#8157](https://github.com/parse-community/parse-server/issues/8157)) ([3b775a1](https://github.com/parse-community/parse-server/commit/3b775a1fb8a1878714e3451191438963d688f1b0))
# [5.3.0-alpha.25](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.24...5.3.0-alpha.25) (2022-09-17)
### Bug Fixes
* updating object includes unchanged keys in client response for certain key types ([#8159](https://github.com/parse-community/parse-server/issues/8159)) ([37af1d7](https://github.com/parse-community/parse-server/commit/37af1d78fce5a15039ffe3af7b323c1f1e8582fc))
# [5.3.0-alpha.24](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.23...5.3.0-alpha.24) (2022-09-17)
### Bug Fixes
* query aggregation pipeline cannot handle value of type `Date` when `directAccess: true` ([#8167](https://github.com/parse-community/parse-server/issues/8167)) ([e424137](https://github.com/parse-community/parse-server/commit/e4241374061caef66538de15112fb6bbafb1f5bb))
# [5.3.0-alpha.23](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.22...5.3.0-alpha.23) (2022-09-17)
### Bug Fixes
* liveQuery with `containedIn` not working when object field is an array ([#8128](https://github.com/parse-community/parse-server/issues/8128)) ([1d9605b](https://github.com/parse-community/parse-server/commit/1d9605bc93009263d3811df4d4249034ba6eb8c4))
# [5.3.0-alpha.22](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.21...5.3.0-alpha.22) (2022-09-16)
### Bug Fixes
* brute force guessing of user sensitive data via search patterns (GHSA-2m6g-crv8-p3c6) ([#8146](https://github.com/parse-community/parse-server/issues/8146)) [skip release] ([4c0c7c7](https://github.com/parse-community/parse-server/commit/4c0c7c77b76257878b9bcb05ff9de01c9d790262))
* push notifications `badge` doesn't update with Installation beforeSave trigger ([#8162](https://github.com/parse-community/parse-server/issues/8162)) ([3c75c2b](https://github.com/parse-community/parse-server/commit/3c75c2ba4851fae96a8c19b11a3efde03816c9a1))
# [5.3.0-alpha.21](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.20...5.3.0-alpha.21) (2022-08-05)
### Bug Fixes
* internal indices for classes `_Idempotency` and `_Role` are not protected in defined schema ([#8121](https://github.com/parse-community/parse-server/issues/8121)) ([c16f529](https://github.com/parse-community/parse-server/commit/c16f529f74f92154401bf662f634b3c5fa45e18e))
# [5.3.0-alpha.20](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.19...5.3.0-alpha.20) (2022-07-22)
### Bug Fixes
* security upgrade undici from 5.6.0 to 5.8.0 ([#8108](https://github.com/parse-community/parse-server/issues/8108)) ([4aa016b](https://github.com/parse-community/parse-server/commit/4aa016b7322467422b9fdf05d8e29b9ecf910da7))
# [5.3.0-alpha.19](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.18...5.3.0-alpha.19) (2022-07-03)
### Bug Fixes
* certificate in Apple Game Center auth adapter not validated [skip release] ([#8058](https://github.com/parse-community/parse-server/issues/8058)) ([75af9a2](https://github.com/parse-community/parse-server/commit/75af9a26cc8e9e88a33d1e452c93a0ee6e509f17))
* graphQL query ignores condition `equalTo` with value `false` ([#8032](https://github.com/parse-community/parse-server/issues/8032)) ([7f5a15d](https://github.com/parse-community/parse-server/commit/7f5a15d5df0dfa3515e9f73709d6a49663545f9b))
* invalid file request not properly handled [skip release] ([#8062](https://github.com/parse-community/parse-server/issues/8062)) ([4c9e956](https://github.com/parse-community/parse-server/commit/4c9e95674ad081f13062e8cd30b77b1962d5df57))
* protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [skip release] ([#8076](https://github.com/parse-community/parse-server/issues/8076)) ([9fd4516](https://github.com/parse-community/parse-server/commit/9fd4516cde5c742f9f29dd05468b4a43a85639a6))
# [5.3.0-alpha.18](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.17...5.3.0-alpha.18) (2022-06-17)
### Bug Fixes
* auto-release process may fail if optional back-merging task fails ([#8051](https://github.com/parse-community/parse-server/issues/8051)) ([cf925e7](https://github.com/parse-community/parse-server/commit/cf925e75e87a6989f41e2e2abb2aba4332b1e79f))
# [5.3.0-alpha.17](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.16...5.3.0-alpha.17) (2022-06-17)
### Bug Fixes
* errors in GraphQL do not show the original error but a general `Unexpected Error` ([#8045](https://github.com/parse-community/parse-server/issues/8045)) ([0d81887](https://github.com/parse-community/parse-server/commit/0d818879c217f9c56100a5f59868fa37e6d24b71))
* websocket connection of LiveQuery interrupts frequently ([#8048](https://github.com/parse-community/parse-server/issues/8048)) ([03caae1](https://github.com/parse-community/parse-server/commit/03caae1e611f28079cdddbbe433daaf69e3f595c))
# [5.3.0-alpha.16](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.15...5.3.0-alpha.16) (2022-06-11)
### Bug Fixes
* live query role cache does not clear when a user is added to a role ([#8026](https://github.com/parse-community/parse-server/issues/8026)) ([199dfc1](https://github.com/parse-community/parse-server/commit/199dfc17226d85a78ab85f24362cce740f4ada39))
# [5.3.0-alpha.15](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.14...5.3.0-alpha.15) (2022-06-05)
### Bug Fixes
* interrupted WebSocket connection not closed by LiveQuery server ([#8012](https://github.com/parse-community/parse-server/issues/8012)) ([2d5221e](https://github.com/parse-community/parse-server/commit/2d5221e48012fb7781c0406d543a922d313075ea))
# [5.3.0-alpha.14](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.13...5.3.0-alpha.14) (2022-05-29)
### Features
* align file trigger syntax with class trigger; use the new syntax `Parse.Cloud.beforeSave(Parse.File, (request) => {})`, the old syntax `Parse.Cloud.beforeSaveFile((request) => {})` has been deprecated ([#7966](https://github.com/parse-community/parse-server/issues/7966)) ([c6dcad8](https://github.com/parse-community/parse-server/commit/c6dcad8d167d44912dbd416d328519314c0809bd))
# [5.3.0-alpha.13](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.12...5.3.0-alpha.13) (2022-05-28)
### Features
* selectively enable / disable default authentication adapters ([#7953](https://github.com/parse-community/parse-server/issues/7953)) ([c1e808f](https://github.com/parse-community/parse-server/commit/c1e808f9e807fc49508acbde0d8b3f2b901a1638))
# [5.3.0-alpha.12](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.11...5.3.0-alpha.12) (2022-05-20)
### Bug Fixes
* afterSave trigger removes pointer in Parse object ([#7913](https://github.com/parse-community/parse-server/issues/7913)) ([47d796e](https://github.com/parse-community/parse-server/commit/47d796ea58f65e71612ce37149be692abc9ea97f))
# [5.3.0-alpha.11](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.10...5.3.0-alpha.11) (2022-05-18)
### Features
* replace GraphQL Apollo with GraphQL Yoga ([#7967](https://github.com/parse-community/parse-server/issues/7967)) ([1aa2204](https://github.com/parse-community/parse-server/commit/1aa2204aebfdbe273d54d6d56c6029f7c34aab14))
# [5.3.0-alpha.10](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.9...5.3.0-alpha.10) (2022-05-09)
### Features
* upgrade mongodb from 4.4.1 to 4.5.0 ([#7991](https://github.com/parse-community/parse-server/issues/7991)) ([e692b5d](https://github.com/parse-community/parse-server/commit/e692b5dd8214cdb0ce79bedd30d9aa3cf4de76a5))
# [5.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.8...5.3.0-alpha.9) (2022-05-07)
### Bug Fixes
* depreciate allowClientClassCreation defaulting to true ([#7925](https://github.com/parse-community/parse-server/issues/7925)) ([38ed96a](https://github.com/parse-community/parse-server/commit/38ed96ace534d639db007aa7dd5387b2da8f03ae))
# [5.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.7...5.3.0-alpha.8) (2022-05-06)
### Features
* add support for Node 17 and 18 ([#7896](https://github.com/parse-community/parse-server/issues/7896)) ([3e9f292](https://github.com/parse-community/parse-server/commit/3e9f292d840334244934cee9a34545ac86313549))
# [5.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.6...5.3.0-alpha.7) (2022-04-25)
### Bug Fixes
* security upgrade @parse/fs-files-adapter from 1.2.1 to 1.2.2 ([#7948](https://github.com/parse-community/parse-server/issues/7948)) ([20fc4e2](https://github.com/parse-community/parse-server/commit/20fc4e23b53c91aac657f894bd70d049b7525c37))
# [5.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.5...5.3.0-alpha.6) (2022-04-11)
### Bug Fixes
* peer dependency mismatch for GraphQL dependencies ([#7934](https://github.com/parse-community/parse-server/issues/7934)) ([b7a1d76](https://github.com/parse-community/parse-server/commit/b7a1d7617b4bcac677cecedfeb6ac4a27447083b))
# [5.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.4...5.3.0-alpha.5) (2022-04-09)
### Bug Fixes
* security upgrade moment from 2.29.1 to 2.29.2 ([#7931](https://github.com/parse-community/parse-server/issues/7931)) ([6b68593](https://github.com/parse-community/parse-server/commit/6b68593eaec17e8b183899d2b92699c9ede7625b))
# [5.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.3...5.3.0-alpha.4) (2022-04-04)
### Bug Fixes
* custom database options are not passed to MongoDB GridFS ([#7911](https://github.com/parse-community/parse-server/issues/7911)) ([a72b384](https://github.com/parse-community/parse-server/commit/a72b384f76137a3d83ffb69f65cb25aff1bbab4f))
# [5.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.2...5.3.0-alpha.3) (2022-03-27)
### Features
* add MongoDB 5.2 support ([#7894](https://github.com/parse-community/parse-server/issues/7894)) ([6b4b358](https://github.com/parse-community/parse-server/commit/6b4b358f0842ae920e45652f5e8b2afebc6caf3a))
# [5.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.1...5.3.0-alpha.2) (2022-03-27)
### Bug Fixes
* security upgrade parse push adapter from 4.1.0 to 4.1.2 ([#7893](https://github.com/parse-community/parse-server/issues/7893)) ([ef56e98](https://github.com/parse-community/parse-server/commit/ef56e98ef65041b4d3b7b82cce3473269c27f6fd))
# [5.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/5.2.1-alpha.2...5.3.0-alpha.1) (2022-03-27)
### Features
* add MongoDB 5.1 compatibility ([#7682](https://github.com/parse-community/parse-server/issues/7682)) ([90155cf](https://github.com/parse-community/parse-server/commit/90155cf1680e5e0499b0000e071c6cb0ce3aef96))
## [5.2.1-alpha.2](https://github.com/parse-community/parse-server/compare/5.2.1-alpha.1...5.2.1-alpha.2) (2022-03-26)
### Performance Improvements
* reduce database operations when using the constant parameter in Cloud Function validation ([#7892](https://github.com/parse-community/parse-server/issues/7892)) ([48bd512](https://github.com/parse-community/parse-server/commit/48bd512eeb47666967dff8c5e723ddc5b7801daa))
## [5.2.1-alpha.1](https://github.com/parse-community/parse-server/compare/5.2.0...5.2.1-alpha.1) (2022-03-26)
### Bug Fixes
* return correct response when revert is used in beforeSave ([#7839](https://github.com/parse-community/parse-server/issues/7839)) ([f63fb2b](https://github.com/parse-community/parse-server/commit/f63fb2b338c908f0e7a648d338c26b9daa50c8f2))
# [5.2.0-alpha.3](https://github.com/parse-community/parse-server/compare/5.2.0-alpha.2...5.2.0-alpha.3) (2022-03-24)
### Bug Fixes
* security bump minimist from 1.2.5 to 1.2.6 ([#7884](https://github.com/parse-community/parse-server/issues/7884)) ([c5cf282](https://github.com/parse-community/parse-server/commit/c5cf282d11ffdc023764f8e7539a2bd6bc246fe1))
# [5.2.0-alpha.2](https://github.com/parse-community/parse-server/compare/5.2.0-alpha.1...5.2.0-alpha.2) (2022-03-24)
### Bug Fixes
* sensitive keyword detection may produce false positives ([#7881](https://github.com/parse-community/parse-server/issues/7881)) ([0d6f9e9](https://github.com/parse-community/parse-server/commit/0d6f9e951d9e186e95e96d8869066ce7022bad02))
# [5.2.0-alpha.1](https://github.com/parse-community/parse-server/compare/5.1.1...5.2.0-alpha.1) (2022-03-23)
### Features
* improved LiveQuery error logging with additional information ([#7837](https://github.com/parse-community/parse-server/issues/7837)) ([443a509](https://github.com/parse-community/parse-server/commit/443a5099059538d379fe491793a5871fcbb4f377))
# [5.0.0-alpha.29](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.28...5.0.0-alpha.29) (2022-03-12)
### Features
* bump required node engine to >=12.22.10 ([#7846](https://github.com/parse-community/parse-server/issues/7846)) ([5ace99d](https://github.com/parse-community/parse-server/commit/5ace99d542a11e422af46d9fd6b1d3d2513b34cf))
### BREAKING CHANGES
* This requires Node.js version >=12.22.10. ([5ace99d](5ace99d))
# [5.0.0-alpha.28](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.27...5.0.0-alpha.28) (2022-03-12)
### Bug Fixes
* security vulnerability that allows remote code execution (GHSA-p6h4-93qp-jhcm) ([#7844](https://github.com/parse-community/parse-server/issues/7844)) ([e569f40](https://github.com/parse-community/parse-server/commit/e569f402b1fd8648fb0d1523b71b2a03273902a5))
# [5.0.0-alpha.27](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.26...5.0.0-alpha.27) (2022-03-12)
### Reverts
* update node engine to 2.22.0 ([#7827](https://github.com/parse-community/parse-server/issues/7827)) ([f235412](https://github.com/parse-community/parse-server/commit/f235412c1b6c2b173b7531f285429ea7214b56a2))
# [5.0.0-alpha.26](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.25...5.0.0-alpha.26) (2022-02-25)
### Bug Fixes
* package.json & package-lock.json to reduce vulnerabilities ([#7823](https://github.com/parse-community/parse-server/issues/7823)) ([5ca2288](https://github.com/parse-community/parse-server/commit/5ca228882332b65f3ac05407e6e4da1ee3ef3749))
# [5.0.0-alpha.25](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.24...5.0.0-alpha.25) (2022-02-23)
### Bug Fixes
* upgrade winston from 3.5.0 to 3.5.1 ([#7820](https://github.com/parse-community/parse-server/issues/7820)) ([4af253d](https://github.com/parse-community/parse-server/commit/4af253d1f8654a6f57b5137ad310cdacadc922cc))
# [5.0.0-alpha.24](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.23...5.0.0-alpha.24) (2022-02-10)
### Bug Fixes
* security upgrade follow-redirects from 1.14.7 to 1.14.8 ([#7801](https://github.com/parse-community/parse-server/issues/7801)) ([70088a9](https://github.com/parse-community/parse-server/commit/70088a95a78393da2a4ac68be81e63107747626a))
# [5.0.0-alpha.23](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.22...5.0.0-alpha.23) (2022-02-06)
### Bug Fixes
* server crash using GraphQL due to missing @apollo/client peer dependency ([#7787](https://github.com/parse-community/parse-server/issues/7787)) ([08089d6](https://github.com/parse-community/parse-server/commit/08089d6fcbb215412448ce7d92b21b9fe6c929f2))
# [5.0.0-alpha.22](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.21...5.0.0-alpha.22) (2022-02-06)
### Features
* upgrade to MongoDB Node.js driver 4.x for MongoDB 5.0 support ([#7794](https://github.com/parse-community/parse-server/issues/7794)) ([f88aa2a](https://github.com/parse-community/parse-server/commit/f88aa2a62a533e5344d1c13dd38c5a0b283a480a))
### BREAKING CHANGES
* The MongoDB GridStore adapter has been removed. By default, Parse Server already uses GridFS, so if you do not manually use the GridStore adapter, you can ignore this change. ([f88aa2a](f88aa2a))
# [5.0.0-alpha.21](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.20...5.0.0-alpha.21) (2022-01-25)
### Features
* add Cloud Code context to `ParseObject.fetch` ([#7779](https://github.com/parse-community/parse-server/issues/7779)) ([315290d](https://github.com/parse-community/parse-server/commit/315290d16110110938f80a6b779cc2d1db58c552))
# [5.0.0-alpha.20](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.19...5.0.0-alpha.20) (2022-01-22)
### Bug Fixes
* bump node-fetch from 2.6.1 to 3.1.1 ([#7782](https://github.com/parse-community/parse-server/issues/7782)) ([9082351](https://github.com/parse-community/parse-server/commit/90823514113a1a085ebc818f7109b3fd7591346f))
# [5.0.0-alpha.19](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.18...5.0.0-alpha.19) (2022-01-22)
### Bug Fixes
* bump nanoid from 3.1.25 to 3.2.0 ([#7781](https://github.com/parse-community/parse-server/issues/7781)) ([f5f63bf](https://github.com/parse-community/parse-server/commit/f5f63bfc64d3481ed944ceb5e9f50b33dccd1ce9))
# [5.0.0-alpha.18](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.17...5.0.0-alpha.18) (2022-01-13)
### Bug Fixes
* security upgrade follow-redirects from 1.14.6 to 1.14.7 ([#7769](https://github.com/parse-community/parse-server/issues/7769)) ([8f5a861](https://github.com/parse-community/parse-server/commit/8f5a8618cfa7ed9a2a239a095abffa8f3fd8d31a))
# [5.0.0-alpha.17](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.16...5.0.0-alpha.17) (2022-01-13)
### Bug Fixes
* schema cache not cleared in some cases ([#7678](https://github.com/parse-community/parse-server/issues/7678)) ([5af6e5d](https://github.com/parse-community/parse-server/commit/5af6e5dfaa129b1a350afcba4fb381b21c4cc35d))
# [5.0.0-alpha.16](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.15...5.0.0-alpha.16) (2022-01-02)
### Features
* add Idempotency to Postgres ([#7750](https://github.com/parse-community/parse-server/issues/7750)) ([0c3feaa](https://github.com/parse-community/parse-server/commit/0c3feaaa1751964c0db89f25674935c3354b1538))
# [5.0.0-alpha.15](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.14...5.0.0-alpha.15) (2022-01-02)
### Features
* support `postgresql` protocol in database URI ([#7757](https://github.com/parse-community/parse-server/issues/7757)) ([caf4a23](https://github.com/parse-community/parse-server/commit/caf4a2341f554b28e3918c53e7e897a3ca47bf8b))
# [5.0.0-alpha.14](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.13...5.0.0-alpha.14) (2022-01-02)
### Features
* support relativeTime query constraint on Postgres ([#7747](https://github.com/parse-community/parse-server/issues/7747)) ([16b1b2a](https://github.com/parse-community/parse-server/commit/16b1b2a19714535ca805f2dbb3b561d8f6a519a7))
# [5.0.0-alpha.13](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.12...5.0.0-alpha.13) (2021-12-08)
### Bug Fixes
* node engine compatibility did not include node 16 ([#7739](https://github.com/parse-community/parse-server/issues/7739)) ([ea7c014](https://github.com/parse-community/parse-server/commit/ea7c01400f992a1263543706fe49b6174758a2d6))
# [5.0.0-alpha.12](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.11...5.0.0-alpha.12) (2021-12-06)
### Bug Fixes
* adding or modifying a nested property requires addField permissions ([#7679](https://github.com/parse-community/parse-server/issues/7679)) ([6a6248b](https://github.com/parse-community/parse-server/commit/6a6248b6cb2e732d17131e18e659943b894ed2f1))
# [5.0.0-alpha.11](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.10...5.0.0-alpha.11) (2021-11-29)
### Bug Fixes
* upgrade mime from 2.5.2 to 3.0.0 ([#7725](https://github.com/parse-community/parse-server/issues/7725)) ([f5ef98b](https://github.com/parse-community/parse-server/commit/f5ef98bde32083403c0e30a12162fcc1e52cac37))
# [5.0.0-alpha.10](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.9...5.0.0-alpha.10) (2021-11-29)
### Bug Fixes
* upgrade parse from 3.3.1 to 3.4.0 ([#7723](https://github.com/parse-community/parse-server/issues/7723)) ([d4c1f47](https://github.com/parse-community/parse-server/commit/d4c1f473073764cb0570c633fc4a30669c2ce889))
# [5.0.0-alpha.9](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.8...5.0.0-alpha.9) (2021-11-27)
### Bug Fixes
* unable to use objectId size higher than 19 on GraphQL API ([#7627](https://github.com/parse-community/parse-server/issues/7627)) ([ed86c80](https://github.com/parse-community/parse-server/commit/ed86c807721cc52a1a5a9dea0b768717eec269ed))
# [5.0.0-alpha.8](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.7...5.0.0-alpha.8) (2021-11-18)
### Features
* add support for Node 16 ([#7707](https://github.com/parse-community/parse-server/issues/7707)) ([45cc58c](https://github.com/parse-community/parse-server/commit/45cc58c7e5e640a46c5d508019a3aa81242964b1))
### BREAKING CHANGES
* Removes official Node 15 support which has reached it end-of-life date. ([45cc58c](45cc58c))
# [5.0.0-alpha.7](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.6...5.0.0-alpha.7) (2021-11-12)
### Bug Fixes
* node engine range has no upper limit to exclude incompatible node versions ([#7692](https://github.com/parse-community/parse-server/issues/7692)) ([573558d](https://github.com/parse-community/parse-server/commit/573558d3adcbcc6222c92003829867e1a73eef94))
# [5.0.0-alpha.6](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.5...5.0.0-alpha.6) (2021-11-10)
### Reverts
* refactor: allow ES import for cloud string if package type is module ([b64640c](https://github.com/parse-community/parse-server/commit/b64640c5705f733798783e68d216e957044ef23c))
# [5.0.0-alpha.5](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.4...5.0.0-alpha.5) (2021-11-01)
+121
View File
@@ -1,3 +1,124 @@
# [5.4.0-beta.1](https://github.com/parse-community/parse-server/compare/5.3.0...5.4.0-beta.1) (2022-10-29)
### Bug Fixes
* authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) [skip release] ([#8187](https://github.com/parse-community/parse-server/issues/8187)) ([8c8ec71](https://github.com/parse-community/parse-server/commit/8c8ec715739e0f851338cfed794409ebac66c51b))
* brute force guessing of user sensitive data via search patterns (GHSA-2m6g-crv8-p3c6) ([#8146](https://github.com/parse-community/parse-server/issues/8146)) [skip release] ([4c0c7c7](https://github.com/parse-community/parse-server/commit/4c0c7c77b76257878b9bcb05ff9de01c9d790262))
* certificate in Apple Game Center auth adapter not validated [skip release] ([#8058](https://github.com/parse-community/parse-server/issues/8058)) ([75af9a2](https://github.com/parse-community/parse-server/commit/75af9a26cc8e9e88a33d1e452c93a0ee6e509f17))
* graphQL query ignores condition `equalTo` with value `false` ([#8032](https://github.com/parse-community/parse-server/issues/8032)) ([7f5a15d](https://github.com/parse-community/parse-server/commit/7f5a15d5df0dfa3515e9f73709d6a49663545f9b))
* internal indices for classes `_Idempotency` and `_Role` are not protected in defined schema ([#8121](https://github.com/parse-community/parse-server/issues/8121)) ([c16f529](https://github.com/parse-community/parse-server/commit/c16f529f74f92154401bf662f634b3c5fa45e18e))
* invalid file request not properly handled [skip release] ([#8062](https://github.com/parse-community/parse-server/issues/8062)) ([4c9e956](https://github.com/parse-community/parse-server/commit/4c9e95674ad081f13062e8cd30b77b1962d5df57))
* liveQuery with `containedIn` not working when object field is an array ([#8128](https://github.com/parse-community/parse-server/issues/8128)) ([1d9605b](https://github.com/parse-community/parse-server/commit/1d9605bc93009263d3811df4d4249034ba6eb8c4))
* protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [skip release] ([#8076](https://github.com/parse-community/parse-server/issues/8076)) ([9fd4516](https://github.com/parse-community/parse-server/commit/9fd4516cde5c742f9f29dd05468b4a43a85639a6))
* push notifications `badge` doesn't update with Installation beforeSave trigger ([#8162](https://github.com/parse-community/parse-server/issues/8162)) ([3c75c2b](https://github.com/parse-community/parse-server/commit/3c75c2ba4851fae96a8c19b11a3efde03816c9a1))
* query aggregation pipeline cannot handle value of type `Date` when `directAccess: true` ([#8167](https://github.com/parse-community/parse-server/issues/8167)) ([e424137](https://github.com/parse-community/parse-server/commit/e4241374061caef66538de15112fb6bbafb1f5bb))
* relation constraints in compound queries `Parse.Query.or`, `Parse.Query.and` not working ([#8203](https://github.com/parse-community/parse-server/issues/8203)) ([28f0d26](https://github.com/parse-community/parse-server/commit/28f0d2667787d2ac68726607b811d6f0ef62b9f1))
* security upgrade undici from 5.6.0 to 5.8.0 ([#8108](https://github.com/parse-community/parse-server/issues/8108)) ([4aa016b](https://github.com/parse-community/parse-server/commit/4aa016b7322467422b9fdf05d8e29b9ecf910da7))
* server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) [skip release] ([#8238](https://github.com/parse-community/parse-server/issues/8238)) ([c03908f](https://github.com/parse-community/parse-server/commit/c03908f74e5c9eed834874a89df6c89c1a1e849f))
* session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) [skip release] ([#8180](https://github.com/parse-community/parse-server/issues/8180)) ([37fed30](https://github.com/parse-community/parse-server/commit/37fed3062ccc3ef1dfd49a9fc53318e72b3e4aff))
* sorting by non-existing value throws `INVALID_SERVER_ERROR` on Postgres ([#8157](https://github.com/parse-community/parse-server/issues/8157)) ([3b775a1](https://github.com/parse-community/parse-server/commit/3b775a1fb8a1878714e3451191438963d688f1b0))
* updating object includes unchanged keys in client response for certain key types ([#8159](https://github.com/parse-community/parse-server/issues/8159)) ([37af1d7](https://github.com/parse-community/parse-server/commit/37af1d78fce5a15039ffe3af7b323c1f1e8582fc))
### Features
* add convenience access to Parse Server configuration in Cloud Code via `Parse.Server` ([#8244](https://github.com/parse-community/parse-server/issues/8244)) ([9f11115](https://github.com/parse-community/parse-server/commit/9f111158edf7fd57a65db0c4f9244b37e58cf293))
* add option to change the default value of the `Parse.Query.limit()` constraint ([#8152](https://github.com/parse-community/parse-server/issues/8152)) ([0388956](https://github.com/parse-community/parse-server/commit/038895680894984e569dff54bf5c7b31094f3891))
* add support for MongoDB 6 ([#8242](https://github.com/parse-community/parse-server/issues/8242)) ([aba0081](https://github.com/parse-community/parse-server/commit/aba0081ce1a166a93de57f3928c19a05562b5cc1))
* add support for Postgres 15 ([#8215](https://github.com/parse-community/parse-server/issues/8215)) ([2feb6c4](https://github.com/parse-community/parse-server/commit/2feb6c46080946c984daa351187fa07cd582355d))
* liveQuery support for unsorted distance queries ([#8221](https://github.com/parse-community/parse-server/issues/8221)) ([0f763da](https://github.com/parse-community/parse-server/commit/0f763da17d646b2fec2cd980d3857e46072a8a07))
# [5.3.0-beta.1](https://github.com/parse-community/parse-server/compare/5.2.1...5.3.0-beta.1) (2022-06-17)
### Bug Fixes
* afterSave trigger removes pointer in Parse object ([#7913](https://github.com/parse-community/parse-server/issues/7913)) ([47d796e](https://github.com/parse-community/parse-server/commit/47d796ea58f65e71612ce37149be692abc9ea97f))
* auto-release process may fail if optional back-merging task fails ([#8051](https://github.com/parse-community/parse-server/issues/8051)) ([cf925e7](https://github.com/parse-community/parse-server/commit/cf925e75e87a6989f41e2e2abb2aba4332b1e79f))
* custom database options are not passed to MongoDB GridFS ([#7911](https://github.com/parse-community/parse-server/issues/7911)) ([b1e5565](https://github.com/parse-community/parse-server/commit/b1e5565b22f2eff229571fe9a9500314bd30965b))
* depreciate allowClientClassCreation defaulting to true ([#7925](https://github.com/parse-community/parse-server/issues/7925)) ([38ed96a](https://github.com/parse-community/parse-server/commit/38ed96ace534d639db007aa7dd5387b2da8f03ae))
* errors in GraphQL do not show the original error but a general `Unexpected Error` ([#8045](https://github.com/parse-community/parse-server/issues/8045)) ([0d81887](https://github.com/parse-community/parse-server/commit/0d818879c217f9c56100a5f59868fa37e6d24b71))
* interrupted WebSocket connection not closed by LiveQuery server ([#8012](https://github.com/parse-community/parse-server/issues/8012)) ([2d5221e](https://github.com/parse-community/parse-server/commit/2d5221e48012fb7781c0406d543a922d313075ea))
* live query role cache does not clear when a user is added to a role ([#8026](https://github.com/parse-community/parse-server/issues/8026)) ([199dfc1](https://github.com/parse-community/parse-server/commit/199dfc17226d85a78ab85f24362cce740f4ada39))
* peer dependency mismatch for GraphQL dependencies ([#7934](https://github.com/parse-community/parse-server/issues/7934)) ([0a6faa8](https://github.com/parse-community/parse-server/commit/0a6faa81fa97f8620e7fd05e8c7bbdb4b7da9578))
* return correct response when revert is used in beforeSave ([#7839](https://github.com/parse-community/parse-server/issues/7839)) ([19900fc](https://github.com/parse-community/parse-server/commit/19900fcdf8c9f29a674fb62cf6e4b3341d796891))
* security upgrade @parse/fs-files-adapter from 1.2.1 to 1.2.2 ([#7948](https://github.com/parse-community/parse-server/issues/7948)) ([3a70fda](https://github.com/parse-community/parse-server/commit/3a70fda6798d4143f21046439b5eaf232a31bdb6))
* security upgrade moment from 2.29.1 to 2.29.2 ([#7931](https://github.com/parse-community/parse-server/issues/7931)) ([731c550](https://github.com/parse-community/parse-server/commit/731c5507144bbacff236097e7a2a03bfe54f6e10))
* security upgrade parse push adapter from 4.1.0 to 4.1.2 ([#7893](https://github.com/parse-community/parse-server/issues/7893)) ([93667b4](https://github.com/parse-community/parse-server/commit/93667b4e8402bf13b46c4d3ef12cec6532fd9da7))
* websocket connection of LiveQuery interrupts frequently ([#8048](https://github.com/parse-community/parse-server/issues/8048)) ([03caae1](https://github.com/parse-community/parse-server/commit/03caae1e611f28079cdddbbe433daaf69e3f595c))
### Features
* add MongoDB 5.1 compatibility ([#7682](https://github.com/parse-community/parse-server/issues/7682)) ([022a856](https://github.com/parse-community/parse-server/commit/022a85619d8a2c57a2f2938e245e4d8a47c15276))
* add MongoDB 5.2 support ([#7894](https://github.com/parse-community/parse-server/issues/7894)) ([5bfa716](https://github.com/parse-community/parse-server/commit/5bfa7160d9e35b237cbae1016ed86724aa99f8d7))
* add support for Node 17 and 18 ([#7896](https://github.com/parse-community/parse-server/issues/7896)) ([3e9f292](https://github.com/parse-community/parse-server/commit/3e9f292d840334244934cee9a34545ac86313549))
* align file trigger syntax with class trigger; use the new syntax `Parse.Cloud.beforeSave(Parse.File, (request) => {})`, the old syntax `Parse.Cloud.beforeSaveFile((request) => {})` has been deprecated ([#7966](https://github.com/parse-community/parse-server/issues/7966)) ([c6dcad8](https://github.com/parse-community/parse-server/commit/c6dcad8d167d44912dbd416d328519314c0809bd))
* replace GraphQL Apollo with GraphQL Yoga ([#7967](https://github.com/parse-community/parse-server/issues/7967)) ([1aa2204](https://github.com/parse-community/parse-server/commit/1aa2204aebfdbe273d54d6d56c6029f7c34aab14))
* selectively enable / disable default authentication adapters ([#7953](https://github.com/parse-community/parse-server/issues/7953)) ([c1e808f](https://github.com/parse-community/parse-server/commit/c1e808f9e807fc49508acbde0d8b3f2b901a1638))
* upgrade mongodb from 4.4.1 to 4.5.0 ([#7991](https://github.com/parse-community/parse-server/issues/7991)) ([e692b5d](https://github.com/parse-community/parse-server/commit/e692b5dd8214cdb0ce79bedd30d9aa3cf4de76a5))
### Performance Improvements
* reduce database operations when using the constant parameter in Cloud Function validation ([#7892](https://github.com/parse-community/parse-server/issues/7892)) ([041197f](https://github.com/parse-community/parse-server/commit/041197fb4ca1cd7cf18dc426ce38647267823668))
# [5.2.0-beta.2](https://github.com/parse-community/parse-server/compare/5.2.0-beta.1...5.2.0-beta.2) (2022-03-24)
### Bug Fixes
* security bump minimist from 1.2.5 to 1.2.6 ([#7884](https://github.com/parse-community/parse-server/issues/7884)) ([c5cf282](https://github.com/parse-community/parse-server/commit/c5cf282d11ffdc023764f8e7539a2bd6bc246fe1))
* sensitive keyword detection may produce false positives ([#7881](https://github.com/parse-community/parse-server/issues/7881)) ([0d6f9e9](https://github.com/parse-community/parse-server/commit/0d6f9e951d9e186e95e96d8869066ce7022bad02))
# [5.2.0-beta.1](https://github.com/parse-community/parse-server/compare/5.1.1...5.2.0-beta.1) (2022-03-23)
### Features
* improved LiveQuery error logging with additional information ([#7837](https://github.com/parse-community/parse-server/issues/7837)) ([443a509](https://github.com/parse-community/parse-server/commit/443a5099059538d379fe491793a5871fcbb4f377))
# [5.0.0-beta.10](https://github.com/parse-community/parse-server/compare/5.0.0-beta.9...5.0.0-beta.10) (2022-03-15)
### Bug Fixes
* adding or modifying a nested property requires addField permissions ([#7679](https://github.com/parse-community/parse-server/issues/7679)) ([6a6248b](https://github.com/parse-community/parse-server/commit/6a6248b6cb2e732d17131e18e659943b894ed2f1))
* bump nanoid from 3.1.25 to 3.2.0 ([#7781](https://github.com/parse-community/parse-server/issues/7781)) ([f5f63bf](https://github.com/parse-community/parse-server/commit/f5f63bfc64d3481ed944ceb5e9f50b33dccd1ce9))
* bump node-fetch from 2.6.1 to 3.1.1 ([#7782](https://github.com/parse-community/parse-server/issues/7782)) ([9082351](https://github.com/parse-community/parse-server/commit/90823514113a1a085ebc818f7109b3fd7591346f))
* node engine compatibility did not include node 16 ([#7739](https://github.com/parse-community/parse-server/issues/7739)) ([ea7c014](https://github.com/parse-community/parse-server/commit/ea7c01400f992a1263543706fe49b6174758a2d6))
* node engine range has no upper limit to exclude incompatible node versions ([#7692](https://github.com/parse-community/parse-server/issues/7692)) ([573558d](https://github.com/parse-community/parse-server/commit/573558d3adcbcc6222c92003829867e1a73eef94))
* package.json & package-lock.json to reduce vulnerabilities ([#7823](https://github.com/parse-community/parse-server/issues/7823)) ([5ca2288](https://github.com/parse-community/parse-server/commit/5ca228882332b65f3ac05407e6e4da1ee3ef3749))
* schema cache not cleared in some cases ([#7678](https://github.com/parse-community/parse-server/issues/7678)) ([5af6e5d](https://github.com/parse-community/parse-server/commit/5af6e5dfaa129b1a350afcba4fb381b21c4cc35d))
* security upgrade follow-redirects from 1.14.6 to 1.14.7 ([#7769](https://github.com/parse-community/parse-server/issues/7769)) ([8f5a861](https://github.com/parse-community/parse-server/commit/8f5a8618cfa7ed9a2a239a095abffa8f3fd8d31a))
* security upgrade follow-redirects from 1.14.7 to 1.14.8 ([#7801](https://github.com/parse-community/parse-server/issues/7801)) ([70088a9](https://github.com/parse-community/parse-server/commit/70088a95a78393da2a4ac68be81e63107747626a))
* security vulnerability that allows remote code execution (GHSA-p6h4-93qp-jhcm) ([#7844](https://github.com/parse-community/parse-server/issues/7844)) ([e569f40](https://github.com/parse-community/parse-server/commit/e569f402b1fd8648fb0d1523b71b2a03273902a5))
* server crash using GraphQL due to missing @apollo/client peer dependency ([#7787](https://github.com/parse-community/parse-server/issues/7787)) ([08089d6](https://github.com/parse-community/parse-server/commit/08089d6fcbb215412448ce7d92b21b9fe6c929f2))
* unable to use objectId size higher than 19 on GraphQL API ([#7627](https://github.com/parse-community/parse-server/issues/7627)) ([ed86c80](https://github.com/parse-community/parse-server/commit/ed86c807721cc52a1a5a9dea0b768717eec269ed))
* upgrade mime from 2.5.2 to 3.0.0 ([#7725](https://github.com/parse-community/parse-server/issues/7725)) ([f5ef98b](https://github.com/parse-community/parse-server/commit/f5ef98bde32083403c0e30a12162fcc1e52cac37))
* upgrade parse from 3.3.1 to 3.4.0 ([#7723](https://github.com/parse-community/parse-server/issues/7723)) ([d4c1f47](https://github.com/parse-community/parse-server/commit/d4c1f473073764cb0570c633fc4a30669c2ce889))
* upgrade winston from 3.5.0 to 3.5.1 ([#7820](https://github.com/parse-community/parse-server/issues/7820)) ([4af253d](https://github.com/parse-community/parse-server/commit/4af253d1f8654a6f57b5137ad310cdacadc922cc))
### Features
* add Cloud Code context to `ParseObject.fetch` ([#7779](https://github.com/parse-community/parse-server/issues/7779)) ([315290d](https://github.com/parse-community/parse-server/commit/315290d16110110938f80a6b779cc2d1db58c552))
* add Idempotency to Postgres ([#7750](https://github.com/parse-community/parse-server/issues/7750)) ([0c3feaa](https://github.com/parse-community/parse-server/commit/0c3feaaa1751964c0db89f25674935c3354b1538))
* add support for Node 16 ([#7707](https://github.com/parse-community/parse-server/issues/7707)) ([45cc58c](https://github.com/parse-community/parse-server/commit/45cc58c7e5e640a46c5d508019a3aa81242964b1))
* bump required node engine to >=12.22.10 ([#7846](https://github.com/parse-community/parse-server/issues/7846)) ([5ace99d](https://github.com/parse-community/parse-server/commit/5ace99d542a11e422af46d9fd6b1d3d2513b34cf))
* support `postgresql` protocol in database URI ([#7757](https://github.com/parse-community/parse-server/issues/7757)) ([caf4a23](https://github.com/parse-community/parse-server/commit/caf4a2341f554b28e3918c53e7e897a3ca47bf8b))
* support relativeTime query constraint on Postgres ([#7747](https://github.com/parse-community/parse-server/issues/7747)) ([16b1b2a](https://github.com/parse-community/parse-server/commit/16b1b2a19714535ca805f2dbb3b561d8f6a519a7))
* upgrade to MongoDB Node.js driver 4.x for MongoDB 5.0 support ([#7794](https://github.com/parse-community/parse-server/issues/7794)) ([f88aa2a](https://github.com/parse-community/parse-server/commit/f88aa2a62a533e5344d1c13dd38c5a0b283a480a))
### Reverts
* refactor: allow ES import for cloud string if package type is module ([b64640c](https://github.com/parse-community/parse-server/commit/b64640c5705f733798783e68d216e957044ef23c))
* update node engine to 2.22.0 ([#7827](https://github.com/parse-community/parse-server/issues/7827)) ([f235412](https://github.com/parse-community/parse-server/commit/f235412c1b6c2b173b7531f285429ea7214b56a2))
### BREAKING CHANGES
* This requires Node.js version >=12.22.10. ([5ace99d](5ace99d))
* The MongoDB GridStore adapter has been removed. By default, Parse Server already uses GridFS, so if you do not manually use the GridStore adapter, you can ignore this change. ([f88aa2a](f88aa2a))
* Removes official Node 15 support which has reached it end-of-life date. ([45cc58c](45cc58c))
# [5.0.0-beta.9](https://github.com/parse-community/parse-server/compare/5.0.0-beta.8...5.0.0-beta.9) (2022-03-12)
File diff suppressed because it is too large Load Diff
+5 -8
View File
@@ -1,4 +1,4 @@
'use strict'
'use strict';
const CiVersionCheck = require('./CiVersionCheck');
const mongoVersionList = require('mongodb-version-list');
@@ -14,9 +14,8 @@ async function check() {
* Check the MongoDB versions used in test environments.
*/
async function checkMongoDbVersions() {
const releasedVersions = await new Promise((resolve, reject) => {
mongoVersionList(function(error, versions) {
mongoVersionList(function (error, versions) {
if (error) {
reject(error);
}
@@ -31,14 +30,12 @@ async function checkMongoDbVersions() {
ciEnvironmentsKeyPath: 'jobs.check-mongo.strategy.matrix.include',
ciVersionKey: 'MONGODB_VERSION',
releasedVersions,
latestComponent: CiVersionCheck.versionComponents.path,
latestComponent: CiVersionCheck.versionComponents.minor,
ignoreReleasedVersions: [
'<4.0.0', // Versions reached their MongoDB end-of-life support date
'~4.1.0', // Development release according to MongoDB support
'~4.3.0', // Development release according to MongoDB support
'~4.7.0', // Development release according to MongoDB support
'4.0.26', // Temporarily disabled because not yet available for download via mongodb-runner
],
}).check();
}
@@ -47,7 +44,6 @@ async function checkMongoDbVersions() {
* Check the Nodejs versions used in test environments.
*/
async function checkNodeVersions() {
const allVersions = await allNodeVersions();
const releasedVersions = allVersions.versions;
@@ -62,7 +58,8 @@ async function checkNodeVersions() {
ignoreReleasedVersions: [
'<12.0.0', // These versions have reached their end-of-life support date
'>=13.0.0 <14.0.0', // These versions have reached their end-of-life support date
'>=16.0.0', // This version has not been officially released yet
'>=15.0.0 <16.0.0', // These versions have reached their end-of-life support date
'>=19.0.0', // These versions are not officially supported yet
],
}).check();
}
+12 -8
View File
@@ -75,17 +75,21 @@ class NodeEngineCheck {
// For each file
for (const file of files) {
// Get node version
const contentString = await fs.readFile(file, 'utf-8');
const contentJson = JSON.parse(contentString);
const version = ((contentJson || {}).engines || {}).node;
try {
const contentJson = JSON.parse(contentString);
const version = ((contentJson || {}).engines || {}).node;
// Add response
response.push({
file: file,
nodeVersion: version
});
// Add response
response.push({
file: file,
nodeVersion: version
});
} catch(e) {
console.log(`Ignoring file because it is not valid JSON: ${file}`);
core.warning(`Ignoring file because it is not valid JSON: ${file}`);
}
}
// If results should be cleaned by removing undefined node versions
+1470 -2005
View File
File diff suppressed because it is too large Load Diff
+45 -47
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "5.0.0",
"version": "5.4.2",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -19,55 +19,56 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"@apollographql/graphql-playground-html": "1.6.29",
"@graphql-tools/links": "6.2.5",
"@graphql-tools/stitch": "6.2.4",
"@graphql-tools/utils": "6.2.4",
"@parse/fs-files-adapter": "1.2.0",
"@parse/push-adapter": "3.4.1",
"apollo-server-express": "2.25.2",
"@graphql-yoga/node": "2.6.0",
"@graphql-tools/utils": "8.12.0",
"@graphql-tools/merge": "8.3.6",
"@graphql-tools/schema": "9.0.4",
"@parse/fs-files-adapter": "1.2.2",
"@parse/push-adapter": "4.1.2",
"bcryptjs": "2.4.3",
"body-parser": "1.19.0",
"body-parser": "1.20.1",
"commander": "5.1.0",
"cors": "2.8.5",
"deepcopy": "2.1.0",
"express": "4.17.1",
"follow-redirects": "1.14.8",
"graphql": "15.6.0",
"express": "4.18.1",
"follow-redirects": "1.15.2",
"graphql": "16.6.0",
"graphql-list-fields": "2.0.2",
"graphql-relay": "0.7.0",
"graphql-tag": "2.12.5",
"graphql-upload": "11.0.0",
"graphql-tag": "2.12.6",
"graphql-relay": "0.10.0",
"intersect": "1.0.1",
"jsonwebtoken": "8.5.1",
"jwks-rsa": "1.12.3",
"ldapjs": "2.3.1",
"jsonwebtoken": "9.0.0",
"jwks-rsa": "2.1.4",
"ldapjs": "2.3.3",
"lodash": "4.17.21",
"lru-cache": "5.1.1",
"mime": "2.5.2",
"mongodb": "3.6.11",
"lru-cache": "7.12.0",
"mime": "3.0.0",
"mongodb": "4.10.0",
"mustache": "4.2.0",
"parse": "3.3.1",
"pg-monitor": "1.4.1",
"pg-promise": "10.11.0",
"parse": "3.4.2",
"pg-monitor": "1.5.0",
"pg-promise": "10.12.0",
"pluralize": "8.0.0",
"redis": "3.1.2",
"semver": "7.3.5",
"subscriptions-transport-ws": "0.10.0",
"semver": "7.3.8",
"subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0",
"uuid": "8.3.2",
"winston": "3.3.3",
"winston-daily-rotate-file": "4.5.5",
"ws": "8.2.2"
"uuid": "9.0.0",
"winston": "3.8.1",
"winston-daily-rotate-file": "4.7.1",
"ws": "8.9.0"
},
"devDependencies": {
"@actions/core": "1.2.6",
"graphql-tag": "2.12.6",
"@actions/core": "1.9.1",
"@apollo/client": "3.6.1",
"@babel/cli": "7.10.0",
"@babel/core": "7.10.0",
"@babel/plugin-proposal-object-rest-spread": "7.10.0",
"@babel/plugin-transform-flow-strip-types": "7.9.0",
"@babel/preset-env": "7.10.0",
"@parse/minami": "1.0.0",
"@saithodev/semantic-release-backmerge": "2.1.2",
"@semantic-release/changelog": "5.0.1",
"@semantic-release/commit-analyzer": "8.0.1",
"@semantic-release/git": "9.0.0",
@@ -75,12 +76,7 @@
"@semantic-release/npm": "7.1.3",
"@semantic-release/release-notes-generator": "9.0.3",
"all-node-versions": "8.0.0",
"apollo-cache-inmemory": "1.6.6",
"apollo-client": "2.6.10",
"apollo-link": "1.2.14",
"apollo-link-http": "1.5.17",
"apollo-link-ws": "1.0.20",
"apollo-upload-client": "13.0.0",
"apollo-upload-client": "16.0.0",
"apollo-utilities": "1.3.4",
"babel-eslint": "10.1.0",
"bcrypt-nodejs": "0.0.3",
@@ -90,7 +86,7 @@
"eslint-plugin-flowtype": "5.1.3",
"flow-bin": "0.119.1",
"form-data": "3.0.0",
"husky": "4.2.5",
"husky": "4.3.8",
"jasmine": "3.5.0",
"jasmine-spec-reporter": "7.0.0",
"jsdoc": "3.6.3",
@@ -101,7 +97,7 @@
"mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter",
"mongodb-runner": "4.8.1",
"mongodb-version-list": "1.0.0",
"node-fetch": "2.6.1",
"node-fetch": "3.2.10",
"nyc": "15.1.0",
"prettier": "2.0.5",
"semantic-release": "17.4.6",
@@ -119,15 +115,17 @@
"test:mongodb:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
"test:mongodb:testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine",
"test:mongodb": "npm run test:mongodb:runnerstart --dbversion=$npm_config_dbversion && npm run test:mongodb:testonly --dbversion=$npm_config_dbversion",
"test:mongodb:4.0.27": "npm run test:mongodb --dbversion=4.0.27",
"test:mongodb:4.2.17": "npm run test:mongodb --dbversion=4.2.17",
"test:mongodb:4.4.10": "npm run test:mongodb --dbversion=4.4.10",
"test:mongodb:4.0.28": "npm run test:mongodb --dbversion=4.0.28",
"test:mongodb:4.2.19": "npm run test:mongodb --dbversion=4.2.19",
"test:mongodb:4.4.13": "npm run test:mongodb --dbversion=4.4.13",
"test:mongodb:5.3.2": "npm run test:mongodb --dbversion=5.3.2",
"test:mongodb:6.0.2": "npm run test:mongodb --dbversion=6.0.2",
"posttest:mongodb": "mongodb-runner stop",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.4.10} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.4.10} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine",
"test": "npm run testonly",
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.4.10} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop",
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.4.10} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 nyc jasmine",
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop",
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 nyc jasmine",
"start": "node ./bin/parse-server",
"prettier": "prettier --write {src,spec}/{**/*,*}.js",
"prepare": "npm run build",
@@ -135,7 +133,7 @@
"madge:circular": "node_modules/.bin/madge ./src --circular"
},
"engines": {
"node": ">=12.22.10 <16"
"node": ">=12.22.10 <19"
},
"bin": {
"parse-server": "bin/parse-server"
+2 -5
View File
@@ -28,7 +28,7 @@ async function config() {
console.log(`Running on branch: ${branch}`);
// Set changelog file
const changelogFile = `./changelogs/CHANGELOG_${branch}.md`;
const changelogFile = `./changelogs/CHANGELOG_release.md`;
console.log(`Changelog file output to: ${changelogFile}`);
// Load template file contents
@@ -41,10 +41,7 @@ async function config() {
{ name: 'beta', prerelease: true },
'next-major',
// Long-Term-Support branches
// { name: 'release-1', range: '1.x.x', channel: '1.x' },
// { name: 'release-2', range: '2.x.x', channel: '2.x' },
// { name: 'release-3', range: '3.x.x', channel: '3.x' },
// { name: 'release-4', range: '4.x.x', channel: '4.x' },
{ name: 'release-5.x.x', range: '5.x.x', channel: '5.x.x' },
],
dryRun: false,
debug: true,
+19 -17
View File
@@ -23,23 +23,25 @@ const nestedOptionTypes = [
'PagesRoute',
'PasswordPolicyOptions',
'SecurityOptions',
'SchemaOptions',
];
/** The prefix of environment variables for nested options. */
const nestedOptionEnvPrefix = {
'AccountLockoutOptions' : 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
'CustomPagesOptions' : 'PARSE_SERVER_CUSTOM_PAGES_',
'AccountLockoutOptions': 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
'CustomPagesOptions': 'PARSE_SERVER_CUSTOM_PAGES_',
'DatabaseOptions': 'PARSE_SERVER_DATABASE_',
'FileUploadOptions' : 'PARSE_SERVER_FILE_UPLOAD_',
'IdempotencyOptions' : 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
'LiveQueryOptions' : 'PARSE_SERVER_LIVEQUERY_',
'LiveQueryServerOptions' : 'PARSE_LIVE_QUERY_SERVER_',
'PagesCustomUrlsOptions' : 'PARSE_SERVER_PAGES_CUSTOM_URL_',
'PagesOptions' : 'PARSE_SERVER_PAGES_',
'FileUploadOptions': 'PARSE_SERVER_FILE_UPLOAD_',
'IdempotencyOptions': 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
'LiveQueryOptions': 'PARSE_SERVER_LIVEQUERY_',
'LiveQueryServerOptions': 'PARSE_LIVE_QUERY_SERVER_',
'PagesCustomUrlsOptions': 'PARSE_SERVER_PAGES_CUSTOM_URL_',
'PagesOptions': 'PARSE_SERVER_PAGES_',
'PagesRoute': 'PARSE_SERVER_PAGES_ROUTE_',
'ParseServerOptions' : 'PARSE_SERVER_',
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_',
'ParseServerOptions': 'PARSE_SERVER_',
'PasswordPolicyOptions': 'PARSE_SERVER_PASSWORD_POLICY_',
'SecurityOptions': 'PARSE_SERVER_SECURITY_',
'SchemaOptions': 'PARSE_SERVER_SCHEMA_',
};
function last(array) {
@@ -50,7 +52,7 @@ const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
function toENV(key) {
let str = '';
let previousIsUpper = false;
for(let i = 0; i < key.length; i++) {
for (let i = 0; i < key.length; i++) {
const char = key[i];
if (letters.indexOf(char) >= 0) {
if (!previousIsUpper) {
@@ -273,8 +275,8 @@ function inject(t, list) {
return { results, comments };
}
const makeRequire = function(variableName, module, t) {
const decl = t.variableDeclarator(t.identifier(variableName), t.callExpression(t.identifier('require'), [t.stringLiteral(module)]));
const makeRequire = function (variableName, module, t) {
const decl = t.variableDeclarator(t.identifier(variableName), t.callExpression(t.identifier('require'), [t.stringLiteral(module)]));
return t.variableDeclaration('var', [decl])
}
let docs = ``;
@@ -283,14 +285,14 @@ const plugin = function (babel) {
const moduleExports = t.memberExpression(t.identifier('module'), t.identifier('exports'));
return {
visitor: {
ImportDeclaration: function(path) {
ImportDeclaration: function (path) {
path.remove();
},
Program: function(path) {
Program: function (path) {
// Inject the parser's loader
path.unshiftContainer('body', makeRequire('parsers', './parsers', t));
},
ExportDeclaration: function(path) {
ExportDeclaration: function (path) {
// Export declaration on an interface
if (path.node && path.node.declaration && path.node.declaration.type == 'InterfaceDeclaration') {
const { results, comments } = inject(t, doInterface(path.node.declaration));
@@ -313,6 +315,6 @@ Do not edit manually, but update Options/index.js
`
const babel = require("@babel/core");
const res = babel.transformFileSync('./src/Options/index.js', { plugins: [ plugin, '@babel/transform-flow-strip-types' ], babelrc: false, auxiliaryCommentBefore, sourceMaps: false });
const res = babel.transformFileSync('./src/Options/index.js', { plugins: [plugin, '@babel/transform-flow-strip-types'], babelrc: false, auxiliaryCommentBefore, sourceMaps: false });
require('fs').writeFileSync('./src/Options/Definitions.js', res.code + '\n');
require('fs').writeFileSync('./src/Options/docs.js', docs);
+3 -1
View File
@@ -18,10 +18,13 @@
"it_only_db": true,
"it_only_mongodb_version": true,
"it_only_postgres_version": true,
"it_only_node_version": true,
"fit_only_mongodb_version": true,
"fit_only_node_version": true,
"it_exclude_mongodb_version": true,
"it_exclude_postgres_version": true,
"fit_exclude_mongodb_version": true,
"fit_exclude_node_version": true,
"it_exclude_dbs": true,
"describe_only_db": true,
"describe_only": true,
@@ -31,7 +34,6 @@
"jequal": true,
"create": true,
"arrayContains": true,
"expectAsync": true,
"databaseAdapter": true
},
"rules": {
+15 -14
View File
@@ -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);
});
});
}
);
});
});
});
+268 -54
View File
@@ -419,6 +419,29 @@ describe('AuthenticationProviders', function () {
expect(providerOptions).toEqual(options.facebook);
});
it('should throw error when Facebook request appId is wrong data type', async () => {
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
spyOn(httpsRequest, 'get').and.callFake(() => {
return Promise.resolve({ id: 'a' });
});
const options = {
facebook: {
appIds: 'abcd',
appSecret: 'secret_sauce',
},
};
const authData = {
access_token: 'badtoken',
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'facebook',
options
);
await expectAsync(adapter.validateAppId(appIds, authData, providerOptions)).toBeRejectedWith(
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.')
);
});
it('should handle Facebook appSecret for validating appIds', async () => {
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
spyOn(httpsRequest, 'get').and.callFake(() => {
@@ -441,6 +464,29 @@ describe('AuthenticationProviders', function () {
expect(httpsRequest.get.calls.first().args[0].includes('appsecret_proof')).toBe(true);
});
it('should throw error when Facebook request appId is wrong data type', async () => {
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
spyOn(httpsRequest, 'get').and.callFake(() => {
return Promise.resolve({ id: 'a' });
});
const options = {
facebook: {
appIds: 'abcd',
appSecret: 'secret_sauce',
},
};
const authData = {
access_token: 'badtoken',
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'facebook',
options
);
await expectAsync(adapter.validateAppId(appIds, authData, providerOptions)).toBeRejectedWith(
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.')
);
});
it('should handle Facebook appSecret for validating auth data', async () => {
const httpsRequest = require('../lib/Adapters/Auth/httpsRequest');
spyOn(httpsRequest, 'get').and.callFake(() => {
@@ -477,6 +523,34 @@ describe('AuthenticationProviders', function () {
expect(appIds).toEqual(['a', 'b']);
expect(providerOptions).toEqual(options.custom);
});
it('can disable provider', async () => {
await reconfigureServer({
auth: {
myoauth: {
enabled: false,
module: path.resolve(__dirname, 'support/myoauth'), // relative path as it's run from src
},
},
});
const provider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
await expectAsync(Parse.User._logInWith('myoauth')).toBeRejectedWith(
new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, 'This authentication method is unsupported.')
);
});
it('can depreciate', async () => {
const Deprecator = require('../lib/Deprecator/Deprecator');
const spy = spyOn(Deprecator, 'logRuntimeDeprecation').and.callFake(() => {});
const provider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
await Parse.User._logInWith('myoauth');
expect(spy).toHaveBeenCalledWith({
usage: 'auth.myoauth',
solution: 'auth.myoauth.enabled: true',
});
});
});
describe('instagram auth adapter', () => {
@@ -522,6 +596,7 @@ describe('instagram auth adapter', () => {
describe('google auth adapter', () => {
const google = require('../lib/Adapters/Auth/google');
const jwt = require('jsonwebtoken');
const authUtils = require('../lib/Adapters/Auth/utils');
it('should throw error with missing id_token', async () => {
try {
@@ -544,7 +619,7 @@ describe('google auth adapter', () => {
// it('should throw error if public key used to encode token is not available', async () => {
// const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
// try {
// spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
// spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
// await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {});
// fail();
@@ -563,7 +638,7 @@ describe('google auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const result = await google.validateAuthData(
@@ -579,7 +654,7 @@ describe('google auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
@@ -603,7 +678,7 @@ describe('google auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
@@ -625,7 +700,7 @@ describe('google auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
try {
@@ -1328,6 +1403,7 @@ describe('apple signin auth adapter', () => {
const apple = require('../lib/Adapters/Auth/apple');
const jwt = require('jsonwebtoken');
const util = require('util');
const authUtils = require('../lib/Adapters/Auth/utils');
it('(using client id as string) should throw error with missing id_token', async () => {
try {
@@ -1362,7 +1438,7 @@ describe('apple signin auth adapter', () => {
it('should throw error if public key used to encode token is not available', async () => {
const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } };
try {
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
await apple.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
@@ -1384,7 +1460,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
@@ -1401,7 +1477,7 @@ describe('apple signin auth adapter', () => {
it('should not verify invalid id_token', async () => {
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1438,7 +1514,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1460,7 +1536,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1482,7 +1558,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1502,7 +1578,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1530,7 +1606,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1559,7 +1635,7 @@ describe('apple signin auth adapter', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1631,7 +1707,7 @@ describe('apple signin auth adapter', () => {
sub: 'a_different_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return { kid: '123', rsaPublicKey: 'the_rsa_public_key' };
};
@@ -1652,8 +1728,44 @@ describe('apple signin auth adapter', () => {
describe('Apple Game Center Auth adapter', () => {
const gcenter = require('../lib/Adapters/Auth/gcenter');
const fs = require('fs');
const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
const testCert2 = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
it('can load adapter', async () => {
const options = {
gcenter: {
rootCertificateUrl:
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
});
it('validateAuthData should validate', async () => {
const options = {
gcenter: {
rootCertificateUrl:
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
// real token is used
const authData = {
id: 'G:1965586982',
@@ -1664,48 +1776,149 @@ describe('Apple Game Center Auth adapter', () => {
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
};
try {
await gcenter.validateAuthData(authData);
} catch (e) {
fail();
}
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
await gcenter.validateAuthData(authData);
});
it('validateAuthData invalid signature id', async () => {
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-6.cer'] = testCert2;
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
{}
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
timestamp: 1565257031287,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
bundleId: 'com.example.com',
};
try {
await gcenter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe('Apple Game Center - invalid signature');
}
await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith(
new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature')
);
});
it('validateAuthData invalid public key url', async () => {
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'invalid.com',
timestamp: 1565257031287,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
it('validateAuthData invalid public key http url', async () => {
const options = {
gcenter: {
rootCertificateUrl:
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
const publicKeyUrls = [
'example.com',
'http://static.gc.apple.com/public-key/gc-prod-4.cer',
'https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg',
'https://example.com/ \\.apple.com/public_key.cer',
'https://example.com/ &.apple.com/public_key.cer',
];
await Promise.all(
publicKeyUrls.map(publicKeyUrl =>
expectAsync(
gcenter.validateAuthData({
id: 'G:1965586982',
timestamp: 1565257031287,
publicKeyUrl,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'com.example.com',
})
).toBeRejectedWith(
new Parse.Error(
Parse.Error.SCRIPT_FAILED,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
)
)
)
);
});
try {
await gcenter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: invalid.com');
}
it('should not validate Symantec Cert', async () => {
const options = {
gcenter: {
rootCertificateUrl:
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
expect(() =>
gcenter.verifyPublicKeyIssuer(
testCert,
'https://static.gc.apple.com/public-key/gc-prod-4.cer'
)
);
});
it('adapter should load default cert', async () => {
const options = {
gcenter: {},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
const previous = new Date();
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
const duration = new Date().getTime() - previous.getTime();
expect(duration <= 1).toBe(true);
});
it('adapter should throw', async () => {
const options = {
gcenter: {
rootCertificateUrl: 'https://example.com',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await expectAsync(
adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
)
).toBeRejectedWith(
new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
)
);
});
});
@@ -1761,6 +1974,7 @@ describe('facebook limited auth adapter', () => {
const facebook = require('../lib/Adapters/Auth/facebook');
const jwt = require('jsonwebtoken');
const util = require('util');
const authUtils = require('../lib/Adapters/Auth/utils');
// TODO: figure out a way to run this test alongside facebook classic tests
xit('(using client id as string) should throw error with missing id_token', async () => {
@@ -1799,7 +2013,7 @@ describe('facebook limited auth adapter', () => {
header: { kid: '789', alg: 'RS256' },
};
try {
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
await facebook.validateAuthData(
{ id: 'the_user_id', token: 'the_token' },
@@ -1823,7 +2037,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const fakeGetSigningKeyAsyncFunction = () => {
return {
@@ -1845,7 +2059,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -1887,7 +2101,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -1914,7 +2128,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -1941,7 +2155,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -1966,7 +2180,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -1999,7 +2213,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -2033,7 +2247,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
@@ -2119,7 +2333,7 @@ describe('facebook limited auth adapter', () => {
const fakeDecodedToken = {
header: { kid: '123', alg: 'RS256' },
};
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
const fakeGetSigningKeyAsyncFunction = () => {
return {
kid: '123',
+10 -9
View File
@@ -737,7 +737,8 @@ describe('cloud validator', () => {
});
it('basic beforeSaveFile skipWithMasterKey', async done => {
Parse.Cloud.beforeSaveFile(
Parse.Cloud.beforeSave(
Parse.File,
() => {
throw 'beforeSaveFile should have resolved using master key.';
},
@@ -1431,7 +1432,7 @@ describe('cloud validator', () => {
});
it('validate beforeSaveFile', async done => {
Parse.Cloud.beforeSaveFile(() => {}, validatorSuccess);
Parse.Cloud.beforeSave(Parse.File, () => {}, validatorSuccess);
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
const result = await file.save({ useMasterKey: true });
@@ -1440,7 +1441,7 @@ describe('cloud validator', () => {
});
it('validate beforeSaveFile fail', async done => {
Parse.Cloud.beforeSaveFile(() => {}, validatorFail);
Parse.Cloud.beforeSave(Parse.File, () => {}, validatorFail);
try {
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
await file.save({ useMasterKey: true });
@@ -1452,7 +1453,7 @@ describe('cloud validator', () => {
});
it('validate afterSaveFile', async done => {
Parse.Cloud.afterSaveFile(() => {}, validatorSuccess);
Parse.Cloud.afterSave(Parse.File, () => {}, validatorSuccess);
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
const result = await file.save({ useMasterKey: true });
@@ -1461,7 +1462,7 @@ describe('cloud validator', () => {
});
it('validate afterSaveFile fail', async done => {
Parse.Cloud.beforeSaveFile(() => {}, validatorFail);
Parse.Cloud.beforeSave(Parse.File, () => {}, validatorFail);
try {
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
await file.save({ useMasterKey: true });
@@ -1473,7 +1474,7 @@ describe('cloud validator', () => {
});
it('validate beforeDeleteFile', async done => {
Parse.Cloud.beforeDeleteFile(() => {}, validatorSuccess);
Parse.Cloud.beforeDelete(Parse.File, () => {}, validatorSuccess);
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
await file.save();
@@ -1482,7 +1483,7 @@ describe('cloud validator', () => {
});
it('validate beforeDeleteFile fail', async done => {
Parse.Cloud.beforeDeleteFile(() => {}, validatorFail);
Parse.Cloud.beforeDelete(Parse.File, () => {}, validatorFail);
try {
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
await file.save();
@@ -1495,7 +1496,7 @@ describe('cloud validator', () => {
});
it('validate afterDeleteFile', async done => {
Parse.Cloud.afterDeleteFile(() => {}, validatorSuccess);
Parse.Cloud.afterDelete(Parse.File, () => {}, validatorSuccess);
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
await file.save();
@@ -1504,7 +1505,7 @@ describe('cloud validator', () => {
});
it('validate afterDeleteFile fail', async done => {
Parse.Cloud.afterDeleteFile(() => {}, validatorFail);
Parse.Cloud.afterDelete(Parse.File, () => {}, validatorFail);
try {
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
await file.save();
+276 -42
View File
@@ -50,6 +50,16 @@ describe('Cloud Code', () => {
});
});
it('can get config', () => {
const config = Parse.Server;
let currentConfig = Config.get('test');
expect(Object.keys(config)).toEqual(Object.keys(currentConfig));
config.silent = false;
Parse.Server = config;
currentConfig = Config.get('test');
expect(currentConfig.silent).toBeFalse();
});
it('show warning on duplicate cloud functions', done => {
const logger = require('../lib/logger').logger;
spyOn(logger, 'warn').and.callFake(() => {});
@@ -1494,6 +1504,136 @@ describe('Cloud Code', () => {
});
});
it('before save can revert fields', async () => {
Parse.Cloud.beforeSave('TestObject', ({ object }) => {
object.revert('foo');
return object;
});
Parse.Cloud.afterSave('TestObject', ({ object }) => {
expect(object.get('foo')).toBeUndefined();
return object;
});
const obj = new TestObject();
obj.set('foo', 'bar');
await obj.save();
expect(obj.get('foo')).toBeUndefined();
await obj.fetch();
expect(obj.get('foo')).toBeUndefined();
});
it('before save can revert fields with existing object', async () => {
Parse.Cloud.beforeSave(
'TestObject',
({ object }) => {
object.revert('foo');
return object;
},
{
skipWithMasterKey: true,
}
);
Parse.Cloud.afterSave(
'TestObject',
({ object }) => {
expect(object.get('foo')).toBe('bar');
return object;
},
{
skipWithMasterKey: true,
}
);
const obj = new TestObject();
obj.set('foo', 'bar');
await obj.save(null, { useMasterKey: true });
expect(obj.get('foo')).toBe('bar');
obj.set('foo', 'yolo');
await obj.save();
expect(obj.get('foo')).toBe('bar');
});
it('can unset in afterSave', async () => {
Parse.Cloud.beforeSave('TestObject', ({ object }) => {
if (!object.existed()) {
object.set('secret', true);
return object;
}
object.revert('secret');
});
Parse.Cloud.afterSave('TestObject', ({ object }) => {
object.unset('secret');
});
Parse.Cloud.beforeFind(
'TestObject',
({ query }) => {
query.exclude('secret');
},
{
skipWithMasterKey: true,
}
);
const obj = new TestObject();
await obj.save();
expect(obj.get('secret')).toBeUndefined();
await obj.fetch();
expect(obj.get('secret')).toBeUndefined();
await obj.fetch({ useMasterKey: true });
expect(obj.get('secret')).toBe(true);
});
it('should revert in beforeSave', async () => {
Parse.Cloud.beforeSave('MyObject', ({ object }) => {
if (!object.existed()) {
object.set('count', 0);
return object;
}
object.revert('count');
return object;
});
const obj = await new Parse.Object('MyObject').save();
expect(obj.get('count')).toBe(0);
obj.set('count', 10);
await obj.save();
expect(obj.get('count')).toBe(0);
await obj.fetch();
expect(obj.get('count')).toBe(0);
});
it('pointer should not be cleared by triggers', async () => {
Parse.Cloud.afterSave('MyObject', () => {});
const foo = await new Parse.Object('Test', { foo: 'bar' }).save();
const obj = await new Parse.Object('MyObject', { foo }).save();
const foo2 = obj.get('foo');
expect(foo2.get('foo')).toBe('bar');
});
it('can set a pointer in triggers', async () => {
Parse.Cloud.beforeSave('MyObject', () => {});
Parse.Cloud.afterSave(
'MyObject',
async ({ object }) => {
const foo = await new Parse.Object('Test', { foo: 'bar' }).save();
object.set({ foo });
await object.save(null, { useMasterKey: true });
},
{
skipWithMasterKey: true,
}
);
const obj = await new Parse.Object('MyObject').save();
const foo2 = obj.get('foo');
expect(foo2.get('foo')).toBe('bar');
});
it('beforeSave should not sanitize database', async done => {
const { adapter } = Config.get(Parse.applicationId).database;
const spy = spyOn(adapter, 'findOneAndUpdate').and.callThrough();
@@ -1860,6 +2000,36 @@ describe('afterSave hooks', () => {
const myObject = new MyObject();
myObject.save().then(() => done());
});
it('should unset in afterSave', async () => {
Parse.Cloud.afterSave(
'MyObject',
({ object }) => {
object.unset('secret');
},
{
skipWithMasterKey: true,
}
);
const obj = new Parse.Object('MyObject');
obj.set('secret', 'bar');
await obj.save();
expect(obj.get('secret')).toBeUndefined();
await obj.fetch();
expect(obj.get('secret')).toBe('bar');
});
it('should unset', async () => {
Parse.Cloud.beforeSave('MyObject', ({ object }) => {
object.set('secret', 'hidden');
});
Parse.Cloud.afterSave('MyObject', ({ object }) => {
object.unset('secret');
});
const obj = await new Parse.Object('MyObject').save();
expect(obj.get('secret')).toBeUndefined();
});
});
describe('beforeDelete hooks', () => {
@@ -1999,6 +2169,25 @@ describe('beforeFind hooks', () => {
});
});
it('should have object found with nested relational data query', async () => {
const obj1 = Parse.Object.extend('TestObject');
const obj2 = Parse.Object.extend('TestObject2');
let item2 = new obj2();
item2 = await item2.save();
let item1 = new obj1();
const relation = item1.relation('rel');
relation.add(item2);
item1 = await item1.save();
Parse.Cloud.beforeFind('TestObject', req => {
const additionalQ = new Parse.Query('TestObject');
additionalQ.equalTo('rel', item2);
return Parse.Query.and(req.query, additionalQ);
});
const q = new Parse.Query('TestObject');
const res = await q.first();
expect(res.id).toEqual(item1.id);
});
it('should use the modified exclude query', async () => {
Parse.Cloud.beforeFind('MyObject', req => {
const q = req.query;
@@ -3205,13 +3394,28 @@ describe('afterLogin hook', () => {
const query = new Parse.Query(TestObject);
await query.find({ context: { a: 'a' } });
});
it('beforeFind and afterFind should have access to context while making fetch call', async () => {
Parse.Cloud.beforeFind('TestObject', req => {
expect(req.context.a).toEqual('a');
expect(req.context.b).toBeUndefined();
req.context.b = 'b';
});
Parse.Cloud.afterFind('TestObject', req => {
expect(req.context.a).toEqual('a');
expect(req.context.b).toEqual('b');
});
const obj = new TestObject();
await obj.save();
await obj.fetch({ context: { a: 'a' } });
});
});
describe('saveFile hooks', () => {
it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
Parse.Cloud.beforeSaveFile(() => {
Parse.Cloud.beforeSave(Parse.File, () => {
const newFile = new Parse.File('some-file.txt');
newFile._url = 'http://www.somewhere.com/parse/files/some-app-id/some-file.txt';
return newFile;
@@ -3226,7 +3430,7 @@ describe('saveFile hooks', () => {
it('beforeSaveFile should throw error', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeSaveFile(() => {
Parse.Cloud.beforeSave(Parse.File, () => {
throw new Parse.Error(400, 'some-error-message');
});
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
@@ -3240,8 +3444,8 @@ describe('saveFile hooks', () => {
it('beforeSaveFile should change values of uploaded file by editing fileObject directly', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
Parse.Cloud.beforeSaveFile(async req => {
expect(req.triggerName).toEqual('beforeSaveFile');
Parse.Cloud.beforeSave(Parse.File, async req => {
expect(req.triggerName).toEqual('beforeSave');
expect(req.master).toBe(true);
req.file.addMetadata('foo', 'bar');
req.file.addTag('tagA', 'some-tag');
@@ -3269,8 +3473,8 @@ describe('saveFile hooks', () => {
it('beforeSaveFile should change values by returning new fileObject', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
Parse.Cloud.beforeSaveFile(async req => {
expect(req.triggerName).toEqual('beforeSaveFile');
Parse.Cloud.beforeSave(Parse.File, async req => {
expect(req.triggerName).toEqual('beforeSave');
expect(req.fileSize).toBe(3);
const newFile = new Parse.File('donald_duck.pdf', [4, 5, 6], 'application/pdf');
newFile.setMetadata({ foo: 'bar' });
@@ -3303,8 +3507,8 @@ describe('saveFile hooks', () => {
it('beforeSaveFile should contain metadata and tags saved from client', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
Parse.Cloud.beforeSaveFile(async req => {
expect(req.triggerName).toEqual('beforeSaveFile');
Parse.Cloud.beforeSave(Parse.File, async req => {
expect(req.triggerName).toEqual('beforeSave');
expect(req.fileSize).toBe(3);
expect(req.file).toBeInstanceOf(Parse.File);
expect(req.file.name()).toBe('popeye.txt');
@@ -3332,7 +3536,7 @@ describe('saveFile hooks', () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const config = Config.get('test');
config.filesController.options.preserveFileName = true;
Parse.Cloud.beforeSaveFile(async ({ file }) => {
Parse.Cloud.beforeSave(Parse.File, async ({ file }) => {
expect(file.name()).toBe('popeye.txt');
const fileData = await file.getData();
const newFile = new Parse.File('2020-04-01.txt', { base64: fileData });
@@ -3346,13 +3550,13 @@ describe('saveFile hooks', () => {
it('afterSaveFile should set fileSize to null if beforeSave returns an already saved file', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
Parse.Cloud.beforeSaveFile(req => {
Parse.Cloud.beforeSave(Parse.File, req => {
expect(req.fileSize).toBe(3);
const newFile = new Parse.File('some-file.txt');
newFile._url = 'http://www.somewhere.com/parse/files/some-app-id/some-file.txt';
return newFile;
});
Parse.Cloud.afterSaveFile(req => {
Parse.Cloud.afterSave(Parse.File, req => {
expect(req.fileSize).toBe(null);
});
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
@@ -3365,7 +3569,7 @@ describe('saveFile hooks', () => {
it('afterSaveFile should throw error', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.afterSaveFile(async () => {
Parse.Cloud.afterSave(Parse.File, async () => {
throw new Parse.Error(400, 'some-error-message');
});
const filename = 'donald_duck.pdf';
@@ -3379,11 +3583,11 @@ describe('saveFile hooks', () => {
it('afterSaveFile should call with fileObject', async done => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeSaveFile(async req => {
Parse.Cloud.beforeSave(Parse.File, async req => {
req.file.setTags({ tagA: 'some-tag' });
req.file.setMetadata({ foo: 'bar' });
});
Parse.Cloud.afterSaveFile(async req => {
Parse.Cloud.afterSave(Parse.File, async req => {
expect(req.master).toBe(true);
expect(req.file._tags).toEqual({ tagA: 'some-tag' });
expect(req.file._metadata).toEqual({ foo: 'bar' });
@@ -3395,13 +3599,13 @@ describe('saveFile hooks', () => {
it('afterSaveFile should change fileSize when file data changes', async done => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeSaveFile(async req => {
Parse.Cloud.beforeSave(Parse.File, async req => {
expect(req.fileSize).toBe(3);
expect(req.master).toBe(true);
const newFile = new Parse.File('donald_duck.pdf', [4, 5, 6, 7, 8, 9], 'application/pdf');
return newFile;
});
Parse.Cloud.afterSaveFile(async req => {
Parse.Cloud.afterSave(Parse.File, async req => {
expect(req.fileSize).toBe(6);
expect(req.master).toBe(true);
done();
@@ -3412,7 +3616,7 @@ describe('saveFile hooks', () => {
it('beforeDeleteFile should call with fileObject', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeDeleteFile(req => {
Parse.Cloud.beforeDelete(Parse.File, req => {
expect(req.file).toBeInstanceOf(Parse.File);
expect(req.file._name).toEqual('popeye.txt');
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
@@ -3424,7 +3628,7 @@ describe('saveFile hooks', () => {
it('beforeDeleteFile should throw error', async done => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeDeleteFile(() => {
Parse.Cloud.beforeDelete(Parse.File, () => {
throw new Error('some error message');
});
const file = new Parse.File('popeye.txt');
@@ -3438,12 +3642,12 @@ describe('saveFile hooks', () => {
it('afterDeleteFile should call with fileObject', async done => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeDeleteFile(req => {
Parse.Cloud.beforeDelete(Parse.File, req => {
expect(req.file).toBeInstanceOf(Parse.File);
expect(req.file._name).toEqual('popeye.txt');
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
});
Parse.Cloud.afterDeleteFile(req => {
Parse.Cloud.afterDelete(Parse.File, req => {
expect(req.file).toBeInstanceOf(Parse.File);
expect(req.file._name).toEqual('popeye.txt');
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
@@ -3455,7 +3659,7 @@ describe('saveFile hooks', () => {
it('beforeSaveFile should not change file if nothing is returned', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeSaveFile(() => {
Parse.Cloud.beforeSave(Parse.File, () => {
return;
});
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
@@ -3464,7 +3668,7 @@ describe('saveFile hooks', () => {
});
it('throw custom error from beforeSaveFile', async done => {
Parse.Cloud.beforeSaveFile(() => {
Parse.Cloud.beforeSave(Parse.File, () => {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail');
});
try {
@@ -3478,7 +3682,7 @@ describe('saveFile hooks', () => {
});
it('throw empty error from beforeSaveFile', async done => {
Parse.Cloud.beforeSaveFile(() => {
Parse.Cloud.beforeSave(Parse.File, () => {
throw null;
});
try {
@@ -3490,6 +3694,55 @@ describe('saveFile hooks', () => {
done();
}
});
it('legacy hooks', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const logger = require('../lib/logger').logger;
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
const triggers = {
beforeSaveFile(req) {
req.file.setTags({ tagA: 'some-tag' });
req.file.setMetadata({ foo: 'bar' });
expect(req.triggerName).toEqual('beforeSave');
expect(req.master).toBe(true);
},
afterSaveFile(req) {
expect(req.master).toBe(true);
expect(req.file._tags).toEqual({ tagA: 'some-tag' });
expect(req.file._metadata).toEqual({ foo: 'bar' });
},
beforeDeleteFile(req) {
expect(req.file).toBeInstanceOf(Parse.File);
expect(req.file._name).toEqual('popeye.txt');
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
expect(req.fileSize).toBe(null);
},
afterDeleteFile(req) {
expect(req.file).toBeInstanceOf(Parse.File);
expect(req.file._name).toEqual('popeye.txt');
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
},
};
for (const key in triggers) {
spyOn(triggers, key).and.callThrough();
Parse.Cloud[key](triggers[key]);
}
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
await file.save({ useMasterKey: true });
await new Parse.File('popeye.txt', [1, 2, 3], 'text/plain').destroy({ useMasterKey: true });
await new Promise(resolve => setTimeout(resolve, 100));
for (const key in triggers) {
expect(triggers[key]).toHaveBeenCalled();
expect(logSpy).toHaveBeenCalledWith(
`DeprecationWarning: Parse.Cloud.${key} is deprecated and will be removed in a future version. Use Parse.Cloud.${key.replace(
'File',
''
)}(Parse.File, (request) => {})`
);
}
});
});
describe('sendEmail', () => {
@@ -3516,23 +3769,4 @@ describe('sendEmail', () => {
'Failed to send email because no mail adapter is configured for Parse Server.'
);
});
it('should have object found with nested relational data query', async () => {
const obj1 = Parse.Object.extend('TestObject');
const obj2 = Parse.Object.extend('TestObject2');
let item2 = new obj2();
item2 = await item2.save();
let item1 = new obj1();
const relation = item1.relation('rel');
relation.add(item2);
item1 = await item1.save();
Parse.Cloud.beforeFind('TestObject', req => {
const additionalQ = new Parse.Query('TestObject');
additionalQ.equalTo('rel', item2);
return Parse.Query.and(req.query, additionalQ);
});
const q = new Parse.Query('TestObject');
const res = await q.first();
expect(res.id).toEqual(item1.id);
});
});
+36
View File
@@ -432,6 +432,42 @@ describe('DefinedSchemas', () => {
expect(testSchema.indexes).toBeUndefined();
expect(userSchema.indexes).toEqual(expectedIndexes);
});
it('should detect protected indexes for _User class', () => {
const definedSchema = new DefinedSchemas({}, {});
const protectedUserIndexes = ['_id_', 'case_insensitive_email', 'username_1', 'email_1'];
protectedUserIndexes.forEach(field => {
expect(definedSchema.isProtectedIndex('_User', field)).toEqual(true);
});
expect(definedSchema.isProtectedIndex('_User', 'test')).toEqual(false);
});
it('should detect protected indexes for _Role class', () => {
const definedSchema = new DefinedSchemas({}, {});
expect(definedSchema.isProtectedIndex('_Role', 'name_1')).toEqual(true);
expect(definedSchema.isProtectedIndex('_Role', 'test')).toEqual(false);
});
it('should detect protected indexes for _Idempotency class', () => {
const definedSchema = new DefinedSchemas({}, {});
expect(definedSchema.isProtectedIndex('_Idempotency', 'reqId_1')).toEqual(true);
expect(definedSchema.isProtectedIndex('_Idempotency', 'test')).toEqual(false);
});
it('should not detect protected indexes on user defined class', () => {
const definedSchema = new DefinedSchemas({}, {});
const protectedIndexes = [
'case_insensitive_email',
'username_1',
'email_1',
'reqId_1',
'name_1',
];
protectedIndexes.forEach(field => {
expect(definedSchema.isProtectedIndex('ExampleClass', field)).toEqual(false);
});
expect(definedSchema.isProtectedIndex('ExampleClass', '_id_')).toEqual(true);
});
});
describe('ClassLevelPermissions', () => {
+35 -22
View File
@@ -3,7 +3,6 @@ const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapte
.WinstonLoggerAdapter;
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
.GridFSBucketAdapter;
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
const Config = require('../lib/Config');
const FilesController = require('../lib/Controllers/FilesController').default;
const databaseURI = 'mongodb://localhost:27017/parse';
@@ -24,8 +23,8 @@ const mockAdapter = {
describe('FilesController', () => {
it('should properly expand objects', done => {
const config = Config.get(Parse.applicationId);
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
const filesController = new FilesController(gridStoreAdapter);
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
const filesController = new FilesController(gridFSAdapter);
const result = filesController.expandFilesInObject(config, function () {});
expect(result).toBeUndefined();
@@ -44,6 +43,20 @@ describe('FilesController', () => {
done();
});
it_only_db('mongo')('should pass databaseOptions to GridFSBucketAdapter', async () => {
await reconfigureServer({
databaseURI: 'mongodb://localhost:27017/parse',
filesAdapter: null,
databaseAdapter: null,
databaseOptions: {
retryWrites: true,
},
});
const config = Config.get(Parse.applicationId);
expect(config.database.adapter._mongoOptions.retryWrites).toBeTrue();
expect(config.filesController.adapter._mongoOptions.retryWrites).toBeTrue();
});
it('should create a server log on failure', done => {
const logController = new LoggerController(new WinstonLoggerAdapter());
@@ -88,19 +101,19 @@ describe('FilesController', () => {
it('should add a unique hash to the file name when the preserveFileName option is false', done => {
const config = Config.get(Parse.applicationId);
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
spyOn(gridStoreAdapter, 'createFile');
gridStoreAdapter.createFile.and.returnValue(Promise.resolve());
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
spyOn(gridFSAdapter, 'createFile');
gridFSAdapter.createFile.and.returnValue(Promise.resolve());
const fileName = 'randomFileName.pdf';
const regexEscapedFileName = fileName.replace(/\./g, '\\$&');
const filesController = new FilesController(gridStoreAdapter, null, {
const filesController = new FilesController(gridFSAdapter, null, {
preserveFileName: false,
});
filesController.createFile(config, fileName);
expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1);
expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toMatch(
expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1);
expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toMatch(
`^.{32}_${regexEscapedFileName}$`
);
@@ -109,42 +122,42 @@ describe('FilesController', () => {
it('should not add a unique hash to the file name when the preserveFileName option is true', done => {
const config = Config.get(Parse.applicationId);
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
spyOn(gridStoreAdapter, 'createFile');
gridStoreAdapter.createFile.and.returnValue(Promise.resolve());
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
spyOn(gridFSAdapter, 'createFile');
gridFSAdapter.createFile.and.returnValue(Promise.resolve());
const fileName = 'randomFileName.pdf';
const filesController = new FilesController(gridStoreAdapter, null, {
const filesController = new FilesController(gridFSAdapter, null, {
preserveFileName: true,
});
filesController.createFile(config, fileName);
expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1);
expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName);
expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1);
expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName);
done();
});
it('should handle adapter without getMetadata', async () => {
const gridStoreAdapter = new GridFSBucketAdapter(databaseURI);
gridStoreAdapter.getMetadata = null;
const filesController = new FilesController(gridStoreAdapter);
const gridFSAdapter = new GridFSBucketAdapter(databaseURI);
gridFSAdapter.getMetadata = null;
const filesController = new FilesController(gridFSAdapter);
const result = await filesController.getMetadata();
expect(result).toEqual({});
});
it('should reject slashes in file names', done => {
const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
const fileName = 'foo/randomFileName.pdf';
expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null);
expect(gridFSAdapter.validateFilename(fileName)).not.toBe(null);
done();
});
it('should also reject slashes in file names', done => {
const gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse');
const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse');
const fileName = 'foo/randomFileName.pdf';
expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null);
expect(gridFSAdapter.validateFilename(fileName)).not.toBe(null);
done();
});
});
+3 -16
View File
@@ -1,4 +1,3 @@
const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
.GridFSBucketAdapter;
const { randomString } = require('../lib/cryptoUtils');
@@ -14,25 +13,13 @@ async function expectMissingFile(gfsAdapter, name) {
}
}
describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
describe_only_db('mongo')('GridFSBucket', () => {
beforeEach(async () => {
const gsAdapter = new GridStoreAdapter(databaseURI);
const gsAdapter = new GridFSBucketAdapter(databaseURI);
const db = await gsAdapter._connect();
await db.dropDatabase();
});
it('a file created in GridStore should be available in GridFS', async () => {
const gsAdapter = new GridStoreAdapter(databaseURI);
const gfsAdapter = new GridFSBucketAdapter(databaseURI);
await expectMissingFile(gfsAdapter, 'myFileName');
const originalString = 'abcdefghi';
await gsAdapter.createFile('myFileName', originalString);
const gsResult = await gsAdapter.getFileData('myFileName');
expect(gsResult.toString('utf8')).toBe(originalString);
const gfsResult = await gfsAdapter.getFileData('myFileName');
expect(gfsResult.toString('utf8')).toBe(originalString);
});
it('should save an encrypted file that can only be decrypted by a GridFS adapter with the encryptionKey', async () => {
const unencryptedAdapter = new GridFSBucketAdapter(databaseURI);
const encryptedAdapter = new GridFSBucketAdapter(
@@ -451,7 +438,7 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
await db.admin().serverStatus();
expect(false).toBe(true);
} catch (e) {
expect(e.message).toEqual('topology was destroyed');
expect(e.message).toEqual('Client must be connected before running operations');
}
});
});
-111
View File
@@ -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');
}
});
});
+38 -4
View File
@@ -6,11 +6,14 @@ const rest = require('../lib/rest');
const auth = require('../lib/Auth');
const uuid = require('uuid');
describe_only_db('mongo')('Idempotency', () => {
describe('Idempotency', () => {
// Parameters
/** Enable TTL expiration simulated by removing entry instead of waiting for MongoDB TTL monitor which
runs only every 60s, so it can take up to 119s until entry removal - ain't nobody got time for that */
const SIMULATE_TTL = true;
const ttl = 2;
const maxTimeOut = 4000;
// Helpers
async function deleteRequestEntry(reqId) {
const config = Config.get(Parse.applicationId);
@@ -38,9 +41,14 @@ describe_only_db('mongo')('Idempotency', () => {
}
await setup({
paths: ['functions/.*', 'jobs/.*', 'classes/.*', 'users', 'installations'],
ttl: 30,
ttl: ttl,
});
});
afterAll(() => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
});
// Tests
it('should enforce idempotency for cloud code function', async () => {
let counter = 0;
@@ -56,7 +64,7 @@ describe_only_db('mongo')('Idempotency', () => {
'X-Parse-Request-Id': 'abc-123',
},
};
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(30);
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(ttl);
await request(params);
await request(params).then(fail, e => {
expect(e.status).toEqual(400);
@@ -83,12 +91,38 @@ describe_only_db('mongo')('Idempotency', () => {
if (SIMULATE_TTL) {
await deleteRequestEntry('abc-123');
} else {
await new Promise(resolve => setTimeout(resolve, 130000));
await new Promise(resolve => setTimeout(resolve, maxTimeOut));
}
await expectAsync(request(params)).toBeResolved();
expect(counter).toBe(2);
});
it_only_db('postgres')(
'should delete request entry when postgress ttl function is called',
async () => {
const client = Config.get(Parse.applicationId).database.adapter._client;
let counter = 0;
Parse.Cloud.define('myFunction', () => {
counter++;
});
const params = {
method: 'POST',
url: 'http://localhost:8378/1/functions/myFunction',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Request-Id': 'abc-123',
},
};
await expectAsync(request(params)).toBeResolved();
await expectAsync(request(params)).toBeRejected();
await new Promise(resolve => setTimeout(resolve, maxTimeOut));
await client.one('SELECT idempotency_delete_expired_records()');
await expectAsync(request(params)).toBeResolved();
expect(counter).toBe(2);
}
);
it('should enforce idempotency for cloud code jobs', async () => {
let counter = 0;
Parse.Cloud.job('myJob', () => {
+3 -118
View File
@@ -158,78 +158,6 @@ describe('middlewares', () => {
});
});
it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1', 'ip2'],
});
fakeReq.connection = { remoteAddress: 'ip3' };
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1', 'ip2'],
});
fakeReq.connection = { remoteAddress: 'ip1' };
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1', 'ip2'],
});
fakeReq.socket = { remoteAddress: 'ip3' };
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1', 'ip2'],
});
fakeReq.socket = { remoteAddress: 'ip1' };
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should not succeed if the connection.socket.remoteAddress does not belong to masterKeyIps list', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1', 'ip2'],
});
fakeReq.connection = { socket: { remoteAddress: 'ip3' } };
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1', 'ip2'],
});
fakeReq.connection = { socket: { remoteAddress: 'ip1' } };
fakeReq.headers['x-parse-master-key'] = 'masterKey';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should allow any ip to use masterKey if masterKeyIps is empty', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
@@ -243,52 +171,9 @@ describe('middlewares', () => {
});
});
it('should succeed if xff header does belong to masterKeyIps', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1'],
});
fakeReq.headers['x-parse-master-key'] = 'masterKey';
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should succeed if xff header with one ip does belong to masterKeyIps', done => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1'],
});
fakeReq.headers['x-parse-master-key'] = 'masterKey';
fakeReq.headers['x-forwarded-for'] = 'ip1';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should not succeed if xff header does not belong to masterKeyIps', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip4'],
});
fakeReq.headers['x-parse-master-key'] = 'masterKey';
fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
});
it('should not succeed if xff header is empty and masterKeyIps is set', () => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey',
masterKeyIps: ['ip1'],
});
fakeReq.headers['x-parse-master-key'] = 'masterKey';
fakeReq.headers['x-forwarded-for'] = '';
middlewares.handleParseHeaders(fakeReq, fakeRes);
expect(fakeRes.status).toHaveBeenCalledWith(403);
it('can set trust proxy', async () => {
const server = await reconfigureServer({ trustProxy: 1 });
expect(server.app.parent.settings['trust proxy']).toBe(1);
});
it('should properly expose the headers', () => {
+59 -32
View File
@@ -1,7 +1,7 @@
'use strict';
const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default;
const { MongoClient } = require('mongodb');
const { MongoClient, Collection } = require('mongodb');
const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
const request = require('../lib/request');
const Config = require('../lib/Config');
@@ -101,7 +101,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
done.fail('Find succeeded despite taking too long!');
},
err => {
expect(err.name).toEqual('MongoError');
expect(err.name).toEqual('MongoServerError');
expect(err.code).toEqual(50);
expect(err.message).toMatch('operation exceeded time limit');
done();
@@ -283,7 +283,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
await adapter.database.admin().serverStatus();
expect(false).toBe(true);
} catch (e) {
expect(e.message).toEqual('topology was destroyed');
expect(e.message).toEqual('Client must be connected before running operations');
}
});
@@ -308,7 +308,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
});
it('should use index for caseInsensitive query', async () => {
it_only_mongodb_version('<5.1>=6')('should use index for caseInsensitive query', async () => {
const user = new Parse.User();
user.set('username', 'Bugs');
user.set('password', 'Bunny');
@@ -342,6 +342,40 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
expect(postIndexPlan.executionStats.executionStages.stage).toBe('FETCH');
});
it_only_mongodb_version('>=5.1<6')('should use index for caseInsensitive query', async () => {
const user = new Parse.User();
user.set('username', 'Bugs');
user.set('password', 'Bunny');
await user.signUp();
const database = Config.get(Parse.applicationId).database;
await database.adapter.dropAllIndexes('_User');
const preIndexPlan = await database.find(
'_User',
{ username: 'bugs' },
{ caseInsensitive: true, explain: true }
);
const schema = await new Parse.Schema('_User').get();
await database.adapter.ensureIndex(
'_User',
schema,
['username'],
'case_insensitive_username',
true
);
const postIndexPlan = await database.find(
'_User',
{ username: 'bugs' },
{ caseInsensitive: true, explain: true }
);
expect(preIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('COLLSCAN');
expect(postIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
});
it('should delete field without index', async () => {
const database = Config.get(Parse.applicationId).database;
const obj = new Parse.Object('MyObject');
@@ -392,8 +426,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
const myObject = new Parse.Object('MyObject');
await myObject.save();
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
await request({
method: 'POST',
@@ -412,9 +445,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
});
let found = false;
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
Collection.prototype.findOneAndUpdate.calls.all().forEach(call => {
found = true;
expect(call.args[2].session.transaction.state).not.toBe('NO_TRANSACTION');
expect(call.args[2].session.transaction.state).toBe('TRANSACTION_COMMITTED');
});
expect(found).toBe(true);
});
@@ -423,8 +456,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
const myObject = new Parse.Object('MyObject');
await myObject.save();
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
await request({
method: 'POST',
@@ -443,9 +475,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
});
let found = false;
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
Collection.prototype.findOneAndUpdate.calls.all().forEach(call => {
found = true;
expect(call.args[2].session).toBe(undefined);
expect(call.args[2].session).toBeFalsy();
});
expect(found).toBe(true);
});
@@ -454,8 +486,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
const myObject = new Parse.Object('MyObject');
await myObject.save();
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
await request({
method: 'POST',
@@ -473,9 +504,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
});
let found = false;
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
Collection.prototype.findOneAndUpdate.calls.all().forEach(call => {
found = true;
expect(call.args[2].session).toBe(undefined);
expect(call.args[2].session).toBeFalsy();
});
expect(found).toBe(true);
});
@@ -484,8 +515,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
const myObject = new Parse.Object('MyObject');
await myObject.save();
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough();
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
await request({
method: 'PUT',
@@ -495,30 +525,28 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
});
let found = false;
databaseAdapter.database.serverConfig.command.calls.all().forEach(call => {
Collection.prototype.findOneAndUpdate.calls.all().forEach(call => {
found = true;
expect(call.args[2].session).toBe(undefined);
expect(call.args[2].session).toBeFalsy();
});
expect(found).toBe(true);
});
it('should not use transactions when using SDK insert', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
spyOn(databaseAdapter.database.serverConfig, 'insert').and.callThrough();
spyOn(Collection.prototype, 'insertOne').and.callThrough();
const myObject = new Parse.Object('MyObject');
await myObject.save();
const calls = databaseAdapter.database.serverConfig.insert.calls.all();
const calls = Collection.prototype.insertOne.calls.all();
expect(calls.length).toBeGreaterThan(0);
calls.forEach(call => {
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
expect(call.args[1].session).toBeFalsy();
});
});
it('should not use transactions when using SDK update', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
spyOn(databaseAdapter.database.serverConfig, 'update').and.callThrough();
spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough();
const myObject = new Parse.Object('MyObject');
await myObject.save();
@@ -526,26 +554,25 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
myObject.set('myAttribute', 'myValue');
await myObject.save();
const calls = databaseAdapter.database.serverConfig.update.calls.all();
const calls = Collection.prototype.findOneAndUpdate.calls.all();
expect(calls.length).toBeGreaterThan(0);
calls.forEach(call => {
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
expect(call.args[2].session).toBeFalsy();
});
});
it('should not use transactions when using SDK delete', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
spyOn(databaseAdapter.database.serverConfig, 'remove').and.callThrough();
spyOn(Collection.prototype, 'deleteMany').and.callThrough();
const myObject = new Parse.Object('MyObject');
await myObject.save();
await myObject.destroy();
const calls = databaseAdapter.database.serverConfig.remove.calls.all();
const calls = Collection.prototype.deleteMany.calls.all();
expect(calls.length).toBeGreaterThan(0);
calls.forEach(call => {
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
expect(call.args[1].session).toBeFalsy();
});
});
});
+12 -11
View File
@@ -4,6 +4,7 @@
const transform = require('../lib/Adapters/Storage/Mongo/MongoTransform');
const dd = require('deep-diff');
const mongodb = require('mongodb');
const Utils = require('../lib/Utils');
describe('parseObjectToMongoObjectForCreate', () => {
it('a basic number', done => {
@@ -592,7 +593,7 @@ describe('relativeTimeToDate', () => {
describe('In the future', () => {
it('should parse valid natural time', () => {
const text = 'in 1 year 2 weeks 12 days 10 hours 24 minutes 30 seconds';
const { result, status, info } = transform.relativeTimeToDate(text, now);
const { result, status, info } = Utils.relativeTimeToDate(text, now);
expect(result.toISOString()).toBe('2018-10-22T23:52:46.617Z');
expect(status).toBe('success');
expect(info).toBe('future');
@@ -602,7 +603,7 @@ describe('relativeTimeToDate', () => {
describe('In the past', () => {
it('should parse valid natural time', () => {
const text = '2 days 12 hours 1 minute 12 seconds ago';
const { result, status, info } = transform.relativeTimeToDate(text, now);
const { result, status, info } = Utils.relativeTimeToDate(text, now);
expect(result.toISOString()).toBe('2017-09-24T01:27:04.617Z');
expect(status).toBe('success');
expect(info).toBe('past');
@@ -612,7 +613,7 @@ describe('relativeTimeToDate', () => {
describe('From now', () => {
it('should equal current time', () => {
const text = 'now';
const { result, status, info } = transform.relativeTimeToDate(text, now);
const { result, status, info } = Utils.relativeTimeToDate(text, now);
expect(result.toISOString()).toBe('2017-09-26T13:28:16.617Z');
expect(status).toBe('success');
expect(info).toBe('present');
@@ -621,54 +622,54 @@ describe('relativeTimeToDate', () => {
describe('Error cases', () => {
it('should error if string is completely gibberish', () => {
expect(transform.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
expect(Utils.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
status: 'error',
info: "Time should either start with 'in' or end with 'ago'",
});
});
it('should error if string contains neither `ago` nor `in`', () => {
expect(transform.relativeTimeToDate('12 hours 1 minute')).toEqual({
expect(Utils.relativeTimeToDate('12 hours 1 minute')).toEqual({
status: 'error',
info: "Time should either start with 'in' or end with 'ago'",
});
});
it('should error if there are missing units or numbers', () => {
expect(transform.relativeTimeToDate('in 12 hours 1')).toEqual({
expect(Utils.relativeTimeToDate('in 12 hours 1')).toEqual({
status: 'error',
info: 'Invalid time string. Dangling unit or number.',
});
expect(transform.relativeTimeToDate('12 hours minute ago')).toEqual({
expect(Utils.relativeTimeToDate('12 hours minute ago')).toEqual({
status: 'error',
info: 'Invalid time string. Dangling unit or number.',
});
});
it('should error on floating point numbers', () => {
expect(transform.relativeTimeToDate('in 12.3 hours')).toEqual({
expect(Utils.relativeTimeToDate('in 12.3 hours')).toEqual({
status: 'error',
info: "'12.3' is not an integer.",
});
});
it('should error if numbers are invalid', () => {
expect(transform.relativeTimeToDate('12 hours 123a minute ago')).toEqual({
expect(Utils.relativeTimeToDate('12 hours 123a minute ago')).toEqual({
status: 'error',
info: "'123a' is not an integer.",
});
});
it('should error on invalid interval units', () => {
expect(transform.relativeTimeToDate('4 score 7 years ago')).toEqual({
expect(Utils.relativeTimeToDate('4 score 7 years ago')).toEqual({
status: 'error',
info: "Invalid interval: 'score'",
});
});
it("should error when string contains 'ago' and 'in'", () => {
expect(transform.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
expect(Utils.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
status: 'error',
info: "Time cannot have both 'in' and 'ago'",
});
+146 -49
View File
@@ -951,57 +951,154 @@ describe('miscellaneous', function () {
);
});
it('should return the updated fields on PUT', done => {
it('return the updated fields on PUT', async () => {
const obj = new Parse.Object('GameScore');
obj
.save({ a: 'hello', c: 1, d: ['1'], e: ['1'], f: ['1', '2'] })
.then(() => {
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': 'yolo',
};
request({
method: 'PUT',
headers: headers,
url: 'http://localhost:8378/1/classes/GameScore/' + obj.id,
body: JSON.stringify({
a: 'b',
c: { __op: 'Increment', amount: 2 },
d: { __op: 'Add', objects: ['2'] },
e: { __op: 'AddUnique', objects: ['1', '2'] },
f: { __op: 'Remove', objects: ['2'] },
selfThing: {
__type: 'Pointer',
className: 'GameScore',
objectId: obj.id,
},
}),
}).then(response => {
try {
const body = response.data;
expect(body.a).toBeUndefined();
expect(body.c).toEqual(3); // 2+1
expect(body.d.length).toBe(2);
expect(body.d.indexOf('1') > -1).toBe(true);
expect(body.d.indexOf('2') > -1).toBe(true);
expect(body.e.length).toBe(2);
expect(body.e.indexOf('1') > -1).toBe(true);
expect(body.e.indexOf('2') > -1).toBe(true);
expect(body.f.length).toBe(1);
expect(body.f.indexOf('1') > -1).toBe(true);
// return nothing on other self
expect(body.selfThing).toBeUndefined();
// updatedAt is always set
expect(body.updatedAt).not.toBeUndefined();
} catch (e) {
fail(e);
}
done();
});
const pointer = new Parse.Object('Child');
await pointer.save();
obj.set(
'point',
new Parse.GeoPoint({
latitude: 37.4848,
longitude: -122.1483,
})
.catch(done.fail);
);
obj.set('array', ['obj1', 'obj2']);
obj.set('objects', { a: 'b' });
obj.set('string', 'abc');
obj.set('bool', true);
obj.set('number', 1);
obj.set('date', new Date());
obj.set('pointer', pointer);
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': 'yolo',
};
const saveResponse = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/classes/GameScore',
body: JSON.stringify({
a: 'hello',
c: 1,
d: ['1'],
e: ['1'],
f: ['1', '2'],
...obj.toJSON(),
}),
});
expect(Object.keys(saveResponse.data).sort()).toEqual(['createdAt', 'objectId']);
obj.id = saveResponse.data.objectId;
const response = await request({
method: 'PUT',
headers: headers,
url: 'http://localhost:8378/1/classes/GameScore/' + obj.id,
body: JSON.stringify({
a: 'b',
c: { __op: 'Increment', amount: 2 },
d: { __op: 'Add', objects: ['2'] },
e: { __op: 'AddUnique', objects: ['1', '2'] },
f: { __op: 'Remove', objects: ['2'] },
selfThing: {
__type: 'Pointer',
className: 'GameScore',
objectId: obj.id,
},
}),
});
const body = response.data;
expect(Object.keys(body).sort()).toEqual(['c', 'd', 'e', 'f', 'updatedAt']);
expect(body.a).toBeUndefined();
expect(body.c).toEqual(3); // 2+1
expect(body.d.length).toBe(2);
expect(body.d.indexOf('1') > -1).toBe(true);
expect(body.d.indexOf('2') > -1).toBe(true);
expect(body.e.length).toBe(2);
expect(body.e.indexOf('1') > -1).toBe(true);
expect(body.e.indexOf('2') > -1).toBe(true);
expect(body.f.length).toBe(1);
expect(body.f.indexOf('1') > -1).toBe(true);
expect(body.selfThing).toBeUndefined();
expect(body.updatedAt).not.toBeUndefined();
});
it('should response should not change with triggers', async () => {
const obj = new Parse.Object('GameScore');
const pointer = new Parse.Object('Child');
Parse.Cloud.beforeSave('GameScore', request => {
return request.object;
});
Parse.Cloud.afterSave('GameScore', request => {
return request.object;
});
await pointer.save();
obj.set(
'point',
new Parse.GeoPoint({
latitude: 37.4848,
longitude: -122.1483,
})
);
obj.set('array', ['obj1', 'obj2']);
obj.set('objects', { a: 'b' });
obj.set('string', 'abc');
obj.set('bool', true);
obj.set('number', 1);
obj.set('date', new Date());
obj.set('pointer', pointer);
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': 'yolo',
};
const saveResponse = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/classes/GameScore',
body: JSON.stringify({
a: 'hello',
c: 1,
d: ['1'],
e: ['1'],
f: ['1', '2'],
...obj.toJSON(),
}),
});
expect(Object.keys(saveResponse.data).sort()).toEqual(['createdAt', 'objectId']);
obj.id = saveResponse.data.objectId;
const response = await request({
method: 'PUT',
headers: headers,
url: 'http://localhost:8378/1/classes/GameScore/' + obj.id,
body: JSON.stringify({
a: 'b',
c: { __op: 'Increment', amount: 2 },
d: { __op: 'Add', objects: ['2'] },
e: { __op: 'AddUnique', objects: ['1', '2'] },
f: { __op: 'Remove', objects: ['2'] },
selfThing: {
__type: 'Pointer',
className: 'GameScore',
objectId: obj.id,
},
}),
});
const body = response.data;
expect(Object.keys(body).sort()).toEqual(['c', 'd', 'e', 'f', 'updatedAt']);
expect(body.a).toBeUndefined();
expect(body.c).toEqual(3); // 2+1
expect(body.d.length).toBe(2);
expect(body.d.indexOf('1') > -1).toBe(true);
expect(body.d.indexOf('2') > -1).toBe(true);
expect(body.e.length).toBe(2);
expect(body.e.indexOf('1') > -1).toBe(true);
expect(body.e.indexOf('2') > -1).toBe(true);
expect(body.f.length).toBe(1);
expect(body.f.indexOf('1') > -1).toBe(true);
expect(body.selfThing).toBeUndefined();
expect(body.updatedAt).not.toBeUndefined();
});
it('test cloud function error handling', done => {
+237 -10
View File
@@ -654,7 +654,236 @@ describe('Parse.File testing', () => {
});
});
xdescribe('Gridstore Range tests', () => {
describe('getting files', () => {
it('does not crash on file request with invalid app ID', async () => {
const res1 = await request({
url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt',
}).catch(e => e);
expect(res1.status).toBe(403);
expect(res1.data).toEqual({ code: 119, error: 'Invalid application ID.' });
// Ensure server did not crash
const res2 = await request({ url: 'http://localhost:8378/1/health' });
expect(res2.status).toEqual(200);
expect(res2.data).toEqual({ status: 'ok' });
});
it('does not crash on file request with invalid path', async () => {
const res1 = await request({
url: 'http://localhost:8378/1/files/invalid-id//invalid-path/%20/invalid-file.txt',
}).catch(e => e);
expect(res1.status).toBe(403);
expect(res1.data).toEqual({ error: 'unauthorized' });
// Ensure server did not crash
const res2 = await request({ url: 'http://localhost:8378/1/health' });
expect(res2.status).toEqual(200);
expect(res2.data).toEqual({ status: 'ok' });
});
it('does not crash on file metadata request with invalid app ID', async () => {
const res1 = await request({
url: `http://localhost:8378/1/files/invalid-id/metadata/invalid-file.txt`,
});
expect(res1.status).toBe(200);
expect(res1.data).toEqual({});
// Ensure server did not crash
const res2 = await request({ url: 'http://localhost:8378/1/health' });
expect(res2.status).toEqual(200);
expect(res2.data).toEqual({ status: 'ok' });
});
});
describe_only_db('mongo')('Gridstore Range', () => {
it('supports bytes range out of range', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=15000-18000',
},
});
expect(file.headers['content-range']).toBe('bytes 1212-1212/1212');
});
it('supports bytes range if end greater than start', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=15000-100',
},
});
expect(file.headers['content-range']).toBe('bytes 100-1212/1212');
});
it('supports bytes range if end is undefined', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=100-',
},
});
expect(file.headers['content-range']).toBe('bytes 100-1212/1212');
});
it('supports bytes range if start and end undefined', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=abc-efs',
},
}).catch(e => e);
expect(file.headers['content-range']).toBeUndefined();
});
it('supports bytes range if start and end undefined', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
},
}).catch(e => e);
expect(file.headers['content-range']).toBeUndefined();
});
it('supports bytes range if end is greater than size', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=0-2000',
},
}).catch(e => e);
expect(file.headers['content-range']).toBe('bytes 0-1212/1212');
});
it('supports bytes range if end is greater than size', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=0-2000',
},
}).catch(e => e);
expect(file.headers['content-range']).toBe('bytes 0-1212/1212');
});
it('supports bytes range with 0 length', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: 'a',
}).catch(e => e);
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=-2000',
},
}).catch(e => e);
expect(file.headers['content-range']).toBe('bytes 0-1/1');
});
it('supports range requests', done => {
const headers = {
'Content-Type': 'application/octet-stream',
@@ -743,7 +972,7 @@ describe('Parse.File testing', () => {
});
});
xit('supports getting last n bytes', done => {
it('supports getting last n bytes', done => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
@@ -841,8 +1070,8 @@ describe('Parse.File testing', () => {
});
});
it('fails to stream unknown file', done => {
request({
it('fails to stream unknown file', async () => {
const response = await request({
url: 'http://localhost:8378/1/files/test/file.txt',
headers: {
'Content-Type': 'application/octet-stream',
@@ -850,12 +1079,10 @@ describe('Parse.File testing', () => {
'X-Parse-REST-API-Key': 'rest',
Range: 'bytes=13-240',
},
}).then(response => {
expect(response.status).toBe(404);
const body = response.text;
expect(body).toEqual('File not found.');
done();
});
}).catch(e => e);
expect(response.status).toBe(404);
const body = response.text;
expect(body).toEqual('File not found.');
});
});
-4
View File
@@ -57,7 +57,6 @@ describe('ParseGraphQLSchema', () => {
it('should load a brand new GraphQL Schema if Parse Schema changes', async () => {
await parseGraphQLSchema.load();
const parseClasses = parseGraphQLSchema.parseClasses;
const parseCachedClasses = parseGraphQLSchema.parseCachedClasses;
const parseClassTypes = parseGraphQLSchema.parseClassTypes;
const graphQLSchema = parseGraphQLSchema.graphQLSchema;
const graphQLTypes = parseGraphQLSchema.graphQLTypes;
@@ -70,7 +69,6 @@ describe('ParseGraphQLSchema', () => {
await new Promise(resolve => setTimeout(resolve, 200));
await parseGraphQLSchema.load();
expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses);
expect(parseCachedClasses).not.toBe(parseGraphQLSchema.parseCachedClasses);
expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes);
expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema);
expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes);
@@ -94,7 +92,6 @@ describe('ParseGraphQLSchema', () => {
});
await parseGraphQLSchema.load();
const parseClasses = parseGraphQLSchema.parseClasses;
const parseCachedClasses = parseGraphQLSchema.parseCachedClasses;
const parseClassTypes = parseGraphQLSchema.parseClassTypes;
const graphQLSchema = parseGraphQLSchema.graphQLSchema;
const graphQLTypes = parseGraphQLSchema.graphQLTypes;
@@ -109,7 +106,6 @@ describe('ParseGraphQLSchema', () => {
await new Promise(resolve => setTimeout(resolve, 200));
await parseGraphQLSchema.load();
expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses);
expect(parseCachedClasses).not.toBe(parseGraphQLSchema.parseCachedClasses);
expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes);
expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema);
expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes);
+352 -124
View File
@@ -1,7 +1,7 @@
const http = require('http');
const express = require('express');
const req = require('../lib/request');
const fetch = require('node-fetch');
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
const FormData = require('form-data');
const ws = require('ws');
require('./helper');
@@ -9,13 +9,17 @@ const { updateCLP } = require('./support/dev');
const pluralize = require('pluralize');
const { getMainDefinition } = require('apollo-utilities');
const { ApolloLink, split } = require('apollo-link');
const { createHttpLink } = require('apollo-link-http');
const { InMemoryCache } = require('apollo-cache-inmemory');
const { createUploadLink } = require('apollo-upload-client');
const { SubscriptionClient } = require('subscriptions-transport-ws');
const { WebSocketLink } = require('apollo-link-ws');
const ApolloClient = require('apollo-client').default;
const { WebSocketLink } = require('@apollo/client/link/ws');
const { mergeSchemas } = require('@graphql-tools/schema');
const {
ApolloClient,
InMemoryCache,
ApolloLink,
split,
createHttpLink,
} = require('@apollo/client/core');
const gql = require('graphql-tag');
const { toGlobalId } = require('graphql-relay');
const {
@@ -29,7 +33,7 @@ const {
} = require('graphql');
const { ParseServer } = require('../');
const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer');
const ReadPreference = require('mongodb').ReadPreference;
const { ReadPreference, Collection } = require('mongodb');
const { v4: uuidv4 } = require('uuid');
function handleError(e) {
@@ -95,6 +99,24 @@ describe('ParseGraphQLServer', () => {
});
});
describe('_getServer', () => {
it('should only return new server on schema changes', async () => {
parseGraphQLServer.server = undefined;
const server1 = await parseGraphQLServer._getServer();
const server2 = await parseGraphQLServer._getServer();
expect(server1).toBe(server2);
// Trigger a schema change
const obj = new Parse.Object('SomeClass');
await obj.save();
const server3 = await parseGraphQLServer._getServer();
const server4 = await parseGraphQLServer._getServer();
expect(server3).not.toBe(server2);
expect(server3).toBe(server4);
});
});
describe('_getGraphQLOptions', () => {
const req = {
info: new Object(),
@@ -103,11 +125,15 @@ describe('ParseGraphQLServer', () => {
};
it("should return schema and context with req's info, config and auth", async () => {
const options = await parseGraphQLServer._getGraphQLOptions(req);
const options = await parseGraphQLServer._getGraphQLOptions();
expect(options.multipart).toEqual({
fileSize: 20971520,
});
expect(options.schema).toEqual(parseGraphQLServer.parseGraphQLSchema.graphQLSchema);
expect(options.context.info).toEqual(req.info);
expect(options.context.config).toEqual(req.config);
expect(options.context.auth).toEqual(req.auth);
const contextResponse = options.context({ req });
expect(contextResponse.info).toEqual(req.info);
expect(contextResponse.config).toEqual(req.config);
expect(contextResponse.auth).toEqual(req.auth);
});
it('should load GraphQL schema in every call', async () => {
@@ -464,7 +490,7 @@ describe('ParseGraphQLServer', () => {
}
});
it('should be cors enabled', async () => {
it('should be cors enabled and scope the response within the source origin', async () => {
let checked = false;
const apolloClient = new ApolloClient({
link: new ApolloLink((operation, forward) => {
@@ -473,7 +499,7 @@ describe('ParseGraphQLServer', () => {
const {
response: { headers },
} = context;
expect(headers.get('access-control-allow-origin')).toEqual('*');
expect(headers.get('access-control-allow-origin')).toEqual('http://example.com');
checked = true;
return response;
});
@@ -483,7 +509,7 @@ describe('ParseGraphQLServer', () => {
fetch,
headers: {
...headers,
Origin: 'http://someorigin.com',
Origin: 'http://example.com',
},
})
),
@@ -501,14 +527,25 @@ describe('ParseGraphQLServer', () => {
});
it('should handle Parse headers', async () => {
let checked = false;
const test = {
context: ({ req: { info, config, auth } }) => {
expect(req.info).toBeDefined();
expect(req.config).toBeDefined();
expect(req.auth).toBeDefined();
return {
info,
config,
auth,
};
},
};
const contextSpy = spyOn(test, 'context');
const originalGetGraphQLOptions = parseGraphQLServer._getGraphQLOptions;
parseGraphQLServer._getGraphQLOptions = async req => {
expect(req.info).toBeDefined();
expect(req.config).toBeDefined();
expect(req.auth).toBeDefined();
checked = true;
return await originalGetGraphQLOptions.bind(parseGraphQLServer)(req);
parseGraphQLServer._getGraphQLOptions = async () => {
return {
schema: await parseGraphQLServer.parseGraphQLSchema.load(),
context: test.context,
};
};
const health = (
await apolloClient.query({
@@ -520,7 +557,7 @@ describe('ParseGraphQLServer', () => {
})
).data.health;
expect(health).toBeTruthy();
expect(checked).toBeTruthy();
expect(contextSpy).toHaveBeenCalledTimes(1);
parseGraphQLServer._getGraphQLOptions = originalGetGraphQLOptions;
});
});
@@ -2597,11 +2634,23 @@ describe('ParseGraphQLServer', () => {
// "SecondaryObject:bBRgmzIRRM" < "SecondaryObject:nTMcuVbATY" true
// base64("SecondaryObject:bBRgmzIRRM"") < base64(""SecondaryObject:nTMcuVbATY"") false
// "U2Vjb25kYXJ5T2JqZWN0OmJCUmdteklSUk0=" < "U2Vjb25kYXJ5T2JqZWN0Om5UTWN1VmJBVFk=" false
const originalIds = [
getSecondaryObjectsResult.data.secondaryObject2.objectId,
getSecondaryObjectsResult.data.secondaryObject4.objectId,
];
expect(
findSecondaryObjectsResult.data.secondaryObjects.edges[0].node.objectId
).toBeLessThan(
findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId
);
).not.toBe(findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId);
expect(
originalIds.includes(
findSecondaryObjectsResult.data.secondaryObjects.edges[0].node.objectId
)
).toBeTrue();
expect(
originalIds.includes(
findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId
)
).toBeTrue();
const createPrimaryObjectResult = await apolloClient.mutate({
mutation: gql`
@@ -4458,8 +4507,7 @@ describe('ParseGraphQLServer', () => {
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
await apolloClient.query({
query: gql`
@@ -4483,13 +4531,13 @@ describe('ParseGraphQLServer', () => {
let foundGraphQLClassReadPreference = false;
let foundUserClassReadPreference = false;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
foundGraphQLClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY);
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY);
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
foundUserClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY);
expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY);
}
});
@@ -4505,8 +4553,7 @@ describe('ParseGraphQLServer', () => {
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
await apolloClient.query({
query: gql`
@@ -4530,13 +4577,13 @@ describe('ParseGraphQLServer', () => {
let foundGraphQLClassReadPreference = false;
let foundUserClassReadPreference = false;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
foundGraphQLClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
foundUserClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
}
});
@@ -4549,8 +4596,7 @@ describe('ParseGraphQLServer', () => {
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
await apolloClient.query({
query: gql`
@@ -4577,13 +4623,13 @@ describe('ParseGraphQLServer', () => {
let foundGraphQLClassReadPreference = false;
let foundUserClassReadPreference = false;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
foundGraphQLClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
foundUserClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.NEAREST);
expect(call.args[1].readPreference).toBe(ReadPreference.NEAREST);
}
});
@@ -5441,8 +5487,7 @@ describe('ParseGraphQLServer', () => {
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
await apolloClient.query({
query: gql`
@@ -5467,13 +5512,13 @@ describe('ParseGraphQLServer', () => {
let foundGraphQLClassReadPreference = false;
let foundUserClassReadPreference = false;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
foundGraphQLClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY);
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY);
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
foundUserClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY);
expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY);
}
});
@@ -5486,8 +5531,7 @@ describe('ParseGraphQLServer', () => {
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
await apolloClient.query({
query: gql`
@@ -5512,13 +5556,13 @@ describe('ParseGraphQLServer', () => {
let foundGraphQLClassReadPreference = false;
let foundUserClassReadPreference = false;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
foundGraphQLClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
foundUserClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
}
});
@@ -5531,8 +5575,7 @@ describe('ParseGraphQLServer', () => {
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
await apolloClient.query({
query: gql`
@@ -5559,13 +5602,13 @@ describe('ParseGraphQLServer', () => {
let foundGraphQLClassReadPreference = false;
let foundUserClassReadPreference = false;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
foundGraphQLClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
foundUserClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.NEAREST);
expect(call.args[1].readPreference).toBe(ReadPreference.NEAREST);
}
});
@@ -5579,8 +5622,7 @@ describe('ParseGraphQLServer', () => {
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const databaseAdapter = parseServer.config.databaseController.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
await apolloClient.query({
query: gql`
@@ -5617,13 +5659,13 @@ describe('ParseGraphQLServer', () => {
let foundGraphQLClassReadPreference = false;
let foundUserClassReadPreference = false;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) {
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) {
foundGraphQLClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY);
} else if (call.args[0].ns.collection.indexOf('_User') >= 0) {
expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY);
} else if (call.object.s.namespace.collection.indexOf('_User') >= 0) {
foundUserClassReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.NEAREST);
expect(call.args[1].readPreference).toBe(ReadPreference.NEAREST);
}
});
@@ -6778,14 +6820,14 @@ describe('ParseGraphQLServer', () => {
expect(queryResult.data.customers.edges.length).toEqual(1);
} catch (e) {
console.log(JSON.stringify(e));
console.error(JSON.stringify(e));
}
});
});
describe('Files Mutations', () => {
describe('Create', () => {
it('should return File object', async () => {
it_only_node_version('<17')('should return File object', async () => {
const clientMutationId = uuidv4();
parseServer = await global.reconfigureServer({
@@ -9088,7 +9130,7 @@ describe('ParseGraphQLServer', () => {
expect(result6[0].node.name).toEqual('imACountry3');
});
it('should support files', async () => {
it_only_node_version('<17')('should support files', async () => {
try {
parseServer = await global.reconfigureServer({
publicServerURL: 'http://localhost:13377/parse',
@@ -9099,15 +9141,15 @@ describe('ParseGraphQLServer', () => {
'operations',
JSON.stringify({
query: `
mutation CreateFile($input: CreateFileInput!) {
createFile(input: $input) {
fileInfo {
name
url
mutation CreateFile($input: CreateFileInput!) {
createFile(input: $input) {
fileInfo {
name
url
}
}
}
}
`,
`,
variables: {
input: {
upload: null,
@@ -9168,46 +9210,46 @@ describe('ParseGraphQLServer', () => {
'operations',
JSON.stringify({
query: `
mutation CreateSomeObject(
$fields1: CreateSomeClassFieldsInput
$fields2: CreateSomeClassFieldsInput
$fields3: CreateSomeClassFieldsInput
) {
createSomeClass1: createSomeClass(
input: { fields: $fields1 }
mutation CreateSomeObject(
$fields1: CreateSomeClassFieldsInput
$fields2: CreateSomeClassFieldsInput
$fields3: CreateSomeClassFieldsInput
) {
someClass {
id
someField {
name
url
createSomeClass1: createSomeClass(
input: { fields: $fields1 }
) {
someClass {
id
someField {
name
url
}
}
}
createSomeClass2: createSomeClass(
input: { fields: $fields2 }
) {
someClass {
id
someField {
name
url
}
}
}
createSomeClass3: createSomeClass(
input: { fields: $fields3 }
) {
someClass {
id
someField {
name
url
}
}
}
}
createSomeClass2: createSomeClass(
input: { fields: $fields2 }
) {
someClass {
id
someField {
name
url
}
}
}
createSomeClass3: createSomeClass(
input: { fields: $fields3 }
) {
someClass {
id
someField {
name
url
}
}
}
}
`,
`,
variables: {
fields1: {
someField: { file: someFieldValue },
@@ -9336,6 +9378,51 @@ describe('ParseGraphQLServer', () => {
}
});
it_only_node_version('<17')('should not upload if file is too large', async () => {
parseGraphQLServer.parseServer.config.maxUploadSize = '1kb';
const body = new FormData();
body.append(
'operations',
JSON.stringify({
query: `
mutation CreateFile($input: CreateFileInput!) {
createFile(input: $input) {
fileInfo {
name
url
}
}
}
`,
variables: {
input: {
upload: null,
},
},
})
);
body.append('map', JSON.stringify({ 1: ['variables.input.upload'] }));
body.append(
'1',
Buffer.alloc(parseGraphQLServer._transformMaxUploadSizeToBytes('2kb'), 1),
{
filename: 'myFileName.txt',
contentType: 'text/plain',
}
);
const res = await fetch('http://localhost:13377/graphql', {
method: 'POST',
headers,
body,
});
const result = JSON.parse(await res.text());
expect(res.status).toEqual(500);
expect(result.errors[0].message).toEqual('File size limit exceeded: 1024 bytes');
});
it('should support object values', async () => {
try {
const someObjectFieldValue = {
@@ -9429,6 +9516,130 @@ describe('ParseGraphQLServer', () => {
}
});
it('should support where argument on object field that contains false boolean value or 0 number value', async () => {
try {
const someObjectFieldValue1 = {
foo: { bar: true, baz: 100 },
};
const someObjectFieldValue2 = {
foo: { bar: false, baz: 0 },
};
const object1 = new Parse.Object('SomeClass');
await object1.save({
someObjectField: someObjectFieldValue1,
});
const object2 = new Parse.Object('SomeClass');
await object2.save({
someObjectField: someObjectFieldValue2,
});
const whereToObject1 = {
someObjectField: {
equalTo: { key: 'foo.bar', value: true },
notEqualTo: { key: 'foo.baz', value: 0 },
},
};
const whereToObject2 = {
someObjectField: {
notEqualTo: { key: 'foo.bar', value: true },
equalTo: { key: 'foo.baz', value: 0 },
},
};
const whereToAll = {
someObjectField: {
lessThan: { key: 'foo.baz', value: 101 },
},
};
const whereToNone = {
someObjectField: {
notEqualTo: { key: 'foo.bar', value: true },
equalTo: { key: 'foo.baz', value: 1 },
},
};
const queryResult = await apolloClient.query({
query: gql`
query GetSomeObject(
$id1: ID!
$id2: ID!
$whereToObject1: SomeClassWhereInput
$whereToObject2: SomeClassWhereInput
$whereToAll: SomeClassWhereInput
$whereToNone: SomeClassWhereInput
) {
obj1: someClass(id: $id1) {
id
someObjectField
}
obj2: someClass(id: $id2) {
id
someObjectField
}
onlyObj1: someClasses(where: $whereToObject1) {
edges {
node {
id
someObjectField
}
}
}
onlyObj2: someClasses(where: $whereToObject2) {
edges {
node {
id
someObjectField
}
}
}
all: someClasses(where: $whereToAll) {
edges {
node {
id
someObjectField
}
}
}
none: someClasses(where: $whereToNone) {
edges {
node {
id
someObjectField
}
}
}
}
`,
variables: {
id1: object1.id,
id2: object2.id,
whereToObject1,
whereToObject2,
whereToAll,
whereToNone,
},
});
const { obj1, obj2, onlyObj1, onlyObj2, all, none } = queryResult.data;
expect(obj1.someObjectField).toEqual(someObjectFieldValue1);
expect(obj2.someObjectField).toEqual(someObjectFieldValue2);
// Checks class query results
expect(onlyObj1.edges.length).toEqual(1);
expect(onlyObj1.edges[0].node.someObjectField).toEqual(someObjectFieldValue1);
expect(onlyObj2.edges.length).toEqual(1);
expect(onlyObj2.edges[0].node.someObjectField).toEqual(someObjectFieldValue2);
expect(all.edges.length).toEqual(2);
expect(none.edges.length).toEqual(0);
} catch (e) {
handleError(e);
}
});
it('should support object composed queries', async () => {
try {
const someObjectFieldValue1 = {
@@ -10373,7 +10584,7 @@ describe('ParseGraphQLServer', () => {
});
describe('Custom API', () => {
describe('GraphQL Schema Based', () => {
describe('SDL based', () => {
let httpServer;
const headers = {
'X-Parse-Application-Id': 'test',
@@ -10496,7 +10707,7 @@ describe('ParseGraphQLServer', () => {
});
});
describe('SDL Based', () => {
describe('GraphQL Schema Based', () => {
let httpServer;
const headers = {
'X-Parse-Application-Id': 'test',
@@ -10559,6 +10770,12 @@ describe('ParseGraphQLServer', () => {
},
resolve: (p, { message }) => message,
},
errorQuery: {
type: new GraphQLNonNull(GraphQLString),
resolve: () => {
throw new Error('A test error');
},
},
customQueryWithAutoTypeReturn: {
type: SomeClassType,
args: {
@@ -10647,6 +10864,18 @@ describe('ParseGraphQLServer', () => {
expect(result.data.customQuery).toEqual('hello');
});
it('can forward original error of a custom query', async () => {
await expectAsync(
apolloClient.query({
query: gql`
query ErrorQuery {
errorQuery
}
`,
})
).toBeRejectedWithError('A test error');
});
it('can resolve a custom query with auto type return', async () => {
const obj = new Parse.Object('SomeClass');
await obj.save({ name: 'aname', type: 'robot' });
@@ -10793,8 +11022,7 @@ describe('ParseGraphQLServer', () => {
httpServer = http.createServer(expressApp);
parseGraphQLServer = new ParseGraphQLServer(parseServer, {
graphQLPath: '/graphql',
graphQLCustomTypeDefs: ({ autoSchema, stitchSchemas }) =>
stitchSchemas({ subschemas: [autoSchema] }),
graphQLCustomTypeDefs: ({ autoSchema }) => mergeSchemas({ schemas: [autoSchema] }),
});
parseGraphQLServer.applyGraphQL(expressApp);
+50
View File
@@ -1239,6 +1239,56 @@ describe('Installations', () => {
});
});
it('can use push with beforeSave', async () => {
const input = {
deviceToken: '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306',
deviceType: 'ios',
};
await rest.create(config, auth.nobody(config), '_Installation', input);
const functions = {
beforeSave() {},
afterSave() {},
};
spyOn(functions, 'beforeSave').and.callThrough();
spyOn(functions, 'afterSave').and.callThrough();
Parse.Cloud.beforeSave(Parse.Installation, functions.beforeSave);
Parse.Cloud.afterSave(Parse.Installation, functions.afterSave);
await Parse.Push.send({
where: {
deviceType: 'ios',
},
data: {
badge: 'increment',
alert: 'Hello world!',
},
});
await Parse.Push.send({
where: {
deviceType: 'ios',
},
data: {
badge: 'increment',
alert: 'Hello world!',
},
});
await Parse.Push.send({
where: {
deviceType: 'ios',
},
data: {
badge: 'increment',
alert: 'Hello world!',
},
});
await new Promise(resolve => setTimeout(resolve, 1000));
const installation = await new Parse.Query(Parse.Installation).first({ useMasterKey: true });
expect(installation.get('badge')).toEqual(3);
expect(functions.beforeSave).not.toHaveBeenCalled();
expect(functions.afterSave).not.toHaveBeenCalled();
});
// TODO: Look at additional tests from installation_collection_test.go:882
// TODO: Do we need to support _tombstone disabling of installations?
// TODO: Test deletion, badge increments
+214
View File
@@ -319,6 +319,41 @@ describe('ParseLiveQuery', function () {
await object.save();
});
it('can log on afterLiveQueryEvent throw', async () => {
await reconfigureServer({
liveQuery: {
classNames: ['TestObject'],
},
startLiveQueryServer: true,
verbose: false,
silent: true,
});
const object = new TestObject();
await object.save();
const logger = require('../lib/logger').logger;
spyOn(logger, 'error').and.callFake(() => {});
let session = undefined;
Parse.Cloud.afterLiveQueryEvent('TestObject', ({ sessionToken }) => {
session = sessionToken;
/* eslint-disable no-undef */
foo.bar();
/* eslint-enable no-undef */
});
const query = new Parse.Query(TestObject);
query.equalTo('objectId', object.id);
const subscription = await query.subscribe();
object.set({ foo: 'bar' });
await object.save();
await new Promise(resolve => subscription.on('error', resolve));
expect(logger.error).toHaveBeenCalledWith(
`Failed running afterLiveQueryEvent on class TestObject for event update with session ${session} with:\n Error: {"message":"foo is not defined","code":141}`
);
});
it('can handle afterEvent sendEvent to false', async done => {
await reconfigureServer({
liveQuery: {
@@ -566,6 +601,33 @@ describe('ParseLiveQuery', function () {
await query.subscribe();
});
it('can log on beforeConnect throw', async () => {
await reconfigureServer({
liveQuery: {
classNames: ['TestObject'],
},
startLiveQueryServer: true,
verbose: false,
silent: true,
});
const logger = require('../lib/logger').logger;
spyOn(logger, 'error').and.callFake(() => {});
let token = undefined;
Parse.Cloud.beforeConnect(({ sessionToken }) => {
token = sessionToken;
/* eslint-disable no-undef */
foo.bar();
/* eslint-enable no-undef */
});
new Parse.Query(TestObject).subscribe();
await new Promise(resolve => Parse.LiveQuery.on('error', resolve));
Parse.LiveQuery.removeAllListeners('error');
expect(logger.error).toHaveBeenCalledWith(
`Failed running beforeConnect for session ${token} with:\n Error: {"message":"foo is not defined","code":141}`
);
});
it('can handle beforeSubscribe error', async done => {
await reconfigureServer({
liveQuery: {
@@ -594,6 +656,34 @@ describe('ParseLiveQuery', function () {
});
});
it('can log on beforeSubscribe error', async () => {
await reconfigureServer({
liveQuery: {
classNames: ['TestObject'],
},
startLiveQueryServer: true,
verbose: false,
silent: true,
});
const logger = require('../lib/logger').logger;
spyOn(logger, 'error').and.callFake(() => {});
Parse.Cloud.beforeSubscribe(TestObject, () => {
/* eslint-disable no-undef */
foo.bar();
/* eslint-enable no-undef */
});
const query = new Parse.Query(TestObject);
const subscription = await query.subscribe();
await new Promise(resolve => subscription.on('error', resolve));
expect(logger.error).toHaveBeenCalledWith(
`Failed running beforeSubscribe on TestObject for session undefined with:\n Error: {"message":"foo is not defined","code":141}`
);
});
it('can handle mutate beforeSubscribe query', async done => {
await reconfigureServer({
liveQuery: {
@@ -746,6 +836,50 @@ describe('ParseLiveQuery', function () {
}
});
it('LiveQuery should work with changing role', async () => {
await reconfigureServer({
liveQuery: {
classNames: ['Chat'],
},
startLiveQueryServer: true,
});
const user = new Parse.User();
user.setUsername('username');
user.setPassword('password');
await user.signUp();
const role = new Parse.Role('Test', new Parse.ACL(user));
await role.save();
const chatQuery = new Parse.Query('Chat');
const subscription = await chatQuery.subscribe();
subscription.on('create', () => {
fail('should not call create as user is not part of role.');
});
const object = new Parse.Object('Chat');
const acl = new Parse.ACL();
acl.setRoleReadAccess(role, true);
object.setACL(acl);
object.set({ foo: 'bar' });
await object.save(null, { useMasterKey: true });
role.getUsers().add(user);
await new Promise(resolve => setTimeout(resolve, 1000));
await role.save();
await new Promise(resolve => setTimeout(resolve, 1000));
object.set('foo', 'yolo');
await Promise.all([
new Promise(resolve => {
subscription.on('update', obj => {
expect(obj.get('foo')).toBe('yolo');
expect(obj.getACL().toJSON()).toEqual({ 'role:Test': { read: true } });
resolve();
});
}),
object.save(null, { useMasterKey: true }),
]);
});
it('liveQuery on Session class', async done => {
await reconfigureServer({
liveQuery: { classNames: [Parse.Session] },
@@ -976,6 +1110,86 @@ describe('ParseLiveQuery', function () {
}
});
it('should strip out protected fields', async () => {
await reconfigureServer({
liveQuery: { classNames: ['Test'] },
startLiveQueryServer: true,
});
const obj1 = new Parse.Object('Test');
obj1.set('foo', 'foo');
obj1.set('bar', 'bar');
obj1.set('qux', 'qux');
await obj1.save();
const config = Config.get(Parse.applicationId);
const schemaController = await config.database.loadSchema();
await schemaController.updateClass(
'Test',
{},
{
get: { '*': true },
find: { '*': true },
update: { '*': true },
protectedFields: {
'*': ['foo'],
},
}
);
const object = await obj1.fetch();
expect(object.get('foo')).toBe(undefined);
expect(object.get('bar')).toBeDefined();
expect(object.get('qux')).toBeDefined();
const subscription = await new Parse.Query('Test').subscribe();
await Promise.all([
new Promise(resolve => {
subscription.on('update', (obj, original) => {
expect(obj.get('foo')).toBe(undefined);
expect(obj.get('bar')).toBeDefined();
expect(obj.get('qux')).toBeDefined();
expect(original.get('foo')).toBe(undefined);
expect(original.get('bar')).toBeDefined();
expect(original.get('qux')).toBeDefined();
resolve();
});
}),
obj1.save({ foo: 'abc' }),
]);
});
it('can subscribe to query and return object with withinKilometers with last parameter on update', async done => {
await reconfigureServer({
liveQuery: {
classNames: ['TestObject'],
},
startLiveQueryServer: true,
verbose: false,
silent: true,
});
const object = new TestObject();
const firstPoint = new Parse.GeoPoint({ latitude: 40.0, longitude: -30.0 });
object.set({ location: firstPoint });
await object.save();
// unsorted will use $centerSphere operator
const sorted = false;
const query = new Parse.Query(TestObject);
query.withinKilometers(
'location',
new Parse.GeoPoint({ latitude: 40.0, longitude: -30.0 }),
2,
sorted
);
const subscription = await query.subscribe();
subscription.on('update', obj => {
expect(obj.id).toBe(object.id);
done();
});
const secondPoint = new Parse.GeoPoint({ latitude: 40.0, longitude: -30.0 });
object.set({ location: secondPoint });
await object.save();
});
afterEach(async function (done) {
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
client.close();
+17
View File
@@ -666,6 +666,23 @@ describe('Parse.Query Aggregate testing', () => {
});
});
it('should aggregate with Date object (directAccess)', async () => {
const rest = require('../lib/rest');
const auth = require('../lib/Auth');
const TestObject = Parse.Object.extend('TestObject');
const date = new Date();
await new TestObject({ date: date }).save(null, { useMasterKey: true });
const config = Config.get(Parse.applicationId);
const resp = await rest.find(
config,
auth.master(config),
'TestObject',
{},
{ pipeline: [{ $match: { date: { $lte: new Date() } } }] }
);
expect(resp.results.length).toBe(1);
});
it('match comparison query', done => {
const options = Object.assign({}, masterKeyOptions, {
body: {
+220 -6
View File
@@ -27,7 +27,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
await TestUtils.destroyAllDataPermanently(false);
});
it('query find with hint string', async () => {
it_only_mongodb_version('<5.1>=6')('query find with hint string', async () => {
const object = new TestObject();
await object.save();
@@ -39,7 +39,18 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(explain.queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
});
it('query find with hint object', async () => {
it_only_mongodb_version('>=5.1<6')('query find with hint string', async () => {
const object = new TestObject();
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
const explain = await collection._rawFind({ _id: object.id }, { hint: '_id_', explain: true });
expect(explain.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('IXSCAN');
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('<5.1>=6')('query find with hint object', async () => {
const object = new TestObject();
await object.save();
@@ -53,6 +64,20 @@ describe_only_db('mongo')('Parse.Query hint', () => {
});
});
it_only_mongodb_version('>=5.1<6')('query find with hint object', async () => {
const object = new TestObject();
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
const explain = await collection._rawFind(
{ _id: object.id },
{ hint: { _id: 1 }, explain: true }
);
expect(explain.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('IXSCAN');
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('<4.4')('query aggregate with hint string', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
@@ -73,7 +98,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('>=4.4')('query aggregate with hint string', async () => {
it_only_mongodb_version('>=4.4<5.1')('query aggregate with hint string', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
@@ -97,6 +122,54 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('>=5.1<5.2')('query aggregate with hint string', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
explain: true,
});
let { queryPlanner } = result[0].stages[0].$cursor;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
hint: '_id_',
explain: true,
});
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('>=5.2')('query aggregate with hint string', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
explain: true,
});
let queryPlanner = result[0].queryPlanner;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('GROUP');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
hint: '_id_',
explain: true,
});
queryPlanner = result[0].queryPlanner;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('GROUP');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('<4.4')('query aggregate with hint object', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
@@ -117,7 +190,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('>=4.4')('query aggregate with hint object', async () => {
it_only_mongodb_version('>=4.4<5.1')('query aggregate with hint object', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
@@ -142,7 +215,57 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(queryPlanner.winningPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
});
it('query find with hint (rest)', async () => {
it_only_mongodb_version('>=5.1<5.2')('query aggregate with hint object', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
explain: true,
});
let { queryPlanner } = result[0].stages[0].$cursor;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
hint: { _id: 1 },
explain: true,
});
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('>=5.2')('query aggregate with hint object', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
explain: true,
});
let queryPlanner = result[0].queryPlanner;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('GROUP');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
hint: { _id: 1 },
explain: true,
});
queryPlanner = result[0].queryPlanner;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('GROUP');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('<5.1>=6')('query find with hint (rest)', async () => {
const object = new TestObject();
await object.save();
let options = Object.assign({}, masterKeyOptions, {
@@ -167,6 +290,31 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(explain.queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('>=5.1<6')('query find with hint (rest)', async () => {
const object = new TestObject();
await object.save();
let options = Object.assign({}, masterKeyOptions, {
url: Parse.serverURL + '/classes/TestObject',
qs: {
explain: true,
},
});
let response = await request(options);
let explain = response.data.results;
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
options = Object.assign({}, masterKeyOptions, {
url: Parse.serverURL + '/classes/TestObject',
qs: {
explain: true,
hint: '_id_',
},
});
response = await request(options);
explain = response.data.results;
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('<4.4')('query aggregate with hint (rest)', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
@@ -194,7 +342,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('>=4.4')('query aggregate with hint (rest)', async () => {
it_only_mongodb_version('>=4.4<5.1')('query aggregate with hint (rest)', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
let options = Object.assign({}, masterKeyOptions, {
@@ -226,4 +374,70 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
expect(queryPlanner.winningPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('>=5.1<5.2')('query aggregate with hint (rest)', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
let options = Object.assign({}, masterKeyOptions, {
url: Parse.serverURL + '/aggregate/TestObject',
qs: {
explain: true,
group: JSON.stringify({ objectId: '$foo' }),
},
});
let response = await request(options);
let { queryPlanner } = response.data.results[0].stages[0].$cursor;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
options = Object.assign({}, masterKeyOptions, {
url: Parse.serverURL + '/aggregate/TestObject',
qs: {
explain: true,
hint: '_id_',
group: JSON.stringify({ objectId: '$foo' }),
},
});
response = await request(options);
queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('>=5.2')('query aggregate with hint (rest)', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
let options = Object.assign({}, masterKeyOptions, {
url: Parse.serverURL + '/aggregate/TestObject',
qs: {
explain: true,
group: JSON.stringify({ objectId: '$foo' }),
},
});
let response = await request(options);
let queryPlanner = response.data.results[0].queryPlanner;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('GROUP');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
options = Object.assign({}, masterKeyOptions, {
url: Parse.serverURL + '/aggregate/TestObject',
qs: {
explain: true,
hint: '_id_',
group: JSON.stringify({ objectId: '$foo' }),
},
});
response = await request(options);
queryPlanner = response.data.results[0].queryPlanner;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('GROUP');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
});
});
+102 -93
View File
@@ -314,39 +314,57 @@ describe('Parse.Query testing', () => {
equal(results.length, 0);
});
it('query with limit', function (done) {
const baz = new TestObject({ foo: 'baz' });
const qux = new TestObject({ foo: 'qux' });
Parse.Object.saveAll([baz, qux]).then(function () {
const query = new Parse.Query(TestObject);
query.limit(1);
query.find().then(function (results) {
equal(results.length, 1);
done();
});
});
it('query without limit respects default limit', async () => {
await reconfigureServer({ defaultLimit: 1 });
const obj1 = new TestObject({ foo: 'baz' });
const obj2 = new TestObject({ foo: 'qux' });
await Parse.Object.saveAll([obj1, obj2]);
const query = new Parse.Query(TestObject);
const result = await query.find();
expect(result.length).toBe(1);
});
it('query with limit', async () => {
const obj1 = new TestObject({ foo: 'baz' });
const obj2 = new TestObject({ foo: 'qux' });
await Parse.Object.saveAll([obj1, obj2]);
const query = new Parse.Query(TestObject);
query.limit(1);
const result = await query.find();
expect(result.length).toBe(1);
});
it('query with limit overrides default limit', async () => {
await reconfigureServer({ defaultLimit: 2 });
const obj1 = new TestObject({ foo: 'baz' });
const obj2 = new TestObject({ foo: 'qux' });
await Parse.Object.saveAll([obj1, obj2]);
const query = new Parse.Query(TestObject);
query.limit(1);
const result = await query.find();
expect(result.length).toBe(1);
});
it('query with limit equal to maxlimit', async () => {
const baz = new TestObject({ foo: 'baz' });
const qux = new TestObject({ foo: 'qux' });
await reconfigureServer({ maxLimit: 1 });
await Parse.Object.saveAll([baz, qux]);
const obj1 = new TestObject({ foo: 'baz' });
const obj2 = new TestObject({ foo: 'qux' });
await Parse.Object.saveAll([obj1, obj2]);
const query = new Parse.Query(TestObject);
query.limit(1);
const results = await query.find();
equal(results.length, 1);
const result = await query.find();
expect(result.length).toBe(1);
});
it('query with limit exceeding maxlimit', async () => {
const baz = new TestObject({ foo: 'baz' });
const qux = new TestObject({ foo: 'qux' });
await reconfigureServer({ maxLimit: 1 });
await Parse.Object.saveAll([baz, qux]);
const obj1 = new TestObject({ foo: 'baz' });
const obj2 = new TestObject({ foo: 'qux' });
await Parse.Object.saveAll([obj1, obj2]);
const query = new Parse.Query(TestObject);
query.limit(2);
const results = await query.find();
equal(results.length, 1);
const result = await query.find();
expect(result.length).toBe(1);
});
it('containedIn object array queries', function (done) {
@@ -1715,6 +1733,16 @@ describe('Parse.Query testing', () => {
});
});
it('order by non-existing string', async () => {
const strings = ['a', 'b', 'c', 'd'];
const makeBoxedNumber = function (num, i) {
return new BoxedNumber({ number: num, string: strings[i] });
};
await Parse.Object.saveAll([3, 1, 3, 2].map(makeBoxedNumber));
const results = await new Parse.Query(BoxedNumber).ascending('foo').find();
expect(results.length).toBe(4);
});
it('order by descending number then ascending string', function (done) {
const strings = ['a', 'b', 'c', 'd'];
const makeBoxedNumber = function (num, i) {
@@ -4766,7 +4794,7 @@ describe('Parse.Query testing', () => {
.catch(done.fail);
});
it_only_db('mongo')('should handle relative times correctly', function (done) {
it('should handle relative times correctly', async () => {
const now = Date.now();
const obj1 = new Parse.Object('MyCustomObject', {
name: 'obj1',
@@ -4777,94 +4805,75 @@ describe('Parse.Query testing', () => {
ttl: new Date(now - 2 * 24 * 60 * 60 * 1000), // 2 days ago
});
Parse.Object.saveAll([obj1, obj2])
.then(() => {
const q = new Parse.Query('MyCustomObject');
q.greaterThan('ttl', { $relativeTime: 'in 1 day' });
return q.find({ useMasterKey: true });
})
.then(results => {
expect(results.length).toBe(1);
})
.then(() => {
const q = new Parse.Query('MyCustomObject');
q.greaterThan('ttl', { $relativeTime: '1 day ago' });
return q.find({ useMasterKey: true });
})
.then(results => {
expect(results.length).toBe(1);
})
.then(() => {
const q = new Parse.Query('MyCustomObject');
q.lessThan('ttl', { $relativeTime: '5 days ago' });
return q.find({ useMasterKey: true });
})
.then(results => {
expect(results.length).toBe(0);
})
.then(() => {
const q = new Parse.Query('MyCustomObject');
q.greaterThan('ttl', { $relativeTime: '3 days ago' });
return q.find({ useMasterKey: true });
})
.then(results => {
expect(results.length).toBe(2);
})
.then(() => {
const q = new Parse.Query('MyCustomObject');
q.greaterThan('ttl', { $relativeTime: 'now' });
return q.find({ useMasterKey: true });
})
.then(results => {
expect(results.length).toBe(1);
})
.then(() => {
const q = new Parse.Query('MyCustomObject');
q.greaterThan('ttl', { $relativeTime: 'now' });
q.lessThan('ttl', { $relativeTime: 'in 1 day' });
return q.find({ useMasterKey: true });
})
.then(results => {
expect(results.length).toBe(0);
})
.then(() => {
const q = new Parse.Query('MyCustomObject');
q.greaterThan('ttl', { $relativeTime: '1 year 3 weeks ago' });
return q.find({ useMasterKey: true });
})
.then(results => {
expect(results.length).toBe(2);
})
.then(done, done.fail);
await Parse.Object.saveAll([obj1, obj2]);
const q1 = new Parse.Query('MyCustomObject');
q1.greaterThan('ttl', { $relativeTime: 'in 1 day' });
const results1 = await q1.find({ useMasterKey: true });
expect(results1.length).toBe(1);
const q2 = new Parse.Query('MyCustomObject');
q2.greaterThan('ttl', { $relativeTime: '1 day ago' });
const results2 = await q2.find({ useMasterKey: true });
expect(results2.length).toBe(1);
const q3 = new Parse.Query('MyCustomObject');
q3.lessThan('ttl', { $relativeTime: '5 days ago' });
const results3 = await q3.find({ useMasterKey: true });
expect(results3.length).toBe(0);
const q4 = new Parse.Query('MyCustomObject');
q4.greaterThan('ttl', { $relativeTime: '3 days ago' });
const results4 = await q4.find({ useMasterKey: true });
expect(results4.length).toBe(2);
const q5 = new Parse.Query('MyCustomObject');
q5.greaterThan('ttl', { $relativeTime: 'now' });
const results5 = await q5.find({ useMasterKey: true });
expect(results5.length).toBe(1);
const q6 = new Parse.Query('MyCustomObject');
q6.greaterThan('ttl', { $relativeTime: 'now' });
q6.lessThan('ttl', { $relativeTime: 'in 1 day' });
const results6 = await q6.find({ useMasterKey: true });
expect(results6.length).toBe(0);
const q7 = new Parse.Query('MyCustomObject');
q7.greaterThan('ttl', { $relativeTime: '1 year 3 weeks ago' });
const results7 = await q7.find({ useMasterKey: true });
expect(results7.length).toBe(2);
});
it_only_db('mongo')('should error on invalid relative time', function (done) {
it('should error on invalid relative time', async () => {
const obj1 = new Parse.Object('MyCustomObject', {
name: 'obj1',
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
});
await obj1.save({ useMasterKey: true });
const q = new Parse.Query('MyCustomObject');
q.greaterThan('ttl', { $relativeTime: '-12 bananas ago' });
obj1
.save({ useMasterKey: true })
.then(() => q.find({ useMasterKey: true }))
.then(done.fail, () => done());
try {
await q.find({ useMasterKey: true });
fail('Should have thrown error');
} catch (error) {
expect(error.code).toBe(Parse.Error.INVALID_JSON);
}
});
it_only_db('mongo')('should error when using $relativeTime on non-Date field', function (done) {
it('should error when using $relativeTime on non-Date field', async () => {
const obj1 = new Parse.Object('MyCustomObject', {
name: 'obj1',
nonDateField: 'abcd',
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
});
await obj1.save({ useMasterKey: true });
const q = new Parse.Query('MyCustomObject');
q.greaterThan('nonDateField', { $relativeTime: '1 day ago' });
obj1
.save({ useMasterKey: true })
.then(() => q.find({ useMasterKey: true }))
.then(done.fail, () => done());
try {
await q.find({ useMasterKey: true });
fail('Should have thrown error');
} catch (error) {
expect(error.code).toBe(Parse.Error.INVALID_JSON);
}
});
it('should match complex structure with dot notation when using matchesKeyInQuery', function (done) {
+43
View File
@@ -410,6 +410,49 @@ describe('Parse.Relation testing', () => {
});
});
it('or queries with base constraint on relation field', async () => {
const ChildObject = Parse.Object.extend('ChildObject');
const childObjects = [];
for (let i = 0; i < 10; i++) {
childObjects.push(new ChildObject({ x: i }));
}
await Parse.Object.saveAll(childObjects);
const ParentObject = Parse.Object.extend('ParentObject');
const parent = new ParentObject();
parent.set('x', 4);
const relation = parent.relation('toChilds');
relation.add(childObjects[0]);
relation.add(childObjects[1]);
relation.add(childObjects[2]);
const parent2 = new ParentObject();
parent2.set('x', 3);
const relation2 = parent2.relation('toChilds');
relation2.add(childObjects[0]);
relation2.add(childObjects[1]);
relation2.add(childObjects[2]);
const parents = [];
parents.push(parent);
parents.push(parent2);
parents.push(new ParentObject());
await Parse.Object.saveAll(parents);
const query1 = new Parse.Query(ParentObject);
query1.equalTo('x', 4);
const query2 = new Parse.Query(ParentObject);
query2.equalTo('x', 3);
const query = Parse.Query.or(query1, query2);
query.equalTo('toChilds', childObjects[2]);
const list = await query.find();
const objectIds = list.map(item => item.id);
expect(objectIds.indexOf(parent.id)).not.toBe(-1);
expect(objectIds.indexOf(parent2.id)).not.toBe(-1);
equal(list.length, 2, 'There should be 2 results');
});
it('Get query on relation using un-fetched parent object', done => {
// Setup data model
const Wheel = Parse.Object.extend('Wheel');
+109 -215
View File
@@ -15,35 +15,18 @@ describe('ParseServerRESTController', () => {
);
});
it('should handle a get request', done => {
RESTController.request('GET', '/classes/MyObject').then(
res => {
expect(res.results.length).toBe(0);
done();
},
err => {
console.log(err);
jfail(err);
done();
}
);
it('should handle a get request', async () => {
const res = await RESTController.request('GET', '/classes/MyObject');
expect(res.results.length).toBe(0);
});
it('should handle a get request with full serverURL mount path', done => {
RESTController.request('GET', '/1/classes/MyObject').then(
res => {
expect(res.results.length).toBe(0);
done();
},
err => {
jfail(err);
done();
}
);
it('should handle a get request with full serverURL mount path', async () => {
const res = await RESTController.request('GET', '/1/classes/MyObject');
expect(res.results.length).toBe(0);
});
it('should handle a POST batch without transaction', done => {
RESTController.request('POST', 'batch', {
it('should handle a POST batch without transaction', async () => {
const res = await RESTController.request('POST', 'batch', {
requests: [
{
method: 'GET',
@@ -59,20 +42,12 @@ describe('ParseServerRESTController', () => {
path: '/classes/MyObject',
},
],
}).then(
res => {
expect(res.length).toBe(3);
done();
},
err => {
jfail(err);
done();
}
);
});
expect(res.length).toBe(3);
});
it('should handle a POST batch with transaction=false', done => {
RESTController.request('POST', 'batch', {
it('should handle a POST batch with transaction=false', async () => {
const res = await RESTController.request('POST', 'batch', {
requests: [
{
method: 'GET',
@@ -89,16 +64,8 @@ describe('ParseServerRESTController', () => {
},
],
transaction: false,
}).then(
res => {
expect(res.length).toBe(3);
done();
},
err => {
jfail(err);
done();
}
);
});
expect(res.length).toBe(3);
});
it('should handle response status', async () => {
@@ -186,54 +153,40 @@ describe('ParseServerRESTController', () => {
}
});
it('should handle a batch request with transaction = true', async done => {
await reconfigureServer();
it('should handle a batch request with transaction = true', async () => {
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
myObject
.save()
.then(() => {
return myObject.destroy();
})
.then(() => {
spyOn(databaseAdapter, 'createObject').and.callThrough();
return RESTController.request('POST', 'batch', {
requests: [
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value2' },
},
],
transaction: true,
}).then(response => {
expect(response.length).toEqual(2);
expect(response[0].success.objectId).toBeDefined();
expect(response[0].success.createdAt).toBeDefined();
expect(response[1].success.objectId).toBeDefined();
expect(response[1].success.createdAt).toBeDefined();
const query = new Parse.Query('MyObject');
return query.find().then(results => {
expect(databaseAdapter.createObject.calls.count() % 2).toBe(0);
for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) {
expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe(
databaseAdapter.createObject.calls.argsFor(i + 1)[3]
);
}
expect(results.map(result => result.get('key')).sort()).toEqual([
'value1',
'value2',
]);
done();
});
});
})
.catch(done.fail);
await myObject.save();
await myObject.destroy();
spyOn(databaseAdapter, 'createObject').and.callThrough();
const response = await RESTController.request('POST', 'batch', {
requests: [
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value2' },
},
],
transaction: true,
});
expect(response.length).toEqual(2);
expect(response[0].success.objectId).toBeDefined();
expect(response[0].success.createdAt).toBeDefined();
expect(response[1].success.objectId).toBeDefined();
expect(response[1].success.createdAt).toBeDefined();
const query = new Parse.Query('MyObject');
const results = await query.find();
expect(databaseAdapter.createObject.calls.count() % 2).toBe(0);
for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) {
expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe(
databaseAdapter.createObject.calls.argsFor(i + 1)[3]
);
}
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
});
it('should not save anything when one operation fails in a transaction', async () => {
@@ -560,21 +513,11 @@ describe('ParseServerRESTController', () => {
});
}
it('should handle a POST request', done => {
RESTController.request('POST', '/classes/MyObject', { key: 'value' })
.then(() => {
return RESTController.request('GET', '/classes/MyObject');
})
.then(res => {
expect(res.results.length).toBe(1);
expect(res.results[0].key).toEqual('value');
done();
})
.catch(err => {
console.log(err);
jfail(err);
done();
});
it('should handle a POST request', async () => {
await RESTController.request('POST', '/classes/MyObject', { key: 'value' });
const res = await RESTController.request('GET', '/classes/MyObject');
expect(res.results.length).toBe(1);
expect(res.results[0].key).toEqual('value');
});
it('should handle a POST request with context', async () => {
@@ -593,125 +536,76 @@ describe('ParseServerRESTController', () => {
);
});
it('ensures sessionTokens are properly handled', done => {
let userId;
Parse.User.signUp('user', 'pass')
.then(user => {
userId = user.id;
const sessionToken = user.getSessionToken();
return RESTController.request('GET', '/users/me', undefined, {
sessionToken,
});
})
.then(res => {
// Result is in JSON format
expect(res.objectId).toEqual(userId);
done();
})
.catch(err => {
console.log(err);
jfail(err);
done();
it('ensures sessionTokens are properly handled', async () => {
const user = await Parse.User.signUp('user', 'pass');
const sessionToken = user.getSessionToken();
const res = await RESTController.request('GET', '/users/me', undefined, {
sessionToken,
});
// Result is in JSON format
expect(res.objectId).toEqual(user.id);
});
it('ensures masterKey is properly handled', async () => {
const user = await Parse.User.signUp('user', 'pass');
const userId = user.id;
await Parse.User.logOut();
const res = await RESTController.request('GET', '/classes/_User', undefined, {
useMasterKey: true,
});
expect(res.results.length).toBe(1);
expect(res.results[0].objectId).toEqual(userId);
});
it('ensures no user is created when passing an empty username', async () => {
try {
await RESTController.request('POST', '/classes/_User', {
username: '',
password: 'world',
});
fail('Success callback should not be called when passing an empty username.');
} catch (err) {
expect(err.code).toBe(Parse.Error.USERNAME_MISSING);
expect(err.message).toBe('bad or missing username');
}
});
it('ensures masterKey is properly handled', done => {
let userId;
Parse.User.signUp('user', 'pass')
.then(user => {
userId = user.id;
return Parse.User.logOut().then(() => {
return RESTController.request('GET', '/classes/_User', undefined, {
useMasterKey: true,
});
});
})
.then(
res => {
expect(res.results.length).toBe(1);
expect(res.results[0].objectId).toEqual(userId);
done();
},
err => {
jfail(err);
done();
}
);
it('ensures no user is created when passing an empty password', async () => {
try {
await RESTController.request('POST', '/classes/_User', {
username: 'hello',
password: '',
});
fail('Success callback should not be called when passing an empty password.');
} catch (err) {
expect(err.code).toBe(Parse.Error.PASSWORD_MISSING);
expect(err.message).toBe('password is required');
}
});
it('ensures no user is created when passing an empty username', done => {
RESTController.request('POST', '/classes/_User', {
username: '',
password: 'world',
}).then(
() => {
jfail(new Error('Success callback should not be called when passing an empty username.'));
done();
},
err => {
expect(err.code).toBe(Parse.Error.USERNAME_MISSING);
expect(err.message).toBe('bad or missing username');
done();
}
);
});
it('ensures no user is created when passing an empty password', done => {
RESTController.request('POST', '/classes/_User', {
username: 'hello',
password: '',
}).then(
() => {
jfail(new Error('Success callback should not be called when passing an empty password.'));
done();
},
err => {
expect(err.code).toBe(Parse.Error.PASSWORD_MISSING);
expect(err.message).toBe('password is required');
done();
}
);
});
it('ensures no session token is created on creating users', done => {
RESTController.request('POST', '/classes/_User', {
it('ensures no session token is created on creating users', async () => {
const user = await RESTController.request('POST', '/classes/_User', {
username: 'hello',
password: 'world',
})
.then(user => {
expect(user.sessionToken).toBeUndefined();
const query = new Parse.Query('_Session');
return query.find({ useMasterKey: true });
})
.then(sessions => {
expect(sessions.length).toBe(0);
done();
}, done.fail);
});
expect(user.sessionToken).toBeUndefined();
const query = new Parse.Query('_Session');
const sessions = await query.find({ useMasterKey: true });
expect(sessions.length).toBe(0);
});
it('ensures a session token is created when passing installationId != cloud', done => {
RESTController.request(
it('ensures a session token is created when passing installationId != cloud', async () => {
const user = await RESTController.request(
'POST',
'/classes/_User',
{ username: 'hello', password: 'world' },
{ installationId: 'my-installation' }
)
.then(user => {
expect(user.sessionToken).not.toBeUndefined();
const query = new Parse.Query('_Session');
return query.find({ useMasterKey: true });
})
.then(
sessions => {
expect(sessions.length).toBe(1);
expect(sessions[0].get('installationId')).toBe('my-installation');
done();
},
err => {
jfail(err);
done();
}
);
);
expect(user.sessionToken).not.toBeUndefined();
const query = new Parse.Query('_Session');
const sessions = await query.find({ useMasterKey: true });
expect(sessions.length).toBe(1);
expect(sessions[0].get('installationId')).toBe('my-installation');
});
it('ensures logIn is saved with installationId', async () => {
+28
View File
@@ -3,6 +3,7 @@
//
'use strict';
const request = require('../lib/request');
function setupTestUsers() {
const user1 = new Parse.User();
@@ -135,4 +136,31 @@ describe('Parse.Session', () => {
fail(err);
});
});
it('cannot edit session with known ID', async () => {
await setupTestUsers();
const [first, second] = await new Parse.Query(Parse.Session).find({ useMasterKey: true });
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-Rest-API-Key': 'rest',
'X-Parse-Session-Token': second.get('sessionToken'),
'Content-Type': 'application/json',
};
const firstUser = first.get('user').id;
const secondUser = second.get('user').id;
const e = await request({
method: 'PUT',
headers,
url: `http://localhost:8378/1/sessions/${first.id}`,
body: JSON.stringify({
foo: 'bar',
user: { __type: 'Pointer', className: '_User', objectId: secondUser },
}),
}).catch(e => e.data);
expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
expect(e.error).toBe('Object not found.');
await Parse.Object.fetchAll([first, second], { useMasterKey: true });
expect(first.get('user').id).toBe(firstUser);
expect(second.get('user').id).toBe(secondUser);
});
});
+1 -1
View File
@@ -2926,7 +2926,7 @@ describe('Parse.User testing', () => {
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve(),
};
reconfigureServer({
await reconfigureServer({
appName: 'unused',
verifyUserEmails: true,
emailAdapter: emailAdapter,
+56
View File
@@ -23,6 +23,7 @@ describe('ParseWebSocketServer', function () {
ws.readyState = 0;
ws.OPEN = 0;
ws.ping = jasmine.createSpy('ping');
ws.terminate = () => {};
parseWebSocketServer.onConnection(ws);
@@ -75,6 +76,61 @@ describe('ParseWebSocketServer', function () {
expect(wssError).toBe('Invalid Packet');
});
it('can handle ping/pong', async () => {
const onConnectCallback = jasmine.createSpy('onConnectCallback');
const http = require('http');
const server = http.createServer();
const parseWebSocketServer = new ParseWebSocketServer(server, onConnectCallback, {
websocketTimeout: 10,
}).server;
const ws = new EventEmitter();
ws.readyState = 0;
ws.OPEN = 0;
ws.ping = jasmine.createSpy('ping');
ws.terminate = jasmine.createSpy('terminate');
parseWebSocketServer.onConnection(ws);
expect(onConnectCallback).toHaveBeenCalled();
expect(ws.waitingForPong).toBe(false);
await new Promise(resolve => setTimeout(resolve, 10));
expect(ws.ping).toHaveBeenCalled();
expect(ws.waitingForPong).toBe(true);
ws.emit('pong');
expect(ws.waitingForPong).toBe(false);
await new Promise(resolve => setTimeout(resolve, 10));
expect(ws.waitingForPong).toBe(true);
expect(ws.terminate).not.toHaveBeenCalled();
server.close();
});
it('closes interrupted connection', async () => {
const onConnectCallback = jasmine.createSpy('onConnectCallback');
const http = require('http');
const server = http.createServer();
const parseWebSocketServer = new ParseWebSocketServer(server, onConnectCallback, {
websocketTimeout: 5,
}).server;
const ws = new EventEmitter();
ws.readyState = 0;
ws.OPEN = 0;
ws.ping = jasmine.createSpy('ping');
ws.terminate = jasmine.createSpy('terminate');
parseWebSocketServer.onConnection(ws);
// Make sure callback is called
expect(onConnectCallback).toHaveBeenCalled();
expect(ws.waitingForPong).toBe(false);
await new Promise(resolve => setTimeout(resolve, 10));
expect(ws.ping).toHaveBeenCalled();
expect(ws.waitingForPong).toBe(true);
await new Promise(resolve => setTimeout(resolve, 10));
expect(ws.terminate).toHaveBeenCalled();
server.close();
});
afterEach(function () {
jasmine.restoreLibrary('ws', 'Server');
});
+39 -56
View File
@@ -4,8 +4,7 @@ const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/Postgre
const postgresURI =
process.env.PARSE_SERVER_TEST_DATABASE_URI ||
'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
const ParseServer = require('../lib/index');
const express = require('express');
//public schema
const databaseOptions1 = {
initOptions: {
@@ -24,72 +23,56 @@ const GameScore = Parse.Object.extend({
className: 'GameScore',
});
function createParseServer(options) {
return new Promise((resolve, reject) => {
const parseServer = new ParseServer.default(
Object.assign({}, defaultConfiguration, options, {
serverURL: 'http://localhost:12668/parse',
serverStartComplete: error => {
if (error) {
reject(error);
} else {
expect(Parse.applicationId).toEqual('test');
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12668);
Parse.serverURL = 'http://localhost:12668/parse';
resolve(server);
}
},
})
);
});
}
describe_only_db('postgres')('Postgres database init options', () => {
let server;
afterAll(done => {
if (server) {
Parse.serverURL = 'http://localhost:8378/1';
server.close(done);
}
});
it('should create server with public schema databaseOptions', done => {
it('should create server with public schema databaseOptions', async () => {
const adapter = new PostgresStorageAdapter({
uri: postgresURI,
collectionPrefix: 'test_',
databaseOptions: databaseOptions1,
});
createParseServer({ databaseAdapter: adapter })
.then(newServer => {
server = newServer;
const score = new GameScore({
score: 1337,
playerName: 'Sean Plott',
cheatMode: false,
});
return score.save();
})
.then(async () => {
await reconfigureServer();
done();
}, done.fail);
await reconfigureServer({
databaseAdapter: adapter,
});
const score = new GameScore({
score: 1337,
playerName: 'Sean Plott',
cheatMode: false,
});
await score.save();
});
it('should fail to create server if schema databaseOptions does not exist', done => {
it('should create server using postgresql uri with public schema databaseOptions', async () => {
const postgresURI2 = new URL(postgresURI);
postgresURI2.protocol = 'postgresql:';
const adapter = new PostgresStorageAdapter({
uri: postgresURI2.toString(),
collectionPrefix: 'test_',
databaseOptions: databaseOptions1,
});
await reconfigureServer({
databaseAdapter: adapter,
});
const score = new GameScore({
score: 1337,
playerName: 'Sean Plott',
cheatMode: false,
});
await score.save();
});
it('should fail to create server if schema databaseOptions does not exist', async () => {
const adapter = new PostgresStorageAdapter({
uri: postgresURI,
collectionPrefix: 'test_',
databaseOptions: databaseOptions2,
});
createParseServer({ databaseAdapter: adapter }).then(done.fail, async () => {
await reconfigureServer();
done();
});
try {
await reconfigureServer({
databaseAdapter: adapter,
});
fail('Should have thrown error');
} catch (error) {
expect(error).toBeDefined();
}
});
});
+151 -1
View File
@@ -149,6 +149,135 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
});
it('$relativeTime should error on $eq', async () => {
const tableName = '_User';
const schema = {
fields: {
objectId: { type: 'String' },
username: { type: 'String' },
email: { type: 'String' },
emailVerified: { type: 'Boolean' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
authData: { type: 'Object' },
},
};
const client = adapter._client;
await adapter.createTable(tableName, schema);
await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [
tableName,
'objectId',
'username',
'Bugs',
'Bunny',
]);
const database = Config.get(Parse.applicationId).database;
await database.loadSchema({ clearCache: true });
try {
await database.find(
tableName,
{
createdAt: {
$eq: {
$relativeTime: '12 days ago',
},
},
},
{}
);
fail('Should have thrown error');
} catch (error) {
expect(error.code).toBe(Parse.Error.INVALID_JSON);
}
await dropTable(client, tableName);
});
it('$relativeTime should error on $ne', async () => {
const tableName = '_User';
const schema = {
fields: {
objectId: { type: 'String' },
username: { type: 'String' },
email: { type: 'String' },
emailVerified: { type: 'Boolean' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
authData: { type: 'Object' },
},
};
const client = adapter._client;
await adapter.createTable(tableName, schema);
await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [
tableName,
'objectId',
'username',
'Bugs',
'Bunny',
]);
const database = Config.get(Parse.applicationId).database;
await database.loadSchema({ clearCache: true });
try {
await database.find(
tableName,
{
createdAt: {
$ne: {
$relativeTime: '12 days ago',
},
},
},
{}
);
fail('Should have thrown error');
} catch (error) {
expect(error.code).toBe(Parse.Error.INVALID_JSON);
}
await dropTable(client, tableName);
});
it('$relativeTime should error on $exists', async () => {
const tableName = '_User';
const schema = {
fields: {
objectId: { type: 'String' },
username: { type: 'String' },
email: { type: 'String' },
emailVerified: { type: 'Boolean' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
authData: { type: 'Object' },
},
};
const client = adapter._client;
await adapter.createTable(tableName, schema);
await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [
tableName,
'objectId',
'username',
'Bugs',
'Bunny',
]);
const database = Config.get(Parse.applicationId).database;
await database.loadSchema({ clearCache: true });
try {
await database.find(
tableName,
{
createdAt: {
$exists: {
$relativeTime: '12 days ago',
},
},
},
{}
);
fail('Should have thrown error');
} catch (error) {
expect(error.code).toBe(Parse.Error.INVALID_JSON);
}
await dropTable(client, tableName);
});
it('should use index for caseInsensitive query using Postgres', async () => {
const tableName = '_User';
const schema = {
@@ -426,9 +555,21 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => {
},
classLevelPermissions: undefined,
});
await new Promise(resolve => setTimeout(resolve, 500));
await new Promise(resolve => setTimeout(resolve, 2000));
expect(adapter._onchange).toHaveBeenCalled();
});
it('Idempotency class should have function', async () => {
await reconfigureServer();
const adapter = Config.get('test').database.adapter;
const client = adapter._client;
const qs =
"SELECT format('%I.%I(%s)', ns.nspname, p.proname, oidvectortypes(p.proargtypes)) FROM pg_proc p INNER JOIN pg_namespace ns ON (p.pronamespace = ns.oid) WHERE p.proname = 'idempotency_delete_expired_records'";
const foundFunction = await client.one(qs);
expect(foundFunction.format).toBe('public.idempotency_delete_expired_records()');
await adapter.deleteIdempotencyFunction();
await client.none(qs);
});
});
describe_only_db('postgres')('PostgresStorageAdapter shutdown', () => {
@@ -438,4 +579,13 @@ describe_only_db('postgres')('PostgresStorageAdapter shutdown', () => {
adapter.handleShutdown();
expect(adapter._client.$pool.ending).toEqual(true);
});
it('handleShutdown, close connection of postgresql uri', () => {
const databaseURI2 = new URL(databaseURI);
databaseURI2.protocol = 'postgresql:';
const adapter = new PostgresStorageAdapter({ uri: databaseURI2.toString() });
expect(adapter._client.$pool.ending).toEqual(false);
adapter.handleShutdown();
expect(adapter._client.$pool.ending).toEqual(true);
});
});
+24 -50
View File
@@ -562,11 +562,7 @@ describe('PushController', () => {
});
const pushStatusId = await sendPush(payload, {}, config, auth);
// it is enqueued so it can take time
await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 1000);
});
await sleep(1000);
Parse.serverURL = 'http://localhost:8378/1'; // GOOD url
const result = await Parse.Push.getPushStatus(pushStatusId);
expect(result).toBeDefined();
@@ -767,7 +763,7 @@ describe('PushController', () => {
});
});
it('should not schedule push when not configured', done => {
it('should not schedule push when not configured', async () => {
const config = Config.get(Parse.applicationId);
const auth = {
isMaster: true,
@@ -800,33 +796,20 @@ describe('PushController', () => {
installations.push(installation);
}
reconfigureServer({
await reconfigureServer({
push: { adapter: pushAdapter },
})
.then(() => {
return Parse.Object.saveAll(installations)
.then(() => {
return pushController.sendPush(payload, {}, config, auth);
})
.then(() => new Promise(resolve => setTimeout(resolve, 300)));
})
.then(() => {
const query = new Parse.Query('_PushStatus');
return query.find({ useMasterKey: true }).then(results => {
expect(results.length).toBe(1);
const pushStatus = results[0];
expect(pushStatus.get('status')).not.toBe('scheduled');
done();
});
})
.catch(err => {
console.error(err);
fail('should not fail');
done();
});
});
await Parse.Object.saveAll(installations);
await pushController.sendPush(payload, {}, config, auth);
await sleep(1000);
const query = new Parse.Query('_PushStatus');
const results = await query.find({ useMasterKey: true });
expect(results.length).toBe(1);
const pushStatus = results[0];
expect(pushStatus.get('status')).not.toBe('scheduled');
});
it('should schedule push when configured', done => {
it('should schedule push when configured', async () => {
const auth = {
isMaster: true,
};
@@ -866,28 +849,19 @@ describe('PushController', () => {
installation.set('deviceType', 'ios');
installations.push(installation);
}
reconfigureServer({
await reconfigureServer({
push: { adapter: pushAdapter },
scheduledPush: true,
})
.then(() => {
const config = Config.get(Parse.applicationId);
return Parse.Object.saveAll(installations)
.then(() => {
return pushController.sendPush(payload, {}, config, auth);
})
.then(() => new Promise(resolve => setTimeout(resolve, 300)));
})
.then(() => {
const query = new Parse.Query('_PushStatus');
return query.find({ useMasterKey: true }).then(results => {
expect(results.length).toBe(1);
const pushStatus = results[0];
expect(pushStatus.get('status')).toBe('scheduled');
});
})
.then(done)
.catch(done.err);
});
const config = Config.get(Parse.applicationId);
await Parse.Object.saveAll(installations);
await pushController.sendPush(payload, {}, config, auth);
await sleep(1000);
const query = new Parse.Query('_PushStatus');
const results = await query.find({ useMasterKey: true });
expect(results.length).toBe(1);
const pushStatus = results[0];
expect(pushStatus.get('status')).toBe('scheduled');
});
it('should not enqueue push when device token is not set', async () => {
+23
View File
@@ -583,6 +583,29 @@ describe('matchesQuery', function () {
expect(matchesQuery(message, q)).toBe(false);
});
it('should support containedIn with array of pointers', () => {
const message = {
id: new Id('Message', 'O2'),
profiles: [pointer('Profile', 'yeahaw'), pointer('Profile', 'yes')],
};
let q = new Parse.Query('Message');
q.containedIn('profiles', [
Parse.Object.fromJSON({ className: 'Profile', objectId: 'no' }),
Parse.Object.fromJSON({ className: 'Profile', objectId: 'yes' }),
]);
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.containedIn('profiles', [
Parse.Object.fromJSON({ className: 'Profile', objectId: 'no' }),
Parse.Object.fromJSON({ className: 'Profile', objectId: 'nope' }),
]);
expect(matchesQuery(message, q)).toBe(false);
});
it('should support notContainedIn with pointers', () => {
let message = {
id: new Id('Message', 'O1'),
+157 -215
View File
@@ -1,9 +1,8 @@
'use strict';
const Parse = require('parse/node');
const ReadPreference = require('mongodb').ReadPreference;
const { ReadPreference, Collection } = require('mongodb');
const request = require('../lib/request');
const Config = require('../lib/Config');
function waitForReplication() {
return new Promise(function (resolve) {
@@ -13,8 +12,6 @@ function waitForReplication() {
describe_only_db('mongo')('Read preference option', () => {
it('should find in primary by default', done => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
@@ -22,7 +19,7 @@ describe_only_db('mongo')('Read preference option', () => {
Parse.Object.saveAll([obj0, obj1])
.then(() => {
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
const query = new Parse.Query('MyObject');
query.equalTo('boolKey', false);
@@ -31,10 +28,10 @@ describe_only_db('mongo')('Read preference option', () => {
expect(results.length).toBe(1);
expect(results[0].get('boolKey')).toBe(false);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY);
expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY);
}
});
@@ -58,15 +55,13 @@ describe_only_db('mongo')('Read preference option', () => {
databaseAdapter: new MongoStorageAdapter(adapterOptions),
});
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
const query = new Parse.Query('MyObject');
query.equalTo('boolKey', false);
@@ -76,10 +71,10 @@ describe_only_db('mongo')('Read preference option', () => {
expect(results[0].get('boolKey')).toBe(false);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = true;
expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.NEAREST);
expect(call.args[1].readPreference).toBe(ReadPreference.NEAREST);
}
});
@@ -87,15 +82,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference in the beforeFind trigger', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'SECONDARY';
@@ -110,9 +103,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(results[0].get('boolKey')).toBe(false);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -120,15 +113,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should check read preference as case insensitive', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'sEcOnDarY';
@@ -144,9 +135,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(results[0].get('boolKey')).toBe(false);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -154,15 +145,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference in the beforeFind trigger even changing query', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.query.equalTo('boolKey', true);
@@ -178,9 +167,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(results[0].get('boolKey')).toBe(true);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -188,15 +177,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference in the beforeFind trigger even returning query', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'SECONDARY';
@@ -216,9 +203,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(results[0].get('boolKey')).toBe(true);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -226,15 +213,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference in the beforeFind trigger even returning promise', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'SECONDARY';
@@ -253,9 +238,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(results[0].get('boolKey')).toBe(true);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -263,15 +248,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference to PRIMARY_PREFERRED', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'PRIMARY_PREFERRED';
@@ -286,9 +269,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(results[0].get('boolKey')).toBe(false);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -296,15 +279,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference to SECONDARY_PREFERRED', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'SECONDARY_PREFERRED';
@@ -319,9 +300,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(results[0].get('boolKey')).toBe(false);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -329,15 +310,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference to NEAREST', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'NEAREST';
@@ -352,9 +331,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(results[0].get('boolKey')).toBe(false);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -362,15 +341,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference for GET', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'SECONDARY';
@@ -383,9 +360,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(result.get('boolKey')).toBe(false);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -393,15 +370,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference for GET using API', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'SECONDARY';
@@ -421,9 +396,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(body.boolKey).toBe(false);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -431,15 +406,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference for GET directly from API', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
await waitForReplication();
const response = await request({
@@ -454,9 +427,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(response.data.boolKey).toBe(false);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -464,15 +437,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference for GET using API through the beforeFind overriding API option', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'SECONDARY_PREFERRED';
@@ -491,9 +462,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(response.data.boolKey).toBe(false);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -501,15 +472,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference for FIND using API through beforeFind trigger', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'SECONDARY';
@@ -528,9 +497,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(response.data.results.length).toEqual(2);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -538,15 +507,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference for FIND directly from API', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
await waitForReplication();
const response = await request({
@@ -561,9 +528,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(response.data.results.length).toEqual(2);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -571,15 +538,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change read preference for FIND using API through the beforeFind overriding API option', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
await Parse.Object.saveAll([obj0, obj1]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'SECONDARY_PREFERRED';
@@ -598,9 +563,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(response.data.results.length).toEqual(2);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -608,15 +573,13 @@ describe_only_db('mongo')('Read preference option', () => {
});
xit('should change read preference for count', done => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject');
obj1.set('boolKey', true);
Parse.Object.saveAll([obj0, obj1]).then(() => {
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject', req => {
req.readPreference = 'SECONDARY';
@@ -631,9 +594,9 @@ describe_only_db('mongo')('Read preference option', () => {
expect(result).toBe(1);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
@@ -659,17 +622,16 @@ describe_only_db('mongo')('Read preference option', () => {
await waitForReplication();
// Spy on DB adapter
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
spyOn(databaseAdapter.database.serverConfig, 'startSession').and.callThrough();
spyOn(Collection.prototype, 'aggregate').and.callThrough();
// Query
const query = new Parse.Query('MyObject');
const results = await query.aggregate([{ match: { boolKey: false } }]);
// Validate
expect(results.length).toBe(1);
let readPreference = null;
databaseAdapter.database.serverConfig.startSession.calls.all().forEach(call => {
if (call.args[0].owner.ns.indexOf('MyObject') > -1) {
readPreference = call.args[0].owner.operation.readPreference.mode;
Collection.prototype.aggregate.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') > -1) {
readPreference = call.args[1].readPreference;
}
});
expect(readPreference).toEqual(ReadPreference.SECONDARY);
@@ -685,8 +647,7 @@ describe_only_db('mongo')('Read preference option', () => {
await waitForReplication();
// Spy on DB adapter
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
// Query
const query = new Parse.Query('MyObject');
query.equalTo('boolKey', false);
@@ -695,9 +656,9 @@ describe_only_db('mongo')('Read preference option', () => {
// Validate
expect(results.length).toBe(1);
let myObjectReadPreference = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) {
myObjectReadPreference = call.args[1].readPreference;
}
});
expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY);
@@ -713,8 +674,7 @@ describe_only_db('mongo')('Read preference option', () => {
await waitForReplication();
// Spy on DB adapter
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
spyOn(databaseAdapter.database.serverConfig, 'startSession').and.callThrough();
spyOn(Collection.prototype, 'aggregate').and.callThrough();
// Query
const query = new Parse.Query('MyObject');
query.readPreference('SECONDARY');
@@ -722,17 +682,15 @@ describe_only_db('mongo')('Read preference option', () => {
// Validate
expect(results.length).toBe(1);
let readPreference = null;
databaseAdapter.database.serverConfig.startSession.calls.all().forEach(call => {
if (call.args[0].owner.ns.indexOf('MyObject') > -1) {
readPreference = call.args[0].owner.operation.readPreference.mode;
Collection.prototype.aggregate.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject') > -1) {
readPreference = call.args[1].readPreference;
}
});
expect(readPreference).toEqual(ReadPreference.SECONDARY);
});
it('should find includes in same replica of readPreference by default', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject0');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject1');
@@ -743,7 +701,7 @@ describe_only_db('mongo')('Read preference option', () => {
obj2.set('myObject1', obj1);
await Parse.Object.saveAll([obj0, obj1, obj2]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject2', req => {
req.readPreference = 'SECONDARY';
@@ -765,15 +723,15 @@ describe_only_db('mongo')('Read preference option', () => {
let myObjectReadPreference0 = null;
let myObjectReadPreference1 = null;
let myObjectReadPreference2 = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[1].readPreference;
}
});
@@ -783,8 +741,6 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change includes read preference', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject0');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject1');
@@ -795,7 +751,7 @@ describe_only_db('mongo')('Read preference option', () => {
obj2.set('myObject1', obj1);
await Parse.Object.saveAll([obj0, obj1, obj2]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject2', req => {
req.readPreference = 'SECONDARY_PREFERRED';
@@ -818,15 +774,15 @@ describe_only_db('mongo')('Read preference option', () => {
let myObjectReadPreference0 = null;
let myObjectReadPreference1 = null;
let myObjectReadPreference2 = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[1].readPreference;
}
});
@@ -836,8 +792,6 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change includes read preference when finding through API', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject0');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject1');
@@ -848,7 +802,7 @@ describe_only_db('mongo')('Read preference option', () => {
obj2.set('myObject1', obj1);
await Parse.Object.saveAll([obj0, obj1, obj2]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
await waitForReplication();
const response = await request({
@@ -873,15 +827,15 @@ describe_only_db('mongo')('Read preference option', () => {
let myObjectReadPreference0 = null;
let myObjectReadPreference1 = null;
let myObjectReadPreference2 = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[1].readPreference;
}
});
@@ -891,8 +845,6 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change includes read preference when getting through API', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject0');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject1');
@@ -903,7 +855,7 @@ describe_only_db('mongo')('Read preference option', () => {
obj2.set('myObject1', obj1);
await Parse.Object.saveAll([obj0, obj1, obj2]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
await waitForReplication();
const response = await request({
@@ -929,15 +881,15 @@ describe_only_db('mongo')('Read preference option', () => {
let myObjectReadPreference0 = null;
let myObjectReadPreference1 = null;
let myObjectReadPreference2 = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[1].readPreference;
}
});
@@ -947,8 +899,6 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should find subqueries in same replica of readPreference by default', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject0');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject1');
@@ -959,7 +909,7 @@ describe_only_db('mongo')('Read preference option', () => {
obj2.set('myObject1', obj1);
await Parse.Object.saveAll([obj0, obj1, obj2]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject2', req => {
req.readPreference = 'SECONDARY';
@@ -982,15 +932,15 @@ describe_only_db('mongo')('Read preference option', () => {
let myObjectReadPreference0 = null;
let myObjectReadPreference1 = null;
let myObjectReadPreference2 = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[1].readPreference;
}
});
@@ -1000,8 +950,6 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change subqueries read preference when using matchesQuery', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject0');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject1');
@@ -1012,7 +960,7 @@ describe_only_db('mongo')('Read preference option', () => {
obj2.set('myObject1', obj1);
await Parse.Object.saveAll([obj0, obj1, obj2]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject2', req => {
req.readPreference = 'SECONDARY_PREFERRED';
@@ -1036,15 +984,15 @@ describe_only_db('mongo')('Read preference option', () => {
let myObjectReadPreference0 = null;
let myObjectReadPreference1 = null;
let myObjectReadPreference2 = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[1].readPreference;
}
});
@@ -1054,8 +1002,6 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change subqueries read preference when using doesNotMatchQuery', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject0');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject1');
@@ -1066,7 +1012,7 @@ describe_only_db('mongo')('Read preference option', () => {
obj2.set('myObject1', obj1);
await Parse.Object.saveAll([obj0, obj1, obj2]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject2', req => {
req.readPreference = 'SECONDARY_PREFERRED';
@@ -1090,15 +1036,15 @@ describe_only_db('mongo')('Read preference option', () => {
let myObjectReadPreference0 = null;
let myObjectReadPreference1 = null;
let myObjectReadPreference2 = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[1].readPreference;
}
});
@@ -1108,8 +1054,6 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change subqueries read preference when using matchesKeyInQuery and doesNotMatchKeyInQuery', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject0');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject1');
@@ -1120,7 +1064,7 @@ describe_only_db('mongo')('Read preference option', () => {
obj2.set('myObject1', obj1);
await Parse.Object.saveAll([obj0, obj1, obj2]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
Parse.Cloud.beforeFind('MyObject2', req => {
req.readPreference = 'SECONDARY_PREFERRED';
@@ -1145,15 +1089,15 @@ describe_only_db('mongo')('Read preference option', () => {
let myObjectReadPreference0 = null;
let myObjectReadPreference1 = null;
let myObjectReadPreference2 = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[1].readPreference;
}
});
@@ -1163,8 +1107,6 @@ describe_only_db('mongo')('Read preference option', () => {
});
it('should change subqueries read preference when using matchesKeyInQuery and doesNotMatchKeyInQuery to find through API', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database.adapter;
const obj0 = new Parse.Object('MyObject0');
obj0.set('boolKey', false);
const obj1 = new Parse.Object('MyObject1');
@@ -1175,7 +1117,7 @@ describe_only_db('mongo')('Read preference option', () => {
obj2.set('myObject1', obj1);
await Parse.Object.saveAll([obj0, obj1, obj2]);
spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough();
spyOn(Collection.prototype, 'find').and.callThrough();
await waitForReplication();
const whereString = JSON.stringify({
@@ -1215,15 +1157,15 @@ describe_only_db('mongo')('Read preference option', () => {
let myObjectReadPreference0 = null;
let myObjectReadPreference1 = null;
let myObjectReadPreference2 = null;
databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => {
if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[0].options.readPreference.mode;
Collection.prototype.find.calls.all().forEach(call => {
if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) {
myObjectReadPreference0 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) {
myObjectReadPreference1 = call.args[1].readPreference;
}
if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[0].options.readPreference.mode;
if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) {
myObjectReadPreference2 = call.args[1].readPreference;
}
});
+94
View File
@@ -191,6 +191,79 @@ describe('rest query', () => {
expect(result.results.length).toEqual(0);
});
it('query internal field', async () => {
const internalFields = [
'_email_verify_token',
'_perishable_token',
'_tombstone',
'_email_verify_token_expires_at',
'_failed_login_count',
'_account_lockout_expires_at',
'_password_changed_at',
'_password_history',
];
await Promise.all([
...internalFields.map(field =>
expectAsync(new Parse.Query(Parse.User).exists(field).find()).toBeRejectedWith(
new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${field}`)
)
),
...internalFields.map(field =>
new Parse.Query(Parse.User).exists(field).find({ useMasterKey: true })
),
]);
});
it('query protected field', async () => {
const user = new Parse.User();
user.setUsername('username1');
user.setPassword('password');
await user.signUp();
const config = Config.get(Parse.applicationId);
const obj = new Parse.Object('Test');
obj.set('owner', user);
obj.set('test', 'test');
obj.set('zip', 1234);
await obj.save();
const schema = await config.database.loadSchema();
await schema.updateClass(
'Test',
{},
{
get: { '*': true },
find: { '*': true },
protectedFields: { [user.id]: ['zip'] },
}
);
await Promise.all([
new Parse.Query('Test').exists('test').find(),
expectAsync(new Parse.Query('Test').exists('zip').find()).toBeRejectedWith(
new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
'This user is not allowed to query zip on class Test'
)
),
]);
});
it('query protected field with matchesQuery', async () => {
const user = new Parse.User();
user.setUsername('username1');
user.setPassword('password');
await user.signUp();
const test = new Parse.Object('TestObject', { user });
await test.save();
const subQuery = new Parse.Query(Parse.User);
subQuery.exists('_perishable_token');
await expectAsync(
new Parse.Query('TestObject').matchesQuery('user', subQuery).find()
).toBeRejectedWith(
new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid key name: _perishable_token')
);
});
it('query with wrongly encoded parameter', done => {
rest
.create(config, nobody, 'TestParameterEncode', { foo: 'bar' })
@@ -429,4 +502,25 @@ describe('RestQuery.each', () => {
done();
});
});
it('test afterSave should not affect save response', async () => {
Parse.Cloud.beforeSave('TestObject2', ({ object }) => {
object.set('addedBeforeSave', true);
});
Parse.Cloud.afterSave('TestObject2', ({ object }) => {
object.set('addedAfterSave', true);
object.unset('initialToRemove');
});
const { response } = await rest.create(config, nobody, 'TestObject2', {
initialSave: true,
initialToRemove: true,
});
expect(Object.keys(response).sort()).toEqual([
'addedAfterSave',
'addedBeforeSave',
'createdAt',
'initialToRemove',
'objectId',
]);
});
});
+110 -84
View File
@@ -89,10 +89,54 @@ describe('batch', () => {
expect(internalURL).toEqual('/classes/Object');
});
it('should handle a batch request without transaction', async done => {
it('should return the proper url with no url provided', () => {
const originalURL = '/parse/batch';
const internalURL = batch.makeBatchRoutingPathFunction(
originalURL,
undefined,
publicServerURL
)('/parse/classes/Object');
expect(internalURL).toEqual('/classes/Object');
});
it('should return the proper url with no public url provided', () => {
const originalURL = '/parse/batch';
const internalURL = batch.makeBatchRoutingPathFunction(
originalURL,
serverURLNaked,
undefined
)('/parse/classes/Object');
expect(internalURL).toEqual('/classes/Object');
});
it('should return the proper url with bad url provided', () => {
const originalURL = '/parse/batch';
const internalURL = batch.makeBatchRoutingPathFunction(
originalURL,
'badurl.com',
publicServerURL
)('/parse/classes/Object');
expect(internalURL).toEqual('/classes/Object');
});
it('should return the proper url with bad public url provided', () => {
const originalURL = '/parse/batch';
const internalURL = batch.makeBatchRoutingPathFunction(
originalURL,
serverURLNaked,
'badurl.com'
)('/parse/classes/Object');
expect(internalURL).toEqual('/classes/Object');
});
it('should handle a batch request without transaction', async () => {
spyOn(databaseAdapter, 'createObject').and.callThrough();
request({
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/batch',
@@ -110,28 +154,25 @@ describe('batch', () => {
},
],
}),
}).then(response => {
expect(response.data.length).toEqual(2);
expect(response.data[0].success.objectId).toBeDefined();
expect(response.data[0].success.createdAt).toBeDefined();
expect(response.data[1].success.objectId).toBeDefined();
expect(response.data[1].success.createdAt).toBeDefined();
const query = new Parse.Query('MyObject');
query.find().then(results => {
expect(databaseAdapter.createObject.calls.count()).toBe(2);
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toEqual(null);
expect(databaseAdapter.createObject.calls.argsFor(1)[3]).toEqual(null);
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
done();
});
});
expect(response.data.length).toEqual(2);
expect(response.data[0].success.objectId).toBeDefined();
expect(response.data[0].success.createdAt).toBeDefined();
expect(response.data[1].success.objectId).toBeDefined();
expect(response.data[1].success.createdAt).toBeDefined();
const query = new Parse.Query('MyObject');
const results = await query.find();
expect(databaseAdapter.createObject.calls.count()).toBe(2);
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toEqual(null);
expect(databaseAdapter.createObject.calls.argsFor(1)[3]).toEqual(null);
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
});
it('should handle a batch request with transaction = false', async done => {
await reconfigureServer();
it('should handle a batch request with transaction = false', async () => {
spyOn(databaseAdapter, 'createObject').and.callThrough();
request({
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/batch',
@@ -150,21 +191,18 @@ describe('batch', () => {
],
transaction: false,
}),
}).then(response => {
expect(response.data.length).toEqual(2);
expect(response.data[0].success.objectId).toBeDefined();
expect(response.data[0].success.createdAt).toBeDefined();
expect(response.data[1].success.objectId).toBeDefined();
expect(response.data[1].success.createdAt).toBeDefined();
const query = new Parse.Query('MyObject');
query.find().then(results => {
expect(databaseAdapter.createObject.calls.count()).toBe(2);
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toEqual(null);
expect(databaseAdapter.createObject.calls.argsFor(1)[3]).toEqual(null);
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
done();
});
});
expect(response.data.length).toEqual(2);
expect(response.data[0].success.objectId).toBeDefined();
expect(response.data[0].success.createdAt).toBeDefined();
expect(response.data[1].success.objectId).toBeDefined();
expect(response.data[1].success.createdAt).toBeDefined();
const query = new Parse.Query('MyObject');
const results = await query.find();
expect(databaseAdapter.createObject.calls.count()).toBe(2);
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toEqual(null);
expect(databaseAdapter.createObject.calls.argsFor(1)[3]).toEqual(null);
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
});
if (
@@ -191,58 +229,45 @@ describe('batch', () => {
}
});
it('should handle a batch request with transaction = true', async done => {
await reconfigureServer();
it('should handle a batch request with transaction = true', async () => {
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
myObject
.save()
.then(() => {
return myObject.destroy();
})
.then(() => {
spyOn(databaseAdapter, 'createObject').and.callThrough();
request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/batch',
body: JSON.stringify({
requests: [
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value2' },
},
],
transaction: true,
}),
}).then(response => {
expect(response.data.length).toEqual(2);
expect(response.data[0].success.objectId).toBeDefined();
expect(response.data[0].success.createdAt).toBeDefined();
expect(response.data[1].success.objectId).toBeDefined();
expect(response.data[1].success.createdAt).toBeDefined();
const query = new Parse.Query('MyObject');
query.find().then(results => {
expect(databaseAdapter.createObject.calls.count() % 2).toBe(0);
for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) {
expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe(
databaseAdapter.createObject.calls.argsFor(i + 1)[3]
);
}
expect(results.map(result => result.get('key')).sort()).toEqual([
'value1',
'value2',
]);
done();
});
});
});
await myObject.save();
await myObject.destroy();
spyOn(databaseAdapter, 'createObject').and.callThrough();
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/batch',
body: JSON.stringify({
requests: [
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value2' },
},
],
transaction: true,
}),
});
expect(response.data.length).toEqual(2);
expect(response.data[0].success.objectId).toBeDefined();
expect(response.data[0].success.createdAt).toBeDefined();
expect(response.data[1].success.objectId).toBeDefined();
expect(response.data[1].success.createdAt).toBeDefined();
const query = new Parse.Query('MyObject');
const results = await query.find();
expect(databaseAdapter.createObject.calls.count() % 2).toBe(0);
for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) {
expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe(
databaseAdapter.createObject.calls.argsFor(i + 1)[3]
);
}
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
});
it('should not save anything when one operation fails in a transaction', async () => {
@@ -350,6 +375,7 @@ describe('batch', () => {
transaction: true,
}),
});
fail();
} catch (error) {
expect(error).toBeDefined();
const query = new Parse.Query('MyObject');
+42
View File
@@ -1,9 +1,15 @@
'use strict';
const dns = require('dns');
const semver = require('semver');
const CurrentSpecReporter = require('./support/CurrentSpecReporter.js');
const { SpecReporter } = require('jasmine-spec-reporter');
const SchemaCache = require('../lib/Adapters/Cache/SchemaCache').default;
// Ensure localhost resolves to ipv4 address first on node v17+
if (dns.setDefaultResultOrder) {
dns.setDefaultResultOrder('ipv4first');
}
// Sets up a Parse API server for testing.
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
jasmine.getEnv().addReporter(new CurrentSpecReporter());
@@ -455,6 +461,15 @@ global.it_only_postgres_version = version => {
}
};
global.it_only_node_version = version => {
const envVersion = process.version;
if (!envVersion || semver.satisfies(envVersion, version)) {
return it;
} else {
return xit;
}
};
global.fit_only_mongodb_version = version => {
const envVersion = process.env.MONGODB_VERSION;
if (!envVersion || semver.satisfies(envVersion, version)) {
@@ -464,6 +479,15 @@ global.fit_only_mongodb_version = version => {
}
};
global.fit_only_node_version = version => {
const envVersion = process.version;
if (!envVersion || semver.satisfies(envVersion, version)) {
return fit;
} else {
return xit;
}
};
global.it_exclude_mongodb_version = version => {
const envVersion = process.env.MONGODB_VERSION;
if (!envVersion || !semver.satisfies(envVersion, version)) {
@@ -482,6 +506,15 @@ global.it_exclude_postgres_version = version => {
}
};
global.it_exclude_node_version = version => {
const envVersion = process.env.NODE_VERSION;
if (!envVersion || !semver.satisfies(envVersion, version)) {
return it;
} else {
return xit;
}
};
global.fit_exclude_mongodb_version = version => {
const envVersion = process.env.MONGODB_VERSION;
if (!envVersion || !semver.satisfies(envVersion, version)) {
@@ -491,6 +524,15 @@ global.fit_exclude_mongodb_version = version => {
}
};
global.fit_exclude_node_version = version => {
const envVersion = process.env.NODE_VERSION;
if (!envVersion || !semver.satisfies(envVersion, version)) {
return fit;
} else {
return xit;
}
};
global.fit_exclude_dbs = excluded => {
if (excluded.indexOf(process.env.PARSE_SERVER_TEST_DB) >= 0) {
return xit;
+22 -1
View File
@@ -462,6 +462,26 @@ describe('server', () => {
.then(done);
});
it('fails if default limit is negative', async () => {
await expectAsync(reconfigureServer({ defaultLimit: -1 })).toBeRejectedWith(
'Default limit must be a value greater than 0.'
);
});
it('fails if default limit is wrong type', async () => {
for (const value of ['invalid', {}, [], true]) {
await expectAsync(reconfigureServer({ defaultLimit: value })).toBeRejectedWith(
'Default limit must be a number.'
);
}
});
it('fails if default limit is zero', async () => {
await expectAsync(reconfigureServer({ defaultLimit: 0 })).toBeRejectedWith(
'Default limit must be a value greater than 0.'
);
});
it('fails if maxLimit is negative', done => {
reconfigureServer({ maxLimit: -100 }).catch(error => {
expect(error).toEqual('Max limit must be a value greater than 0.');
@@ -533,6 +553,7 @@ describe('server', () => {
it('should not fail when Google signin is introduced without the optional clientId', done => {
const jwt = require('jsonwebtoken');
const authUtils = require('../lib/Adapters/Auth/utils');
reconfigureServer({
auth: { google: {} },
@@ -545,7 +566,7 @@ describe('server', () => {
sub: 'the_user_id',
};
const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } };
spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken);
spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken);
spyOn(jwt, 'verify').and.callFake(() => fakeClaim);
const user = new Parse.User();
user
+28
View File
@@ -1866,6 +1866,34 @@ describe('schemas', () => {
});
});
describe('Nested documents', () => {
beforeAll(async () => {
const testSchema = new Parse.Schema('test_7371');
testSchema.setCLP({
create: { ['*']: true },
update: { ['*']: true },
addField: {},
});
testSchema.addObject('a');
await testSchema.save();
});
it('addField permission not required for adding a nested property', async () => {
const obj = new Parse.Object('test_7371');
obj.set('a', {});
await obj.save();
obj.set('a.b', 2);
await obj.save();
});
it('addField permission not required for modifying a nested property', async () => {
const obj = new Parse.Object('test_7371');
obj.set('a', { b: 1 });
await obj.save();
obj.set('a.b', 2);
await obj.save();
});
});
it('should aceept class-level permission with userid of any length', async done => {
await global.reconfigureServer({
customIdSize: 11,
+28
View File
@@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIEvDCCA6SgAwIBAgIQXRHxNXkw1L9z5/3EZ/T/hDANBgkqhkiG9w0BAQsFADB/
MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAd
BgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMDAuBgNVBAMTJ1N5bWFudGVj
IENsYXNzIDMgU0hBMjU2IENvZGUgU2lnbmluZyBDQTAeFw0xODA5MTcwMDAwMDBa
Fw0xOTA5MTcyMzU5NTlaMHMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9y
bmlhMRIwEAYDVQQHDAlDdXBlcnRpbm8xFDASBgNVBAoMC0FwcGxlLCBJbmMuMQ8w
DQYDVQQLDAZHQyBTUkUxFDASBgNVBAMMC0FwcGxlLCBJbmMuMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA06fwIi8fgKrTQu7cBcFkJVF6+Tqvkg7MKJTM
IOYPPQtPF3AZYPsbUoRKAD7/JXrxxOSVJ7vU1mP77tYG8TcUteZ3sAwvt2dkRbm7
ZO6DcmSggv1Dg4k3goNw4GYyCY4Z2/8JSmsQ80Iv/UOOwynpBziEeZmJ4uck6zlA
17cDkH48LBpKylaqthym5bFs9gj11pto7mvyb5BTcVuohwi6qosvbs/4VGbC2Nsz
ie416nUZfv+xxoXH995gxR2mw5cDdeCew7pSKxEhvYjT2nVdQF0q/hnPMFnOaEyT
q79n3gwFXyt0dy8eP6KBF7EW9J6b7ubu/j7h+tQfxPM+gTXOBQIDAQABo4IBPjCC
ATowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
AwMwYQYDVR0gBFowWDBWBgZngQwBBAEwTDAjBggrBgEFBQcCARYXaHR0cHM6Ly9k
LnN5bWNiLmNvbS9jcHMwJQYIKwYBBQUHAgIwGQwXaHR0cHM6Ly9kLnN5bWNiLmNv
bS9ycGEwHwYDVR0jBBgwFoAUljtT8Hkzl699g+8uK8zKt4YecmYwKwYDVR0fBCQw
IjAgoB6gHIYaaHR0cDovL3N2LnN5bWNiLmNvbS9zdi5jcmwwVwYIKwYBBQUHAQEE
SzBJMB8GCCsGAQUFBzABhhNodHRwOi8vc3Yuc3ltY2QuY29tMCYGCCsGAQUFBzAC
hhpodHRwOi8vc3Yuc3ltY2IuY29tL3N2LmNydDANBgkqhkiG9w0BAQsFAAOCAQEA
I/j/PcCNPebSAGrcqSFBSa2mmbusOX01eVBg8X0G/z8Z+ZWUfGFzDG0GQf89MPxV
woec+nZuqui7o9Bg8s8JbHV0TC52X14CbTj9w/qBF748WbH9gAaTkrJYPm+MlNhu
tjEuQdNl/YXVMvQW4O8UMHTi09GyJQ0NC4q92Wxvx1m/qzjvTLvrXHGQ9pEHhPyz
vfBLxQkWpNoCNKU7UeESyH06XOrGc9MsII9deeKsDJp9a0jtx+pP4MFVtFME9SSQ
tMBs0It7WwEf7qcRLpialxKwY2EzQ9g4WnANHqo18PrDBE10TFpZPzUh7JhMViVr
EEbl0YdElmF8Hlamah/yNw==
-----END CERTIFICATE-----
+42
View File
@@ -0,0 +1,42 @@
-----BEGIN CERTIFICATE-----
MIIHbDCCBVSgAwIBAgIQAwuBj1pc45FkhpmTbIvZOjANBgkqhkiG9w0BAQsFADBp
MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMT
OERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0
IDIwMjEgQ0ExMB4XDTIxMDcyOTAwMDAwMFoXDTIyMDcyODIzNTk1OVowcTELMAkG
A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCUN1cGVydGlu
bzETMBEGA1UEChMKQXBwbGUgSW5jLjEPMA0GA1UECxMGR0MgU1JFMRMwEQYDVQQD
EwpBcHBsZSBJbmMuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyGXC
hfNKtSFUkayI4RGDl1T7cTqs9Ni6vnwJpU/9nTT3BWWxZ2Yng4muIhMeA3oZfDZu
T1ShS5y3CQV9/9SaUU1NNfnPxenvrrE8xSn8a9bo2adTrn9ASrEMqRD6bp+fS5Cp
kFHYH+VD5a8XTyOuDGpQyqIpUpYqGABXITWrEpjnpAw1IjMaeNO9sYJkWuLdw0gg
IMpBqmiiJXHgasl8D59S93PVHD1xkEjZcPT9NEWJXSRHUW+Xe+JUhrFSzEfjyWNS
spgJrnVtv4ec30Uz0qUC683lkfE446VPiIyo3xmjh3rs3G75JYJd5925YVM0uz1U
Wn0VmOTN5s81V6CBdYRc3J0sCGd5QEmDo4pwPwCMej+fT6fktIXUWZ1i/ycI1//m
Vc4kkuyiJ2msv8GSACPG6XkL+zKTjYC+GElj/WCX+hVJKzsYtL51zRr4KNnqhG7/
GK5kJ9eVTgTEKqdB0DZ7ZpOD3EoE2D9kj4zaoq/7r6Syi7Efw230zDMQyIJnoUQc
GDWUR2ZPQ+U+aUOKdWpgbhy4vOzTi24hOVcACbvc/CFTQ2gI7SfCSao9WLVqqGO5
waHhoOidTYY9Ey2PQvYHqXm5R2Ol+3V+GQl0NkiDt5kc7OpYIm7cDyQ04ZaHnUDt
ZljI5N1fdlhYVKntEzX4sNhcx1pNB1C/T5Wfw68CAwEAAaOCAgYwggICMB8GA1Ud
IwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRS7TCGHb7iPnHR
/odXPWLpAxPVKjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMw
gbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp
Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j
cmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0
ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMD4GA1UdIAQ3
MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQu
Y29tL0NQUzCBlAYIKwYBBQUHAQEEgYcwgYQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9v
Y3NwLmRpZ2ljZXJ0LmNvbTBcBggrBgEFBQcwAoZQaHR0cDovL2NhY2VydHMuZGln
aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hB
Mzg0MjAyMUNBMS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEA
uk71YLf55ne94hEeQtYsjCn38Tw3h78CH195J8H4T4r2p7p9MPjrA2zz+ZXza+kb
z5OTZ9k1/nu9vKnh4ljZS33uTh5AcdWhQNUeSuByjhVu+YTnVKqVYH/jaZXEFFe/
4/n23Shn2xN5jtkCEwYeqEaO6+8uBCFQldnUgbSag2Le9s/lICUJvGsKTAUhEGrK
R4u4OyJGGk8JO5Ozbnoe1AGBK9pKMWOAl+SY/b/CLLTgypwZwD/6xszM1MhcfzPS
aBbJ7MX2Uiq91/PNJdPnZI/PoqAQEzDL+5MZnwKwNpeC1rH8ZhlCn1BXbxI5jemw
Tfo2U6cDN1ObJ4LBzsVioWA0KoNnp4eWkMmbGGH5iWRcwoCjhkzot8VvXoll0uSe
F9v1RMOCM+Vcr++MYdJxdoQDNMunEoUnpHQbreHSLMcwPUhSNO4+EtZA86hob2u0
6yMXdAi9pEs9Aj13LAW74MCDrToCzoa2ZaisvxbRfQSpXryUQEnqpuQqCVjglxaJ
FIMhV0DRWIaLF9vhv6zF9kL77qr+arLd/wJlXubtD/P9tJZRlEh6/0iHvyyH2+Rg
u05//UQ7ex/j15PLFSVkQXIFPpN1ZgN0FrJKAJOL+MWiB5RncKxjin8Y9xfC3XKS
fbV6c7J9AGi8bE8aFMM2ISg7v/dOQzcLPPScWbe5cTg=
-----END CERTIFICATE-----
+113
View File
@@ -109,6 +109,67 @@ describe('Vulnerabilities', () => {
);
});
it('denies expanding existing object with polluted keys', async () => {
const obj = await new Parse.Object('RCE', { a: { foo: [] } }).save();
await reconfigureServer({
requestKeywordDenylist: ['foo'],
});
obj.addUnique('a.foo', 'abc');
await expectAsync(obj.save()).toBeRejectedWith(
new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Prohibited keyword in request data: "foo".`)
);
});
it('denies creating a cloud trigger with polluted data', async () => {
Parse.Cloud.beforeSave('TestObject', ({ object }) => {
object.set('obj', {
constructor: {
prototype: {
dummy: 0,
},
},
});
});
await expectAsync(new Parse.Object('TestObject').save()).toBeRejectedWith(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
'Prohibited keyword in request data: {"key":"constructor"}.'
)
);
});
it('denies creating a hook with polluted data', async () => {
const express = require('express');
const bodyParser = require('body-parser');
const port = 34567;
const hookServerURL = 'http://localhost:' + port;
const app = express();
app.use(bodyParser.json({ type: '*/*' }));
const server = await new Promise(resolve => {
const res = app.listen(port, undefined, () => resolve(res));
});
app.post('/BeforeSave', function (req, res) {
const object = Parse.Object.fromJSON(req.body.object);
object.set('hello', 'world');
object.set('obj', {
constructor: {
prototype: {
dummy: 0,
},
},
});
res.json({ success: object });
});
await Parse.Hooks.createTrigger('TestObject', 'beforeSave', hookServerURL + '/BeforeSave');
await expectAsync(new Parse.Object('TestObject').save()).toBeRejectedWith(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
'Prohibited keyword in request data: {"key":"constructor"}.'
)
);
await new Promise(resolve => server.close(resolve));
});
it('allows BSON type code data in write request with custom denylist', async () => {
await reconfigureServer({
requestKeywordDenylist: [],
@@ -279,5 +340,57 @@ describe('Vulnerabilities', () => {
expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
expect(text.error).toBe('Prohibited keyword in request data: {"value":"aValue[123]*"}.');
});
it('denies BSON type code data in file metadata', async () => {
const str = 'Hello World!';
const data = [];
for (let i = 0; i < str.length; i++) {
data.push(str.charCodeAt(i));
}
const file = new Parse.File('hello.txt', data, 'text/plain');
file.addMetadata('obj', {
_bsontype: 'Code',
code: 'delete Object.prototype.evalFunctions',
});
await expectAsync(file.save()).toBeRejectedWith(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
`Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.`
)
);
});
it('denies BSON type code data in file tags', async () => {
const str = 'Hello World!';
const data = [];
for (let i = 0; i < str.length; i++) {
data.push(str.charCodeAt(i));
}
const file = new Parse.File('hello.txt', data, 'text/plain');
file.addTag('obj', {
_bsontype: 'Code',
code: 'delete Object.prototype.evalFunctions',
});
await expectAsync(file.save()).toBeRejectedWith(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
`Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.`
)
);
});
});
describe('Ignore non-matches', () => {
it('ignores write request that contains only fraction of denied keyword', async () => {
await reconfigureServer({
requestKeywordDenylist: [{ key: 'abc' }],
});
// Initially saving an object executes the keyword detection in RestWrite.js
const obj = new TestObject({ a: { b: { c: 0 } } });
await expectAsync(obj.save()).toBeResolved();
// Modifying a nested key executes the keyword detection in DatabaseController.js
obj.increment('a.b.c');
await expectAsync(obj.save()).toBeResolved();
});
});
});
+2 -10
View File
@@ -5,6 +5,7 @@ const Parse = require('parse/node').Parse;
const jwksClient = require('jwks-rsa');
const util = require('util');
const jwt = require('jsonwebtoken');
const authUtils = require('./utils');
const TOKEN_ISSUER = 'https://appleid.apple.com';
@@ -30,21 +31,12 @@ const getAppleKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
return key;
};
const getHeaderFromToken = token => {
const decodedToken = jwt.decode(token, { complete: true });
if (!decodedToken) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
}
return decodedToken.header;
};
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
if (!token) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
}
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
const ONE_HOUR_IN_MS = 3600000;
let jwtClaims;
+12 -19
View File
@@ -5,6 +5,7 @@ const jwksClient = require('jwks-rsa');
const util = require('util');
const jwt = require('jsonwebtoken');
const httpsRequest = require('./httpsRequest');
const authUtils = require('./utils');
const TOKEN_ISSUER = 'https://facebook.com';
@@ -32,22 +33,23 @@ function validateGraphToken(authData, options) {
});
}
function validateGraphAppId(appIds, authData, options) {
async function validateGraphAppId(appIds, authData, options) {
var access_token = authData.access_token;
if (process.env.TESTING && access_token === 'test') {
return Promise.resolve();
return;
}
if (!Array.isArray(appIds)) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.');
}
if (!appIds.length) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Facebook auth is not configured.');
}
return graphRequest(
'app?access_token=' + access_token + getAppSecretPath(authData, options)
).then(data => {
if (data && appIds.indexOf(data.id) != -1) {
return;
}
const data = await graphRequest(
`app?access_token=${access_token}${getAppSecretPath(authData, options)}`
);
if (!data || !appIds.includes(data.id)) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Facebook auth is invalid for this user.');
});
}
}
const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
@@ -72,21 +74,12 @@ const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
return key;
};
const getHeaderFromToken = token => {
const decodedToken = jwt.decode(token, { complete: true });
if (!decodedToken) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'provided token does not decode as JWT');
}
return decodedToken.header;
};
const verifyIdToken = async ({ token, id }, { clientId, cacheMaxEntries, cacheMaxAge }) => {
if (!token) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'id token is invalid for this user.');
}
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
const ONE_HOUR_IN_MS = 3600000;
let jwtClaims;
+100 -26
View File
@@ -14,20 +14,17 @@ const authData = {
const { Parse } = require('parse/node');
const crypto = require('crypto');
const https = require('https');
const url = require('url');
const { pki } = require('node-forge');
const ca = { cert: null, url: null };
const cache = {}; // (publicKey -> cert) cache
function verifyPublicKeyUrl(publicKeyUrl) {
const parsedUrl = url.parse(publicKeyUrl);
if (parsedUrl.protocol !== 'https:') {
try {
const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
return regex.test(publicKeyUrl);
} catch (error) {
return false;
}
const hostnameParts = parsedUrl.hostname.split('.');
const length = hostnameParts.length;
const domainParts = hostnameParts.slice(length - 2, length);
const domain = domainParts.join('.');
return domain === 'apple.com';
}
function convertX509CertToPEM(X509Cert) {
@@ -40,7 +37,7 @@ function convertX509CertToPEM(X509Cert) {
return pemPreFix + certBody + pemPostFix;
}
function getAppleCertificate(publicKeyUrl) {
async function getAppleCertificate(publicKeyUrl) {
if (!verifyPublicKeyUrl(publicKeyUrl)) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
@@ -50,26 +47,59 @@ function getAppleCertificate(publicKeyUrl) {
if (cache[publicKeyUrl]) {
return cache[publicKeyUrl];
}
const url = new URL(publicKeyUrl);
const headOptions = {
hostname: url.hostname,
path: url.pathname,
method: 'HEAD',
};
const cert_headers = await new Promise((resolve, reject) =>
https.get(headOptions, res => resolve(res.headers)).on('error', reject)
);
const validContentTypes = ['application/x-x509-ca-cert', 'application/pkix-cert'];
if (
!validContentTypes.includes(cert_headers['content-type']) ||
cert_headers['content-length'] == null ||
cert_headers['content-length'] > 10000
) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
);
}
const { certificate, headers } = await getCertificate(publicKeyUrl);
if (headers['cache-control']) {
const expire = headers['cache-control'].match(/max-age=([0-9]+)/);
if (expire) {
cache[publicKeyUrl] = certificate;
// we'll expire the cache entry later, as per max-age
setTimeout(() => {
delete cache[publicKeyUrl];
}, parseInt(expire[1], 10) * 1000);
}
}
return verifyPublicKeyIssuer(certificate, publicKeyUrl);
}
function getCertificate(url, buffer) {
return new Promise((resolve, reject) => {
https
.get(publicKeyUrl, res => {
let data = '';
.get(url, res => {
const data = [];
res.on('data', chunk => {
data += chunk.toString('base64');
data.push(chunk);
});
res.on('end', () => {
const cert = convertX509CertToPEM(data);
if (res.headers['cache-control']) {
var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
if (expire) {
cache[publicKeyUrl] = cert;
// we'll expire the cache entry later, as per max-age
setTimeout(() => {
delete cache[publicKeyUrl];
}, parseInt(expire[1], 10) * 1000);
}
if (buffer) {
resolve({ certificate: Buffer.concat(data), headers: res.headers });
return;
}
resolve(cert);
let cert = '';
for (const chunk of data) {
cert += chunk.toString('base64');
}
const certificate = convertX509CertToPEM(cert);
resolve({ certificate, headers: res.headers });
});
})
.on('error', reject);
@@ -100,6 +130,30 @@ function verifySignature(publicKey, authData) {
}
}
function verifyPublicKeyIssuer(cert, publicKeyUrl) {
const publicKeyCert = pki.certificateFromPem(cert);
if (!ca.cert) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
);
}
try {
if (!ca.cert.verify(publicKeyCert)) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
);
}
} catch (e) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
);
}
return cert;
}
// Returns a promise that fulfills if this user id is valid.
async function validateAuthData(authData) {
if (!authData.id) {
@@ -111,11 +165,31 @@ async function validateAuthData(authData) {
}
// Returns a promise that fulfills if this app id is valid.
function validateAppId() {
return Promise.resolve();
async function validateAppId(appIds, authData, options = {}) {
if (!options.rootCertificateUrl) {
options.rootCertificateUrl =
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
}
if (ca.url === options.rootCertificateUrl) {
return;
}
const { certificate, headers } = await getCertificate(options.rootCertificateUrl, true);
if (
headers['content-type'] !== 'application/x-pem-file' ||
headers['content-length'] == null ||
headers['content-length'] > 10000
) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
);
}
ca.cert = pki.certificateFromPem(certificate);
ca.url = options.rootCertificateUrl;
}
module.exports = {
validateAppId,
validateAuthData,
cache,
};
+2 -11
View File
@@ -5,6 +5,7 @@ var Parse = require('parse/node').Parse;
const https = require('https');
const jwt = require('jsonwebtoken');
const authUtils = require('./utils');
const TOKEN_ISSUER = 'accounts.google.com';
const HTTPS_TOKEN_ISSUER = 'https://accounts.google.com';
@@ -51,22 +52,12 @@ function getGoogleKeyByKeyId(keyId) {
});
}
function getHeaderFromToken(token) {
const decodedToken = jwt.decode(token, { complete: true });
if (!decodedToken) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
}
return decodedToken.header;
}
async function verifyIdToken({ id_token: token, id }, { clientId }) {
if (!token) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`);
}
const { kid: keyId, alg: algorithm } = getHeaderFromToken(token);
const { kid: keyId, alg: algorithm } = authUtils.getHeaderFromToken(token);
let jwtClaims;
const googleKey = await getGoogleKeyByKeyId(keyId);
+1 -2
View File
@@ -54,7 +54,6 @@
*/
const Parse = require('parse/node').Parse;
const url = require('url');
const querystring = require('querystring');
const httpsRequest = require('./httpsRequest');
@@ -112,7 +111,7 @@ function requestTokenInfo(options, access_token) {
if (!options || !options.tokenIntrospectionEndpointUrl) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, MISSING_URL);
}
const parsedUrl = url.parse(options.tokenIntrospectionEndpointUrl);
const parsedUrl = new URL(options.tokenIntrospectionEndpointUrl);
const postData = querystring.stringify({
token: access_token,
});
+8 -7
View File
@@ -13,17 +13,18 @@ function validateAuthData(authData) {
}
// Returns a promise that fulfills if this app id is valid.
function validateAppId(appIds, authData) {
var access_token = authData.access_token;
async function validateAppId(appIds, authData) {
const access_token = authData.access_token;
if (!Array.isArray(appIds)) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.');
}
if (!appIds.length) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is not configured.');
}
return request('me', access_token).then(data => {
if (data && appIds.indexOf(data.id) != -1) {
return;
}
const data = await request('me', access_token);
if (!data || !appIds.includes(data.id)) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is invalid for this user.');
});
}
}
// A promisey wrapper for Spotify API requests.
+13
View File
@@ -0,0 +1,13 @@
const jwt = require('jsonwebtoken');
const Parse = require('parse/node').Parse;
const getHeaderFromToken = token => {
const decodedToken = jwt.decode(token, { complete: true });
if (!decodedToken) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`);
}
return decodedToken.header;
};
module.exports = {
getHeaderFromToken,
};
+1 -1
View File
@@ -5,7 +5,7 @@ export class LRUCache {
constructor({ ttl = defaults.cacheTTL, maxSize = defaults.cacheMaxSize }) {
this.cache = new LRU({
max: maxSize,
maxAge: ttl,
ttl,
});
}
+24 -11
View File
@@ -1,6 +1,6 @@
/**
GridFSBucketAdapter
Stores files in Mongo using GridStore
Stores files in Mongo using GridFS
Requires the database adapter to be based on mongoclient
@flow weak
@@ -228,22 +228,35 @@ export class GridFSBucketAdapter extends FilesAdapter {
const partialstart = parts[0];
const partialend = parts[1];
const start = parseInt(partialstart, 10);
const end = partialend ? parseInt(partialend, 10) : files[0].length - 1;
const fileLength = files[0].length;
const fileStart = parseInt(partialstart, 10);
const fileEnd = partialend ? parseInt(partialend, 10) : fileLength;
res.writeHead(206, {
'Accept-Ranges': 'bytes',
'Content-Length': end - start + 1,
'Content-Range': 'bytes ' + start + '-' + end + '/' + files[0].length,
'Content-Type': contentType,
});
let start = Math.min(fileStart || 0, fileEnd, fileLength);
let end = Math.max(fileStart || 0, fileEnd) + 1 || fileLength;
if (isNaN(fileStart)) {
start = fileLength - end + 1;
end = fileLength;
}
end = Math.min(end, fileLength);
start = Math.max(start, 0);
res.status(206);
res.header('Accept-Ranges', 'bytes');
res.header('Content-Length', end - start);
res.header('Content-Range', 'bytes ' + start + '-' + end + '/' + fileLength);
res.header('Content-Type', contentType);
const stream = bucket.openDownloadStreamByName(filename);
stream.start(start);
if (end) {
stream.end(end);
}
stream.on('data', chunk => {
res.write(chunk);
});
stream.on('error', () => {
res.sendStatus(404);
stream.on('error', (e) => {
res.status(404);
res.send(e.message);
});
stream.on('end', () => {
res.end();
+4 -181
View File
@@ -1,181 +1,4 @@
/**
GridStoreAdapter
Stores files in Mongo using GridStore
Requires the database adapter to be based on mongoclient
(GridStore is deprecated, Please use GridFSBucket instead)
@flow weak
*/
// @flow-disable-next
import { MongoClient, GridStore, Db } from 'mongodb';
import { FilesAdapter, validateFilename } from './FilesAdapter';
import defaults from '../../defaults';
export class GridStoreAdapter extends FilesAdapter {
_databaseURI: string;
_connectionPromise: Promise<Db>;
_mongoOptions: Object;
constructor(mongoDatabaseURI = defaults.DefaultMongoURI, mongoOptions = {}) {
super();
this._databaseURI = mongoDatabaseURI;
const defaultMongoOptions = {
useNewUrlParser: true,
useUnifiedTopology: true,
};
this._mongoOptions = Object.assign(defaultMongoOptions, mongoOptions);
}
_connect() {
if (!this._connectionPromise) {
this._connectionPromise = MongoClient.connect(this._databaseURI, this._mongoOptions).then(
client => {
this._client = client;
return client.db(client.s.options.dbName);
}
);
}
return this._connectionPromise;
}
// For a given config object, filename, and data, store a file
// Returns a promise
createFile(filename: string, data) {
return this._connect()
.then(database => {
const gridStore = new GridStore(database, filename, 'w');
return gridStore.open();
})
.then(gridStore => {
return gridStore.write(data);
})
.then(gridStore => {
return gridStore.close();
});
}
deleteFile(filename: string) {
return this._connect()
.then(database => {
const gridStore = new GridStore(database, filename, 'r');
return gridStore.open();
})
.then(gridStore => {
return gridStore.unlink();
})
.then(gridStore => {
return gridStore.close();
});
}
getFileData(filename: string) {
return this._connect()
.then(database => {
return GridStore.exist(database, filename).then(() => {
const gridStore = new GridStore(database, filename, 'r');
return gridStore.open();
});
})
.then(gridStore => {
return gridStore.read();
});
}
getFileLocation(config, filename) {
return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename);
}
async handleFileStream(filename: string, req, res, contentType) {
const stream = await this._connect().then(database => {
return GridStore.exist(database, filename).then(() => {
const gridStore = new GridStore(database, filename, 'r');
return gridStore.open();
});
});
handleRangeRequest(stream, req, res, contentType);
}
handleShutdown() {
if (!this._client) {
return Promise.resolve();
}
return this._client.close(false);
}
validateFilename(filename) {
return validateFilename(filename);
}
}
// handleRangeRequest is licensed under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/).
// Author: LEROIB at weightingformypizza (https://weightingformypizza.wordpress.com/2015/06/24/stream-html5-media-content-like-video-audio-from-mongodb-using-express-and-gridstore/).
function handleRangeRequest(stream, req, res, contentType) {
const buffer_size = 1024 * 1024; //1024Kb
// Range request, partial stream the file
const parts = req
.get('Range')
.replace(/bytes=/, '')
.split('-');
let [start, end] = parts;
const notEnded = !end && end !== 0;
const notStarted = !start && start !== 0;
// No end provided, we want all bytes
if (notEnded) {
end = stream.length - 1;
}
// No start provided, we're reading backwards
if (notStarted) {
start = stream.length - end;
end = start + end - 1;
}
// Data exceeds the buffer_size, cap
if (end - start >= buffer_size) {
end = start + buffer_size - 1;
}
const contentLength = end - start + 1;
res.writeHead(206, {
'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length,
'Accept-Ranges': 'bytes',
'Content-Length': contentLength,
'Content-Type': contentType,
});
stream.seek(start, function () {
// Get gridFile stream
const gridFileStream = stream.stream(true);
let bufferAvail = 0;
let remainingBytesToWrite = contentLength;
let totalBytesWritten = 0;
// Write to response
gridFileStream.on('data', function (data) {
bufferAvail += data.length;
if (bufferAvail > 0) {
// slice returns the same buffer if overflowing
// safe to call in any case
const buffer = data.slice(0, remainingBytesToWrite);
// Write the buffer
res.write(buffer);
// Increment total
totalBytesWritten += buffer.length;
// Decrement remaining
remainingBytesToWrite -= data.length;
// Decrement the available buffer
bufferAvail -= buffer.length;
}
// In case of small slices, all values will be good at that point
// we've written enough, end...
if (totalBytesWritten >= contentLength) {
stream.close();
res.end();
this.destroy();
}
});
});
}
export default GridStoreAdapter;
// Note: GridStore was replaced by GridFSBucketAdapter by default in 2018 by @flovilmart
throw new Error(
'GridStoreAdapter: GridStore is no longer supported by parse server and mongodb, use GridFSBucketAdapter instead.'
);
@@ -177,7 +177,7 @@ class MongoSchemaCollection {
insertSchema(schema: any) {
return this._collection
.insertOne(schema)
.then(result => mongoSchemaToParseSchema(result.ops[0]))
.then(() => mongoSchemaToParseSchema(schema))
.catch(error => {
if (error.code === 11000) {
//Mongo's duplicate key error
@@ -479,6 +479,7 @@ export class MongoStorageAdapter implements StorageAdapter {
const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema);
return this._adaptiveCollection(className)
.then(collection => collection.insertOne(mongoObject, transactionalSession))
.then(() => ({ ops: [mongoObject] }))
.catch(error => {
if (error.code === 11000) {
// Duplicate value
@@ -517,8 +518,8 @@ export class MongoStorageAdapter implements StorageAdapter {
})
.catch(err => this.handleError(err))
.then(
({ result }) => {
if (result.n === 0) {
({ deletedCount }) => {
if (deletedCount === 0) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
}
return Promise.resolve();
@@ -951,6 +952,9 @@ export class MongoStorageAdapter implements StorageAdapter {
// an operator in it (like $gt, $lt, etc). Because of this I felt it was easier to make this a
// recursive method to traverse down to the "leaf node" which is going to be the string.
_convertToDate(value: any): any {
if (value instanceof Date) {
return value;
}
if (typeof value === 'string') {
return new Date(value);
}
+2 -129
View File
@@ -2,6 +2,7 @@ import log from '../../../logger';
import _ from 'lodash';
var mongodb = require('mongodb');
var Parse = require('parse/node').Parse;
const Utils = require('../../../Utils');
const transformKey = (className, fieldName, schema) => {
// Check if the schema is known since it's a built-in field.
@@ -634,133 +635,6 @@ function transformTopLevelAtom(atom, field) {
}
}
function relativeTimeToDate(text, now = new Date()) {
text = text.toLowerCase();
let parts = text.split(' ');
// Filter out whitespace
parts = parts.filter(part => part !== '');
const future = parts[0] === 'in';
const past = parts[parts.length - 1] === 'ago';
if (!future && !past && text !== 'now') {
return {
status: 'error',
info: "Time should either start with 'in' or end with 'ago'",
};
}
if (future && past) {
return {
status: 'error',
info: "Time cannot have both 'in' and 'ago'",
};
}
// strip the 'ago' or 'in'
if (future) {
parts = parts.slice(1);
} else {
// past
parts = parts.slice(0, parts.length - 1);
}
if (parts.length % 2 !== 0 && text !== 'now') {
return {
status: 'error',
info: 'Invalid time string. Dangling unit or number.',
};
}
const pairs = [];
while (parts.length) {
pairs.push([parts.shift(), parts.shift()]);
}
let seconds = 0;
for (const [num, interval] of pairs) {
const val = Number(num);
if (!Number.isInteger(val)) {
return {
status: 'error',
info: `'${num}' is not an integer.`,
};
}
switch (interval) {
case 'yr':
case 'yrs':
case 'year':
case 'years':
seconds += val * 31536000; // 365 * 24 * 60 * 60
break;
case 'wk':
case 'wks':
case 'week':
case 'weeks':
seconds += val * 604800; // 7 * 24 * 60 * 60
break;
case 'd':
case 'day':
case 'days':
seconds += val * 86400; // 24 * 60 * 60
break;
case 'hr':
case 'hrs':
case 'hour':
case 'hours':
seconds += val * 3600; // 60 * 60
break;
case 'min':
case 'mins':
case 'minute':
case 'minutes':
seconds += val * 60;
break;
case 'sec':
case 'secs':
case 'second':
case 'seconds':
seconds += val;
break;
default:
return {
status: 'error',
info: `Invalid interval: '${interval}'`,
};
}
}
const milliseconds = seconds * 1000;
if (future) {
return {
status: 'success',
info: 'future',
result: new Date(now.valueOf() + milliseconds),
};
} else if (past) {
return {
status: 'success',
info: 'past',
result: new Date(now.valueOf() - milliseconds),
};
} else {
return {
status: 'success',
info: 'present',
result: new Date(now.valueOf()),
};
}
}
// Transforms a query constraint from REST API format to Mongo format.
// A constraint is something with fields like $lt.
// If it is not a valid constraint but it could be a valid something
@@ -813,7 +687,7 @@ function transformConstraint(constraint, field, count = false) {
);
}
const parserResult = relativeTimeToDate(val.$relativeTime);
const parserResult = Utils.relativeTimeToDate(val.$relativeTime);
if (parserResult.status === 'success') {
answer[key] = parserResult.result;
break;
@@ -1556,7 +1430,6 @@ module.exports = {
transformUpdate,
transformWhere,
mongoObjectToParseObject,
relativeTimeToDate,
transformConstraint,
transformPointerString,
};
@@ -1,18 +1,16 @@
const url = require('url');
const fs = require('fs');
function getDatabaseOptionsFromURI(uri) {
const databaseOptions = {};
const parsedURI = url.parse(uri);
const queryParams = parseQueryParams(parsedURI.query);
const authParts = parsedURI.auth ? parsedURI.auth.split(':') : [];
const parsedURI = new URL(uri);
const queryParams = parseQueryParams(parsedURI.searchParams.toString());
databaseOptions.host = parsedURI.hostname || 'localhost';
databaseOptions.port = parsedURI.port ? parseInt(parsedURI.port) : 5432;
databaseOptions.database = parsedURI.pathname ? parsedURI.pathname.substr(1) : undefined;
databaseOptions.user = authParts.length > 0 ? authParts[0] : '';
databaseOptions.password = authParts.length > 1 ? authParts[1] : '';
databaseOptions.user = parsedURI.username;
databaseOptions.password = parsedURI.password;
if (queryParams.ssl && queryParams.ssl.toLowerCase() === 'true') {
databaseOptions.ssl = true;
@@ -7,12 +7,14 @@ import _ from 'lodash';
// @flow-disable-next
import { v4 as uuidv4 } from 'uuid';
import sql from './sql';
import { StorageAdapter } from '../StorageAdapter';
import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter';
const Utils = require('../../../Utils');
const PostgresRelationDoesNotExistError = '42P01';
const PostgresDuplicateRelationError = '42P07';
const PostgresDuplicateColumnError = '42701';
const PostgresMissingColumnError = '42703';
const PostgresDuplicateObjectError = '42710';
const PostgresUniqueIndexViolationError = '23505';
const logger = require('../../../logger');
@@ -22,9 +24,6 @@ const debug = function (...args: any) {
log.debug.apply(log, args);
};
import { StorageAdapter } from '../StorageAdapter';
import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter';
const parseTypeToPostgresType = type => {
switch (type.type) {
case 'String':
@@ -92,6 +91,22 @@ const toPostgresValue = value => {
return value;
};
const toPostgresValueCastType = value => {
const postgresValue = toPostgresValue(value);
let castType;
switch (typeof postgresValue) {
case 'number':
castType = 'double precision';
break;
case 'boolean':
castType = 'boolean';
break;
default:
castType = undefined;
}
return castType;
};
const transformValue = value => {
if (typeof value === 'object' && value.__type === 'Pointer') {
return value.objectId;
@@ -370,9 +385,17 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
);
} else {
if (fieldName.indexOf('.') >= 0) {
const constraintFieldName = transformDotField(fieldName);
const castType = toPostgresValueCastType(fieldValue.$ne);
const constraintFieldName = castType
? `CAST ((${transformDotField(fieldName)}) AS ${castType})`
: transformDotField(fieldName);
patterns.push(
`(${constraintFieldName} <> $${index} OR ${constraintFieldName} IS NULL)`
`(${constraintFieldName} <> $${index + 1} OR ${constraintFieldName} IS NULL)`
);
} else if (typeof fieldValue.$ne === 'object' && fieldValue.$ne.$relativeTime) {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
'$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'
);
} else {
patterns.push(`($${index}:name <> $${index + 1} OR $${index}:name IS NULL)`);
@@ -397,8 +420,17 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
index += 1;
} else {
if (fieldName.indexOf('.') >= 0) {
const castType = toPostgresValueCastType(fieldValue.$eq);
const constraintFieldName = castType
? `CAST ((${transformDotField(fieldName)}) AS ${castType})`
: transformDotField(fieldName);
values.push(fieldValue.$eq);
patterns.push(`${transformDotField(fieldName)} = $${index++}`);
patterns.push(`${constraintFieldName} = $${index++}`);
} else if (typeof fieldValue.$eq === 'object' && fieldValue.$eq.$relativeTime) {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
'$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'
);
} else {
values.push(fieldName, fieldValue.$eq);
patterns.push(`$${index}:name = $${index + 1}`);
@@ -513,7 +545,12 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
}
if (typeof fieldValue.$exists !== 'undefined') {
if (fieldValue.$exists) {
if (typeof fieldValue.$exists === 'object' && fieldValue.$exists.$relativeTime) {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
'$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'
);
} else if (fieldValue.$exists) {
patterns.push(`$${index}:name IS NOT NULL`);
} else {
patterns.push(`$${index}:name IS NULL`);
@@ -757,24 +794,33 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus
Object.keys(ParseToPosgresComparator).forEach(cmp => {
if (fieldValue[cmp] || fieldValue[cmp] === 0) {
const pgComparator = ParseToPosgresComparator[cmp];
const postgresValue = toPostgresValue(fieldValue[cmp]);
let constraintFieldName;
let postgresValue = toPostgresValue(fieldValue[cmp]);
if (fieldName.indexOf('.') >= 0) {
let castType;
switch (typeof postgresValue) {
case 'number':
castType = 'double precision';
break;
case 'boolean':
castType = 'boolean';
break;
default:
castType = undefined;
}
const castType = toPostgresValueCastType(fieldValue[cmp]);
constraintFieldName = castType
? `CAST ((${transformDotField(fieldName)}) AS ${castType})`
: transformDotField(fieldName);
} else {
if (typeof postgresValue === 'object' && postgresValue.$relativeTime) {
if (schema.fields[fieldName].type !== 'Date') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
'$relativeTime can only be used with Date field'
);
}
const parserResult = Utils.relativeTimeToDate(postgresValue.$relativeTime);
if (parserResult.status === 'success') {
postgresValue = toPostgresValue(parserResult.result);
} else {
console.error('Error while parsing relative date', parserResult);
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $relativeTime (${postgresValue.$relativeTime}) value. ${parserResult.info}`
);
}
}
constraintFieldName = `$${index++}:name`;
values.push(fieldName);
}
@@ -873,15 +919,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
'CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )'
)
.catch(error => {
if (
error.code === PostgresDuplicateRelationError ||
error.code === PostgresUniqueIndexViolationError ||
error.code === PostgresDuplicateObjectError
) {
// Table already exists, must have been created by a different request. Ignore error.
} else {
throw error;
}
throw error;
});
}
@@ -2416,6 +2454,11 @@ export class PostgresStorageAdapter implements StorageAdapter {
? fieldNames.map((fieldName, index) => `lower($${index + 3}:name) varchar_pattern_ops`)
: fieldNames.map((fieldName, index) => `$${index + 3}:name`);
const qs = `CREATE INDEX IF NOT EXISTS $1:name ON $2:name (${constraintPatterns.join()})`;
const setIdempotencyFunction =
options.setIdempotencyFunction !== undefined ? options.setIdempotencyFunction : false;
if (setIdempotencyFunction) {
await this.ensureIdempotencyFunctionExists(options);
}
await conn.none(qs, [indexNameOptions.name, className, ...fieldNames]).catch(error => {
if (
error.code === PostgresDuplicateRelationError &&
@@ -2436,6 +2479,24 @@ export class PostgresStorageAdapter implements StorageAdapter {
}
});
}
async deleteIdempotencyFunction(options?: Object = {}): Promise<any> {
const conn = options.conn !== undefined ? options.conn : this._client;
const qs = 'DROP FUNCTION IF EXISTS idempotency_delete_expired_records()';
return conn.none(qs).catch(error => {
throw error;
});
}
async ensureIdempotencyFunctionExists(options?: Object = {}): Promise<any> {
const conn = options.conn !== undefined ? options.conn : this._client;
const ttlOptions = options.ttl !== undefined ? `${options.ttl} seconds` : '60 seconds';
const qs =
'CREATE OR REPLACE FUNCTION idempotency_delete_expired_records() RETURNS void LANGUAGE plpgsql AS $$ BEGIN DELETE FROM "_Idempotency" WHERE expire < NOW() - INTERVAL $1; END; $$;';
return conn.none(qs, [ttlOptions]).catch(error => {
throw error;
});
}
}
function convertPolygonToSQL(polygon) {
+9
View File
@@ -230,6 +230,15 @@ Auth.prototype.cacheRoles = function () {
return true;
};
Auth.prototype.clearRoleCache = function (sessionToken) {
if (!this.cacheController) {
return false;
}
this.cacheController.role.del(this.user.id);
this.cacheController.user.del(sessionToken);
return true;
};
Auth.prototype.getRolesByIds = async function (ins) {
const results = [];
// Build an OR query across all parentRoles
+15
View File
@@ -12,6 +12,7 @@ import {
PagesOptions,
SecurityOptions,
SchemaOptions,
ParseServerOptions,
} from './Options/Definitions';
import { isBoolean, isString } from 'lodash';
@@ -63,6 +64,7 @@ export class Config {
revokeSessionOnPasswordReset,
expireInactiveSessions,
sessionLength,
defaultLimit,
maxLimit,
emailVerifyTokenValidityDuration,
accountLockout,
@@ -110,6 +112,7 @@ export class Config {
}
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
this.validateMasterKeyIps(masterKeyIps);
this.validateDefaultLimit(defaultLimit);
this.validateMaxLimit(maxLimit);
this.validateAllowHeaders(allowHeaders);
this.validateIdempotencyOptions(idempotencyOptions);
@@ -453,6 +456,18 @@ export class Config {
}
}
static validateDefaultLimit(defaultLimit) {
if (defaultLimit == null) {
defaultLimit = ParseServerOptions.defaultLimit.default;
}
if (typeof defaultLimit !== 'number') {
throw 'Default limit must be a number.';
}
if (defaultLimit <= 0) {
throw 'Default limit must be a value greater than 0.';
}
}
static validateMaxLimit(maxLimit) {
if (maxLimit <= 0) {
throw 'Max limit must be a value greater than 0.';
+95 -73
View File
@@ -11,9 +11,11 @@ import intersect from 'intersect';
// @flow-disable-next
import deepcopy from 'deepcopy';
import logger from '../logger';
import Utils from '../Utils';
import * as SchemaController from './SchemaController';
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter';
import SchemaCache from '../Adapters/Cache/SchemaCache';
import type { LoadSchemaOptions } from './types';
import type { ParseServerOptions } from '../Options';
@@ -53,31 +55,27 @@ const transformObjectACL = ({ ACL, ...result }) => {
return result;
};
const specialQuerykeys = [
'$and',
'$or',
'$nor',
'_rperm',
'_wperm',
'_perishable_token',
const specialQueryKeys = ['$and', '$or', '$nor', '_rperm', '_wperm'];
const specialMasterQueryKeys = [
...specialQueryKeys,
'_email_verify_token',
'_perishable_token',
'_tombstone',
'_email_verify_token_expires_at',
'_account_lockout_expires_at',
'_failed_login_count',
'_account_lockout_expires_at',
'_password_changed_at',
'_password_history',
];
const isSpecialQueryKey = key => {
return specialQuerykeys.indexOf(key) >= 0;
};
const validateQuery = (query: any): void => {
const validateQuery = (query: any, isMaster: boolean, update: boolean): void => {
if (query.ACL) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
}
if (query.$or) {
if (query.$or instanceof Array) {
query.$or.forEach(validateQuery);
query.$or.forEach(value => validateQuery(value, isMaster, update));
} else {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.');
}
@@ -85,7 +83,7 @@ const validateQuery = (query: any): void => {
if (query.$and) {
if (query.$and instanceof Array) {
query.$and.forEach(validateQuery);
query.$and.forEach(value => validateQuery(value, isMaster, update));
} else {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.');
}
@@ -93,7 +91,7 @@ const validateQuery = (query: any): void => {
if (query.$nor) {
if (query.$nor instanceof Array && query.$nor.length > 0) {
query.$nor.forEach(validateQuery);
query.$nor.forEach(value => validateQuery(value, isMaster, update));
} else {
throw new Parse.Error(
Parse.Error.INVALID_QUERY,
@@ -113,7 +111,11 @@ const validateQuery = (query: any): void => {
}
}
}
if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
if (
!key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/) &&
((!specialQueryKeys.includes(key) && !isMaster && !update) ||
(update && isMaster && !specialMasterQueryKeys.includes(key)))
) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`);
}
});
@@ -125,7 +127,7 @@ const filterSensitiveData = (
aclGroup: any[],
auth: any,
operation: any,
schema: SchemaController.SchemaController,
schema: SchemaController.SchemaController | any,
className: string,
protectedFields: null | Array<any>,
object: any
@@ -134,7 +136,8 @@ const filterSensitiveData = (
if (auth && auth.user) userId = auth.user.id;
// replace protectedFields when using pointer-permissions
const perms = schema.getClassLevelPermissions(className);
const perms =
schema && schema.getClassLevelPermissions ? schema.getClassLevelPermissions(className) : {};
if (perms) {
const isReadOperation = ['get', 'find'].indexOf(operation) > -1;
@@ -205,27 +208,24 @@ const filterSensitiveData = (
perms.protectedFields.temporaryKeys.forEach(k => delete object[k]);
}
if (!isUserClass) {
return object;
if (isUserClass) {
object.password = object._hashed_password;
delete object._hashed_password;
delete object.sessionToken;
}
object.password = object._hashed_password;
delete object._hashed_password;
delete object.sessionToken;
if (isMaster) {
return object;
}
delete object._email_verify_token;
delete object._perishable_token;
delete object._perishable_token_expires_at;
delete object._tombstone;
delete object._email_verify_token_expires_at;
delete object._failed_login_count;
delete object._account_lockout_expires_at;
delete object._password_changed_at;
delete object._password_history;
for (const key in object) {
if (key.charAt(0) === '_') {
delete object[key];
}
}
if (!isUserClass) {
return object;
}
if (aclGroup.indexOf(object.objectId) > -1) {
return object;
@@ -364,12 +364,14 @@ class DatabaseController {
schemaPromise: ?Promise<SchemaController.SchemaController>;
_transactionalSession: ?any;
options: ParseServerOptions;
idempotencyOptions: any;
constructor(adapter: StorageAdapter, options: ParseServerOptions) {
this.adapter = adapter;
// We don't want a mutable this.schema, because then you could have
// one request that uses different schemas for different parts of
// it. Instead, use loadSchema to get a schema.
this.options = options || {};
this.idempotencyOptions = this.options.idempotencyOptions || {};
// Prevent mutable this.schema, otherwise one request could use
// multiple schemas, so instead use loadSchema to get a schema.
this.schemaPromise = null;
this._transactionalSession = null;
this.options = options;
@@ -510,7 +512,7 @@ class DatabaseController {
if (acl) {
query = addWriteACL(query, acl);
}
validateQuery(query);
validateQuery(query, isMaster, true);
return schemaController
.getOneSchema(className, true)
.catch(error => {
@@ -756,7 +758,7 @@ class DatabaseController {
if (acl) {
query = addWriteACL(query, acl);
}
validateQuery(query);
validateQuery(query, isMaster, false);
return schemaController
.getOneSchema(className)
.catch(error => {
@@ -862,7 +864,7 @@ class DatabaseController {
if (object[field] && object[field].__op && object[field].__op === 'Delete') {
return false;
}
return schemaFields.indexOf(field) < 0;
return schemaFields.indexOf(getRootFieldName(field)) < 0;
});
if (newKeys.length > 0) {
// adds a marker that new field is being adding during update
@@ -927,32 +929,32 @@ class DatabaseController {
reduceInRelation(className: string, query: any, schema: any): Promise<any> {
// Search for an in-relation or equal-to-relation
// Make it sequential for now, not sure of paralleization side effects
const promises = [];
if (query['$or']) {
const ors = query['$or'];
return Promise.all(
ors.map((aQuery, index) => {
promises.push(
...ors.map((aQuery, index) => {
return this.reduceInRelation(className, aQuery, schema).then(aQuery => {
query['$or'][index] = aQuery;
});
})
).then(() => {
return Promise.resolve(query);
});
);
}
if (query['$and']) {
const ors = query['$and'];
return Promise.all(
ors.map((aQuery, index) => {
const ands = query['$and'];
promises.push(
...ands.map((aQuery, index) => {
return this.reduceInRelation(className, aQuery, schema).then(aQuery => {
query['$and'][index] = aQuery;
});
})
).then(() => {
return Promise.resolve(query);
});
);
}
const promises = Object.keys(query).map(key => {
const otherKeys = Object.keys(query).map(key => {
if (key === '$and' || key === '$or') {
return;
}
const t = schema.getExpectedType(className, key);
if (!t || t.type !== 'Relation') {
return Promise.resolve(query);
@@ -1014,7 +1016,7 @@ class DatabaseController {
});
});
return Promise.all(promises).then(() => {
return Promise.all([...promises, ...otherKeys]).then(() => {
return Promise.resolve(query);
});
}
@@ -1205,6 +1207,9 @@ class DatabaseController {
`Invalid field name: ${fieldName}.`
);
}
if (!schema.fields[fieldName.split('.')[0]] && fieldName !== 'score') {
delete sort[fieldName];
}
});
return (isMaster
? Promise.resolve()
@@ -1248,7 +1253,7 @@ class DatabaseController {
query = addReadACL(query, aclGroup);
}
}
validateQuery(query);
validateQuery(query, isMaster, false);
if (count) {
if (!classExists) {
return 0;
@@ -1529,14 +1534,17 @@ class DatabaseController {
}
addProtectedFields(
schema: SchemaController.SchemaController,
schema: SchemaController.SchemaController | any,
className: string,
query: any = {},
aclGroup: any[] = [],
auth: any = {},
queryOptions: FullQueryOptions = {}
): null | string[] {
const perms = schema.getClassLevelPermissions(className);
const perms =
schema && schema.getClassLevelPermissions
? schema.getClassLevelPermissions(className)
: schema;
if (!perms) return null;
const protectedFields = perms.protectedFields;
@@ -1681,9 +1689,7 @@ class DatabaseController {
};
await this.loadSchema().then(schema => schema.enforceClassExists('_User'));
await this.loadSchema().then(schema => schema.enforceClassExists('_Role'));
if (this.adapter instanceof MongoStorageAdapter) {
await this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency'));
}
await this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency'));
await this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']).catch(error => {
logger.warn('Unable to ensure uniqueness for usernames: ', error);
@@ -1719,18 +1725,28 @@ class DatabaseController {
logger.warn('Unable to ensure uniqueness for role name: ', error);
throw error;
});
if (this.adapter instanceof MongoStorageAdapter) {
await this.adapter
.ensureUniqueness('_Idempotency', requiredIdempotencyFields, ['reqId'])
.catch(error => {
logger.warn('Unable to ensure uniqueness for idempotency request ID: ', error);
throw error;
});
await this.adapter
.ensureIndex('_Idempotency', requiredIdempotencyFields, ['expire'], 'ttl', false, {
await this.adapter
.ensureUniqueness('_Idempotency', requiredIdempotencyFields, ['reqId'])
.catch(error => {
logger.warn('Unable to ensure uniqueness for idempotency request ID: ', error);
throw error;
});
const isMongoAdapter = this.adapter instanceof MongoStorageAdapter;
const isPostgresAdapter = this.adapter instanceof PostgresStorageAdapter;
if (isMongoAdapter || isPostgresAdapter) {
let options = {};
if (isMongoAdapter) {
options = {
ttl: 0,
})
};
} else if (isPostgresAdapter) {
options = this.idempotencyOptions;
options.setIdempotencyFunction = true;
}
await this.adapter
.ensureIndex('_Idempotency', requiredIdempotencyFields, ['expire'], 'ttl', false, options)
.catch(error => {
logger.warn('Unable to create TTL index for idempotency expire date: ', error);
throw error;
@@ -1752,8 +1768,12 @@ class DatabaseController {
if (this.options && this.options.requestKeywordDenylist) {
// Scan request data for denied keywords
for (const keyword of this.options.requestKeywordDenylist) {
const isMatch = (a, b) => (typeof a === 'string' && new RegExp(a).test(b)) || a === b;
if (isMatch(firstKey, keyword.key)) {
const match = Utils.objectContainsKeyValue(
{ [firstKey]: true, [nextPath]: true },
keyword.key,
true
);
if (match) {
throw new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
@@ -1793,9 +1813,11 @@ class DatabaseController {
return Promise.resolve(response);
}
static _validateQuery: any => void;
static _validateQuery: (any, boolean, boolean) => void;
static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
}
module.exports = DatabaseController;
// Expose validateQuery for tests
module.exports._validateQuery = validateQuery;
module.exports.filterSensitiveData = filterSensitiveData;
+7
View File
@@ -56,6 +56,13 @@ export class LiveQueryController {
return false;
}
clearCachedRoles(user: any) {
if (!user) {
return;
}
return this.liveQueryPublisher.onClearCachedRoles(user);
}
_makePublisherRequest(currentObject: any, originalObject: any, classLevelPermissions: ?any): any {
const req = {
object: currentObject,
+6 -6
View File
@@ -1,7 +1,6 @@
import { Parse } from 'parse/node';
import AdaptableController from './AdaptableController';
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
import url from 'url';
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
const LOG_STRING_TRUNCATE_LENGTH = 1000;
@@ -38,15 +37,16 @@ export class LoggerController extends AdaptableController {
});
}
maskSensitiveUrl(urlString) {
const urlObj = url.parse(urlString, true);
const query = urlObj.query;
maskSensitiveUrl(path) {
const urlString = 'http://localhost' + path; // prepend dummy string to make a real URL
const urlObj = new URL(urlString);
const query = urlObj.searchParams;
let sanitizedQuery = '?';
for (const key in query) {
for (const [key, value] of query) {
if (key !== 'password') {
// normal value
sanitizedQuery += key + '=' + query[key] + '&';
sanitizedQuery += key + '=' + value + '&';
} else {
// password value, redact it
sanitizedQuery += key + '=' + '********' + '&';
+10 -3
View File
@@ -150,9 +150,15 @@ const defaultColumns: { [string]: SchemaFields } = Object.freeze({
},
});
// fields required for read or write operations on their respective classes.
const requiredColumns = Object.freeze({
_Product: ['productIdentifier', 'icon', 'order', 'title', 'subtitle'],
_Role: ['name', 'ACL'],
read: {
_User: ['username'],
},
write: {
_Product: ['productIdentifier', 'icon', 'order', 'title', 'subtitle'],
_Role: ['name', 'ACL'],
},
});
const invalidColumns = ['length'];
@@ -1269,7 +1275,7 @@ export default class SchemaController {
// Validates that all the properties are set for the object
validateRequiredColumns(className: string, object: any, query: any) {
const columns = requiredColumns[className];
const columns = requiredColumns.write[className];
if (!columns || columns.length == 0) {
return Promise.resolve(this);
}
@@ -1600,4 +1606,5 @@ export {
convertSchemaToAdapterSchema,
VolatileClassesSchemas,
SchemaController,
requiredColumns,
};
+12 -4
View File
@@ -2,7 +2,6 @@ import authDataManager from '../Adapters/Auth';
import { ParseServerOptions } from '../Options';
import { loadAdapter } from '../Adapters/AdapterLoader';
import defaults from '../defaults';
import url from 'url';
// Controllers
import { LoggerController } from './LoggerController';
import { FilesController } from './FilesController';
@@ -92,12 +91,20 @@ export function getLoggerController(options: ParseServerOptions): LoggerControll
}
export function getFilesController(options: ParseServerOptions): FilesController {
const { appId, databaseURI, filesAdapter, databaseAdapter, preserveFileName, fileKey } = options;
const {
appId,
databaseURI,
databaseOptions = {},
filesAdapter,
databaseAdapter,
preserveFileName,
fileKey,
} = options;
if (!filesAdapter && databaseAdapter) {
throw 'When using an explicit database adapter, you must also use an explicit filesAdapter.';
}
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
return new GridFSBucketAdapter(databaseURI, {}, fileKey);
return new GridFSBucketAdapter(databaseURI, databaseOptions, fileKey);
});
return new FilesController(filesControllerAdapter, appId, {
preserveFileName,
@@ -220,13 +227,14 @@ export function getAuthDataManager(options: ParseServerOptions) {
export function getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions) {
let protocol;
try {
const parsedURI = url.parse(databaseURI);
const parsedURI = new URL(databaseURI);
protocol = parsedURI.protocol ? parsedURI.protocol.toLowerCase() : null;
} catch (e) {
/* */
}
switch (protocol) {
case 'postgres:':
case 'postgresql:':
return new PostgresStorageAdapter({
uri: databaseURI,
collectionPrefix,
+1
View File
@@ -23,4 +23,5 @@ module.exports = [
"Additionally, the environment variable 'PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS' will be deprecated and renamed to 'PARSE_SERVER_DIRECT_ACCESS' in a future version; it is currently possible to use either one.",
},
{ optionKey: 'enforcePrivateUsers', changeNewDefault: 'true' },
{ optionKey: 'allowClientClassCreation', changeNewDefault: 'false' },
];
+34 -66
View File
@@ -1,8 +1,8 @@
import Parse from 'parse/node';
import { GraphQLSchema, GraphQLObjectType, DocumentNode, GraphQLNamedType } from 'graphql';
import { stitchSchemas } from '@graphql-tools/stitch';
import { mergeSchemas } from '@graphql-tools/schema';
import { mergeTypeDefs } from '@graphql-tools/merge';
import { isDeepStrictEqual } from 'util';
import { SchemaDirectiveVisitor } from '@graphql-tools/utils';
import requiredParameter from '../requiredParameter';
import * as defaultGraphQLTypes from './loaders/defaultGraphQLTypes';
import * as parseClassTypes from './loaders/parseClassTypes';
@@ -89,14 +89,19 @@ class ParseGraphQLSchema {
this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs;
this.appId = params.appId || requiredParameter('You must provide the appId!');
this.schemaCache = SchemaCache;
this.logCache = {};
}
async load() {
const { parseGraphQLConfig } = await this._initializeSchemaAndConfig();
const parseClasses = await this._getClassesForSchema(parseGraphQLConfig);
const parseClassesArray = await this._getClassesForSchema(parseGraphQLConfig);
const functionNames = await this._getFunctionNames();
const functionNamesString = JSON.stringify(functionNames);
const functionNamesString = functionNames.join();
const parseClasses = parseClassesArray.reduce((acc, clazz) => {
acc[clazz.className] = clazz;
return acc;
}, {});
if (
!this._hasSchemaInputChanged({
parseClasses,
@@ -127,7 +132,7 @@ class ParseGraphQLSchema {
defaultRelaySchema.load(this);
schemaTypes.load(this);
this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach(
this._getParseClassesWithConfig(parseClassesArray, parseGraphQLConfig).forEach(
([parseClass, parseClassConfig]) => {
// Some times schema return the _auth_data_ field
// it will lead to unstable graphql generation order
@@ -155,7 +160,7 @@ class ParseGraphQLSchema {
}
);
defaultGraphQLTypes.loadArrayResult(this, parseClasses);
defaultGraphQLTypes.loadArrayResult(this, parseClassesArray);
defaultGraphQLQueries.load(this);
defaultGraphQLMutations.load(this);
@@ -198,9 +203,8 @@ class ParseGraphQLSchema {
if (this.graphQLCustomTypeDefs) {
schemaDirectives.load(this);
if (typeof this.graphQLCustomTypeDefs.getTypeMap === 'function') {
// In following code we use underscore attr to avoid js var un ref
// In following code we use underscore attr to keep the direct variable reference
const customGraphQLSchemaTypeMap = this.graphQLCustomTypeDefs._typeMap;
const findAndReplaceLastType = (parent, key) => {
if (parent[key].name) {
@@ -275,51 +279,18 @@ class ParseGraphQLSchema {
this.graphQLSchema = await this.graphQLCustomTypeDefs({
directivesDefinitionsSchema: this.graphQLSchemaDirectivesDefinitions,
autoSchema: this.graphQLAutoSchema,
stitchSchemas,
graphQLSchemaDirectives: this.graphQLSchemaDirectives,
});
} else {
this.graphQLSchema = stitchSchemas({
schemas: [
this.graphQLSchemaDirectivesDefinitions,
this.graphQLAutoSchema,
this.graphQLSchema = mergeSchemas({
schemas: [this.graphQLAutoSchema],
typeDefs: mergeTypeDefs([
this.graphQLCustomTypeDefs,
],
mergeDirectives: true,
this.graphQLSchemaDirectivesDefinitions,
]),
});
this.graphQLSchema = this.graphQLSchemaDirectives(this.graphQLSchema);
}
// Only merge directive when string schema provided
const graphQLSchemaTypeMap = this.graphQLSchema.getTypeMap();
Object.keys(graphQLSchemaTypeMap).forEach(graphQLSchemaTypeName => {
const graphQLSchemaType = graphQLSchemaTypeMap[graphQLSchemaTypeName];
if (
typeof graphQLSchemaType.getFields === 'function' &&
this.graphQLCustomTypeDefs.definitions
) {
const graphQLCustomTypeDef = this.graphQLCustomTypeDefs.definitions.find(
definition => definition.name.value === graphQLSchemaTypeName
);
if (graphQLCustomTypeDef) {
const graphQLSchemaTypeFieldMap = graphQLSchemaType.getFields();
Object.keys(graphQLSchemaTypeFieldMap).forEach(graphQLSchemaTypeFieldName => {
const graphQLSchemaTypeField = graphQLSchemaTypeFieldMap[graphQLSchemaTypeFieldName];
if (!graphQLSchemaTypeField.astNode) {
const astNode = graphQLCustomTypeDef.fields.find(
field => field.name.value === graphQLSchemaTypeFieldName
);
if (astNode) {
graphQLSchemaTypeField.astNode = astNode;
}
}
});
}
}
});
SchemaDirectiveVisitor.visitSchemaDirectives(
this.graphQLSchema,
this.graphQLSchemaDirectives
);
} else {
this.graphQLSchema = this.graphQLAutoSchema;
}
@@ -327,6 +298,14 @@ class ParseGraphQLSchema {
return this.graphQLSchema;
}
_logOnce(severity, message) {
if (this.logCache[message]) {
return;
}
this.log[severity](message);
this.logCache[message] = true;
}
addGraphQLType(type, throwError = false, ignoreReserved = false, ignoreConnection = false) {
if (
(!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) ||
@@ -337,7 +316,7 @@ class ParseGraphQLSchema {
if (throwError) {
throw new Error(message);
}
this.log.warn(message);
this._logOnce('warn', message);
return undefined;
}
this.graphQLTypes.push(type);
@@ -353,7 +332,7 @@ class ParseGraphQLSchema {
if (throwError) {
throw new Error(message);
}
this.log.warn(message);
this._logOnce('warn', message);
return undefined;
}
this.graphQLQueries[fieldName] = field;
@@ -369,7 +348,7 @@ class ParseGraphQLSchema {
if (throwError) {
throw new Error(message);
}
this.log.warn(message);
this._logOnce('warn', message);
return undefined;
}
this.graphQLMutations[fieldName] = field;
@@ -478,7 +457,8 @@ class ParseGraphQLSchema {
if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(functionName)) {
return true;
} else {
this.log.warn(
this._logOnce(
'warn',
`Function ${functionName} could not be added to the auto schema because GraphQL names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/.`
);
return false;
@@ -500,29 +480,17 @@ class ParseGraphQLSchema {
const { parseClasses, parseGraphQLConfig, functionNamesString } = params;
// First init
if (!this.parseCachedClasses || !this.graphQLSchema) {
const thisParseClassesObj = parseClasses.reduce((acc, clzz) => {
acc[clzz.className] = clzz;
return acc;
}, {});
this.parseCachedClasses = thisParseClassesObj;
if (!this.graphQLSchema) {
return true;
}
const newParseCachedClasses = parseClasses.reduce((acc, clzz) => {
acc[clzz.className] = clzz;
return acc;
}, {});
if (
isDeepStrictEqual(this.parseGraphQLConfig, parseGraphQLConfig) &&
this.functionNamesString === functionNamesString &&
isDeepStrictEqual(this.parseCachedClasses, newParseCachedClasses)
isDeepStrictEqual(this.parseClasses, parseClasses)
) {
return false;
}
this.parseCachedClasses = newParseCachedClasses;
return true;
}
}
+30 -30
View File
@@ -1,8 +1,5 @@
import corsMiddleware from 'cors';
import bodyParser from 'body-parser';
import { graphqlUploadExpress } from 'graphql-upload';
import { graphqlExpress } from 'apollo-server-express/dist/expressApollo';
import { renderPlaygroundPage } from '@apollographql/graphql-playground-html';
import { createServer, renderGraphiQL } from '@graphql-yoga/node';
import { execute, subscribe } from 'graphql';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { handleParseErrors, handleParseHeaders } from '../middlewares';
@@ -32,18 +29,20 @@ class ParseGraphQLServer {
});
}
async _getGraphQLOptions(req) {
async _getGraphQLOptions() {
try {
return {
schema: await this.parseGraphQLSchema.load(),
context: {
info: req.info,
config: req.config,
auth: req.auth,
},
formatError: error => {
// Allow to console.log here to debug
return error;
context: ({ req: { info, config, auth } }) => ({
info,
config,
auth,
}),
maskedErrors: false,
multipart: {
fileSize: this._transformMaxUploadSizeToBytes(
this.parseServer.config.maxUploadSize || '20mb'
),
},
};
} catch (e) {
@@ -52,6 +51,17 @@ class ParseGraphQLServer {
}
}
async _getServer() {
const schemaRef = this.parseGraphQLSchema.graphQLSchema;
const newSchemaRef = await this.parseGraphQLSchema.load();
if (schemaRef === newSchemaRef && this._server) {
return this._server;
}
const options = await this._getGraphQLOptions();
this._server = createServer(options);
return this._server;
}
_transformMaxUploadSizeToBytes(maxUploadSize) {
const unitMap = {
kb: 1,
@@ -70,22 +80,13 @@ class ParseGraphQLServer {
requiredParameter('You must provide an Express.js app instance!');
}
app.use(
this.config.graphQLPath,
graphqlUploadExpress({
maxFileSize: this._transformMaxUploadSizeToBytes(
this.parseServer.config.maxUploadSize || '20mb'
),
})
);
app.use(this.config.graphQLPath, corsMiddleware());
app.use(this.config.graphQLPath, bodyParser.json());
app.use(this.config.graphQLPath, handleParseHeaders);
app.use(this.config.graphQLPath, handleParseErrors);
app.use(
this.config.graphQLPath,
graphqlExpress(async req => await this._getGraphQLOptions(req))
);
app.use(this.config.graphQLPath, async (req, res) => {
const server = await this._getServer();
return server(req, res);
});
}
applyPlayground(app) {
@@ -98,14 +99,13 @@ class ParseGraphQLServer {
(_req, res) => {
res.setHeader('Content-Type', 'text/html');
res.write(
renderPlaygroundPage({
renderGraphiQL({
endpoint: this.config.graphQLPath,
version: '1.7.25',
subscriptionEndpoint: this.config.subscriptionsPath,
headers: {
headers: JSON.stringify({
'X-Parse-Application-Id': this.parseServer.config.appId,
'X-Parse-Master-Key': this.parseServer.config.masterKey,
},
}),
})
);
res.end();
+5 -19
View File
@@ -12,9 +12,7 @@ const needToGetAllKeys = (fields, keys, parseClasses) =>
if (fields[key[0]]) {
if (fields[key[0]].type === 'Relation') return false;
if (fields[key[0]].type === 'Pointer') {
const subClass = parseClasses.find(
({ className: parseClassName }) => fields[key[0]].targetClass === parseClassName
);
const subClass = parseClasses[fields[key[0]].targetClass];
if (subClass && subClass.fields[key[1]]) {
// Current sub key is not custom
return false;
@@ -48,17 +46,11 @@ const getObject = async (
) => {
const options = {};
try {
if (
!needToGetAllKeys(
parseClasses.find(({ className: parseClassName }) => className === parseClassName).fields,
keys,
parseClasses
)
) {
if (!needToGetAllKeys(parseClasses[className].fields, keys, parseClasses)) {
options.keys = keys;
}
} catch (e) {
console.log(e);
console.error(e);
}
if (include) {
options.include = include;
@@ -165,13 +157,7 @@ const findObjects = async (
// Silently replace the limit on the query with the max configured
options.limit = config.maxLimit;
}
if (
!needToGetAllKeys(
parseClasses.find(({ className: parseClassName }) => className === parseClassName).fields,
keys,
parseClasses
)
) {
if (!needToGetAllKeys(parseClasses[className].fields, keys, parseClasses)) {
options.keys = keys;
}
if (includeAll === true) {
@@ -286,7 +272,7 @@ const calculateSkipAndLimit = (skipInput, first, after, last, before, maxLimit)
}
if ((skip || 0) >= before) {
// If the before index is less then the skip, no objects will be returned
// If the before index is less than the skip, no objects will be returned
limit = 0;
} else if ((!limit && limit !== 0) || (skip || 0) + limit > before) {
// If there is no limit set, the limit is calculated. Or, if the limit (plus skip) is bigger than the before index, the new limit is set.
+11 -6
View File
@@ -15,7 +15,6 @@ import {
GraphQLUnionType,
} from 'graphql';
import { toGlobalId } from 'graphql-relay';
import { GraphQLUpload } from '@graphql-tools/links';
class TypeValidationError extends Error {
constructor(value, type) {
@@ -223,6 +222,11 @@ const DATE = new GraphQLScalarType({
},
});
const GraphQLUpload = new GraphQLScalarType({
name: 'Upload',
description: 'The Upload scalar type represents a file upload.',
});
const BYTES = new GraphQLScalarType({
name: 'Bytes',
description:
@@ -1190,8 +1194,8 @@ const ELEMENT = new GraphQLObjectType({
// Default static union type, we update types and resolveType function later
let ARRAY_RESULT;
const loadArrayResult = (parseGraphQLSchema, parseClasses) => {
const classTypes = parseClasses
const loadArrayResult = (parseGraphQLSchema, parseClassesArray) => {
const classTypes = parseClassesArray
.filter(parseClass =>
parseGraphQLSchema.parseClassTypes[parseClass.className].classGraphQLOutputType ? true : false
)
@@ -1206,12 +1210,12 @@ const loadArrayResult = (parseGraphQLSchema, parseClasses) => {
resolveType: value => {
if (value.__type === 'Object' && value.className && value.objectId) {
if (parseGraphQLSchema.parseClassTypes[value.className]) {
return parseGraphQLSchema.parseClassTypes[value.className].classGraphQLOutputType;
return parseGraphQLSchema.parseClassTypes[value.className].classGraphQLOutputType.name;
} else {
return ELEMENT;
return ELEMENT.name;
}
} else {
return ELEMENT;
return ELEMENT.name;
}
},
});
@@ -1265,6 +1269,7 @@ const load = parseGraphQLSchema => {
};
export {
GraphQLUpload,
TypeValidationError,
parseStringValue,
parseIntValue,
+1 -1
View File
@@ -39,7 +39,7 @@ const load = parseGraphQLSchema => {
}
},
obj => {
return parseGraphQLSchema.parseClassTypes[obj.className].classGraphQLOutputType;
return parseGraphQLSchema.parseClassTypes[obj.className].classGraphQLOutputType.name;
}
);
+8 -18
View File
@@ -1,43 +1,33 @@
import { GraphQLNonNull } from 'graphql';
import { mutationWithClientMutationId } from 'graphql-relay';
import { GraphQLUpload } from '@graphql-tools/links';
import Parse from 'parse/node';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import logger from '../../logger';
const handleUpload = async (upload, config) => {
const { createReadStream, filename, mimetype } = await upload;
let data = null;
if (createReadStream) {
const stream = createReadStream();
data = await new Promise((resolve, reject) => {
const chunks = [];
stream
.on('error', reject)
.on('data', chunk => chunks.push(chunk))
.on('end', () => resolve(Buffer.concat(chunks)));
});
}
const data = Buffer.from(await upload.arrayBuffer());
const fileName = upload.name;
const type = upload.type;
if (!data || !data.length) {
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.');
}
if (filename.length > 128) {
if (fileName.length > 128) {
throw new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.');
}
if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
if (!fileName.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
throw new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename contains invalid characters.');
}
try {
return {
fileInfo: await config.filesController.createFile(config, filename, data, mimetype),
fileInfo: await config.filesController.createFile(config, fileName, data, type),
};
} catch (e) {
logger.error('Error creating a file: ', e);
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `Could not store file: ${filename}.`);
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `Could not store file: ${fileName}.`);
}
};
@@ -48,7 +38,7 @@ const load = parseGraphQLSchema => {
inputFields: {
upload: {
description: 'This is the new file to be created and uploaded.',
type: new GraphQLNonNull(GraphQLUpload),
type: new GraphQLNonNull(defaultGraphQLTypes.GraphQLUpload),
},
},
outputFields: {
+42 -41
View File
@@ -1,8 +1,7 @@
import gql from 'graphql-tag';
import { SchemaDirectiveVisitor } from '@graphql-tools/utils';
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
import { FunctionsRouter } from '../../Routers/FunctionsRouter';
export const definitions = gql`
export const definitions = `
directive @resolve(to: String) on FIELD_DEFINITION
directive @mock(with: Any!) on FIELD_DEFINITION
`;
@@ -10,46 +9,48 @@ export const definitions = gql`
const load = parseGraphQLSchema => {
parseGraphQLSchema.graphQLSchemaDirectivesDefinitions = definitions;
class ResolveDirectiveVisitor extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
field.resolve = async (_source, args, context) => {
try {
const { config, auth, info } = context;
let functionName = field.name;
if (this.args.to) {
functionName = this.args.to;
}
return (
await FunctionsRouter.handleCloudFunction({
params: {
functionName,
},
config,
auth,
info,
body: args,
})
).response.result;
} catch (e) {
parseGraphQLSchema.handleError(e);
const resolveDirective = schema =>
mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: fieldConfig => {
const directive = getDirective(schema, fieldConfig, 'resolve')?.[0];
if (directive) {
const { to: targetCloudFunction } = directive;
fieldConfig.resolve = async (_source, args, context, gqlInfo) => {
try {
const { config, auth, info } = context;
const functionName = targetCloudFunction || gqlInfo.fieldName;
return (
await FunctionsRouter.handleCloudFunction({
params: {
functionName,
},
config,
auth,
info,
body: args,
})
).response.result;
} catch (e) {
parseGraphQLSchema.handleError(e);
}
};
}
};
}
}
return fieldConfig;
},
});
parseGraphQLSchema.graphQLSchemaDirectives.resolve = ResolveDirectiveVisitor;
const mockDirective = schema =>
mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: fieldConfig => {
const directive = getDirective(schema, fieldConfig, 'mock')?.[0];
if (directive) {
const { with: mockValue } = directive;
fieldConfig.resolve = async () => mockValue;
}
return fieldConfig;
},
});
class MockDirectiveVisitor extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
field.resolve = () => {
return this.args.with;
};
}
}
parseGraphQLSchema.graphQLSchemaDirectives.mock = MockDirectiveVisitor;
parseGraphQLSchema.graphQLSchemaDirectives = schema => mockDirective(resolveDirective(schema));
};
export { load };
+13 -13
View File
@@ -29,19 +29,19 @@ const SCHEMA_FIELD = new GraphQLInterfaceType({
},
resolveType: value =>
({
String: SCHEMA_STRING_FIELD,
Number: SCHEMA_NUMBER_FIELD,
Boolean: SCHEMA_BOOLEAN_FIELD,
Array: SCHEMA_ARRAY_FIELD,
Object: SCHEMA_OBJECT_FIELD,
Date: SCHEMA_DATE_FIELD,
File: SCHEMA_FILE_FIELD,
GeoPoint: SCHEMA_GEO_POINT_FIELD,
Polygon: SCHEMA_POLYGON_FIELD,
Bytes: SCHEMA_BYTES_FIELD,
Pointer: SCHEMA_POINTER_FIELD,
Relation: SCHEMA_RELATION_FIELD,
ACL: SCHEMA_ACL_FIELD,
String: SCHEMA_STRING_FIELD.name,
Number: SCHEMA_NUMBER_FIELD.name,
Boolean: SCHEMA_BOOLEAN_FIELD.name,
Array: SCHEMA_ARRAY_FIELD.name,
Object: SCHEMA_OBJECT_FIELD.name,
Date: SCHEMA_DATE_FIELD.name,
File: SCHEMA_FILE_FIELD.name,
GeoPoint: SCHEMA_GEO_POINT_FIELD.name,
Polygon: SCHEMA_POLYGON_FIELD.name,
Bytes: SCHEMA_BYTES_FIELD.name,
Pointer: SCHEMA_POINTER_FIELD.name,
Relation: SCHEMA_RELATION_FIELD.name,
ACL: SCHEMA_ACL_FIELD.name,
}[value.type]),
});
+2 -2
View File
@@ -1,5 +1,5 @@
import Parse from 'parse/node';
import { ApolloError } from 'apollo-server-core';
import { GraphQLYogaError } from '@graphql-yoga/node';
export function enforceMasterKeyAccess(auth) {
if (!auth.isMaster) {
@@ -16,7 +16,7 @@ export function toGraphQLError(error) {
code = Parse.Error.INTERNAL_SERVER_ERROR;
message = 'Internal server error';
}
return new ApolloError(message, code);
return new GraphQLYogaError(message, { code });
}
export const extractKeysAndInclude = selectedFields => {
+1 -1
View File
@@ -14,7 +14,7 @@ const transformTypes = async (
classGraphQLUpdateType,
config: { isCreateEnabled, isUpdateEnabled },
} = parseGraphQLSchema.parseClassTypes[className];
const parseClass = parseGraphQLSchema.parseClasses.find(clazz => clazz.className === className);
const parseClass = parseGraphQLSchema.parseClasses[className];
if (fields) {
const classGraphQLCreateTypeFields =
isCreateEnabled && classGraphQLCreateType ? classGraphQLCreateType.getFields() : null;
+2 -2
View File
@@ -51,7 +51,7 @@ const transformQueryConstraintInputToParse = (
parentConstraints,
parseClasses
) => {
const fields = parseClasses.find(parseClass => parseClass.className === className).fields;
const fields = parseClasses[className].fields;
if (parentFieldName === 'id' && className) {
Object.keys(constraints).forEach(constraintName => {
const constraintValue = constraints[constraintName];
@@ -108,7 +108,7 @@ const transformQueryConstraintInputToParse = (
* }
* }
*/
if (fieldValue.key && fieldValue.value && parentConstraints && parentFieldName) {
if (fieldValue.key && fieldValue.value !== undefined && parentConstraints && parentFieldName) {
delete parentConstraints[parentFieldName];
parentConstraints[`${parentFieldName}.${fieldValue.key}`] = {
...parentConstraints[`${parentFieldName}.${fieldValue.key}`],
+10
View File
@@ -19,6 +19,13 @@ class ParseCloudCodePublisher {
this._onCloudCodeMessage(Parse.applicationId + 'afterDelete', request);
}
onClearCachedRoles(user: Parse.Object) {
this.parsePublisher.publish(
Parse.applicationId + 'clearCache',
JSON.stringify({ userId: user.id })
);
}
// Request is the request object from cloud code functions. request.object is a ParseObject.
_onCloudCodeMessage(type: string, request: any): void {
logger.verbose(
@@ -33,6 +40,9 @@ class ParseCloudCodePublisher {
if (request.original) {
message.originalParseObject = request.original._toFullJSON();
}
if (request.classLevelPermissions) {
message.classLevelPermissions = request.classLevelPermissions;
}
this.parsePublisher.publish(type, JSON.stringify(message));
}
}
+117 -53
View File
@@ -10,11 +10,18 @@ import { ParsePubSub } from './ParsePubSub';
import SchemaController from '../Controllers/SchemaController';
import _ from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { runLiveQueryEventHandlers, getTrigger, runTrigger, toJSONwithObjects } from '../triggers';
import {
runLiveQueryEventHandlers,
getTrigger,
runTrigger,
resolveError,
toJSONwithObjects,
} from '../triggers';
import { getAuthForSessionToken, Auth } from '../Auth';
import { getCacheController } from '../Controllers';
import { getCacheController, getDatabaseController } from '../Controllers';
import LRU from 'lru-cache';
import UserRouter from '../Routers/UsersRouter';
import DatabaseController from '../Controllers/DatabaseController';
class ParseLiveQueryServer {
clients: Map;
@@ -58,7 +65,7 @@ class ParseLiveQueryServer {
// The main benefit is to be able to reuse the same user / session token resolution.
this.authCache = new LRU({
max: 500, // 500 concurrent
maxAge: config.cacheTimeout,
ttl: config.cacheTimeout,
});
// Initialize websocket server
this.parseWebSocketServer = new ParseWebSocketServer(
@@ -71,6 +78,7 @@ class ParseLiveQueryServer {
this.subscriber = ParsePubSub.createSubscriber(config);
this.subscriber.subscribe(Parse.applicationId + 'afterSave');
this.subscriber.subscribe(Parse.applicationId + 'afterDelete');
this.subscriber.subscribe(Parse.applicationId + 'clearCache');
// Register message handler for subscriber. When publisher get messages, it will publish message
// to the subscribers and the handler will be called.
this.subscriber.on('message', (channel, messageStr) => {
@@ -82,6 +90,10 @@ class ParseLiveQueryServer {
logger.error('unable to parse message', messageStr, e);
return;
}
if (channel === Parse.applicationId + 'clearCache') {
this._clearCachedRoles(message.userId);
return;
}
this._inflateParseObject(message);
if (channel === Parse.applicationId + 'afterSave') {
this._onAfterSave(message);
@@ -185,23 +197,18 @@ class ParseLiveQueryServer {
if (res.object && typeof res.object.toJSON === 'function') {
deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
}
if (
(deletedParseObject.className === '_User' ||
deletedParseObject.className === '_Session') &&
!client.hasMasterKey
) {
delete deletedParseObject.sessionToken;
delete deletedParseObject.authData;
}
client.pushDelete(requestId, deletedParseObject);
} catch (error) {
Client.pushError(
client.parseWebSocket,
error.code || Parse.Error.SCRIPT_FAILED,
error.message || error,
false,
requestId
await this._filterSensitiveData(
classLevelPermissions,
res,
client,
requestId,
op,
subscription.query
);
client.pushDelete(requestId, deletedParseObject);
} catch (e) {
const error = resolveError(e);
Client.pushError(client.parseWebSocket, error.code, error.message, false, requestId);
logger.error(
`Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` +
JSON.stringify(error)
@@ -344,28 +351,21 @@ class ParseLiveQueryServer {
res.original.className || className
);
}
if (
(currentParseObject.className === '_User' ||
currentParseObject.className === '_Session') &&
!client.hasMasterKey
) {
delete currentParseObject.sessionToken;
delete originalParseObject?.sessionToken;
delete currentParseObject.authData;
delete originalParseObject?.authData;
}
await this._filterSensitiveData(
classLevelPermissions,
res,
client,
requestId,
op,
subscription.query
);
const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
if (client[functionName]) {
client[functionName](requestId, currentParseObject, originalParseObject);
}
} catch (error) {
Client.pushError(
client.parseWebSocket,
error.code || Parse.Error.SCRIPT_FAILED,
error.message || error,
false,
requestId
);
} catch (e) {
const error = resolveError(e);
Client.pushError(client.parseWebSocket, error.code, error.message, false, requestId);
logger.error(
`Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` +
JSON.stringify(error)
@@ -478,6 +478,32 @@ class ParseLiveQueryServer {
return matchesQuery(parseObject, subscription.query);
}
async _clearCachedRoles(userId: string) {
try {
const validTokens = await new Parse.Query(Parse.Session)
.equalTo('user', Parse.User.createWithoutData(userId))
.find({ useMasterKey: true });
await Promise.all(
validTokens.map(async token => {
const sessionToken = token.get('sessionToken');
const authPromise = this.authCache.get(sessionToken);
if (!authPromise) {
return;
}
const [auth1, auth2] = await Promise.all([
authPromise,
getAuthForSessionToken({ cacheController: this.cacheController, sessionToken }),
]);
auth1.auth?.clearRoleCache(sessionToken);
auth2.auth?.clearRoleCache(sessionToken);
this.authCache.del(sessionToken);
})
);
} catch (e) {
logger.verbose(`Could not clear role cache. ${e}`);
}
}
getAuthForSessionToken(sessionToken: ?string): Promise<{ auth: ?Auth, userId: ?string }> {
if (!sessionToken) {
return Promise.resolve({});
@@ -550,6 +576,54 @@ class ParseLiveQueryServer {
// return rolesQuery.find({useMasterKey:true});
}
async _filterSensitiveData(
classLevelPermissions: ?any,
res: any,
client: any,
requestId: number,
op: string,
query: any
) {
const subscriptionInfo = client.getSubscriptionInfo(requestId);
const aclGroup = ['*'];
let clientAuth;
if (typeof subscriptionInfo !== 'undefined') {
const { userId, auth } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken);
if (userId) {
aclGroup.push(userId);
}
clientAuth = auth;
}
const filter = obj => {
if (!obj) {
return;
}
let protectedFields = classLevelPermissions?.protectedFields || [];
if (!client.hasMasterKey && !Array.isArray(protectedFields)) {
protectedFields = getDatabaseController(this.config).addProtectedFields(
classLevelPermissions,
res.object.className,
query,
aclGroup,
clientAuth
);
}
return DatabaseController.filterSensitiveData(
client.hasMasterKey,
aclGroup,
clientAuth,
op,
classLevelPermissions,
res.object.className,
protectedFields,
obj,
query
);
};
res.object = filter(res.object);
res.original = filter(res.original);
}
_getCLPOperation(query: any) {
return typeof query === 'object' &&
Object.keys(query).length == 1 &&
@@ -584,7 +658,6 @@ class ParseLiveQueryServer {
if (!acl_has_roles) {
return false;
}
const roleNames = await auth.getUserRoles();
// Finally, see if any of the user's roles allow them read access
for (const role of roleNames) {
@@ -681,13 +754,9 @@ class ParseLiveQueryServer {
logger.info(`Create new client: ${parseWebsocket.clientId}`);
client.pushConnect();
runLiveQueryEventHandlers(req);
} catch (error) {
Client.pushError(
parseWebsocket,
error.code || Parse.Error.SCRIPT_FAILED,
error.message || error,
false
);
} catch (e) {
const error = resolveError(e);
Client.pushError(parseWebsocket, error.code, error.message, false);
logger.error(
`Failed running beforeConnect for session ${request.sessionToken} with:\n Error: ` +
JSON.stringify(error)
@@ -827,16 +896,11 @@ class ParseLiveQueryServer {
installationId: client.installationId,
});
} catch (e) {
Client.pushError(
parseWebsocket,
e.code || Parse.Error.SCRIPT_FAILED,
e.message || e,
false,
request.requestId
);
const error = resolveError(e);
Client.pushError(parseWebsocket, error.code, error.message, false, request.requestId);
logger.error(
`Failed running beforeSubscribe on ${className} for session ${request.sessionToken} with:\n Error: ` +
JSON.stringify(e)
JSON.stringify(error)
);
}
}
+7 -1
View File
@@ -14,6 +14,10 @@ export class ParseWebSocketServer {
logger.info('Parse LiveQuery Server started running');
};
wss.onConnection = ws => {
ws.waitingForPong = false;
ws.on('pong', () => {
ws.waitingForPong = false;
});
ws.on('error', error => {
logger.error(error.message);
logger.error(inspect(ws, false));
@@ -21,10 +25,12 @@ export class ParseWebSocketServer {
onConnect(new ParseWebSocket(ws));
// Send ping to client periodically
const pingIntervalId = setInterval(() => {
if (ws.readyState == ws.OPEN) {
if (!ws.waitingForPong) {
ws.ping();
ws.waitingForPong = true;
} else {
clearInterval(pingIntervalId);
ws.terminate();
}
}, config.websocketTimeout || 10 * 1000);
};
+29 -3
View File
@@ -103,8 +103,18 @@ function contains(haystack: Array, needle: any): boolean {
return true;
}
}
return false;
}
if (Array.isArray(needle)) {
for (const need of needle) {
if (contains(haystack, need)) {
return true;
}
}
}
return haystack.indexOf(needle) > -1;
}
/**
@@ -334,9 +344,25 @@ function matchesKeyConstraints(object, key, constraints) {
return true;
}
case '$geoWithin': {
const points = compareTo.$polygon.map(geoPoint => [geoPoint.latitude, geoPoint.longitude]);
const polygon = new Parse.Polygon(points);
return polygon.containsPoint(object[key]);
if (compareTo.$polygon) {
const points = compareTo.$polygon.map(geoPoint => [
geoPoint.latitude,
geoPoint.longitude,
]);
const polygon = new Parse.Polygon(points);
return polygon.containsPoint(object[key]);
}
if (compareTo.$centerSphere) {
const [WGS84Point, maxDistance] = compareTo.$centerSphere;
const centerPoint = new Parse.GeoPoint({
latitude: WGS84Point[1],
longitude: WGS84Point[0],
});
const point = new Parse.GeoPoint(object[key]);
const distance = point.radiansTo(centerPoint);
return distance <= maxDistance;
}
break;
}
case '$geoIntersects': {
const polygon = new Parse.Polygon(object[key].coordinates);
+1 -1
View File
@@ -19,7 +19,7 @@ class SessionTokenCache {
constructor(timeout: number = 30 * 24 * 60 * 60 * 1000, maxSize: number = 10000) {
this.cache = new LRU({
max: maxSize,
maxAge: timeout,
ttl: timeout,
});
}
+73 -41
View File
@@ -5,6 +5,51 @@ Do not edit manually, but update Options/index.js
*/
var parsers = require('./parsers');
module.exports.SchemaOptions = {
afterMigration: {
env: 'PARSE_SERVER_SCHEMA_AFTER_MIGRATION',
help: 'Execute a callback after running schema migrations.',
},
beforeMigration: {
env: 'PARSE_SERVER_SCHEMA_BEFORE_MIGRATION',
help: 'Execute a callback before running schema migrations.',
},
definitions: {
env: 'PARSE_SERVER_SCHEMA_DEFINITIONS',
help:
'Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema',
required: true,
action: parsers.objectParser,
default: [],
},
deleteExtraFields: {
env: 'PARSE_SERVER_SCHEMA_DELETE_EXTRA_FIELDS',
help:
'Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.',
action: parsers.booleanParser,
default: false,
},
lockSchemas: {
env: 'PARSE_SERVER_SCHEMA_LOCK_SCHEMAS',
help:
'Is true if Parse Server will reject any attempts to modify the schema while the server is running.',
action: parsers.booleanParser,
default: false,
},
recreateModifiedFields: {
env: 'PARSE_SERVER_SCHEMA_RECREATE_MODIFIED_FIELDS',
help:
'Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.',
action: parsers.booleanParser,
default: false,
},
strict: {
env: 'PARSE_SERVER_SCHEMA_STRICT',
help: 'Is true if Parse Server should exit if schema update fail.',
action: parsers.booleanParser,
default: false,
},
};
module.exports.ParseServerOptions = {
accountLockout: {
env: 'PARSE_SERVER_ACCOUNT_LOCKOUT',
@@ -50,7 +95,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_AUTH_PROVIDERS',
help:
'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication',
action: parsers.objectParser,
action: parsers.arrayParser,
},
cacheAdapter: {
env: 'PARSE_SERVER_CACHE_ADAPTER',
@@ -95,7 +140,8 @@ module.exports.ParseServerOptions = {
},
databaseAdapter: {
env: 'PARSE_SERVER_DATABASE_ADAPTER',
help: 'Adapter module for the database',
help:
'Adapter module for the database; any options that are not explicitly described here are passed directly to the database client.',
action: parsers.moduleOrObjectParser,
},
databaseOptions: {
@@ -109,6 +155,12 @@ module.exports.ParseServerOptions = {
required: true,
default: 'mongodb://localhost:27017/parse',
},
defaultLimit: {
env: 'PARSE_SERVER_DEFAULT_LIMIT',
help: 'Default value for limit option on queries, defaults to `100`.',
action: parsers.numberParser('defaultLimit'),
default: 100,
},
directAccess: {
env: 'PARSE_SERVER_DIRECT_ACCESS',
help:
@@ -385,6 +437,11 @@ module.exports.ParseServerOptions = {
action: parsers.booleanParser,
default: false,
},
schema: {
env: 'PARSE_SERVER_SCHEMA',
help: 'Defined schema',
action: parsers.objectParser,
},
security: {
env: 'PARSE_SERVER_SECURITY',
help: 'The security options to identify and report weak security settings.',
@@ -420,6 +477,13 @@ module.exports.ParseServerOptions = {
help: 'Starts the liveQuery server',
action: parsers.booleanParser,
},
trustProxy: {
env: 'PARSE_SERVER_TRUST_PROXY',
help:
'The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.',
action: parsers.objectParser,
default: [],
},
userSensitiveFields: {
env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS',
help:
@@ -464,45 +528,6 @@ module.exports.SecurityOptions = {
default: false,
},
};
module.exports.SchemaOptions = {
definitions: {
help: 'The schema definitions.',
default: [],
},
strict: {
env: 'PARSE_SERVER_SCHEMA_STRICT',
help: 'Is true if Parse Server should exit if schema update fail.',
action: parsers.booleanParser,
default: true,
},
deleteExtraFields: {
env: 'PARSE_SERVER_SCHEMA_DELETE_EXTRA_FIELDS',
help:
'Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.',
action: parsers.booleanParser,
default: false,
},
recreateModifiedFields: {
env: 'PARSE_SERVER_SCHEMA_RECREATE_MODIFIED_FIELDS',
help:
'Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.',
action: parsers.booleanParser,
default: false,
},
lockSchemas: {
env: 'PARSE_SERVER_SCHEMA_LOCK',
help:
'Is true if Parse Server will reject any attempts to modify the schema while the server is running.',
action: parsers.booleanParser,
default: false,
},
beforeMigration: {
help: 'Execute a callback before running schema migrations.',
},
afterMigration: {
help: 'Execute a callback after running schema migrations.',
},
};
module.exports.PagesOptions = {
customRoutes: {
env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES',
@@ -865,3 +890,10 @@ module.exports.DatabaseOptions = {
default: false,
},
};
module.exports.AuthAdapter = {
enabled: {
help: 'Is `true` if the auth adapter is enabled, `false` otherwise.',
action: parsers.booleanParser,
default: true,
},
};
+21 -2
View File
@@ -1,3 +1,14 @@
/**
* @interface SchemaOptions
* @property {Function} afterMigration Execute a callback after running schema migrations.
* @property {Function} beforeMigration Execute a callback before running schema migrations.
* @property {Any} definitions Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema
* @property {Boolean} deleteExtraFields Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.
* @property {Boolean} lockSchemas Is true if Parse Server will reject any attempts to modify the schema while the server is running.
* @property {Boolean} recreateModifiedFields Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.
* @property {Boolean} strict Is true if Parse Server should exit if schema update fail.
*/
/**
* @interface ParseServerOptions
* @property {AccountLockoutOptions} accountLockout The account lockout policy for failed login attempts.
@@ -8,7 +19,7 @@
* @property {Adapter<AnalyticsAdapter>} analyticsAdapter Adapter module for the analytics
* @property {String} appId Your Parse Application ID
* @property {String} appName Sets the app name
* @property {Any} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
* @property {AuthAdapter[]} auth Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
* @property {Adapter<CacheAdapter>} cacheAdapter Adapter module for the cache
* @property {Number} cacheMaxSize Sets the maximum size for the in memory cache, defaults to 10000
* @property {Number} cacheTTL Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)
@@ -17,9 +28,10 @@
* @property {Number|Boolean} cluster Run with cluster, optionally set the number of processes default to os.cpus().length
* @property {String} collectionPrefix A collection prefix for the classes
* @property {CustomPagesOptions} customPages custom pages for password validation and reset
* @property {Adapter<StorageAdapter>} databaseAdapter Adapter module for the database
* @property {Adapter<StorageAdapter>} databaseAdapter Adapter module for the database; any options that are not explicitly described here are passed directly to the database client.
* @property {DatabaseOptions} databaseOptions Options to pass to the database client
* @property {String} databaseURI The full URI to your database. Supported databases are mongodb or postgres.
* @property {Number} defaultLimit Default value for limit option on queries, defaults to `100`.
* @property {Boolean} directAccess Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.<br><br>If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.<br><br>⚠️ In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`.
* @property {String} dotNetKey Key for Unity and .Net SDK
* @property {Adapter<MailAdapter>} emailAdapter Adapter module for email sending
@@ -68,6 +80,7 @@
* @property {String} restAPIKey Key for REST calls
* @property {Boolean} revokeSessionOnPasswordReset When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
* @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false.
* @property {SchemaOptions} schema Defined schema
* @property {SecurityOptions} security The security options to identify and report weak security settings.
* @property {Function} serverCloseComplete Callback when server has closed
* @property {Function} serverStartComplete Callback when server has started
@@ -75,6 +88,7 @@
* @property {Number} sessionLength Session duration, in seconds, defaults to 1 year
* @property {Boolean} silent Disables console output
* @property {Boolean} startLiveQueryServer Starts the liveQuery server
* @property {Any} trustProxy The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
* @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields
* @property {Boolean} verbose Set the logging to verbose
* @property {Boolean} verifyUserEmails Set to `true` to require users to verify their email address to complete the sign-up process.<br><br>Default is `false`.
@@ -196,3 +210,8 @@
* @interface DatabaseOptions
* @property {Boolean} enableSchemaHooks Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required.
*/
/**
* @interface AuthAdapter
* @property {Boolean} enabled Is `true` if the auth adapter is enabled, `false` otherwise.
*/
+41 -4
View File
@@ -8,7 +8,28 @@ import { MailAdapter } from '../Adapters/Email/MailAdapter';
import { PubSubAdapter } from '../Adapters/PubSub/PubSubAdapter';
import { WSSAdapter } from '../Adapters/WebSocketServer/WSSAdapter';
import { CheckGroup } from '../Security/CheckGroup';
import type { SchemaOptions } from '../SchemaMigrations/Migrations';
export interface SchemaOptions {
/* Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema
:DEFAULT: [] */
definitions: any;
/* Is true if Parse Server should exit if schema update fail.
:DEFAULT: false */
strict: ?boolean;
/* Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.
:DEFAULT: false */
deleteExtraFields: ?boolean;
/* Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.
:DEFAULT: false */
recreateModifiedFields: ?boolean;
/* Is true if Parse Server will reject any attempts to modify the schema while the server is running.
:DEFAULT: false */
lockSchemas: ?boolean;
/* Execute a callback before running schema migrations. */
beforeMigration: ?() => void | Promise<void>;
/* Execute a callback after running schema migrations. */
afterMigration: ?() => void | Promise<void>;
}
type Adapter<T> = string | any | T;
type NumberOrBoolean = number | boolean;
@@ -71,7 +92,7 @@ export interface ParseServerOptions {
/* Options to pass to the database client
:ENV: PARSE_SERVER_DATABASE_OPTIONS */
databaseOptions: ?DatabaseOptions;
/* Adapter module for the database */
/* Adapter module for the database; any options that are not explicitly described here are passed directly to the database client. */
databaseAdapter: ?Adapter<StorageAdapter>;
/* Full path to your cloud code main.js */
cloud: ?string;
@@ -119,7 +140,7 @@ export interface ParseServerOptions {
allowCustomObjectId: ?boolean;
/* Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication
:ENV: PARSE_SERVER_AUTH_PROVIDERS */
auth: ?any;
auth: ?(AuthAdapter[]);
/* Max file size for uploads, defaults to 20mb
:DEFAULT: 20mb */
maxUploadSize: ?string;
@@ -173,6 +194,9 @@ export interface ParseServerOptions {
/* Session duration, in seconds, defaults to 1 year
:DEFAULT: 31536000 */
sessionLength: ?number;
/* Default value for limit option on queries, defaults to `100`.
:DEFAULT: 100 */
defaultLimit: ?number;
/* Max value for limit option on queries, defaults to unlimited */
maxLimit: ?number;
/* Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.
@@ -214,6 +238,9 @@ export interface ParseServerOptions {
cluster: ?NumberOrBoolean;
/* middleware for express server, can be string or function */
middleware: ?((() => void) | string);
/* The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the <a href="https://expressjs.com/en/guide/behind-proxies.html">express trust proxy settings</a> documentation. Defaults to `false`.
:DEFAULT: false */
trustProxy: ?any;
/* Starts the liveQuery server */
startLiveQueryServer: ?boolean;
/* Live query server configuration options (will start the liveQuery server) */
@@ -246,7 +273,9 @@ export interface ParseServerOptions {
playgroundPath: ?string;
/* Callback when server has started */
serverStartComplete: ?(error: ?Error) => void;
/* Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema */
/* Defined schema
:ENV: PARSE_SERVER_SCHEMA
*/
schema: ?SchemaOptions;
/* Callback when server has closed */
serverCloseComplete: ?() => void;
@@ -483,3 +512,11 @@ export interface DatabaseOptions {
:DEFAULT: false */
enableSchemaHooks: ?boolean;
}
export interface AuthAdapter {
/* Is `true` if the auth adapter is enabled, `false` otherwise.
:DEFAULT: true
:ENV:
*/
enabled: ?boolean;
}
+13
View File
@@ -304,6 +304,9 @@ class ParseServer {
options
);
}
if (options.trustProxy) {
app.set('trust proxy', options.trustProxy);
}
/* istanbul ignore next */
if (!process.env.TESTING) {
configureListeners(this);
@@ -374,6 +377,16 @@ class ParseServer {
function addParseCloud() {
const ParseCloud = require('./cloud-code/Parse.Cloud');
Object.defineProperty(Parse, 'Server', {
get() {
return Config.get(Parse.applicationId);
},
set(newVal) {
newVal.appId = Parse.applicationId;
Config.put(newVal);
},
configurable: true,
});
Object.assign(Parse.Cloud, ParseCloud);
global.Parse = Parse;
}
+3 -4
View File
@@ -1,7 +1,6 @@
const Config = require('./Config');
const Auth = require('./Auth');
const RESTController = require('parse/lib/node/RESTController');
const URL = require('url');
const Parse = require('parse/node');
function getSessionToken(options) {
@@ -38,9 +37,9 @@ function ParseServerRESTController(applicationId, router) {
if (!config) {
config = Config.get(applicationId);
}
const serverURL = URL.parse(config.serverURL);
if (path.indexOf(serverURL.path) === 0) {
path = path.slice(serverURL.path.length, path.length);
const serverURL = new URL(config.serverURL);
if (path.indexOf(serverURL.pathname) === 0) {
path = path.slice(serverURL.pathname.length, path.length);
}
if (path[0] !== '/') {
+27
View File
@@ -202,6 +202,9 @@ RestQuery.prototype.execute = function (executeOptions) {
.then(() => {
return this.buildRestWhere();
})
.then(() => {
return this.denyProtectedFields();
})
.then(() => {
return this.handleIncludeAll();
})
@@ -688,6 +691,30 @@ RestQuery.prototype.runCount = function () {
});
};
RestQuery.prototype.denyProtectedFields = async function () {
if (this.auth.isMaster) {
return;
}
const schemaController = await this.config.database.loadSchema();
const protectedFields =
this.config.database.addProtectedFields(
schemaController,
this.className,
this.restWhere,
this.findOptions.acl,
this.auth,
this.findOptions
) || [];
for (const key of protectedFields) {
if (this.restWhere[key]) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
`This user is not allowed to query ${key} on class ${this.className}`
);
}
}
};
// Augments this.response with all pointers on an object
RestQuery.prototype.handleIncludeAll = function () {
if (!this.includeAll) {

Some files were not shown because too many files have changed in this diff Show More