Compare commits

..
124 Commits
Author SHA1 Message Date
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 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
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 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
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
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
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 4c2aa63fd2 fix: certificate in Apple Game Center auth adapter not validated [skip release] (#8055) 2022-06-17 19:32:30 +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
67 changed files with 3356 additions and 2202 deletions
+52 -30
View File
@@ -6,13 +6,13 @@ on:
branches:
- '**'
env:
NODE_VERSION: 16.13.0
NODE_VERSION: 16.14.2
PARSE_SERVER_TEST_TIMEOUT: 20000
jobs:
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 +33,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 +53,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 +73,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 +90,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,51 +101,71 @@ jobs:
strategy:
matrix:
include:
- name: MongoDB 5.2, ReplicaSet, WiredTiger
MONGODB_VERSION: 5.2.1
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 14.19.1
- name: MongoDB 5.1, ReplicaSet, WiredTiger
MONGODB_VERSION: 5.1.1
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 14.19.1
- name: MongoDB 5.0, ReplicaSet, WiredTiger
MONGODB_VERSION: 5.0.3
MONGODB_VERSION: 5.0.6
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 16.13.0
NODE_VERSION: 16.14.2
- name: MongoDB 4.4, ReplicaSet, WiredTiger
MONGODB_VERSION: 4.4.10
MONGODB_VERSION: 4.4.13
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 16.13.0
NODE_VERSION: 16.14.2
- name: MongoDB 4.2, ReplicaSet, WiredTiger
MONGODB_VERSION: 4.2.17
MONGODB_VERSION: 4.2.19
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 16.13.0
NODE_VERSION: 16.14.2
- name: MongoDB 4.0, ReplicaSet, WiredTiger
MONGODB_VERSION: 4.0.27
MONGODB_VERSION: 4.0.28
MONGODB_TOPOLOGY: replicaset
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 16.13.0
NODE_VERSION: 16.14.2
- 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: 16.13.0
NODE_VERSION: 16.14.2
- name: Redis Cache
PARSE_SERVER_TEST_CACHE: redis
MONGODB_VERSION: 4.4.10
MONGODB_VERSION: 4.4.13
MONGODB_TOPOLOGY: standalone
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 16.13.0
NODE_VERSION: 16.14.2
- 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
NODE_VERSION: 12.22.11
- name: Node 14
MONGODB_VERSION: 4.4.10
MONGODB_VERSION: 4.4.13
MONGODB_TOPOLOGY: standalone
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 14.18.1
NODE_VERSION: 14.19.1
- name: Node 17
MONGODB_VERSION: 4.4.13
MONGODB_TOPOLOGY: standalone
MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 17.9.0
- name: Node 18
MONGODB_VERSION: 4.4.13
MONGODB_TOPOLOGY: standalone
MONGODB_STORAGE_ENGINE: wiredTiger
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
@@ -158,6 +178,8 @@ jobs:
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,26 +205,26 @@ jobs:
include:
- name: PostgreSQL 11, PostGIS 3.0
POSTGRES_IMAGE: postgis/postgis:11-3.0
NODE_VERSION: 16.13.0
NODE_VERSION: 16.14.2
- name: PostgreSQL 11, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:11-3.1
NODE_VERSION: 16.13.0
NODE_VERSION: 16.14.2
- name: PostgreSQL 11, PostGIS 3.2
POSTGRES_IMAGE: postgis/postgis:11-3.2
NODE_VERSION: 16.13.0
NODE_VERSION: 16.14.2
- name: PostgreSQL 12, PostGIS 3.2
POSTGRES_IMAGE: postgis/postgis:12-3.2
NODE_VERSION: 16.13.0
NODE_VERSION: 16.14.2
- name: PostgreSQL 13, PostGIS 3.2
POSTGRES_IMAGE: postgis/postgis:13-3.2
NODE_VERSION: 16.13.0
NODE_VERSION: 16.14.2
- name: PostgreSQL 14, PostGIS 3.2
POSTGRES_IMAGE: postgis/postgis:14-3.2
NODE_VERSION: 16.13.0
NODE_VERSION: 16.14.2
fail-fast: false
name: ${{ matrix.name }}
timeout-minutes: 15
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
services:
redis:
image: redis
+8
View File
@@ -24,6 +24,8 @@
- [Merging](#merging)
- [Breaking Change](#breaking-change-1)
- [Reverting](#reverting)
- [Releasing](#releasing)
- [General Considerations](#general-considerations)
- [Major Release / Long-Term-Support](#major-release--long-term-support)
- [Versioning](#versioning)
- [Code of Conduct](#code-of-conduct)
@@ -379,6 +381,12 @@ If the commit reverts a previous commit, use the prefix `revert:`, followed by t
This reverts commit 1234567890abcdef.
```
## Releasing
### General Considerations
- The `package-lock.json` file has to be deleted and recreated by npm from scratch in regular intervals using the `npm i` command. It is not enough to only update the file via automated security pull requests (e.g. dependabot, snyk), that can create inconsistencies between sub-devependencies of a dependency and increase the chances of vulnerabilities. The file should be recreated once every release cycle which is usually monthly.
### Major Release / Long-Term-Support
Long-Term-Support (LTS) is provided for the previous Parse Server major version. For example, Parse Server 4.x will receive security updates until Parse Server 5.x is superseded by Parse Server 6.x and becomes the new LTS version. While the current major version is published on branch `release`, a LTS version is published on branch `release-#.x.x`, for example `release-4.x.x` for the Parse Server 4.x LTS branch.
+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."
+32 -20
View File
@@ -7,8 +7,8 @@
[![Coverage](https://img.shields.io/codecov/c/github/parse-community/parse-server/alpha.svg)](https://codecov.io/github/parse-community/parse-server?branch=alpha)
[![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-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)
[![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.0,_5.1,_5.2-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)
[![npm latest version](https://img.shields.io/npm/v/parse-server/latest.svg)](https://www.npmjs.com/package/parse-server)
@@ -20,6 +20,7 @@
[![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)
---
@@ -110,36 +111,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 16 | 16.13.0 | April 2024 | ✅ Yes |
| Node.js 17 | 17.x | June 2022 | ❌ Not tested |
| Version | Latest Version | End-of-Life | Compatible |
|------------|----------------|-------------|------------|
| Node.js 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 |
| Version | Latest Version | End-of-Life | Compatible |
|-------------|----------------|-------------|------------|
| MongoDB 4.0 | 4.0.28 | April 2022 | ✅ Yes |
| MongoDB 4.2 | 4.2.19 | TBD | ✅ Yes |
| MongoDB 4.4 | 4.4.13 | TBD | ✅ Yes |
| MongoDB 5.0 | 5.0.6 | TBD | ✅ Yes |
| MongoDB 5.1 | 5.1.1 | TBD | ✅ Yes |
| MongoDB 5.2 | 5.2.1 | TBD | ✅ Yes |
#### PostgreSQL
Parse Server is continuously tested with the most recent releases of PostgreSQL and PostGIS to ensure compatibility, using [PostGIS docker images](https://registry.hub.docker.com/r/postgis/postgis/tags?page=1&ordering=last_updated). We follow the [PostgreSQL support schedule](https://www.postgresql.org/support/versioning) and [PostGIS support schedule](https://www.postgis.net/eol_policy/) and only test against versions that are officially supported and have not reached their end-of-life date. Due to the extensive PostgreSQL support duration of 5 years, Parse Server drops support if a version is older than 3.5 years and a newer version has been available for at least 2.5 years.
| Version | PostGIS Version | End-of-Life | Parse Server Support End | Compatible |
|-------------|-----------------|---------------|--------------------------|------------|
| Postgres 11 | 3.0, 3.1, 3.2 | November 2023 | April 2022 | ✅ Yes |
| Postgres 12 | 3.2 | November 2024 | April 2023 | ✅ Yes |
| Postgres 13 | 3.2 | November 2025 | April 2024 | ✅ Yes |
| Postgres 14 | 3.2 | November 2026 | April 2025 | ✅ Yes |
| Postgres 11 | 3.0, 3.1, 3.2 | November 2023 | April 2022 | ✅ Yes |
| Postgres 12 | 3.2 | November 2024 | April 2023 | ✅ Yes |
| Postgres 13 | 3.2 | November 2025 | April 2024 | ✅ Yes |
| Postgres 14 | 3.2 | November 2026 | April 2025 | ✅ Yes |
### Locally
```bash
$ npm install -g parse-server mongodb-runner
$ mongodb-runner start
@@ -232,7 +240,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).
@@ -382,6 +389,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:
@@ -414,6 +422,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 |
@@ -509,6 +518,7 @@ Identical requests are identified by their request header `X-Parse-Request-Id`.
Deduplication is only done for object creation and update (`POST` and `PUT` requests). Deduplication is not done for object finding and deletion (`GET` and `DELETE` requests), as these operations are already idempotent by definition.
### Configuration example <!-- omit in toc -->
```
let api = new ParseServer({
idempotencyOptions: {
@@ -517,6 +527,7 @@ let api = new ParseServer({
}
}
```
### Parameters <!-- omit in toc -->
| Parameter | Optional | Type | Default value | Example values | Environment variable | Description |
@@ -549,6 +560,7 @@ Assuming the script above is named, `parse_idempotency_delete_expired_records.sh
## Localization
### Pages
**Caution, this is an experimental feature that may not be appropriate for production.**
Custom pages as well as feature pages (e.g. password reset, email verification) can be localized with the `pages` option in the Parse Server configuration:
+155
View File
@@ -1,3 +1,158 @@
# [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)
+41
View File
@@ -1,3 +1,44 @@
# [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)
+108
View File
@@ -1,3 +1,111 @@
# [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))
## [5.2.8](https://github.com/parse-community/parse-server/compare/5.2.7...5.2.8) (2022-10-14)
### Bug Fixes
* server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) ([#8235](https://github.com/parse-community/parse-server/issues/8235)) ([066f296](https://github.com/parse-community/parse-server/commit/066f29673ab4030b6b5b90c0c0326f7d3fe7612a))
## [5.2.7](https://github.com/parse-community/parse-server/compare/5.2.6...5.2.7) (2022-09-20)
### Bug Fixes
* authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) ([#8185](https://github.com/parse-community/parse-server/issues/8185)) ([ecf0814](https://github.com/parse-community/parse-server/commit/ecf0814499bde31ab6082b6e42854aa65ad2e03e))
## [5.2.6](https://github.com/parse-community/parse-server/compare/5.2.5...5.2.6) (2022-09-20)
### 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))
## [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))
## [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))
## [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))
## [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))
## [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))
# [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))
## [5.1.1](https://github.com/parse-community/parse-server/compare/5.1.0...5.1.1) (2022-03-18)
+1 -1
View File
@@ -61,7 +61,7 @@ async function checkNodeVersions() {
'<12.0.0', // These versions have reached their end-of-life support date
'>=13.0.0 <14.0.0', // These versions have reached their end-of-life support date
'>=15.0.0 <16.0.0', // These versions have reached their end-of-life support date
'>=17.0.0', // These versions are not officially supported yet
'>=19.0.0', // These versions are not officially supported yet
],
}).check();
}
+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
+632 -1444
View File
File diff suppressed because it is too large Load Diff
+35 -34
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "5.2.0-beta.1",
"version": "5.3.0",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -19,50 +19,49 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"@apollographql/graphql-playground-html": "1.6.29",
"@graphql-tools/links": "8.2.2",
"@apollo/client": "3.5.8",
"@graphql-tools/stitch": "6.2.4",
"@graphql-tools/utils": "6.2.4",
"@parse/fs-files-adapter": "1.2.1",
"@parse/push-adapter": "4.1.0",
"apollo-server-express": "2.25.2",
"@graphql-yoga/node": "2.6.0",
"@graphql-tools/utils": "8.6.12",
"@graphql-tools/merge": "8.2.13",
"@graphql-tools/schema": "8.3.13",
"@parse/fs-files-adapter": "1.2.2",
"@parse/push-adapter": "4.1.2",
"bcryptjs": "2.4.3",
"body-parser": "1.19.1",
"body-parser": "1.20.0",
"commander": "5.1.0",
"cors": "2.8.5",
"deepcopy": "2.1.0",
"express": "4.17.2",
"follow-redirects": "1.14.8",
"graphql": "15.7.1",
"express": "4.18.1",
"follow-redirects": "1.15.0",
"graphql": "16.5.0",
"graphql-list-fields": "2.0.2",
"graphql-relay": "0.7.0",
"graphql-tag": "2.12.6",
"graphql-upload": "11.0.0",
"graphql-relay": "0.10.0",
"intersect": "1.0.1",
"jsonwebtoken": "8.5.1",
"jwks-rsa": "2.0.5",
"ldapjs": "2.3.1",
"jwks-rsa": "2.1.3",
"ldapjs": "2.3.2",
"lodash": "4.17.21",
"lru-cache": "6.0.0",
"lru-cache": "7.10.1",
"mime": "3.0.0",
"mongodb": "4.3.1",
"mongodb": "4.6.0",
"mustache": "4.2.0",
"parse": "3.4.1",
"parse": "3.4.2",
"pg-monitor": "1.4.1",
"pg-promise": "10.11.1",
"pluralize": "8.0.0",
"redis": "3.1.2",
"semver": "7.3.5",
"semver": "7.3.7",
"subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0",
"uuid": "8.3.2",
"winston": "3.5.1",
"winston-daily-rotate-file": "4.6.0",
"ws": "8.2.3"
"winston": "3.7.2",
"winston-daily-rotate-file": "4.6.1",
"ws": "8.6.0"
},
"devDependencies": {
"graphql-tag": "2.12.6",
"@actions/core": "1.2.6",
"@apollo/client": "3.6.1",
"@babel/cli": "7.10.0",
"@babel/core": "7.10.0",
"@babel/plugin-proposal-object-rest-spread": "7.10.0",
@@ -98,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": "3.1.1",
"node-fetch": "3.2.4",
"nyc": "15.1.0",
"prettier": "2.0.5",
"semantic-release": "17.4.6",
@@ -116,16 +115,18 @@
"test:mongodb:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
"test:mongodb:testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine",
"test:mongodb": "npm run test:mongodb:runnerstart --dbversion=$npm_config_dbversion && npm run test:mongodb:testonly --dbversion=$npm_config_dbversion",
"test:mongodb:4.0.27": "npm run test:mongodb --dbversion=4.0.27",
"test:mongodb:4.2.17": "npm run test:mongodb --dbversion=4.2.17",
"test:mongodb:4.4.10": "npm run test:mongodb --dbversion=4.4.10",
"test:mongodb:5.0.5": "npm run test:mongodb --dbversion=5.0.5",
"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.0.6": "npm run test:mongodb --dbversion=5.0.6",
"test:mongodb:5.1.1": "npm run test:mongodb --dbversion=5.1.1",
"test:mongodb:5.2.1": "npm run test:mongodb --dbversion=5.2.1",
"posttest:mongodb": "mongodb-runner stop",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.2.1} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.2.1} 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:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop",
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 nyc jasmine",
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.2.1} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop",
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.2.1} 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",
@@ -133,7 +134,7 @@
"madge:circular": "node_modules/.bin/madge ./src --circular"
},
"engines": {
"node": ">=12.22.10 <17"
"node": ">=12.22.10 <19"
},
"bin": {
"parse-server": "bin/parse-server"
+6 -6
View File
@@ -83,21 +83,21 @@ async function config() {
['@semantic-release/git', {
assets: [changelogFile, 'package.json', 'package-lock.json', 'npm-shrinkwrap.json'],
}],
['@semantic-release/github', {
successComment: getReleaseComment(),
labels: ['type:ci'],
releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
}],
// Back-merge module runs last because if it fails it should not impede the release process
[
"@saithodev/semantic-release-backmerge",
{
"branches": [
{ from: "beta", to: "alpha" },
{ from: "release", to: "beta" },
{ from: "release", to: "alpha" },
]
}
],
['@semantic-release/github', {
successComment: getReleaseComment(),
labels: ['type:ci'],
releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
}],
],
};
+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": {
+213 -48
View File
@@ -441,6 +441,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 +500,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', () => {
@@ -1652,8 +1703,43 @@ describe('apple signin auth adapter', () => {
describe('Apple Game Center Auth adapter', () => {
const gcenter = require('../lib/Adapters/Auth/gcenter');
const fs = require('fs');
const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
it('can load adapter', async () => {
const options = {
gcenter: {
rootCertificateUrl:
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
});
it('validateAuthData should validate', async () => {
const options = {
gcenter: {
rootCertificateUrl:
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
// real token is used
const authData = {
id: 'G:1965586982',
@@ -1664,68 +1750,147 @@ describe('Apple Game Center Auth adapter', () => {
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
};
try {
await gcenter.validateAuthData(authData);
} catch (e) {
fail();
}
gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
await gcenter.validateAuthData(authData);
});
it('validateAuthData invalid signature id', async () => {
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
{}
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
timestamp: 1565257031287,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
bundleId: 'com.example.com',
};
try {
await gcenter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe('Apple Game Center - invalid signature');
}
});
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',
};
try {
await gcenter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: invalid.com');
}
await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith(
new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature')
);
});
it('validateAuthData invalid public key http url', async () => {
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'http://static.gc.apple.com/public-key/gc-prod-4.cer',
timestamp: 1565257031287,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
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: http://static.gc.apple.com/public-key/gc-prod-4.cer'
);
}
it('should not validate Symantec Cert', async () => {
const options = {
gcenter: {
rootCertificateUrl:
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
expect(() =>
gcenter.verifyPublicKeyIssuer(
testCert,
'https://static.gc.apple.com/public-key/gc-prod-4.cer'
)
);
});
it('adapter should load default cert', async () => {
const options = {
gcenter: {},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
const previous = new Date();
await adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
);
const duration = new Date().getTime() - previous.getTime();
expect(duration).toEqual(0);
});
it('adapter should throw', async () => {
const options = {
gcenter: {
rootCertificateUrl: 'https://example.com',
},
};
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
'gcenter',
options
);
await expectAsync(
adapter.validateAppId(
appIds,
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
providerOptions
)
).toBeRejectedWith(
new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
)
);
});
});
+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();
+232 -23
View File
@@ -1494,6 +1494,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 +1990,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', () => {
@@ -3245,7 +3405,7 @@ describe('saveFile hooks', () => {
it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
Parse.Cloud.beforeSaveFile(() => {
Parse.Cloud.beforeSave(Parse.File, () => {
const newFile = new Parse.File('some-file.txt');
newFile._url = 'http://www.somewhere.com/parse/files/some-app-id/some-file.txt';
return newFile;
@@ -3260,7 +3420,7 @@ describe('saveFile hooks', () => {
it('beforeSaveFile should throw error', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeSaveFile(() => {
Parse.Cloud.beforeSave(Parse.File, () => {
throw new Parse.Error(400, 'some-error-message');
});
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
@@ -3274,8 +3434,8 @@ describe('saveFile hooks', () => {
it('beforeSaveFile should change values of uploaded file by editing fileObject directly', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
Parse.Cloud.beforeSaveFile(async req => {
expect(req.triggerName).toEqual('beforeSaveFile');
Parse.Cloud.beforeSave(Parse.File, async req => {
expect(req.triggerName).toEqual('beforeSave');
expect(req.master).toBe(true);
req.file.addMetadata('foo', 'bar');
req.file.addTag('tagA', 'some-tag');
@@ -3303,8 +3463,8 @@ describe('saveFile hooks', () => {
it('beforeSaveFile should change values by returning new fileObject', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
Parse.Cloud.beforeSaveFile(async req => {
expect(req.triggerName).toEqual('beforeSaveFile');
Parse.Cloud.beforeSave(Parse.File, async req => {
expect(req.triggerName).toEqual('beforeSave');
expect(req.fileSize).toBe(3);
const newFile = new Parse.File('donald_duck.pdf', [4, 5, 6], 'application/pdf');
newFile.setMetadata({ foo: 'bar' });
@@ -3337,8 +3497,8 @@ describe('saveFile hooks', () => {
it('beforeSaveFile should contain metadata and tags saved from client', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
Parse.Cloud.beforeSaveFile(async req => {
expect(req.triggerName).toEqual('beforeSaveFile');
Parse.Cloud.beforeSave(Parse.File, async req => {
expect(req.triggerName).toEqual('beforeSave');
expect(req.fileSize).toBe(3);
expect(req.file).toBeInstanceOf(Parse.File);
expect(req.file.name()).toBe('popeye.txt');
@@ -3366,7 +3526,7 @@ describe('saveFile hooks', () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const config = Config.get('test');
config.filesController.options.preserveFileName = true;
Parse.Cloud.beforeSaveFile(async ({ file }) => {
Parse.Cloud.beforeSave(Parse.File, async ({ file }) => {
expect(file.name()).toBe('popeye.txt');
const fileData = await file.getData();
const newFile = new Parse.File('2020-04-01.txt', { base64: fileData });
@@ -3380,13 +3540,13 @@ describe('saveFile hooks', () => {
it('afterSaveFile should set fileSize to null if beforeSave returns an already saved file', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
Parse.Cloud.beforeSaveFile(req => {
Parse.Cloud.beforeSave(Parse.File, req => {
expect(req.fileSize).toBe(3);
const newFile = new Parse.File('some-file.txt');
newFile._url = 'http://www.somewhere.com/parse/files/some-app-id/some-file.txt';
return newFile;
});
Parse.Cloud.afterSaveFile(req => {
Parse.Cloud.afterSave(Parse.File, req => {
expect(req.fileSize).toBe(null);
});
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
@@ -3399,7 +3559,7 @@ describe('saveFile hooks', () => {
it('afterSaveFile should throw error', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.afterSaveFile(async () => {
Parse.Cloud.afterSave(Parse.File, async () => {
throw new Parse.Error(400, 'some-error-message');
});
const filename = 'donald_duck.pdf';
@@ -3413,11 +3573,11 @@ describe('saveFile hooks', () => {
it('afterSaveFile should call with fileObject', async done => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeSaveFile(async req => {
Parse.Cloud.beforeSave(Parse.File, async req => {
req.file.setTags({ tagA: 'some-tag' });
req.file.setMetadata({ foo: 'bar' });
});
Parse.Cloud.afterSaveFile(async req => {
Parse.Cloud.afterSave(Parse.File, async req => {
expect(req.master).toBe(true);
expect(req.file._tags).toEqual({ tagA: 'some-tag' });
expect(req.file._metadata).toEqual({ foo: 'bar' });
@@ -3429,13 +3589,13 @@ describe('saveFile hooks', () => {
it('afterSaveFile should change fileSize when file data changes', async done => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeSaveFile(async req => {
Parse.Cloud.beforeSave(Parse.File, async req => {
expect(req.fileSize).toBe(3);
expect(req.master).toBe(true);
const newFile = new Parse.File('donald_duck.pdf', [4, 5, 6, 7, 8, 9], 'application/pdf');
return newFile;
});
Parse.Cloud.afterSaveFile(async req => {
Parse.Cloud.afterSave(Parse.File, async req => {
expect(req.fileSize).toBe(6);
expect(req.master).toBe(true);
done();
@@ -3446,7 +3606,7 @@ describe('saveFile hooks', () => {
it('beforeDeleteFile should call with fileObject', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeDeleteFile(req => {
Parse.Cloud.beforeDelete(Parse.File, req => {
expect(req.file).toBeInstanceOf(Parse.File);
expect(req.file._name).toEqual('popeye.txt');
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
@@ -3458,7 +3618,7 @@ describe('saveFile hooks', () => {
it('beforeDeleteFile should throw error', async done => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeDeleteFile(() => {
Parse.Cloud.beforeDelete(Parse.File, () => {
throw new Error('some error message');
});
const file = new Parse.File('popeye.txt');
@@ -3472,12 +3632,12 @@ describe('saveFile hooks', () => {
it('afterDeleteFile should call with fileObject', async done => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeDeleteFile(req => {
Parse.Cloud.beforeDelete(Parse.File, req => {
expect(req.file).toBeInstanceOf(Parse.File);
expect(req.file._name).toEqual('popeye.txt');
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
});
Parse.Cloud.afterDeleteFile(req => {
Parse.Cloud.afterDelete(Parse.File, req => {
expect(req.file).toBeInstanceOf(Parse.File);
expect(req.file._name).toEqual('popeye.txt');
expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt');
@@ -3489,7 +3649,7 @@ describe('saveFile hooks', () => {
it('beforeSaveFile should not change file if nothing is returned', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeSaveFile(() => {
Parse.Cloud.beforeSave(Parse.File, () => {
return;
});
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
@@ -3498,7 +3658,7 @@ describe('saveFile hooks', () => {
});
it('throw custom error from beforeSaveFile', async done => {
Parse.Cloud.beforeSaveFile(() => {
Parse.Cloud.beforeSave(Parse.File, () => {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail');
});
try {
@@ -3512,7 +3672,7 @@ describe('saveFile hooks', () => {
});
it('throw empty error from beforeSaveFile', async done => {
Parse.Cloud.beforeSaveFile(() => {
Parse.Cloud.beforeSave(Parse.File, () => {
throw null;
});
try {
@@ -3524,6 +3684,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', () => {
+14
View File
@@ -43,6 +43,20 @@ describe('FilesController', () => {
done();
});
it_only_db('mongo')('should pass databaseOptions to GridFSBucketAdapter', async () => {
await reconfigureServer({
databaseURI: 'mongodb://localhost:27017/parse',
filesAdapter: null,
databaseAdapter: null,
databaseOptions: {
retryWrites: true,
},
});
const config = Config.get(Parse.applicationId);
expect(config.database.adapter._mongoOptions.retryWrites).toBeTrue();
expect(config.filesController.adapter._mongoOptions.retryWrites).toBeTrue();
});
it('should create a server log on failure', done => {
const logController = new LoggerController(new WinstonLoggerAdapter());
+35 -1
View File
@@ -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')('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')('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');
+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',
},
}).catch(e => e);
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);
+161 -65
View File
@@ -12,6 +12,7 @@ const { getMainDefinition } = require('apollo-utilities');
const { createUploadLink } = require('apollo-upload-client');
const { SubscriptionClient } = require('subscriptions-transport-ws');
const { WebSocketLink } = require('@apollo/client/link/ws');
const { mergeSchemas } = require('@graphql-tools/schema');
const {
ApolloClient,
InMemoryCache,
@@ -98,6 +99,24 @@ describe('ParseGraphQLServer', () => {
});
});
describe('_getServer', () => {
it('should only return new server on schema changes', async () => {
parseGraphQLServer.server = undefined;
const server1 = await parseGraphQLServer._getServer();
const server2 = await parseGraphQLServer._getServer();
expect(server1).toBe(server2);
// Trigger a schema change
const obj = new Parse.Object('SomeClass');
await obj.save();
const server3 = await parseGraphQLServer._getServer();
const server4 = await parseGraphQLServer._getServer();
expect(server3).not.toBe(server2);
expect(server3).toBe(server4);
});
});
describe('_getGraphQLOptions', () => {
const req = {
info: new Object(),
@@ -106,11 +125,15 @@ describe('ParseGraphQLServer', () => {
};
it("should return schema and context with req's info, config and auth", async () => {
const options = await parseGraphQLServer._getGraphQLOptions(req);
const options = await parseGraphQLServer._getGraphQLOptions();
expect(options.multipart).toEqual({
fileSize: 20971520,
});
expect(options.schema).toEqual(parseGraphQLServer.parseGraphQLSchema.graphQLSchema);
expect(options.context.info).toEqual(req.info);
expect(options.context.config).toEqual(req.config);
expect(options.context.auth).toEqual(req.auth);
const contextResponse = options.context({ req });
expect(contextResponse.info).toEqual(req.info);
expect(contextResponse.config).toEqual(req.config);
expect(contextResponse.auth).toEqual(req.auth);
});
it('should load GraphQL schema in every call', async () => {
@@ -467,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) => {
@@ -476,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;
});
@@ -486,7 +509,7 @@ describe('ParseGraphQLServer', () => {
fetch,
headers: {
...headers,
Origin: 'http://someorigin.com',
Origin: 'http://example.com',
},
})
),
@@ -504,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({
@@ -523,7 +557,7 @@ describe('ParseGraphQLServer', () => {
})
).data.health;
expect(health).toBeTruthy();
expect(checked).toBeTruthy();
expect(contextSpy).toHaveBeenCalledTimes(1);
parseGraphQLServer._getGraphQLOptions = originalGetGraphQLOptions;
});
});
@@ -6786,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({
@@ -9096,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',
@@ -9107,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,
@@ -9176,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 },
@@ -9344,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 = {
@@ -10381,7 +10460,7 @@ describe('ParseGraphQLServer', () => {
});
describe('Custom API', () => {
describe('GraphQL Schema Based', () => {
describe('SDL based', () => {
let httpServer;
const headers = {
'X-Parse-Application-Id': 'test',
@@ -10504,7 +10583,7 @@ describe('ParseGraphQLServer', () => {
});
});
describe('SDL Based', () => {
describe('GraphQL Schema Based', () => {
let httpServer;
const headers = {
'X-Parse-Application-Id': 'test',
@@ -10567,6 +10646,12 @@ describe('ParseGraphQLServer', () => {
},
resolve: (p, { message }) => message,
},
errorQuery: {
type: new GraphQLNonNull(GraphQLString),
resolve: () => {
throw new Error('A test error');
},
},
customQueryWithAutoTypeReturn: {
type: SomeClassType,
args: {
@@ -10655,6 +10740,18 @@ describe('ParseGraphQLServer', () => {
expect(result.data.customQuery).toEqual('hello');
});
it('can forward original error of a custom query', async () => {
await expectAsync(
apolloClient.query({
query: gql`
query ErrorQuery {
errorQuery
}
`,
})
).toBeRejectedWithError('A test error');
});
it('can resolve a custom query with auto type return', async () => {
const obj = new Parse.Object('SomeClass');
await obj.save({ name: 'aname', type: 'robot' });
@@ -10801,8 +10898,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);
+90
View File
@@ -836,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] },
@@ -1066,6 +1110,52 @@ 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' }),
]);
});
afterEach(async function (done) {
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
client.close();
+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')('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')('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')('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')('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')('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')('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 });
});
});
+28
View File
@@ -135,4 +135,32 @@ describe('Parse.Session', () => {
fail(err);
});
});
it('cannot edit session with known ID', async () => {
const request = require('../lib/request');
await setupTestUsers();
const [first, second] = await new Parse.Query(Parse.Session).find({ useMasterKey: true });
const headers = {
'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');
});
+95
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,26 @@ 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',
'updatedAt',
]);
});
});
+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;
+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-----
+14
View File
@@ -280,4 +280,18 @@ describe('Vulnerabilities', () => {
expect(text.error).toBe('Prohibited keyword in request data: {"value":"aValue[123]*"}.');
});
});
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();
});
});
});
+10 -9
View File
@@ -32,22 +32,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) => {
+98 -27
View File
@@ -14,20 +14,14 @@ const authData = {
const { Parse } = require('parse/node');
const crypto = require('crypto');
const https = require('https');
const { pki } = require('node-forge');
const ca = { cert: null, url: null };
const cache = {}; // (publicKey -> cert) cache
function verifyPublicKeyUrl(publicKeyUrl) {
try {
const parsedUrl = new URL(publicKeyUrl);
if (parsedUrl.protocol !== 'https:') {
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';
const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
return regex.test(publicKeyUrl);
} catch (error) {
return false;
}
@@ -43,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,
@@ -53,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);
@@ -103,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) {
@@ -114,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,
};
+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.
+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,
});
}
+23 -10
View File
@@ -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();
+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
+47 -43
View File
@@ -11,6 +11,7 @@ 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';
@@ -54,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.');
}
@@ -86,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.');
}
@@ -94,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,
@@ -114,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}`);
}
});
@@ -126,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
@@ -135,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;
@@ -206,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;
@@ -513,7 +512,7 @@ class DatabaseController {
if (acl) {
query = addWriteACL(query, acl);
}
validateQuery(query);
validateQuery(query, isMaster, true);
return schemaController
.getOneSchema(className, true)
.catch(error => {
@@ -759,7 +758,7 @@ class DatabaseController {
if (acl) {
query = addWriteACL(query, acl);
}
validateQuery(query);
validateQuery(query, isMaster, false);
return schemaController
.getOneSchema(className)
.catch(error => {
@@ -1251,7 +1250,7 @@ class DatabaseController {
query = addReadACL(query, aclGroup);
}
}
validateQuery(query);
validateQuery(query, isMaster, false);
if (count) {
if (!classExists) {
return 0;
@@ -1532,14 +1531,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;
@@ -1763,8 +1765,8 @@ 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: undefined }, keyword.key, undefined);
if (match) {
throw new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
@@ -1804,9 +1806,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,
+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,
};
+10 -2
View File
@@ -91,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,
+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();
+4 -18
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) {
+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;
+1 -1
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];
+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));
}
}
+105 -22
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, resolveError, 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,14 +197,14 @@ 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;
}
await this._filterSensitiveData(
classLevelPermissions,
res,
client,
requestId,
op,
subscription.query
);
client.pushDelete(requestId, deletedParseObject);
} catch (e) {
const error = resolveError(e);
@@ -339,16 +351,14 @@ 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);
@@ -468,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({});
@@ -540,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 &&
@@ -574,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) {
+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);
};
+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,
});
}
+58 -40
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',
@@ -385,6 +430,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.',
@@ -464,45 +514,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 +876,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,
},
};
+18 -1
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)
@@ -68,6 +79,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
@@ -196,3 +208,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.
*/
+34 -3
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;
@@ -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;
@@ -246,7 +267,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 +506,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;
}
+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) {
+73 -29
View File
@@ -15,6 +15,8 @@ var ClientSDK = require('./ClientSDK');
import RestQuery from './RestQuery';
import _ from 'lodash';
import logger from './logger';
import Deprecator from './Deprecator/Deprecator';
import { requiredColumns } from './Controllers/SchemaController';
// query and data are both provided in REST API format. So data
// types are encoded by plain old objects.
@@ -95,6 +97,7 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
// Shared SchemaController to be reused to reduce the number of loadSchema() calls per request
// Once set the schemaData should be immutable
this.validSchemaController = null;
this.pendingOps = {};
}
// A convenient method to perform all the steps of processing the
@@ -225,18 +228,11 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
return Promise.resolve();
}
// Cloud code gets a bit of extra data for its objects
var extraData = { className: this.className };
if (this.query && this.query.objectId) {
extraData.objectId = this.query.objectId;
}
const { originalObject, updatedObject } = this.buildParseObjects();
let originalObject = null;
const updatedObject = this.buildUpdatedObject(extraData);
if (this.query && this.query.objectId) {
// This is an update for existing object.
originalObject = triggers.inflate(extraData, this.originalData);
}
const stateController = Parse.CoreManager.getObjectStateController();
const [pending] = stateController.getPendingOps(updatedObject._getStateIdentifier());
this.pendingOps = { ...pending };
return Promise.resolve()
.then(() => {
@@ -435,7 +431,14 @@ RestWrite.prototype.handleAuthDataValidation = function (authData) {
return Promise.resolve();
}
const validateAuthData = this.config.authDataManager.getValidatorForProvider(provider);
if (!validateAuthData) {
const authProvider = (this.config.auth || {})[provider] || {};
if (authProvider.enabled == null) {
Deprecator.logRuntimeDeprecation({
usage: `auth.${provider}`,
solution: `auth.${provider}.enabled: true`,
});
}
if (!validateAuthData || authProvider.enabled === false) {
throw new Parse.Error(
Parse.Error.UNSUPPORTED_SERVICE,
'This authentication method is unsupported.'
@@ -1015,6 +1018,20 @@ RestWrite.prototype.handleSession = function () {
} else if (this.data.sessionToken) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME);
}
if (!this.auth.isMaster) {
this.query = {
$and: [
this.query,
{
user: {
__type: 'Pointer',
className: '_User',
objectId: this.auth.user.id,
},
},
],
};
}
}
if (!this.query && !this.auth.isMaster) {
@@ -1323,6 +1340,9 @@ RestWrite.prototype.runDatabaseOperation = function () {
if (this.className === '_Role') {
this.config.cacheController.role.clear();
if (this.config.liveQueryController) {
this.config.liveQueryController.clearCachedRoles(this.auth.user);
}
}
if (this.className === '_User' && this.query && this.auth.isUnauthenticated()) {
@@ -1531,20 +1551,7 @@ RestWrite.prototype.runAfterSaveTrigger = function () {
return Promise.resolve();
}
var extraData = { className: this.className };
if (this.query && this.query.objectId) {
extraData.objectId = this.query.objectId;
}
// Build the original object, we only do this for a update write.
let originalObject;
if (this.query && this.query.objectId) {
originalObject = triggers.inflate(extraData, this.originalData);
}
// Build the inflated object, different from beforeSave, originalData is not empty
// since developers can change data in the beforeSave.
const updatedObject = this.buildUpdatedObject(extraData);
const { originalObject, updatedObject } = this.buildParseObjects();
updatedObject._handleSaveResponse(this.response.response, this.response.status || 200);
this.config.database.loadSchema().then(schemaController => {
@@ -1569,8 +1576,15 @@ RestWrite.prototype.runAfterSaveTrigger = function () {
this.context
)
.then(result => {
if (result && typeof result === 'object') {
const jsonReturned = result && !result._toFullJSON;
if (jsonReturned) {
this.pendingOps = {};
this.response.response = result;
} else {
this.response.response = this._updateResponseWithData(
(result || updatedObject).toJSON(),
this.data
);
}
})
.catch(function (err) {
@@ -1604,7 +1618,13 @@ RestWrite.prototype.sanitizedData = function () {
};
// Returns an updated copy of the object
RestWrite.prototype.buildUpdatedObject = function (extraData) {
RestWrite.prototype.buildParseObjects = function () {
const extraData = { className: this.className, objectId: this.query?.objectId };
let originalObject;
if (this.query && this.query.objectId) {
originalObject = triggers.inflate(extraData, this.originalData);
}
const className = Parse.Object.fromJSON(extraData);
const readOnlyAttributes = className.constructor.readOnlyAttributes
? className.constructor.readOnlyAttributes()
@@ -1642,7 +1662,7 @@ RestWrite.prototype.buildUpdatedObject = function (extraData) {
delete sanitized[attribute];
}
updatedObject.set(sanitized);
return updatedObject;
return { updatedObject, originalObject };
};
RestWrite.prototype.cleanUserAuthData = function () {
@@ -1662,6 +1682,30 @@ RestWrite.prototype.cleanUserAuthData = function () {
};
RestWrite.prototype._updateResponseWithData = function (response, data) {
const { updatedObject } = this.buildParseObjects();
const stateController = Parse.CoreManager.getObjectStateController();
const [pending] = stateController.getPendingOps(updatedObject._getStateIdentifier());
for (const key in this.pendingOps) {
if (!pending[key]) {
data[key] = this.originalData ? this.originalData[key] : { __op: 'Delete' };
this.storage.fieldsChangedByTrigger.push(key);
}
}
const skipKeys = [
'objectId',
'createdAt',
'updatedAt',
...(requiredColumns.read[this.className] || []),
];
for (const key in response) {
if (skipKeys.includes(key)) {
continue;
}
const value = response[key];
if (value == null || (value.__type && value.__type === 'Pointer') || data[key] === value) {
delete response[key];
}
}
if (_.isEmpty(this.storage.fieldsChangedByTrigger)) {
return response;
}
+19 -13
View File
@@ -66,6 +66,12 @@ export class FilesRouter {
getHandler(req, res) {
const config = Config.get(req.params.appId);
if (!config) {
res.status(403);
const err = new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid application ID.');
res.json({ code: err.code, error: err.message });
return;
}
const filesController = config.filesController;
const filename = req.params.filename;
const contentType = mime.getType(filename);
@@ -141,7 +147,7 @@ export class FilesRouter {
try {
// run beforeSaveFile trigger
const triggerResult = await triggers.maybeRunFileTrigger(
triggers.Types.beforeSaveFile,
triggers.Types.beforeSave,
fileObject,
config,
req.auth
@@ -194,12 +200,7 @@ export class FilesRouter {
};
}
// run afterSaveFile trigger
await triggers.maybeRunFileTrigger(
triggers.Types.afterSaveFile,
fileObject,
config,
req.auth
);
await triggers.maybeRunFileTrigger(triggers.Types.afterSave, fileObject, config, req.auth);
res.status(201);
res.set('Location', saveResult.url);
res.json(saveResult);
@@ -222,7 +223,7 @@ export class FilesRouter {
file._url = filesController.adapter.getFileLocation(req.config, filename);
const fileObject = { file, fileSize: null };
await triggers.maybeRunFileTrigger(
triggers.Types.beforeDeleteFile,
triggers.Types.beforeDelete,
fileObject,
req.config,
req.auth
@@ -231,7 +232,7 @@ export class FilesRouter {
await filesController.deleteFile(req.config, filename);
// run afterDeleteFile trigger
await triggers.maybeRunFileTrigger(
triggers.Types.afterDeleteFile,
triggers.Types.afterDelete,
fileObject,
req.config,
req.auth
@@ -250,10 +251,10 @@ export class FilesRouter {
}
async metadataHandler(req, res) {
const config = Config.get(req.params.appId);
const { filesController } = config;
const { filename } = req.params;
try {
const config = Config.get(req.params.appId);
const { filesController } = config;
const { filename } = req.params;
const data = await filesController.getMetadata(filename);
res.status(200);
res.json(data);
@@ -265,5 +266,10 @@ export class FilesRouter {
}
function isFileStreamable(req, filesController) {
return req.get('Range') && typeof filesController.adapter.handleFileStream === 'function';
const range = (req.get('Range') || '/-/').split('-');
const start = Number(range[0]);
const end = Number(range[1]);
return (
(!isNaN(start) || !isNaN(end)) && typeof filesController.adapter.handleFileStream === 'function'
);
}
+10 -11
View File
@@ -1,5 +1,15 @@
// @flow
export interface SchemaOptions {
definitions: JSONSchema[];
strict: ?boolean;
deleteExtraFields: ?boolean;
recreateModifiedFields: ?boolean;
lockSchemas: ?boolean;
beforeMigration: ?() => void | Promise<void>;
afterMigration: ?() => void | Promise<void>;
}
export type FieldValueType =
| 'String'
| 'Boolean'
@@ -35,17 +45,6 @@ export interface IndexesInterface {
[key: string]: IndexInterface;
}
export interface SchemaOptions {
definitions: JSONSchema[];
strict: ?boolean;
deleteExtraFields: ?boolean;
recreateModifiedFields: ?boolean;
lockSchemas: ?boolean;
/* Callback when server has started and before running schemas migration operations if schemas key provided */
beforeMigration: ?() => void | Promise<void>;
afterMigration: ?() => void | Promise<void>;
}
export type CLPOperation = 'find' | 'count' | 'get' | 'update' | 'create' | 'delete';
// @Typescript 4.1+ // type CLPPermission = 'requiresAuthentication' | '*' | `user:${string}` | `role:${string}`
+3 -3
View File
@@ -341,9 +341,9 @@ class Utils {
* @returns {Boolean} True if a match was found, false otherwise.
*/
static objectContainsKeyValue(obj, key, value) {
const isMatch = (a, b) => (typeof a === 'string' && new RegExp(a).test(b)) || a === b;
const isKeyMatch = k => isMatch(key, k);
const isValueMatch = v => isMatch(value, v);
const isMatch = (a, b) => (typeof a === 'string' && new RegExp(b).test(a)) || a === b;
const isKeyMatch = k => isMatch(k, key);
const isValueMatch = v => isMatch(v, value);
for (const [k, v] of Object.entries(obj)) {
if (key !== undefined && value === undefined && isKeyMatch(k)) {
return true;
+31 -35
View File
@@ -134,7 +134,7 @@ ParseCloud.job = function (functionName, handler) {
*
* **Available in Cloud Code only.**
*
* If you want to use beforeSave for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User}), you should pass the class itself and not the String for arg1.
* If you want to use beforeSave for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User} or {@link Parse.File}), you should pass the class itself and not the String for arg1.
*
* ```
* Parse.Cloud.beforeSave('MyCustomClass', (request) => {
@@ -171,7 +171,7 @@ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) {
*
* **Available in Cloud Code only.**
*
* If you want to use beforeDelete for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User}), you should pass the class itself and not the String for arg1.
* If you want to use beforeDelete for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User} or {@link Parse.File}), you should pass the class itself and not the String for arg1.
* ```
* Parse.Cloud.beforeDelete('MyCustomClass', (request) => {
* // code here
@@ -300,7 +300,7 @@ ParseCloud.afterLogout = function (handler) {
*
* **Available in Cloud Code only.**
*
* If you want to use afterSave for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User}), you should pass the class itself and not the String for arg1.
* If you want to use afterSave for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User} or {@link Parse.File}), you should pass the class itself and not the String for arg1.
*
* ```
* Parse.Cloud.afterSave('MyCustomClass', async function(request) {
@@ -337,7 +337,7 @@ ParseCloud.afterSave = function (parseClass, handler, validationHandler) {
*
* **Available in Cloud Code only.**
*
* If you want to use afterDelete for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User}), you should pass the class itself and not the String for arg1.
* If you want to use afterDelete for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User} or {@link Parse.File}), you should pass the class itself and not the String for arg1.
* ```
* Parse.Cloud.afterDelete('MyCustomClass', async (request) => {
* // code here
@@ -373,7 +373,7 @@ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) {
*
* **Available in Cloud Code only.**
*
* If you want to use beforeFind for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User}), you should pass the class itself and not the String for arg1.
* If you want to use beforeFind for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User} or {@link Parse.File}), you should pass the class itself and not the String for arg1.
* ```
* Parse.Cloud.beforeFind('MyCustomClass', async (request) => {
* // code here
@@ -409,7 +409,7 @@ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) {
*
* **Available in Cloud Code only.**
*
* If you want to use afterFind for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User}), you should pass the class itself and not the String for arg1.
* If you want to use afterFind for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User} or {@link Parse.File}), you should pass the class itself and not the String for arg1.
* ```
* Parse.Cloud.afterFind('MyCustomClass', async (request) => {
* // code here
@@ -458,18 +458,17 @@ ParseCloud.afterFind = function (parseClass, handler, validationHandler) {
*```
*
* @method beforeSaveFile
* @deprecated
* @name Parse.Cloud.beforeSaveFile
* @param {Function} func The function to run before saving a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}.
* @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}.
*/
ParseCloud.beforeSaveFile = function (handler, validationHandler) {
validateValidator(validationHandler);
triggers.addFileTrigger(
triggers.Types.beforeSaveFile,
handler,
Parse.applicationId,
validationHandler
);
Deprecator.logRuntimeDeprecation({
usage: 'Parse.Cloud.beforeSaveFile',
solution: 'Use Parse.Cloud.beforeSave(Parse.File, (request) => {})',
});
ParseCloud.beforeSave(Parse.File, handler, validationHandler);
};
/**
@@ -490,18 +489,17 @@ ParseCloud.beforeSaveFile = function (handler, validationHandler) {
*```
*
* @method afterSaveFile
* @deprecated
* @name Parse.Cloud.afterSaveFile
* @param {Function} func The function to run after saving a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}.
* @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}.
*/
ParseCloud.afterSaveFile = function (handler, validationHandler) {
validateValidator(validationHandler);
triggers.addFileTrigger(
triggers.Types.afterSaveFile,
handler,
Parse.applicationId,
validationHandler
);
Deprecator.logRuntimeDeprecation({
usage: 'Parse.Cloud.afterSaveFile',
solution: 'Use Parse.Cloud.afterSave(Parse.File, (request) => {})',
});
ParseCloud.afterSave(Parse.File, handler, validationHandler);
};
/**
@@ -522,18 +520,17 @@ ParseCloud.afterSaveFile = function (handler, validationHandler) {
*```
*
* @method beforeDeleteFile
* @deprecated
* @name Parse.Cloud.beforeDeleteFile
* @param {Function} func The function to run before deleting a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}.
* @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}.
*/
ParseCloud.beforeDeleteFile = function (handler, validationHandler) {
validateValidator(validationHandler);
triggers.addFileTrigger(
triggers.Types.beforeDeleteFile,
handler,
Parse.applicationId,
validationHandler
);
Deprecator.logRuntimeDeprecation({
usage: 'Parse.Cloud.beforeDeleteFile',
solution: 'Use Parse.Cloud.beforeDelete(Parse.File, (request) => {})',
});
ParseCloud.beforeDelete(Parse.File, handler, validationHandler);
};
/**
@@ -554,18 +551,17 @@ ParseCloud.beforeDeleteFile = function (handler, validationHandler) {
*```
*
* @method afterDeleteFile
* @deprecated
* @name Parse.Cloud.afterDeleteFile
* @param {Function} func The function to after before deleting a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}.
* @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}.
*/
ParseCloud.afterDeleteFile = function (handler, validationHandler) {
validateValidator(validationHandler);
triggers.addFileTrigger(
triggers.Types.afterDeleteFile,
handler,
Parse.applicationId,
validationHandler
);
Deprecator.logRuntimeDeprecation({
usage: 'Parse.Cloud.afterDeleteFile',
solution: 'Use Parse.Cloud.afterDelete(Parse.File, (request) => {})',
});
ParseCloud.afterDelete(Parse.File, handler, validationHandler);
};
/**
@@ -636,7 +632,7 @@ ParseCloud.sendEmail = function (data) {
*
* **Available in Cloud Code only.**
*
* If you want to use beforeSubscribe for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User}), you should pass the class itself and not the String for arg1.
* If you want to use beforeSubscribe for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User} or {@link Parse.File}), you should pass the class itself and not the String for arg1.
* ```
* Parse.Cloud.beforeSubscribe('MyCustomClass', (request) => {
* // code here
+6 -16
View File
@@ -12,16 +12,11 @@ export const Types = {
afterDelete: 'afterDelete',
beforeFind: 'beforeFind',
afterFind: 'afterFind',
beforeSaveFile: 'beforeSaveFile',
afterSaveFile: 'afterSaveFile',
beforeDeleteFile: 'beforeDeleteFile',
afterDeleteFile: 'afterDeleteFile',
beforeConnect: 'beforeConnect',
beforeSubscribe: 'beforeSubscribe',
afterEvent: 'afterEvent',
};
const FileClassName = '@File';
const ConnectClassName = '@Connect';
const baseStore = function () {
@@ -50,6 +45,9 @@ export function getClassName(parseClass) {
if (parseClass && parseClass.className) {
return parseClass.className;
}
if (parseClass && parseClass.name) {
return parseClass.name.replace('Parse', '@');
}
return parseClass;
}
@@ -140,11 +138,6 @@ export function addTrigger(type, className, handler, applicationId, validationHa
add(Category.Validators, `${type}.${className}`, validationHandler, applicationId);
}
export function addFileTrigger(type, handler, applicationId, validationHandler) {
add(Category.Triggers, `${type}.${FileClassName}`, handler, applicationId);
add(Category.Validators, `${type}.${FileClassName}`, validationHandler, applicationId);
}
export function addConnectTrigger(type, handler, applicationId, validationHandler) {
add(Category.Triggers, `${type}.${ConnectClassName}`, handler, applicationId);
add(Category.Validators, `${type}.${ConnectClassName}`, validationHandler, applicationId);
@@ -207,10 +200,6 @@ export async function runTrigger(trigger, name, request, auth) {
return await trigger(request);
}
export function getFileTrigger(type, applicationId) {
return getTrigger(FileClassName, type, applicationId);
}
export function triggerExists(className: string, type: string, applicationId: string): boolean {
return getTrigger(className, type, applicationId) != undefined;
}
@@ -738,7 +727,7 @@ async function builtInTriggerValidator(options, request, auth) {
}
if (opt.constant && request.object) {
if (request.original) {
request.object.set(key, request.original.get(key));
request.object.revert(key);
} else if (opt.default != null) {
request.object.set(key, opt.default);
}
@@ -961,7 +950,8 @@ export function getRequestFileObject(triggerType, auth, fileObject, config) {
}
export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) {
const fileTrigger = getFileTrigger(triggerType, config.applicationId);
const FileClassName = getClassName(Parse.File);
const fileTrigger = getTrigger(FileClassName, triggerType, config.applicationId);
if (typeof fileTrigger === 'function') {
try {
const request = getRequestFileObject(triggerType, auth, fileObject, config);