mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd8a11bc27 | ||
|
|
60c5a73d25 | ||
|
|
3e983c41ff | ||
|
|
6728da1e35 | ||
|
|
2458a8c58d | ||
|
|
50eed3cffe | ||
|
|
12e174bcb6 | ||
|
|
254954064f | ||
|
|
50409aa866 | ||
|
|
8011b2fdac | ||
|
|
4c1befabf2 | ||
|
|
066f29673a | ||
|
|
1a2b1b9bc1 | ||
|
|
e6dc487963 | ||
|
|
ecf0814499 | ||
|
|
83cdc89be9 | ||
|
|
7aac70cca6 | ||
|
|
6d0b2f5346 | ||
|
|
f0db4ca4a4 | ||
|
|
83fd16c1b9 | ||
|
|
e39d51bd32 | ||
|
|
636d16e0f9 | ||
|
|
1a04a347cf | ||
|
|
4c2aa63fd2 | ||
|
|
ed7a25d5be | ||
|
|
b9a69381d2 | ||
|
|
2235678de3 | ||
|
|
cf925e75e8 | ||
|
|
16c7656188 | ||
|
|
0d818879c2 | ||
|
|
03caae1e61 | ||
|
|
72fac8a5fc | ||
|
|
5f7d392a06 | ||
|
|
528690b7b6 | ||
|
|
f67cc99948 | ||
|
|
61ba1b2913 | ||
|
|
c484094759 | ||
|
|
5de4c4723f | ||
|
|
199dfc1722 | ||
|
|
0cd902b8c2 | ||
|
|
0dc2843503 | ||
|
|
2d5221e480 | ||
|
|
468e98785f | ||
|
|
9e506969c3 | ||
|
|
a2491ff036 | ||
|
|
5e15864dc9 | ||
|
|
c6dcad8d16 | ||
|
|
ac283d3cc0 | ||
|
|
c1e808f9e8 | ||
|
|
88b4d9dda1 | ||
|
|
34a4cca240 | ||
|
|
28901aa35f | ||
|
|
91bb235bc7 | ||
|
|
51fb14be3f | ||
|
|
8fb9a4a8bc | ||
|
|
47d796ea58 | ||
|
|
2e750b78cc | ||
|
|
a176de94d2 | ||
|
|
0081e1c81c | ||
|
|
1aa2204aeb | ||
|
|
b2ae2e1db4 | ||
|
|
b10182f35e | ||
|
|
2b1dc5c46a | ||
|
|
e692b5dd82 | ||
|
|
364e346548 | ||
|
|
4462b39d48 | ||
|
|
c35d74d176 | ||
|
|
330286d22b | ||
|
|
ad5e82f2b7 | ||
|
|
38ed96ace5 | ||
|
|
d691591630 | ||
|
|
2eb56039e2 | ||
|
|
3e9f292d84 | ||
|
|
68b15c298e | ||
|
|
39fbcde612 | ||
|
|
8f7393f847 | ||
|
|
0294d45078 | ||
|
|
b84acd578a | ||
|
|
261ce010dd | ||
|
|
e6d7d8f0a2 | ||
|
|
3fb6b2b4ab | ||
|
|
e849cf52d7 | ||
|
|
3c60d6f299 | ||
|
|
19717b8f84 | ||
|
|
3a70fda679 | ||
|
|
e0b588351a | ||
|
|
a31a1ff777 | ||
|
|
a68d0502bd | ||
|
|
89b412caff | ||
|
|
da99abeaec | ||
|
|
dd40dfa4d9 | ||
|
|
0a6faa81fa | ||
|
|
f47d58471e | ||
|
|
731c550714 | ||
|
|
f51d638246 | ||
|
|
519b6bac68 | ||
|
|
b1e5565b22 | ||
|
|
a169663304 | ||
|
|
e47b61de46 | ||
|
|
341f9f71e3 | ||
|
|
91eca2e45f | ||
|
|
5bfa7160d9 | ||
|
|
38eb5ace37 | ||
|
|
93667b4e84 | ||
|
|
58cd870a1e | ||
|
|
022a85619d | ||
|
|
94e27ef850 | ||
|
|
041197fb4c | ||
|
|
884f4c0ecd | ||
|
|
19900fcdf8 | ||
|
|
d609c72783 |
+52
-30
@@ -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,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."
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
[](https://codecov.io/github/parse-community/parse-server?branch=alpha)
|
||||
[](https://github.com/parse-community/parse-dashboard/releases)
|
||||
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://nodejs.org)
|
||||
[](https://www.mongodb.com)
|
||||
[](https://www.postgresql.org)
|
||||
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
@@ -20,6 +20,7 @@
|
||||
[![License][license-svg]][license-link]
|
||||
[](https://community.parseplatform.org/c/parse-server)
|
||||
[](https://twitter.com/intent/follow?screen_name=ParsePlatform)
|
||||
[](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:
|
||||
|
||||
@@ -1,3 +1,144 @@
|
||||
# [5.3.0-alpha.18](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.17...5.3.0-alpha.18) (2022-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* auto-release process may fail if optional back-merging task fails ([#8051](https://github.com/parse-community/parse-server/issues/8051)) ([cf925e7](https://github.com/parse-community/parse-server/commit/cf925e75e87a6989f41e2e2abb2aba4332b1e79f))
|
||||
|
||||
# [5.3.0-alpha.17](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.16...5.3.0-alpha.17) (2022-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* errors in GraphQL do not show the original error but a general `Unexpected Error` ([#8045](https://github.com/parse-community/parse-server/issues/8045)) ([0d81887](https://github.com/parse-community/parse-server/commit/0d818879c217f9c56100a5f59868fa37e6d24b71))
|
||||
* websocket connection of LiveQuery interrupts frequently ([#8048](https://github.com/parse-community/parse-server/issues/8048)) ([03caae1](https://github.com/parse-community/parse-server/commit/03caae1e611f28079cdddbbe433daaf69e3f595c))
|
||||
|
||||
# [5.3.0-alpha.16](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.15...5.3.0-alpha.16) (2022-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* live query role cache does not clear when a user is added to a role ([#8026](https://github.com/parse-community/parse-server/issues/8026)) ([199dfc1](https://github.com/parse-community/parse-server/commit/199dfc17226d85a78ab85f24362cce740f4ada39))
|
||||
|
||||
# [5.3.0-alpha.15](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.14...5.3.0-alpha.15) (2022-06-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* interrupted WebSocket connection not closed by LiveQuery server ([#8012](https://github.com/parse-community/parse-server/issues/8012)) ([2d5221e](https://github.com/parse-community/parse-server/commit/2d5221e48012fb7781c0406d543a922d313075ea))
|
||||
|
||||
# [5.3.0-alpha.14](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.13...5.3.0-alpha.14) (2022-05-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* align file trigger syntax with class trigger; use the new syntax `Parse.Cloud.beforeSave(Parse.File, (request) => {})`, the old syntax `Parse.Cloud.beforeSaveFile((request) => {})` has been deprecated ([#7966](https://github.com/parse-community/parse-server/issues/7966)) ([c6dcad8](https://github.com/parse-community/parse-server/commit/c6dcad8d167d44912dbd416d328519314c0809bd))
|
||||
|
||||
# [5.3.0-alpha.13](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.12...5.3.0-alpha.13) (2022-05-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* selectively enable / disable default authentication adapters ([#7953](https://github.com/parse-community/parse-server/issues/7953)) ([c1e808f](https://github.com/parse-community/parse-server/commit/c1e808f9e807fc49508acbde0d8b3f2b901a1638))
|
||||
|
||||
# [5.3.0-alpha.12](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.11...5.3.0-alpha.12) (2022-05-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* afterSave trigger removes pointer in Parse object ([#7913](https://github.com/parse-community/parse-server/issues/7913)) ([47d796e](https://github.com/parse-community/parse-server/commit/47d796ea58f65e71612ce37149be692abc9ea97f))
|
||||
|
||||
# [5.3.0-alpha.11](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.10...5.3.0-alpha.11) (2022-05-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* replace GraphQL Apollo with GraphQL Yoga ([#7967](https://github.com/parse-community/parse-server/issues/7967)) ([1aa2204](https://github.com/parse-community/parse-server/commit/1aa2204aebfdbe273d54d6d56c6029f7c34aab14))
|
||||
|
||||
# [5.3.0-alpha.10](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.9...5.3.0-alpha.10) (2022-05-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* upgrade mongodb from 4.4.1 to 4.5.0 ([#7991](https://github.com/parse-community/parse-server/issues/7991)) ([e692b5d](https://github.com/parse-community/parse-server/commit/e692b5dd8214cdb0ce79bedd30d9aa3cf4de76a5))
|
||||
|
||||
# [5.3.0-alpha.9](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.8...5.3.0-alpha.9) (2022-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* depreciate allowClientClassCreation defaulting to true ([#7925](https://github.com/parse-community/parse-server/issues/7925)) ([38ed96a](https://github.com/parse-community/parse-server/commit/38ed96ace534d639db007aa7dd5387b2da8f03ae))
|
||||
|
||||
# [5.3.0-alpha.8](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.7...5.3.0-alpha.8) (2022-05-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for Node 17 and 18 ([#7896](https://github.com/parse-community/parse-server/issues/7896)) ([3e9f292](https://github.com/parse-community/parse-server/commit/3e9f292d840334244934cee9a34545ac86313549))
|
||||
|
||||
# [5.3.0-alpha.7](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.6...5.3.0-alpha.7) (2022-04-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade @parse/fs-files-adapter from 1.2.1 to 1.2.2 ([#7948](https://github.com/parse-community/parse-server/issues/7948)) ([20fc4e2](https://github.com/parse-community/parse-server/commit/20fc4e23b53c91aac657f894bd70d049b7525c37))
|
||||
|
||||
# [5.3.0-alpha.6](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.5...5.3.0-alpha.6) (2022-04-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* peer dependency mismatch for GraphQL dependencies ([#7934](https://github.com/parse-community/parse-server/issues/7934)) ([b7a1d76](https://github.com/parse-community/parse-server/commit/b7a1d7617b4bcac677cecedfeb6ac4a27447083b))
|
||||
|
||||
# [5.3.0-alpha.5](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.4...5.3.0-alpha.5) (2022-04-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade moment from 2.29.1 to 2.29.2 ([#7931](https://github.com/parse-community/parse-server/issues/7931)) ([6b68593](https://github.com/parse-community/parse-server/commit/6b68593eaec17e8b183899d2b92699c9ede7625b))
|
||||
|
||||
# [5.3.0-alpha.4](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.3...5.3.0-alpha.4) (2022-04-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* custom database options are not passed to MongoDB GridFS ([#7911](https://github.com/parse-community/parse-server/issues/7911)) ([a72b384](https://github.com/parse-community/parse-server/commit/a72b384f76137a3d83ffb69f65cb25aff1bbab4f))
|
||||
|
||||
# [5.3.0-alpha.3](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.2...5.3.0-alpha.3) (2022-03-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add MongoDB 5.2 support ([#7894](https://github.com/parse-community/parse-server/issues/7894)) ([6b4b358](https://github.com/parse-community/parse-server/commit/6b4b358f0842ae920e45652f5e8b2afebc6caf3a))
|
||||
|
||||
# [5.3.0-alpha.2](https://github.com/parse-community/parse-server/compare/5.3.0-alpha.1...5.3.0-alpha.2) (2022-03-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* security upgrade parse push adapter from 4.1.0 to 4.1.2 ([#7893](https://github.com/parse-community/parse-server/issues/7893)) ([ef56e98](https://github.com/parse-community/parse-server/commit/ef56e98ef65041b4d3b7b82cce3473269c27f6fd))
|
||||
|
||||
# [5.3.0-alpha.1](https://github.com/parse-community/parse-server/compare/5.2.1-alpha.2...5.3.0-alpha.1) (2022-03-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add MongoDB 5.1 compatibility ([#7682](https://github.com/parse-community/parse-server/issues/7682)) ([90155cf](https://github.com/parse-community/parse-server/commit/90155cf1680e5e0499b0000e071c6cb0ce3aef96))
|
||||
|
||||
## [5.2.1-alpha.2](https://github.com/parse-community/parse-server/compare/5.2.1-alpha.1...5.2.1-alpha.2) (2022-03-26)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* reduce database operations when using the constant parameter in Cloud Function validation ([#7892](https://github.com/parse-community/parse-server/issues/7892)) ([48bd512](https://github.com/parse-community/parse-server/commit/48bd512eeb47666967dff8c5e723ddc5b7801daa))
|
||||
|
||||
## [5.2.1-alpha.1](https://github.com/parse-community/parse-server/compare/5.2.0...5.2.1-alpha.1) (2022-03-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* return correct response when revert is used in beforeSave ([#7839](https://github.com/parse-community/parse-server/issues/7839)) ([f63fb2b](https://github.com/parse-community/parse-server/commit/f63fb2b338c908f0e7a648d338c26b9daa50c8f2))
|
||||
|
||||
# [5.2.0-alpha.3](https://github.com/parse-community/parse-server/compare/5.2.0-alpha.2...5.2.0-alpha.3) (2022-03-24)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,36 @@
|
||||
# [5.3.0-beta.1](https://github.com/parse-community/parse-server/compare/5.2.1...5.3.0-beta.1) (2022-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* afterSave trigger removes pointer in Parse object ([#7913](https://github.com/parse-community/parse-server/issues/7913)) ([47d796e](https://github.com/parse-community/parse-server/commit/47d796ea58f65e71612ce37149be692abc9ea97f))
|
||||
* auto-release process may fail if optional back-merging task fails ([#8051](https://github.com/parse-community/parse-server/issues/8051)) ([cf925e7](https://github.com/parse-community/parse-server/commit/cf925e75e87a6989f41e2e2abb2aba4332b1e79f))
|
||||
* custom database options are not passed to MongoDB GridFS ([#7911](https://github.com/parse-community/parse-server/issues/7911)) ([b1e5565](https://github.com/parse-community/parse-server/commit/b1e5565b22f2eff229571fe9a9500314bd30965b))
|
||||
* depreciate allowClientClassCreation defaulting to true ([#7925](https://github.com/parse-community/parse-server/issues/7925)) ([38ed96a](https://github.com/parse-community/parse-server/commit/38ed96ace534d639db007aa7dd5387b2da8f03ae))
|
||||
* errors in GraphQL do not show the original error but a general `Unexpected Error` ([#8045](https://github.com/parse-community/parse-server/issues/8045)) ([0d81887](https://github.com/parse-community/parse-server/commit/0d818879c217f9c56100a5f59868fa37e6d24b71))
|
||||
* interrupted WebSocket connection not closed by LiveQuery server ([#8012](https://github.com/parse-community/parse-server/issues/8012)) ([2d5221e](https://github.com/parse-community/parse-server/commit/2d5221e48012fb7781c0406d543a922d313075ea))
|
||||
* live query role cache does not clear when a user is added to a role ([#8026](https://github.com/parse-community/parse-server/issues/8026)) ([199dfc1](https://github.com/parse-community/parse-server/commit/199dfc17226d85a78ab85f24362cce740f4ada39))
|
||||
* peer dependency mismatch for GraphQL dependencies ([#7934](https://github.com/parse-community/parse-server/issues/7934)) ([0a6faa8](https://github.com/parse-community/parse-server/commit/0a6faa81fa97f8620e7fd05e8c7bbdb4b7da9578))
|
||||
* return correct response when revert is used in beforeSave ([#7839](https://github.com/parse-community/parse-server/issues/7839)) ([19900fc](https://github.com/parse-community/parse-server/commit/19900fcdf8c9f29a674fb62cf6e4b3341d796891))
|
||||
* security upgrade @parse/fs-files-adapter from 1.2.1 to 1.2.2 ([#7948](https://github.com/parse-community/parse-server/issues/7948)) ([3a70fda](https://github.com/parse-community/parse-server/commit/3a70fda6798d4143f21046439b5eaf232a31bdb6))
|
||||
* security upgrade moment from 2.29.1 to 2.29.2 ([#7931](https://github.com/parse-community/parse-server/issues/7931)) ([731c550](https://github.com/parse-community/parse-server/commit/731c5507144bbacff236097e7a2a03bfe54f6e10))
|
||||
* security upgrade parse push adapter from 4.1.0 to 4.1.2 ([#7893](https://github.com/parse-community/parse-server/issues/7893)) ([93667b4](https://github.com/parse-community/parse-server/commit/93667b4e8402bf13b46c4d3ef12cec6532fd9da7))
|
||||
* websocket connection of LiveQuery interrupts frequently ([#8048](https://github.com/parse-community/parse-server/issues/8048)) ([03caae1](https://github.com/parse-community/parse-server/commit/03caae1e611f28079cdddbbe433daaf69e3f595c))
|
||||
|
||||
### Features
|
||||
|
||||
* add MongoDB 5.1 compatibility ([#7682](https://github.com/parse-community/parse-server/issues/7682)) ([022a856](https://github.com/parse-community/parse-server/commit/022a85619d8a2c57a2f2938e245e4d8a47c15276))
|
||||
* add MongoDB 5.2 support ([#7894](https://github.com/parse-community/parse-server/issues/7894)) ([5bfa716](https://github.com/parse-community/parse-server/commit/5bfa7160d9e35b237cbae1016ed86724aa99f8d7))
|
||||
* add support for Node 17 and 18 ([#7896](https://github.com/parse-community/parse-server/issues/7896)) ([3e9f292](https://github.com/parse-community/parse-server/commit/3e9f292d840334244934cee9a34545ac86313549))
|
||||
* align file trigger syntax with class trigger; use the new syntax `Parse.Cloud.beforeSave(Parse.File, (request) => {})`, the old syntax `Parse.Cloud.beforeSaveFile((request) => {})` has been deprecated ([#7966](https://github.com/parse-community/parse-server/issues/7966)) ([c6dcad8](https://github.com/parse-community/parse-server/commit/c6dcad8d167d44912dbd416d328519314c0809bd))
|
||||
* replace GraphQL Apollo with GraphQL Yoga ([#7967](https://github.com/parse-community/parse-server/issues/7967)) ([1aa2204](https://github.com/parse-community/parse-server/commit/1aa2204aebfdbe273d54d6d56c6029f7c34aab14))
|
||||
* selectively enable / disable default authentication adapters ([#7953](https://github.com/parse-community/parse-server/issues/7953)) ([c1e808f](https://github.com/parse-community/parse-server/commit/c1e808f9e807fc49508acbde0d8b3f2b901a1638))
|
||||
* upgrade mongodb from 4.4.1 to 4.5.0 ([#7991](https://github.com/parse-community/parse-server/issues/7991)) ([e692b5d](https://github.com/parse-community/parse-server/commit/e692b5dd8214cdb0ce79bedd30d9aa3cf4de76a5))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* reduce database operations when using the constant parameter in Cloud Function validation ([#7892](https://github.com/parse-community/parse-server/issues/7892)) ([041197f](https://github.com/parse-community/parse-server/commit/041197fb4ca1cd7cf18dc426ce38647267823668))
|
||||
|
||||
# [5.2.0-beta.2](https://github.com/parse-community/parse-server/compare/5.2.0-beta.1...5.2.0-beta.2) (2022-03-24)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,92 @@
|
||||
## [5.3.3](https://github.com/parse-community/parse-server/compare/5.3.2...5.3.3) (2022-11-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Prototype pollution via Cloud Code Webhooks; fixes security vulnerability [GHSA-93vw-8fm5-p2jf](https://github.com/parse-community/parse-server/security/advisories/GHSA-93vw-8fm5-p2jf) ([#8305](https://github.com/parse-community/parse-server/issues/8305)) ([60c5a73](https://github.com/parse-community/parse-server/commit/60c5a73d257e0d536056b38bdafef8b7130524d8))
|
||||
|
||||
## [5.3.2](https://github.com/parse-community/parse-server/compare/5.3.1...5.3.2) (2022-11-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Parse Server option `requestKeywordDenylist` can be bypassed via Cloud Code Webhooks or Triggers; fixes security vulnerability [GHSA-xprv-wvh7-qqqx](https://github.com/parse-community/parse-server/security/advisories/GHSA-xprv-wvh7-qqqx) ([#8302](https://github.com/parse-community/parse-server/issues/8302)) ([6728da1](https://github.com/parse-community/parse-server/commit/6728da1e3591db1e27031d335d64d8f25546a06f))
|
||||
|
||||
## [5.3.1](https://github.com/parse-community/parse-server/compare/5.3.0...5.3.1) (2022-11-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-prm5-8g2m-24gg](https://github.com/parse-community/parse-server/security/advisories/GHSA-prm5-8g2m-24gg) ([#8295](https://github.com/parse-community/parse-server/issues/8295)) ([50eed3c](https://github.com/parse-community/parse-server/commit/50eed3cffe80fadfb4bdac52b2783a18da2cfc4f))
|
||||
|
||||
# [5.3.0](https://github.com/parse-community/parse-server/compare/5.2.8...5.3.0) (2022-10-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* afterSave trigger removes pointer in Parse object ([#7913](https://github.com/parse-community/parse-server/issues/7913)) ([47d796e](https://github.com/parse-community/parse-server/commit/47d796ea58f65e71612ce37149be692abc9ea97f))
|
||||
* 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)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -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
@@ -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
|
||||
|
||||
Generated
+629
-1441
File diff suppressed because it is too large
Load Diff
+35
-34
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "5.2.4",
|
||||
"version": "5.3.3",
|
||||
"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"
|
||||
|
||||
@@ -88,6 +88,7 @@ async function config() {
|
||||
labels: ['type:ci'],
|
||||
releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
|
||||
}],
|
||||
// Back-merge module runs last because if it fails it should not impede the release process
|
||||
[
|
||||
"@saithodev/semantic-release-backmerge",
|
||||
{
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -1654,6 +1705,7 @@ 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: {
|
||||
@@ -1671,6 +1723,7 @@ describe('Apple Game Center Auth adapter', () => {
|
||||
providerOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('validateAuthData should validate', async () => {
|
||||
const options = {
|
||||
gcenter: {
|
||||
|
||||
@@ -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
@@ -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', () => {
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
+199
-10
@@ -692,7 +692,198 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
xdescribe('Gridstore Range tests', () => {
|
||||
describe_only_db('mongo')('Gridstore Range', () => {
|
||||
it('supports bytes range out of range', async () => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
};
|
||||
const response = await request({
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1//files/file.txt ',
|
||||
body: repeat('argle bargle', 100),
|
||||
});
|
||||
const b = response.data;
|
||||
const file = await request({
|
||||
url: b.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Parse-Application-Id': 'test',
|
||||
Range: 'bytes=15000-18000',
|
||||
},
|
||||
}).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',
|
||||
@@ -781,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',
|
||||
@@ -879,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',
|
||||
@@ -888,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.');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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] },
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2926,7 +2926,7 @@ describe('Parse.User testing', () => {
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve(),
|
||||
};
|
||||
reconfigureServer({
|
||||
await reconfigureServer({
|
||||
appName: 'unused',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -109,6 +109,67 @@ describe('Vulnerabilities', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('denies expanding existing object with polluted keys', async () => {
|
||||
const obj = await new Parse.Object('RCE', { a: { foo: [] } }).save();
|
||||
await reconfigureServer({
|
||||
requestKeywordDenylist: ['foo'],
|
||||
});
|
||||
obj.addUnique('a.foo', 'abc');
|
||||
await expectAsync(obj.save()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Prohibited keyword in request data: "foo".`)
|
||||
);
|
||||
});
|
||||
|
||||
it('denies creating a cloud trigger with polluted data', async () => {
|
||||
Parse.Cloud.beforeSave('TestObject', ({ object }) => {
|
||||
object.set('obj', {
|
||||
constructor: {
|
||||
prototype: {
|
||||
dummy: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
await expectAsync(new Parse.Object('TestObject').save()).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
'Prohibited keyword in request data: {"key":"constructor"}.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('denies creating a hook with polluted data', async () => {
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const port = 34567;
|
||||
const hookServerURL = 'http://localhost:' + port;
|
||||
const app = express();
|
||||
app.use(bodyParser.json({ type: '*/*' }));
|
||||
const server = await new Promise(resolve => {
|
||||
const res = app.listen(port, undefined, () => resolve(res));
|
||||
});
|
||||
app.post('/BeforeSave', function (req, res) {
|
||||
const object = Parse.Object.fromJSON(req.body.object);
|
||||
object.set('hello', 'world');
|
||||
object.set('obj', {
|
||||
constructor: {
|
||||
prototype: {
|
||||
dummy: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
res.json({ success: object });
|
||||
});
|
||||
await Parse.Hooks.createTrigger('TestObject', 'beforeSave', hookServerURL + '/BeforeSave');
|
||||
await expectAsync(new Parse.Object('TestObject').save()).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
'Prohibited keyword in request data: {"key":"constructor"}.'
|
||||
)
|
||||
);
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
});
|
||||
|
||||
it('allows BSON type code data in write request with custom denylist', async () => {
|
||||
await reconfigureServer({
|
||||
requestKeywordDenylist: [],
|
||||
@@ -279,6 +340,44 @@ describe('Vulnerabilities', () => {
|
||||
expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
|
||||
expect(text.error).toBe('Prohibited keyword in request data: {"value":"aValue[123]*"}.');
|
||||
});
|
||||
|
||||
it('denies BSON type code data in file metadata', async () => {
|
||||
const str = 'Hello World!';
|
||||
const data = [];
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
data.push(str.charCodeAt(i));
|
||||
}
|
||||
const file = new Parse.File('hello.txt', data, 'text/plain');
|
||||
file.addMetadata('obj', {
|
||||
_bsontype: 'Code',
|
||||
code: 'delete Object.prototype.evalFunctions',
|
||||
});
|
||||
await expectAsync(file.save()).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
`Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('denies BSON type code data in file tags', async () => {
|
||||
const str = 'Hello World!';
|
||||
const data = [];
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
data.push(str.charCodeAt(i));
|
||||
}
|
||||
const file = new Parse.File('hello.txt', data, 'text/plain');
|
||||
file.addTag('obj', {
|
||||
_bsontype: 'Code',
|
||||
code: 'delete Object.prototype.evalFunctions',
|
||||
});
|
||||
await expectAsync(file.save()).toBeRejectedWith(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
`Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ignore non-matches', () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -5,7 +5,7 @@ export class LRUCache {
|
||||
constructor({ ttl = defaults.cacheTTL, maxSize = defaults.cacheMaxSize }) {
|
||||
this.cache = new LRU({
|
||||
max: maxSize,
|
||||
maxAge: ttl,
|
||||
ttl,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -55,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.');
|
||||
}
|
||||
@@ -87,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.');
|
||||
}
|
||||
@@ -95,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,
|
||||
@@ -115,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}`);
|
||||
}
|
||||
});
|
||||
@@ -208,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;
|
||||
@@ -515,7 +512,7 @@ class DatabaseController {
|
||||
if (acl) {
|
||||
query = addWriteACL(query, acl);
|
||||
}
|
||||
validateQuery(query);
|
||||
validateQuery(query, isMaster, true);
|
||||
return schemaController
|
||||
.getOneSchema(className, true)
|
||||
.catch(error => {
|
||||
@@ -761,7 +758,7 @@ class DatabaseController {
|
||||
if (acl) {
|
||||
query = addWriteACL(query, acl);
|
||||
}
|
||||
validateQuery(query);
|
||||
validateQuery(query, isMaster, false);
|
||||
return schemaController
|
||||
.getOneSchema(className)
|
||||
.catch(error => {
|
||||
@@ -1253,7 +1250,7 @@ class DatabaseController {
|
||||
query = addReadACL(query, aclGroup);
|
||||
}
|
||||
}
|
||||
validateQuery(query);
|
||||
validateQuery(query, isMaster, false);
|
||||
if (count) {
|
||||
if (!classExists) {
|
||||
return 0;
|
||||
@@ -1768,7 +1765,11 @@ class DatabaseController {
|
||||
if (this.options && this.options.requestKeywordDenylist) {
|
||||
// Scan request data for denied keywords
|
||||
for (const keyword of this.options.requestKeywordDenylist) {
|
||||
const match = Utils.objectContainsKeyValue({ firstKey: undefined }, keyword.key, undefined);
|
||||
const match = Utils.objectContainsKeyValue(
|
||||
{ [firstKey]: true, [nextPath]: true },
|
||||
keyword.key,
|
||||
true
|
||||
);
|
||||
if (match) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
@@ -1809,7 +1810,7 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' },
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -39,7 +39,7 @@ const load = parseGraphQLSchema => {
|
||||
}
|
||||
},
|
||||
obj => {
|
||||
return parseGraphQLSchema.parseClassTypes[obj.className].classGraphQLOutputType;
|
||||
return parseGraphQLSchema.parseClassTypes[obj.className].classGraphQLOutputType.name;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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]),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -65,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(
|
||||
@@ -78,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) => {
|
||||
@@ -89,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);
|
||||
@@ -473,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({});
|
||||
@@ -627,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) {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+90
-41
@@ -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.
|
||||
@@ -62,18 +64,7 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.requestKeywordDenylist) {
|
||||
// Scan request data for denied keywords
|
||||
for (const keyword of this.config.requestKeywordDenylist) {
|
||||
const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
|
||||
if (match) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.checkProhibitedKeywords(data);
|
||||
|
||||
// When the operation is complete, this.response may have several
|
||||
// fields.
|
||||
@@ -95,6 +86,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 +217,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(() => {
|
||||
@@ -296,6 +281,7 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
|
||||
delete this.data.objectId;
|
||||
}
|
||||
}
|
||||
this.checkProhibitedKeywords(this.data);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -435,7 +421,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 +1008,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 +1330,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 +1541,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 +1566,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 +1608,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 +1652,7 @@ RestWrite.prototype.buildUpdatedObject = function (extraData) {
|
||||
delete sanitized[attribute];
|
||||
}
|
||||
updatedObject.set(sanitized);
|
||||
return updatedObject;
|
||||
return { updatedObject, originalObject };
|
||||
};
|
||||
|
||||
RestWrite.prototype.cleanUserAuthData = function () {
|
||||
@@ -1662,6 +1672,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;
|
||||
}
|
||||
@@ -1684,5 +1718,20 @@ RestWrite.prototype._updateResponseWithData = function (response, data) {
|
||||
return response;
|
||||
};
|
||||
|
||||
RestWrite.prototype.checkProhibitedKeywords = function (data) {
|
||||
if (this.config.requestKeywordDenylist) {
|
||||
// Scan request data for denied keywords
|
||||
for (const keyword of this.config.requestKeywordDenylist) {
|
||||
const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
|
||||
if (match) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default RestWrite;
|
||||
module.exports = RestWrite;
|
||||
|
||||
+28
-10
@@ -7,6 +7,7 @@ import mime from 'mime';
|
||||
import logger from '../logger';
|
||||
const triggers = require('../triggers');
|
||||
const http = require('http');
|
||||
const Utils = require('../Utils');
|
||||
|
||||
const downloadFileFromURI = uri => {
|
||||
return new Promise((res, rej) => {
|
||||
@@ -140,6 +141,23 @@ export class FilesRouter {
|
||||
const base64 = req.body.toString('base64');
|
||||
const file = new Parse.File(filename, { base64 }, contentType);
|
||||
const { metadata = {}, tags = {} } = req.fileData || {};
|
||||
if (req.config && req.config.requestKeywordDenylist) {
|
||||
// Scan request data for denied keywords
|
||||
for (const keyword of req.config.requestKeywordDenylist) {
|
||||
const match =
|
||||
Utils.objectContainsKeyValue(metadata, keyword.key, keyword.value) ||
|
||||
Utils.objectContainsKeyValue(tags, keyword.key, keyword.value);
|
||||
if (match) {
|
||||
next(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_KEY_NAME,
|
||||
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
file.setTags(tags);
|
||||
file.setMetadata(metadata);
|
||||
const fileSize = Buffer.byteLength(req.body);
|
||||
@@ -147,7 +165,7 @@ export class FilesRouter {
|
||||
try {
|
||||
// run beforeSaveFile trigger
|
||||
const triggerResult = await triggers.maybeRunFileTrigger(
|
||||
triggers.Types.beforeSaveFile,
|
||||
triggers.Types.beforeSave,
|
||||
fileObject,
|
||||
config,
|
||||
req.auth
|
||||
@@ -200,12 +218,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);
|
||||
@@ -228,7 +241,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
|
||||
@@ -237,7 +250,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
|
||||
@@ -271,5 +284,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'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user