diff --git a/.babelrc b/.babelrc index a199154b82..9151969bde 100644 --- a/.babelrc +++ b/.babelrc @@ -7,8 +7,7 @@ ["@babel/preset-env", { "targets": { "node": "12" - }, - "exclude": ["proposal-dynamic-import"] + } }] ], "sourceMaps": "inline" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 628903944d..2463901a91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: branches: - '**' env: - NODE_VERSION: 14.18.1 + NODE_VERSION: 16.13.0 PARSE_SERVER_TEST_TIMEOUT: 20000 jobs: check-ci: @@ -105,43 +105,43 @@ jobs: MONGODB_VERSION: 5.0.3 MONGODB_TOPOLOGY: replicaset MONGODB_STORAGE_ENGINE: wiredTiger - NODE_VERSION: 14.18.1 + NODE_VERSION: 16.13.0 - name: MongoDB 4.4, ReplicaSet, WiredTiger MONGODB_VERSION: 4.4.10 MONGODB_TOPOLOGY: replicaset MONGODB_STORAGE_ENGINE: wiredTiger - NODE_VERSION: 14.18.1 + NODE_VERSION: 16.13.0 - name: MongoDB 4.2, ReplicaSet, WiredTiger MONGODB_VERSION: 4.2.17 MONGODB_TOPOLOGY: replicaset MONGODB_STORAGE_ENGINE: wiredTiger - NODE_VERSION: 14.18.1 + NODE_VERSION: 16.13.0 - name: MongoDB 4.0, ReplicaSet, WiredTiger MONGODB_VERSION: 4.0.27 MONGODB_TOPOLOGY: replicaset MONGODB_STORAGE_ENGINE: wiredTiger - NODE_VERSION: 14.18.1 + NODE_VERSION: 16.13.0 - name: MongoDB 4.0, Standalone, MMAPv1 MONGODB_VERSION: 4.0.27 MONGODB_TOPOLOGY: standalone MONGODB_STORAGE_ENGINE: mmapv1 - NODE_VERSION: 14.18.1 + NODE_VERSION: 16.13.0 - name: Redis Cache PARSE_SERVER_TEST_CACHE: redis MONGODB_VERSION: 4.4.10 MONGODB_TOPOLOGY: standalone MONGODB_STORAGE_ENGINE: wiredTiger - NODE_VERSION: 14.18.1 + NODE_VERSION: 16.13.0 - name: Node 12 MONGODB_VERSION: 4.4.10 MONGODB_TOPOLOGY: standalone MONGODB_STORAGE_ENGINE: wiredTiger NODE_VERSION: 12.22.7 - - name: Node 15 + - name: Node 14 MONGODB_VERSION: 4.4.10 MONGODB_TOPOLOGY: standalone MONGODB_STORAGE_ENGINE: wiredTiger - NODE_VERSION: 15.14.0 + NODE_VERSION: 14.18.1 fail-fast: false name: ${{ matrix.name }} timeout-minutes: 15 @@ -183,19 +183,22 @@ jobs: include: - name: PostgreSQL 11, PostGIS 3.0 POSTGRES_IMAGE: postgis/postgis:11-3.0 - NODE_VERSION: 14.18.1 + NODE_VERSION: 16.13.0 - name: PostgreSQL 11, PostGIS 3.1 POSTGRES_IMAGE: postgis/postgis:11-3.1 - NODE_VERSION: 14.18.1 - - name: PostgreSQL 12, PostGIS 3.1 - POSTGRES_IMAGE: postgis/postgis:12-3.1 - NODE_VERSION: 14.18.1 - - name: PostgreSQL 13, PostGIS 3.1 - POSTGRES_IMAGE: postgis/postgis:13-3.1 - NODE_VERSION: 14.18.1 - - name: PostgreSQL 14, PostGIS 3.1 - POSTGRES_IMAGE: postgis/postgis:14-3.1 - NODE_VERSION: 14.18.1 + NODE_VERSION: 16.13.0 + - name: PostgreSQL 11, PostGIS 3.2 + POSTGRES_IMAGE: postgis/postgis:11-3.2 + NODE_VERSION: 16.13.0 + - name: PostgreSQL 12, PostGIS 3.2 + POSTGRES_IMAGE: postgis/postgis:12-3.2 + NODE_VERSION: 16.13.0 + - name: PostgreSQL 13, PostGIS 3.2 + POSTGRES_IMAGE: postgis/postgis:13-3.2 + NODE_VERSION: 16.13.0 + - name: PostgreSQL 14, PostGIS 3.2 + POSTGRES_IMAGE: postgis/postgis:14-3.2 + NODE_VERSION: 16.13.0 fail-fast: false name: ${{ matrix.name }} timeout-minutes: 15 diff --git a/.madgerc b/.madgerc new file mode 100644 index 0000000000..4b9aa24bc2 --- /dev/null +++ b/.madgerc @@ -0,0 +1,10 @@ +{ + "detectiveOptions": { + "ts": { + "skipTypeImports": true + }, + "es6": { + "skipTypeImports": true + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index a977705686..7c12e7cf72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Details: - Purpose: official release - Suitable environment: production - +- Suitable environment: development ## 🔥 [Alpha Releases][log_alpha] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e620fbc78..ddc99a811a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,8 @@ - [Pull Request](#pull-request) - [Breaking Change](#breaking-change) - [Merging](#merging) + - [Breaking Change](#breaking-change-1) + - [Reverting](#reverting) - [Versioning](#versioning) - [Code of Conduct](#code-of-conduct) @@ -154,7 +156,7 @@ If your pull request introduces a change that may affect the storage or retrieva [PostGIS images (select one with v2.2 or higher) on docker dashboard](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://registry.hub.docker.com/_/postgres/) image and will work out-of-the-box (as long as you create a user with the necessary extensions for each of your Parse databases; see below). To launch the compatible Postgres instance, copy and paste the following line into your shell: ``` -docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:13-3.1-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database +docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:13-3.2-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database ``` To stop the Postgres instance: @@ -162,7 +164,7 @@ To stop the Postgres instance: docker stop parse-postgres ``` -You can also use the [postgis/postgis:13-3.1-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines: +You can also use the [postgis/postgis:13-3.2-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines: ``` #Install additional scripts. These are run in abc order during initial start @@ -335,8 +337,13 @@ If a pull request contains a braking change, the description of the pull request The following guide is for anyone who merges a contributor pull request into the working branch, the working branch into a release branch, a release branch into another release branch, or any other direct commits such as hotfixes into release branches or the working branch. -- For changelog generation, only the commit message set when merging the pull request is relevant. The title and description of the GitHub pull request as authored by the contributor have no influence on the changelog generation. However, the title of the GitHub pull request should be used as the commit message. -- If the pull request contains a breaking change, the commit message must contain the phrase `BREAKING CHANGE`, capitalized and without any formatting, followed by a short description of the breaking change and ideally how the developer should address it, all in a single line. This line should contain more details focusing on the "breaking” aspect of the change and is intended to assist the developer in adapting. Keep it concise, as it will become part of the changelog entry, for example: +- A contributor pull request must be merged into the working branch using `Squash and Merge`, to create a single commit message that describes the change. +- A release branch or the default branch must be merged into another release branch using `Merge Commit`, to preserve each individual commit message that describes its respective change. +- For changelog generation, only the commit message set when merging the pull request is relevant. The title and description of the GitHub pull request as authored by the contributor have no influence on the changelog generation. However, the title of the GitHub pull request should be used as the commit message. See the following chapters for considerations in special scenarios, e.g. merging a breaking change or reverting a commit. + +### Breaking Change + +If the pull request contains a breaking change, the commit message must contain the phrase `BREAKING CHANGE`, capitalized and without any formatting, followed by a short description of the breaking change and ideally how the developer should address it, all in a single line. This line should contain more details focusing on the "breaking” aspect of the change and is intended to assist the developer in adapting. Keep it concise, as it will become part of the changelog entry, for example: ``` fix: remove handle from door @@ -344,8 +351,16 @@ The following guide is for anyone who merges a contributor pull request into the BREAKING CHANGE: You cannot open the door anymore by using a handle. See the [#migration guide](http://example.com) for more details. ``` Keep in mind that in a repository with release automation, merging such a commit message will trigger a release with a major version increment. -- A contributor pull request must be merged into the working branch using `Squash and Merge`, to create a single commit message that describes the change. -- A release branch or the default branch must be merged into another release branch using `Merge Commit`, to preserve each individual commit message that describes its respective change. + +### Reverting + +If the commit reverts a previous commit, use the prefix `revert:`, followed by the header of the reverted commit. In the body of the commit message add `This reverts commit .`, where the hash is the SHA of the commit being reverted. For example: + + ``` + revert: fix: remove handle from door + + This reverts commit 1234567890abcdef. + ``` ## Versioning diff --git a/README.md b/README.md index a25752cc6e..7b507e9c80 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,15 @@ [![Build Status](https://github.com/parse-community/parse-server/workflows/ci/badge.svg?branch=alpha)](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Aalpha) [![Snyk Badge](https://snyk.io/test/github/parse-community/parse-server/badge.svg)](https://snyk.io/test/github/parse-community/parse-server) [![Coverage](https://img.shields.io/codecov/c/github/parse-community/parse-server/alpha.svg)](https://codecov.io/github/parse-community/parse-server?branch=alpha) +[![auto-release](https://img.shields.io/badge/%F0%9F%9A%80-auto--release-9e34eb.svg)](https://github.com/parse-community/parse-dashboard/releases) -[![Node Version](https://img.shields.io/badge/nodejs-12,_14,_15-green.svg?logo=node.js&style=flat)](https://nodejs.org) +[![Node Version](https://img.shields.io/badge/nodejs-12,_14,_16-green.svg?logo=node.js&style=flat)](https://nodejs.org) [![MongoDB Version](https://img.shields.io/badge/mongodb-4.0,_4.2,_4.4,_5.0-green.svg?logo=mongodb&style=flat)](https://www.mongodb.com) [![Postgres Version](https://img.shields.io/badge/postgresql-11,_12,_13,_14-green.svg?logo=postgresql&style=flat)](https://www.postgresql.org) -[![auto-release](https://img.shields.io/badge/%F0%9F%9A%80-auto--release-9e34eb.svg)](https://github.com/parse-community/parse-dashboard/releases) [![npm latest version](https://img.shields.io/npm/v/parse-server/latest.svg)](https://www.npmjs.com/package/parse-server) +[![npm beta version](https://img.shields.io/npm/v/parse-server/beta.svg)](https://www.npmjs.com/package/parse-server) [![npm alpha version](https://img.shields.io/npm/v/parse-server/alpha.svg)](https://www.npmjs.com/package/parse-server) - [![Backers on Open Collective](https://opencollective.com/parse-server/backers/badge.svg)][open-collective-link] [![Sponsors on Open Collective](https://opencollective.com/parse-server/sponsors/badge.svg)][open-collective-link] @@ -29,11 +29,15 @@ The full documentation for Parse Server is available in the [wiki](https://githu --- -A big *thank you* to all our backers and sponsors who support the development of Parse Platform! +A big *thank you* 🙏 to our [sponsors](#sponsors) and [backers](#backers) who support the development of Parse Platform! -### 💎 Diamond Sponsors - -[![Sponsor](https://opencollective.com/parse-server/sponsor/0/avatar.svg)](https://opencollective.com/parse-server/sponsor/0/website) +### Diamond Sponsors + +[![Diamond Sponsors](https://opencollective.com/parse-server/tiers/diamond-sponsor.svg?avatarHeight=70&button=false)](https://opencollective.com/parse-server/contribute/diamond-sponsor-10560) + +#### Bronze Sponsors + +[![Bronze Sponsors](https://opencollective.com/parse-server/tiers/bronze-sponsor.svg?avatarHeight=36&button=false)](https://opencollective.com/parse-server/contribute/bronze-sponsor-10559) --- @@ -112,8 +116,8 @@ Parse Server is continuously tested with the most recent releases of Node.js to |------------|----------------|-------------|---------------| | Node.js 12 | 12.22.7 | April 2022 | ✅ Yes | | Node.js 14 | 14.18.1 | April 2023 | ✅ Yes | -| Node.js 15 | 15.14.0 | June 2021 | ✅ Yes | -| Node.js 16 | 16.x.x | April 2024 | ❌ Not tested | +| Node.js 16 | 16.13.0 | April 2024 | ✅ Yes | +| Node.js 17 | 17.x | June 2022 | ❌ Not tested | #### MongoDB Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and only test against versions that are officially supported and have not reached their end-of-life date. @@ -130,10 +134,10 @@ Parse Server is continuously tested with the most recent releases of PostgreSQL | Version | PostGIS Version | End-of-Life | Parse Server Support End | Compatible | |-------------|-----------------|---------------|--------------------------|------------| -| Postgres 11 | 3.0, 3.1 | November 2023 | April 2022 | ✅ Yes | -| Postgres 12 | 3.1 | November 2024 | April 2023 | ✅ Yes | -| Postgres 13 | 3.1 | November 2025 | April 2024 | ✅ Yes | -| Postgres 14 | 3.1 | November 2026 | April 2025 | ✅ Yes | +| 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 @@ -485,11 +489,12 @@ You can also find more adapters maintained by the community by searching on [npm Parse Server allows developers to choose from several options when hosting files: -* `GridFSBucketAdapter`, which is backed by MongoDB; -* `S3Adapter`, which is backed by [Amazon S3](https://aws.amazon.com/s3/); or -* `GCSAdapter`, which is backed by [Google Cloud Storage](https://cloud.google.com/storage/) +* `GridFSBucketAdapter` - which is backed by MongoDB +* `S3Adapter` - which is backed by [Amazon S3](https://aws.amazon.com/s3/) +* `GCSAdapter` - which is backed by [Google Cloud Storage](https://cloud.google.com/storage/) +* `FSAdapter` - local file storage -`GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters). +`GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using Amazon S3, Google Cloud Storage, or local file storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters). ## Idempotency Enforcement @@ -520,9 +525,26 @@ let api = new ParseServer({ | `idempotencyOptions.paths` | yes | `Array` | `[]` | `.*` (all paths, includes the examples below),
`functions/.*` (all functions),
`jobs/.*` (all jobs),
`classes/.*` (all classes),
`functions/.*` (all functions),
`users` (user creation / update),
`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specify the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. | | `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. | -### Notes +### Postgres + +To use this feature in Postgres, you will need to create a cron job using [pgAdmin](https://www.pgadmin.org/docs/pgadmin4/development/pgagent_jobs.html) or similar to call the Postgres function `idempotency_delete_expired_records()` that deletes expired idempotency records. You can find an example script below. Make sure the script has the same privileges to log into Postgres as Parse Server. -- This feature is currently only available for MongoDB and not for Postgres. +```bash +#!/bin/bash + +set -e +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + SELECT idempotency_delete_expired_records(); +EOSQL + +exec "$@" +``` + +Assuming the script above is named, `parse_idempotency_delete_expired_records.sh`, a cron job that runs the script every 2 minutes may look like: + +```bash +2 * * * * /root/parse_idempotency_delete_expired_records.sh >/dev/null 2>&1 +``` ## Localization diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 1d5c4e5ee9..21863e351e 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,174 @@ +# [5.0.0-alpha.28](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.27...5.0.0-alpha.28) (2022-03-12) + + +### Bug Fixes + +* security vulnerability that allows remote code execution (GHSA-p6h4-93qp-jhcm) ([#7844](https://github.com/parse-community/parse-server/issues/7844)) ([e569f40](https://github.com/parse-community/parse-server/commit/e569f402b1fd8648fb0d1523b71b2a03273902a5)) + +# [5.0.0-alpha.27](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.26...5.0.0-alpha.27) (2022-03-12) + + +### Reverts + +* update node engine to 2.22.0 ([#7827](https://github.com/parse-community/parse-server/issues/7827)) ([f235412](https://github.com/parse-community/parse-server/commit/f235412c1b6c2b173b7531f285429ea7214b56a2)) + +# [5.0.0-alpha.26](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.25...5.0.0-alpha.26) (2022-02-25) + + +### Bug Fixes + +* package.json & package-lock.json to reduce vulnerabilities ([#7823](https://github.com/parse-community/parse-server/issues/7823)) ([5ca2288](https://github.com/parse-community/parse-server/commit/5ca228882332b65f3ac05407e6e4da1ee3ef3749)) + +# [5.0.0-alpha.25](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.24...5.0.0-alpha.25) (2022-02-23) + + +### Bug Fixes + +* upgrade winston from 3.5.0 to 3.5.1 ([#7820](https://github.com/parse-community/parse-server/issues/7820)) ([4af253d](https://github.com/parse-community/parse-server/commit/4af253d1f8654a6f57b5137ad310cdacadc922cc)) + +# [5.0.0-alpha.24](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.23...5.0.0-alpha.24) (2022-02-10) + + +### Bug Fixes + +* security upgrade follow-redirects from 1.14.7 to 1.14.8 ([#7801](https://github.com/parse-community/parse-server/issues/7801)) ([70088a9](https://github.com/parse-community/parse-server/commit/70088a95a78393da2a4ac68be81e63107747626a)) + +# [5.0.0-alpha.23](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.22...5.0.0-alpha.23) (2022-02-06) + + +### Bug Fixes + +* server crash using GraphQL due to missing @apollo/client peer dependency ([#7787](https://github.com/parse-community/parse-server/issues/7787)) ([08089d6](https://github.com/parse-community/parse-server/commit/08089d6fcbb215412448ce7d92b21b9fe6c929f2)) + +# [5.0.0-alpha.22](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.21...5.0.0-alpha.22) (2022-02-06) + + +### Features + +* upgrade to MongoDB Node.js driver 4.x for MongoDB 5.0 support ([#7794](https://github.com/parse-community/parse-server/issues/7794)) ([f88aa2a](https://github.com/parse-community/parse-server/commit/f88aa2a62a533e5344d1c13dd38c5a0b283a480a)) + + +### BREAKING CHANGES + +* The MongoDB GridStore adapter has been removed. By default, Parse Server already uses GridFS, so if you do not manually use the GridStore adapter, you can ignore this change. ([f88aa2a](f88aa2a)) + +# [5.0.0-alpha.21](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.20...5.0.0-alpha.21) (2022-01-25) + + +### Features + +* add Cloud Code context to `ParseObject.fetch` ([#7779](https://github.com/parse-community/parse-server/issues/7779)) ([315290d](https://github.com/parse-community/parse-server/commit/315290d16110110938f80a6b779cc2d1db58c552)) + +# [5.0.0-alpha.20](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.19...5.0.0-alpha.20) (2022-01-22) + + +### Bug Fixes + +* bump node-fetch from 2.6.1 to 3.1.1 ([#7782](https://github.com/parse-community/parse-server/issues/7782)) ([9082351](https://github.com/parse-community/parse-server/commit/90823514113a1a085ebc818f7109b3fd7591346f)) + +# [5.0.0-alpha.19](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.18...5.0.0-alpha.19) (2022-01-22) + + +### Bug Fixes + +* bump nanoid from 3.1.25 to 3.2.0 ([#7781](https://github.com/parse-community/parse-server/issues/7781)) ([f5f63bf](https://github.com/parse-community/parse-server/commit/f5f63bfc64d3481ed944ceb5e9f50b33dccd1ce9)) + +# [5.0.0-alpha.18](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.17...5.0.0-alpha.18) (2022-01-13) + + +### Bug Fixes + +* security upgrade follow-redirects from 1.14.6 to 1.14.7 ([#7769](https://github.com/parse-community/parse-server/issues/7769)) ([8f5a861](https://github.com/parse-community/parse-server/commit/8f5a8618cfa7ed9a2a239a095abffa8f3fd8d31a)) + +# [5.0.0-alpha.17](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.16...5.0.0-alpha.17) (2022-01-13) + + +### Bug Fixes + +* schema cache not cleared in some cases ([#7678](https://github.com/parse-community/parse-server/issues/7678)) ([5af6e5d](https://github.com/parse-community/parse-server/commit/5af6e5dfaa129b1a350afcba4fb381b21c4cc35d)) + +# [5.0.0-alpha.16](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.15...5.0.0-alpha.16) (2022-01-02) + + +### Features + +* add Idempotency to Postgres ([#7750](https://github.com/parse-community/parse-server/issues/7750)) ([0c3feaa](https://github.com/parse-community/parse-server/commit/0c3feaaa1751964c0db89f25674935c3354b1538)) + +# [5.0.0-alpha.15](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.14...5.0.0-alpha.15) (2022-01-02) + + +### Features + +* support `postgresql` protocol in database URI ([#7757](https://github.com/parse-community/parse-server/issues/7757)) ([caf4a23](https://github.com/parse-community/parse-server/commit/caf4a2341f554b28e3918c53e7e897a3ca47bf8b)) + +# [5.0.0-alpha.14](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.13...5.0.0-alpha.14) (2022-01-02) + + +### Features + +* support relativeTime query constraint on Postgres ([#7747](https://github.com/parse-community/parse-server/issues/7747)) ([16b1b2a](https://github.com/parse-community/parse-server/commit/16b1b2a19714535ca805f2dbb3b561d8f6a519a7)) + +# [5.0.0-alpha.13](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.12...5.0.0-alpha.13) (2021-12-08) + + +### Bug Fixes + +* node engine compatibility did not include node 16 ([#7739](https://github.com/parse-community/parse-server/issues/7739)) ([ea7c014](https://github.com/parse-community/parse-server/commit/ea7c01400f992a1263543706fe49b6174758a2d6)) + +# [5.0.0-alpha.12](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.11...5.0.0-alpha.12) (2021-12-06) + + +### Bug Fixes + +* adding or modifying a nested property requires addField permissions ([#7679](https://github.com/parse-community/parse-server/issues/7679)) ([6a6248b](https://github.com/parse-community/parse-server/commit/6a6248b6cb2e732d17131e18e659943b894ed2f1)) + +# [5.0.0-alpha.11](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.10...5.0.0-alpha.11) (2021-11-29) + + +### Bug Fixes + +* upgrade mime from 2.5.2 to 3.0.0 ([#7725](https://github.com/parse-community/parse-server/issues/7725)) ([f5ef98b](https://github.com/parse-community/parse-server/commit/f5ef98bde32083403c0e30a12162fcc1e52cac37)) + +# [5.0.0-alpha.10](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.9...5.0.0-alpha.10) (2021-11-29) + + +### Bug Fixes + +* upgrade parse from 3.3.1 to 3.4.0 ([#7723](https://github.com/parse-community/parse-server/issues/7723)) ([d4c1f47](https://github.com/parse-community/parse-server/commit/d4c1f473073764cb0570c633fc4a30669c2ce889)) + +# [5.0.0-alpha.9](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.8...5.0.0-alpha.9) (2021-11-27) + + +### Bug Fixes + +* unable to use objectId size higher than 19 on GraphQL API ([#7627](https://github.com/parse-community/parse-server/issues/7627)) ([ed86c80](https://github.com/parse-community/parse-server/commit/ed86c807721cc52a1a5a9dea0b768717eec269ed)) + +# [5.0.0-alpha.8](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.7...5.0.0-alpha.8) (2021-11-18) + + +### Features + +* add support for Node 16 ([#7707](https://github.com/parse-community/parse-server/issues/7707)) ([45cc58c](https://github.com/parse-community/parse-server/commit/45cc58c7e5e640a46c5d508019a3aa81242964b1)) + + +### BREAKING CHANGES + +* Removes official Node 15 support which has reached it end-of-life date. ([45cc58c](45cc58c)) + +# [5.0.0-alpha.7](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.6...5.0.0-alpha.7) (2021-11-12) + + +### Bug Fixes + +* node engine range has no upper limit to exclude incompatible node versions ([#7692](https://github.com/parse-community/parse-server/issues/7692)) ([573558d](https://github.com/parse-community/parse-server/commit/573558d3adcbcc6222c92003829867e1a73eef94)) + +# [5.0.0-alpha.6](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.5...5.0.0-alpha.6) (2021-11-10) + + +### Reverts + +* refactor: allow ES import for cloud string if package type is module ([b64640c](https://github.com/parse-community/parse-server/commit/b64640c5705f733798783e68d216e957044ef23c)) + # [5.0.0-alpha.5](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.4...5.0.0-alpha.5) (2021-11-01) diff --git a/ci/ciCheck.js b/ci/ciCheck.js index e1d968be19..2ad5c3e8f3 100644 --- a/ci/ciCheck.js +++ b/ci/ciCheck.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; const CiVersionCheck = require('./CiVersionCheck'); const mongoVersionList = require('mongodb-version-list'); @@ -14,9 +14,8 @@ async function check() { * Check the MongoDB versions used in test environments. */ async function checkMongoDbVersions() { - const releasedVersions = await new Promise((resolve, reject) => { - mongoVersionList(function(error, versions) { + mongoVersionList(function (error, versions) { if (error) { reject(error); } @@ -47,7 +46,6 @@ async function checkMongoDbVersions() { * Check the Nodejs versions used in test environments. */ async function checkNodeVersions() { - const allVersions = await allNodeVersions(); const releasedVersions = allVersions.versions; @@ -62,7 +60,8 @@ async function checkNodeVersions() { ignoreReleasedVersions: [ '<12.0.0', // These versions have reached their end-of-life support date '>=13.0.0 <14.0.0', // These versions have reached their end-of-life support date - '>=16.0.0', // This version has not been officially released yet + '>=15.0.0 <16.0.0', // These versions have reached their end-of-life support date + '>=17.0.0', // These versions are not officially supported yet ], }).check(); } diff --git a/package-lock.json b/package-lock.json index 6b37705ad4..ec5397acc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.0.0-alpha.5", + "version": "5.0.0-alpha.28", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -11,9 +11,9 @@ "dev": true }, "@apollo/client": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.4.8.tgz", - "integrity": "sha512-/cNqTSwc2Dw8q6FDDjdd30+yvhP7rI0Fvl3Hbro0lTtFuhzkevfNyQaI2jAiOrjU6Jc0RbanxULaNrX7UmvjSQ==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.5.8.tgz", + "integrity": "sha512-MAm05+I1ullr64VLpZwon/ISnkMuNLf6vDqgo9wiMhHYBGT4yOAbAIseRdjCHZwfSx/7AUuBgaTNOssZPIr6FQ==", "requires": { "@graphql-typed-document-node/core": "^3.0.0", "@wry/context": "^0.6.0", @@ -24,9 +24,9 @@ "optimism": "^0.16.1", "prop-types": "^15.7.2", "symbol-observable": "^4.0.0", - "ts-invariant": "^0.9.0", + "ts-invariant": "^0.9.4", "tslib": "^2.3.0", - "zen-observable-ts": "^1.1.0" + "zen-observable-ts": "^1.2.0" }, "dependencies": { "@wry/equality": { @@ -38,9 +38,9 @@ } }, "ts-invariant": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.9.1.tgz", - "integrity": "sha512-hSeYibh29ULlHkuEfukcoiyTct+s2RzczMLTv4x3NWC/YrBy7x7ps5eYq/b4Y3Sb9/uAlf54+/5CAEMVxPhuQw==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.9.4.tgz", + "integrity": "sha512-63jtX/ZSwnUNi/WhXjnK8kz4cHHpYS60AnmA6ixz17l7E12a5puCWFlNpkne5Rl0J8TBPVHpGjsj4fxs8ObVLQ==", "requires": { "tslib": "^2.1.0" } @@ -51,11 +51,10 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "zen-observable-ts": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz", - "integrity": "sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.3.tgz", + "integrity": "sha512-hc/TGiPkAWpByykMwDcem3SdUgA4We+0Qb36bItSuJC9xD0XVBZoFHYoadAomDSNf64CG8Ydj0Qb8Od8BUWz5g==", "requires": { - "@types/zen-observable": "0.8.3", "zen-observable": "0.8.15" } } @@ -1104,6 +1103,7 @@ "version": "7.15.3", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.3.tgz", "integrity": "sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==", + "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } @@ -1172,10 +1172,15 @@ "to-fast-properties": "^2.0.0" } }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + }, "@dabh/diagnostics": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", - "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", "requires": { "colorspace": "1.1.x", "enabled": "2.0.x", @@ -1192,6 +1197,37 @@ "tslib": "~2.0.1" } }, + "@graphql-tools/batch-execute": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.3.2.tgz", + "integrity": "sha512-ICWqM+MvEkIPHm18Q0cmkvm134zeQMomBKmTRxyxMNhL/ouz6Nqld52/brSlaHnzA3fczupeRJzZ0YatruGBcQ==", + "requires": { + "@graphql-tools/utils": "^8.6.2", + "dataloader": "2.0.0", + "tslib": "~2.3.0", + "value-or-promise": "1.0.11" + }, + "dependencies": { + "@graphql-tools/utils": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.2.tgz", + "integrity": "sha512-x1DG0cJgpJtImUlNE780B/dfp8pxvVxOD6UeykFH5rHes26S4kGokbgU8F1IgrJ1vAPm/OVBHtd2kicTsPfwdA==", + "requires": { + "tslib": "~2.3.0" + } + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "value-or-promise": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", + "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==" + } + } + }, "@graphql-tools/delegate": { "version": "6.2.4", "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-6.2.4.tgz", @@ -1206,45 +1242,95 @@ } }, "@graphql-tools/links": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/@graphql-tools/links/-/links-6.2.5.tgz", - "integrity": "sha512-XeGDioW7F+HK6HHD/zCeF0HRC9s12NfOXAKv1HC0J7D50F4qqMvhdS/OkjzLoBqsgh/Gm8icRc36B5s0rOA9ig==", - "requires": { - "@graphql-tools/utils": "^7.0.0", - "apollo-link": "1.2.14", - "apollo-upload-client": "14.1.2", - "cross-fetch": "3.0.6", - "form-data": "3.0.0", - "is-promise": "4.0.0", - "tslib": "~2.0.1" + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/links/-/links-8.2.2.tgz", + "integrity": "sha512-lWyRvG4KqVj/3dpuQzZN34TXs9+5ETaT1MxhPHe6LIF/DdNQk4Q4Y7VeET/fZ8ZhbzgweMy0AA+ZkrS2HxBcgw==", + "requires": { + "@graphql-tools/delegate": "^8.5.1", + "@graphql-tools/utils": "^8.6.2", + "apollo-upload-client": "17.0.0", + "form-data": "^4.0.0", + "node-fetch": "^2.6.5", + "tslib": "~2.3.0" }, "dependencies": { + "@graphql-tools/delegate": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-8.5.1.tgz", + "integrity": "sha512-/YPmVxitt57F8sH50pnfXASzOOjEfaUDkX48eF5q6f16+JBncej2zeu+Zm2c68q8MbIxhPlEGfpd0QZeqTvAxw==", + "requires": { + "@graphql-tools/batch-execute": "^8.3.2", + "@graphql-tools/schema": "^8.3.2", + "@graphql-tools/utils": "^8.6.2", + "dataloader": "2.0.0", + "graphql-executor": "0.0.18", + "tslib": "~2.3.0", + "value-or-promise": "1.0.11" + } + }, + "@graphql-tools/merge": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.3.tgz", + "integrity": "sha512-XCSmL6/Xg8259OTWNp69B57CPWiVL69kB7pposFrufG/zaAlI9BS68dgzrxmmSqZV5ZHU4r/6Tbf6fwnEJGiSw==", + "requires": { + "@graphql-tools/utils": "^8.6.2", + "tslib": "~2.3.0" + } + }, + "@graphql-tools/schema": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.2.tgz", + "integrity": "sha512-77feSmIuHdoxMXRbRyxE8rEziKesd/AcqKV6fmxe7Zt+PgIQITxNDew2XJJg7qFTMNM43W77Ia6njUSBxNOkwg==", + "requires": { + "@graphql-tools/merge": "^8.2.3", + "@graphql-tools/utils": "^8.6.2", + "tslib": "~2.3.0", + "value-or-promise": "1.0.11" + } + }, "@graphql-tools/utils": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.10.0.tgz", - "integrity": "sha512-d334r6bo9mxdSqZW6zWboEnnOOFRrAPVQJ7LkU8/6grglrbcu6WhwCLzHb90E94JI3TD3ricC3YGbUqIi9Xg0w==", + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.2.tgz", + "integrity": "sha512-x1DG0cJgpJtImUlNE780B/dfp8pxvVxOD6UeykFH5rHes26S4kGokbgU8F1IgrJ1vAPm/OVBHtd2kicTsPfwdA==", "requires": { - "@ardatan/aggregate-error": "0.0.6", - "camel-case": "4.1.2", - "tslib": "~2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } + "tslib": "~2.3.0" } }, "apollo-upload-client": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-14.1.2.tgz", - "integrity": "sha512-ozaW+4tnVz1rpfwiQwG3RCdCcZ93RV/37ZQbRnObcQ9mjb+zur58sGDPVg9Ef3fiujLmiE/Fe9kdgvIMA3VOjA==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-17.0.0.tgz", + "integrity": "sha512-pue33bWVbdlXAGFPkgz53TTmxVMrKeQr0mdRcftNY+PoHIdbGZD0hoaXHvO6OePJAkFz7OiCFUf98p1G/9+Ykw==", + "requires": { + "extract-files": "^11.0.0" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "requires": { - "@apollo/client": "^3.1.5", - "@babel/runtime": "^7.11.2", - "extract-files": "^9.0.0" + "whatwg-url": "^5.0.0" } + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "value-or-promise": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", + "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==" } } }, @@ -1387,9 +1473,9 @@ } }, "@graphql-typed-document-node/core": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz", - "integrity": "sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==" }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -1703,6 +1789,15 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } } } }, @@ -1738,10 +1833,15 @@ "@octokit/openapi-types": "^11.2.0" } }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + }, "@parse/fs-files-adapter": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@parse/fs-files-adapter/-/fs-files-adapter-1.2.0.tgz", - "integrity": "sha512-kr7Ti2eYOm14p05S86yriJdMtawL6qln3Dn5eekrwY14ih4jrjH/E+QlEpBUSBzN64fluFxciFOyjdbwDGWsGw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@parse/fs-files-adapter/-/fs-files-adapter-1.2.1.tgz", + "integrity": "sha512-jUbmlvql9+5Mz8Q6KSk1jH823MVerhOYK1svayYpF03v75OtDn3p+mAoFvPS5UpRln1kT6BlBnLfw4Hv08SD5Q==" }, "@parse/minami": { "version": "1.0.0", @@ -1750,9 +1850,9 @@ "dev": true }, "@parse/node-apn": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-4.1.1.tgz", - "integrity": "sha512-stWlQE95w5T0vkVYscoq/S3eXPQ1qzdQbKKQ8GAdw4CSNxRWLWgOH50byUR30thnQ93RshLCH5ROkvXMqzzLtw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.1.0.tgz", + "integrity": "sha512-WT3iVwr1Y/Jf4nq4RGNwBdLwm3gTodsb+g3IY98MPSJ7LCNf+R81Nj/nQO5r/twJfN1v5B8cAgfvPGs2rPelvg==", "requires": { "debug": "4.3.2", "jsonwebtoken": "8.5.1", @@ -1786,11 +1886,11 @@ } }, "@parse/push-adapter": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-3.4.1.tgz", - "integrity": "sha512-iev69kbwhXbez5nfEwB2GkCWBLUmRlImQTmHPpLnHBJfATYKyXf/H41WQhBPuwwScZBVp9ABsIsjjKy8iKg3fw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@parse/push-adapter/-/push-adapter-4.1.0.tgz", + "integrity": "sha512-8SOU4zgIr3+wn6Hbge4X/zAYAcJR7puJ3aY2ri+8fqMARgBria4JkIeAyKaTG/mUMHw6Qy5DpYYRe0LjImjZNw==", "requires": { - "@parse/node-apn": "4.1.1", + "@parse/node-apn": "5.1.0", "@parse/node-gcm": "1.0.2", "npmlog": "4.1.2", "parse": "3.3.0" @@ -2153,6 +2253,12 @@ "universalify": "^2.0.0" } }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2305,7 +2411,8 @@ "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true }, "@types/accepts": { "version": "1.3.5", @@ -2556,6 +2663,20 @@ "@types/node": "*" } }, + "@types/webidl-conversions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz", + "integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q==" + }, + "@types/whatwg-url": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.1.tgz", + "integrity": "sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==", + "requires": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, "@types/ws": { "version": "7.4.7", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", @@ -2579,11 +2700,6 @@ "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", "dev": true }, - "@types/zen-observable": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz", - "integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==" - }, "@typescript-eslint/types": { "version": "4.31.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.31.0.tgz", @@ -2743,6 +2859,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, "requires": { "debug": "4" }, @@ -2751,6 +2868,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -2758,7 +2876,8 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, @@ -2910,24 +3029,6 @@ } } }, - "apollo-cache": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.3.5.tgz", - "integrity": "sha512-1XoDy8kJnyWY/i/+gLTEbYLnoiVtS8y7ikBr/IfmML4Qb+CM7dEEbIUOjnY716WqmZ/UpXIxTfJsY7rMcqiCXA==", - "dev": true, - "requires": { - "apollo-utilities": "^1.3.4", - "tslib": "^1.10.0" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, "apollo-cache-control": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.14.0.tgz", @@ -2937,76 +3038,6 @@ "apollo-server-plugin-base": "^0.13.0" } }, - "apollo-cache-inmemory": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.6.tgz", - "integrity": "sha512-L8pToTW/+Xru2FFAhkZ1OA9q4V4nuvfoPecBM34DecAugUZEBhI2Hmpgnzq2hTKZ60LAMrlqiASm0aqAY6F8/A==", - "dev": true, - "requires": { - "apollo-cache": "^1.3.5", - "apollo-utilities": "^1.3.4", - "optimism": "^0.10.0", - "ts-invariant": "^0.4.0", - "tslib": "^1.10.0" - }, - "dependencies": { - "@wry/context": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.4.4.tgz", - "integrity": "sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==", - "dev": true, - "requires": { - "@types/node": ">=6", - "tslib": "^1.9.3" - } - }, - "optimism": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.10.3.tgz", - "integrity": "sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==", - "dev": true, - "requires": { - "@wry/context": "^0.4.0" - } - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, - "apollo-client": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.6.10.tgz", - "integrity": "sha512-jiPlMTN6/5CjZpJOkGeUV0mb4zxx33uXWdj/xQCfAMkuNAC3HN7CvYDyMHHEzmcQ5GV12LszWoQ/VlxET24CtA==", - "dev": true, - "requires": { - "@types/zen-observable": "^0.8.0", - "apollo-cache": "1.3.5", - "apollo-link": "^1.0.0", - "apollo-utilities": "1.3.4", - "symbol-observable": "^1.0.2", - "ts-invariant": "^0.4.0", - "tslib": "^1.10.0", - "zen-observable": "^0.8.0" - }, - "dependencies": { - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, "apollo-datasource": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.9.0.tgz", @@ -3044,62 +3075,6 @@ } } }, - "apollo-link-http": { - "version": "1.5.17", - "resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.17.tgz", - "integrity": "sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==", - "dev": true, - "requires": { - "apollo-link": "^1.2.14", - "apollo-link-http-common": "^0.2.16", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, - "apollo-link-http-common": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz", - "integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==", - "dev": true, - "requires": { - "apollo-link": "^1.2.14", - "ts-invariant": "^0.4.0", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, - "apollo-link-ws": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/apollo-link-ws/-/apollo-link-ws-1.0.20.tgz", - "integrity": "sha512-mjSFPlQxmoLArpHBeUb2Xj+2HDYeTaJqFGOqQ+I8NVJxgL9lJe84PDWcPah/yMLv3rB7QgBDSuZ0xoRFBPlySw==", - "dev": true, - "requires": { - "apollo-link": "^1.2.14", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, "apollo-reporting-protobuf": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.8.0.tgz", @@ -3207,6 +3182,16 @@ "requires": { "node-fetch": "^2.6.1", "util.promisify": "^1.0.0" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + } } }, "apollo-server-errors": { @@ -3300,23 +3285,12 @@ } }, "apollo-upload-client": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-13.0.0.tgz", - "integrity": "sha512-lJ9/bk1BH1lD15WhWRha2J3+LrXrPIX5LP5EwiOUHv8PCORp4EUrcujrA3rI5hZeZygrTX8bshcuMdpqpSrvtA==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-16.0.0.tgz", + "integrity": "sha512-aLhYucyA0T8aBEQ5g+p13qnR9RUyL8xqb8FSZ7e/Kw2KUOsotLUlFluLobqaE7JSUFwc6sKfXIcwB7y4yEjbZg==", "dev": true, "requires": { - "@babel/runtime": "^7.9.2", - "apollo-link": "^1.2.12", - "apollo-link-http-common": "^0.2.14", - "extract-files": "^8.0.0" - }, - "dependencies": { - "extract-files": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-8.1.0.tgz", - "integrity": "sha512-PTGtfthZK79WUMk+avLmwx3NGdU8+iVFXC2NMGxKsn0MnihOG2lvumj+AZo8CTwTrwjXDgZ5tztbRlEdRjBonQ==", - "dev": true - } + "extract-files": "^11.0.0" } }, "apollo-utilities": { @@ -3484,7 +3458,8 @@ "async": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz", - "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==" + "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==", + "dev": true }, "async-each": { "version": "1.0.3", @@ -3529,14 +3504,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "requires": { - "follow-redirects": "^1.14.0" - } - }, "babel-eslint": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", @@ -3649,8 +3616,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "bcrypt-nodejs": { "version": "0.0.3", @@ -3698,6 +3664,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "dev": true, "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -3710,20 +3677,20 @@ "dev": true }, "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==", "requires": { - "bytes": "3.1.0", + "bytes": "3.1.1", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", - "http-errors": "1.7.2", + "http-errors": "1.8.1", "iconv-lite": "0.4.24", "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" + "qs": "6.9.6", + "raw-body": "2.4.2", + "type-is": "~1.6.18" }, "dependencies": { "debug": { @@ -3735,31 +3702,26 @@ } }, "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "requires": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "toidentifier": "1.0.1" } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==" }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" } } }, @@ -3839,13 +3801,13 @@ "bson": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", - "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==" + "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==", + "dev": true }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -3898,9 +3860,9 @@ } }, "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz", + "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==" }, "cache-base": { "version": "1.0.1", @@ -4004,15 +3966,6 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, - "camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "requires": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -4557,12 +4510,12 @@ } }, "color": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", - "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" + "color-convert": "^1.9.3", + "color-string": "^1.6.0" } }, "color-convert": { @@ -4579,9 +4532,9 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "color-string": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", - "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", + "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", "requires": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -4596,14 +4549,15 @@ "colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true }, "colorspace": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", - "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", "requires": { - "color": "3.0.x", + "color": "^3.1.3", "text-hex": "1.0.x" } }, @@ -4680,6 +4634,7 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, "requires": { "safe-buffer": "5.1.2" }, @@ -4687,7 +4642,8 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true } } }, @@ -4816,9 +4772,9 @@ } }, "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, "cookie-signature": { "version": "1.0.6", @@ -4891,14 +4847,6 @@ "cross-spawn": "^7.0.1" } }, - "cross-fetch": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz", - "integrity": "sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==", - "requires": { - "node-fetch": "2.6.1" - } - }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4962,6 +4910,12 @@ "assert-plus": "^1.0.0" } }, + "data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", + "dev": true + }, "dataloader": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.0.0.tgz", @@ -6436,16 +6390,16 @@ } }, "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz", + "integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==", "requires": { "accepts": "~1.3.7", "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", + "body-parser": "1.19.1", + "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.4.0", + "cookie": "0.4.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~1.1.2", @@ -6459,19 +6413,27 @@ "on-finished": "~2.3.0", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", + "proxy-addr": "~2.0.7", + "qs": "6.9.6", "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", "statuses": "~1.5.0", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "dependencies": { + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -6481,19 +6443,9 @@ } }, "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==" } } }, @@ -6642,9 +6594,9 @@ } }, "extract-files": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz", - "integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==" + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz", + "integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==" }, "extsprintf": { "version": "1.4.0", @@ -6734,11 +6686,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "fast-safe-stringify": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz", - "integrity": "sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag==" - }, "fastq": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.12.0.tgz", @@ -6762,6 +6709,16 @@ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz", "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==" }, + "fetch-blob": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.4.tgz", + "integrity": "sha512-Eq5Xv5+VlSrYWEqKrusxY1C3Hm/hjeAsCGVG3ft7pZahlUAChpGZT/Ms1WmSLnEAisEXszjzu/s+ce6HZB2VHA==", + "dev": true, + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, "fetch-node-website": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/fetch-node-website/-/fetch-node-website-5.0.3.tgz", @@ -6846,11 +6803,11 @@ } }, "file-stream-rotator": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.5.7.tgz", - "integrity": "sha512-VYb3HZ/GiAGUCrfeakO8Mp54YGswNUHvL7P09WQcXAJNSj3iQ5QraYSp3cIn1MUyw6uzfgN/EFOarCNa4JvUHQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", "requires": { - "moment": "^2.11.2" + "moment": "^2.29.1" } }, "file-type": { @@ -7070,9 +7027,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "follow-redirects": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.2.tgz", - "integrity": "sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==" + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" }, "for-each": { "version": "0.3.3", @@ -7108,12 +7065,22 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "requires": { + "fetch-blob": "^3.1.2" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7476,9 +7443,14 @@ "dev": true }, "graphql": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.6.0.tgz", - "integrity": "sha512-WJR872Zlc9hckiEPhXgyUftXH48jp2EjO5tgBBOyNMRJZ9fviL2mJBD6CAysk6N5S0r9BTs09Qk39nnJBkvOXQ==" + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.7.1.tgz", + "integrity": "sha512-x34S6gC0/peBZnlK60zCJox/d45A7p6At9oN9EPA3qhoIAlR4LNZmXRLkICBckwwTMJzVdA8cx3QIQZMOl606A==" + }, + "graphql-executor": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/graphql-executor/-/graphql-executor-0.0.18.tgz", + "integrity": "sha512-upUSl7tfZCZ5dWG1XkOvpG70Yk3duZKcCoi/uJso4WxJVT6KIrcK4nZ4+2X/hzx46pL8wAukgYHY6iNmocRN+g==" }, "graphql-extensions": { "version": "0.15.0", @@ -7496,9 +7468,9 @@ "integrity": "sha512-9TSAwcVA3KWw7JWYep5NCk2aw3wl1ayLtbMpmG7l26vh1FZ+gZexNPP+XJfUFyJa71UU0zcKSgtgpsrsA3Xv9Q==" }, "graphql-relay": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/graphql-relay/-/graphql-relay-0.9.0.tgz", - "integrity": "sha512-yNJLCqcjz0XpzpmmckRJCSK8a2ZLwTurwrQ09UyGftONh52PbrGpK1UO4yspvj0c7pC+jkN4ZUqVXG3LRrWkXQ==" + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/graphql-relay/-/graphql-relay-0.7.0.tgz", + "integrity": "sha512-P8eS3IbZRhbfbcfud1Q6VPrIru4hchkb15MuOij+WQo9r0chD5NBIxiVjuRE2iG2EMHxIOrZb8LnMe82+YdITA==" }, "graphql-subscriptions": { "version": "1.2.1", @@ -7509,9 +7481,9 @@ } }, "graphql-tag": { - "version": "2.12.5", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.5.tgz", - "integrity": "sha512-5xNhP4063d16Pz3HBtKprutsPrmHZi5IdUGOWRxA2B6VF7BIRGOHZ5WQvDmJXZuPcBg7rYwaFxvQYjqkSdR3TQ==", + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", "requires": { "tslib": "^2.1.0" }, @@ -7814,6 +7786,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, "requires": { "@tootallnate/once": "1", "agent-base": "6", @@ -7824,6 +7797,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -7831,7 +7805,8 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, @@ -7849,6 +7824,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, "requires": { "agent-base": "6", "debug": "4" @@ -7858,6 +7834,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -7865,7 +7842,8 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, @@ -7966,8 +7944,7 @@ "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { "version": "4.0.6", @@ -8179,6 +8156,11 @@ "loose-envify": "^1.0.0" } }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -8889,6 +8871,14 @@ } } }, + "jose": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9091,41 +9081,29 @@ } }, "jwks-rsa": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.12.3.tgz", - "integrity": "sha512-cFipFDeYYaO9FhhYJcZWX/IyZgc0+g316rcHnDpT2dNRNIE/lMOmWKKqp09TkJoYlNFzrEVODsR4GgXJMgWhnA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.5.tgz", + "integrity": "sha512-fliHfsiBRzEU0nXzSvwnh0hynzGB0WihF+CinKbSRlaqRxbqqKf2xbBPgwc8mzf18/WgwlG8e5eTpfSTBcU4DQ==", "requires": { "@types/express-jwt": "0.0.42", - "axios": "^0.21.1", - "debug": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "jsonwebtoken": "^8.5.1", + "debug": "^4.3.2", + "jose": "^2.0.5", "limiter": "^1.1.5", - "lru-memoizer": "^2.1.2", - "ms": "^2.1.2", - "proxy-from-env": "^1.1.0" + "lru-memoizer": "^2.1.4" }, "dependencies": { "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } } }, "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -9794,14 +9772,14 @@ } }, "logform": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", - "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz", + "integrity": "sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw==", "requires": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", + "@colors/colors": "1.5.0", "fecha": "^4.2.0", "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" }, "dependencies": { @@ -9845,18 +9823,11 @@ "dev": true }, "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "requires": { - "yallist": "^3.0.2" - }, - "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } + "yallist": "^4.0.0" } }, "lru-memoizer": { @@ -10253,9 +10224,9 @@ } }, "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" }, "mime-db": { "version": "1.49.0", @@ -10412,16 +10383,63 @@ "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" }, "mongodb": { - "version": "3.6.11", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.11.tgz", - "integrity": "sha512-4Y4lTFHDHZZdgMaHmojtNAlqkvddX2QQBEN0K//GzxhGwlI9tZ9R0vhbjr1Decw+TF7qK0ZLjQT292XgHRRQgw==", - "requires": { - "bl": "^2.2.1", - "bson": "^1.1.4", - "denque": "^1.4.1", - "optional-require": "^1.0.3", - "safe-buffer": "^5.1.2", - "saslprep": "^1.0.0" + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.3.1.tgz", + "integrity": "sha512-sNa8APSIk+r4x31ZwctKjuPSaeKuvUeNb/fu/3B6dRM02HpEgig7hTHM8A/PJQTlxuC/KFWlDlQjhsk/S43tBg==", + "requires": { + "bson": "^4.6.1", + "denque": "^2.0.1", + "mongodb-connection-string-url": "^2.4.1", + "saslprep": "^1.0.3", + "socks": "^2.6.1" + }, + "dependencies": { + "bson": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.6.1.tgz", + "integrity": "sha512-I1LQ7Hz5zgwR4QquilLNZwbhPw0Apx7i7X9kGMBTsqPdml/03Q9NBtD9nt/19ahjlphktQImrnderxqpzeVDjw==", + "requires": { + "buffer": "^5.6.0" + } + }, + "denque": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", + "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==" + } + } + }, + "mongodb-connection-string-url": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.4.1.tgz", + "integrity": "sha512-d5Kd2bVsKcSA7YI/yo57fSTtMwRQdFkvc5IZwod1RRxJtECeWPPSo7zqcUGJELifRA//Igs4spVtYAmvFCatug==", + "requires": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + }, + "dependencies": { + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "requires": { + "punycode": "^2.1.1" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + }, + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + } } }, "mongodb-core": { @@ -10501,6 +10519,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } } } }, @@ -10535,6 +10562,20 @@ "ms": "2.1.2" } }, + "mongodb": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.3.tgz", + "integrity": "sha512-Psm+g3/wHXhjBEktkxXsFMZvd3nemI0r3IPsE0bU+4//PnvNWKkzhZcEsbPcYiWqe8XqXJJEg4Tgtr7Raw67Yw==", + "dev": true, + "requires": { + "bl": "^2.2.1", + "bson": "^1.1.4", + "denque": "^1.4.1", + "optional-require": "^1.1.8", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -10703,9 +10744,9 @@ "optional": true }, "nanoid": { - "version": "3.1.25", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", - "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", "dev": true }, "nanomatch": { @@ -10771,6 +10812,12 @@ "tslib": "^2.0.3" } }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true + }, "node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -10781,9 +10828,15 @@ } }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.1.1.tgz", + "integrity": "sha512-SMk+vKgU77PYotRdWzqZGTZeuFKlsJ0hu4KPviQKkfY+N3vn2MIzr0rvpnYpR8MtB3IEuhlEcuOLbGvLRlA+yg==", + "dev": true, + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.3", + "formdata-polyfill": "^4.0.10" + } }, "node-forge": { "version": "0.10.0", @@ -13235,9 +13288,10 @@ } }, "optional-require": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.7.tgz", - "integrity": "sha512-cIeRZocXsZnZYn+SevbtSqNlLbeoS4mLzuNn4fvXRMDRNhTGg0sxuKXl0FnZCtnew85LorNxIbZp5OeliILhMw==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz", + "integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==", + "dev": true, "requires": { "require-at": "^1.0.6" } @@ -13511,37 +13565,54 @@ } }, "parse": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/parse/-/parse-3.3.1.tgz", - "integrity": "sha512-jrb8tpeanh49lIXuQYbaJoMzywX9YiBtM17aCvYGfaHYJipSTHABA774t8IZap+F8Pb4GgZ0fM4ObfiuO4395A==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/parse/-/parse-3.4.1.tgz", + "integrity": "sha512-XTMaHfcOwAOiWLraNtPbCzR8o94ZWjOfaZDMM2jZ1ZDB5twtK1B82lPa+N197efZhcQ+QFbW/eDJsBybD48aSQ==", "requires": { - "@babel/runtime": "7.14.8", - "@babel/runtime-corejs3": "7.14.6", + "@babel/runtime": "7.15.4", + "@babel/runtime-corejs3": "7.14.7", "crypto-js": "4.1.1", - "idb-keyval": "5.0.6", + "idb-keyval": "6.0.3", "react-native-crypto-js": "1.0.0", "uuid": "3.4.0", - "ws": "7.5.0", + "ws": "7.5.1", "xmlhttprequest": "1.8.0" }, "dependencies": { "@babel/runtime": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz", - "integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/runtime-corejs3": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.7.tgz", + "integrity": "sha512-Wvzcw4mBYbTagyBVZpAJWI06auSIj033T/yNE0Zn1xcup83MieCddZA7ls3kme17L4NOGBrQ09Q+nKB41RLWBA==", "requires": { + "core-js-pure": "^3.15.0", "regenerator-runtime": "^0.13.4" } }, + "idb-keyval": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.0.3.tgz", + "integrity": "sha512-yh8V7CnE6EQMu9YDwQXhRxwZh4nv+8xm/HV4ZqK4IiYFJBWYGjJuykADJbSP+F/GDXUBwCSSNn/14IpGL81TuA==", + "requires": { + "safari-14-idb-fix": "^3.0.0" + } + }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "ws": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.0.tgz", - "integrity": "sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw==" + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.1.tgz", + "integrity": "sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow==" } } }, @@ -13680,9 +13751,9 @@ "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==" }, "pg-promise": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-10.11.0.tgz", - "integrity": "sha512-UntgHZNv+gpGJKhh+tzGSGHLkniKWV+ZQ8/SNdtvElsg9Aa7ZJ4Fgyl6pl2x0ZtJ7uFNy+OIq3Z+Ei6iplqTDQ==", + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-10.11.1.tgz", + "integrity": "sha512-HAv32WSKf2m2RqHerW5RmANn/mcXIwWXbg/gOfGQcoS0SE+8iBi3Jj4JmoR4PNzSEozo/y/npy4e6F16psOItw==", "requires": { "assert-options": "0.7.0", "pg": "8.7.1", @@ -14031,13 +14102,13 @@ "dev": true }, "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", - "react-is": "^16.8.1" + "react-is": "^16.13.1" } }, "proto-list": { @@ -14055,11 +14126,6 @@ "ipaddr.js": "1.9.1" } }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "ps-node": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/ps-node/-/ps-node-0.1.6.tgz", @@ -14129,37 +14195,32 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", + "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==", "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", + "bytes": "3.1.1", + "http-errors": "1.8.1", "iconv-lite": "0.4.24", "unpipe": "1.0.0" }, "dependencies": { "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "requires": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "toidentifier": "1.0.1" } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" } } }, @@ -14506,7 +14567,8 @@ "require-at": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz", - "integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==" + "integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==", + "dev": true }, "require-directory": { "version": "2.1.1", @@ -14676,6 +14738,11 @@ } } }, + "safari-14-idb-fix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz", + "integrity": "sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog==" + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -14691,6 +14758,11 @@ "ret": "~0.1.10" } }, + "safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==" + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -15057,9 +15129,9 @@ "dev": true }, "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", + "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", "requires": { "debug": "2.6.9", "depd": "~1.1.2", @@ -15068,9 +15140,9 @@ "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "~1.7.2", + "http-errors": "1.8.1", "mime": "1.6.0", - "ms": "2.1.1", + "ms": "2.1.3", "on-finished": "~2.3.0", "range-parser": "~1.2.1", "statuses": "~1.5.0" @@ -15092,15 +15164,15 @@ } }, "http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "requires": { "depd": "~1.1.2", "inherits": "2.0.4", - "setprototypeof": "1.1.1", + "setprototypeof": "1.2.0", "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "toidentifier": "1.0.1" } }, "mime": { @@ -15109,26 +15181,26 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" } } }, "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", + "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.17.1" + "send": "0.17.2" } }, "set-blocking": { @@ -15260,6 +15332,11 @@ } } }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -15394,6 +15471,15 @@ } } }, + "socks": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz", + "integrity": "sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==", + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.1.0" + } + }, "sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -15799,9 +15885,9 @@ } }, "subscriptions-transport-ws": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.10.0.tgz", - "integrity": "sha512-k28LhLn3abJ1mowFW+LP4QGggE0e3hrk55zXbMHyAeZkCUYtC0owepiwqMD3zX8DglQVaxnhE760pESrNSEzpg==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz", + "integrity": "sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==", "requires": { "backo2": "^1.0.2", "eventemitter3": "^3.1.0", @@ -15816,9 +15902,9 @@ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, "ws": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==" + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==" } } }, @@ -16181,6 +16267,11 @@ } } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, "traverse": { "version": "0.6.6", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", @@ -16623,6 +16714,26 @@ "defaults": "^1.0.3" } }, + "web-streams-polyfill": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz", + "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -16657,29 +16768,35 @@ "dev": true }, "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "requires": { - "string-width": "^1.0.2 || 2" + "string-width": "^1.0.2 || 2 || 3 || 4" } }, "winston": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", - "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.5.1.tgz", + "integrity": "sha512-tbRtVy+vsSSCLcZq/8nXZaOie/S2tPXPFt4be/Q3vI/WtYwm7rrwidxVw2GRa38FIXcJ1kUM6MOZ9Jmnk3F3UA==", "requires": { "@dabh/diagnostics": "^2.0.2", - "async": "^3.1.0", + "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.2.0", + "logform": "^2.3.2", "one-time": "^1.0.0", "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", - "winston-transport": "^4.4.0" + "winston-transport": "^4.4.2" }, "dependencies": { + "async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -16693,23 +16810,36 @@ } }, "winston-daily-rotate-file": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-4.5.5.tgz", - "integrity": "sha512-ds0WahIjiDhKCiMXmY799pDBW+58ByqIBtUcsqr4oDoXrAI3Zn+hbgFdUxzMfqA93OG0mPLYVMiotqTgE/WeWQ==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-4.6.0.tgz", + "integrity": "sha512-mvpFb1LYmTvh/vz0dIS/aDCwEm0cvDa8D/tE4xWwdUYolD250wf+n0y1PZ2xr7fbvTLF/PQYqXtFIFrmog03Ow==", "requires": { - "file-stream-rotator": "^0.5.7", + "file-stream-rotator": "^0.6.1", "object-hash": "^2.0.1", "triple-beam": "^1.3.0", "winston-transport": "^4.4.0" } }, "winston-transport": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", - "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", "requires": { - "readable-stream": "^2.3.7", - "triple-beam": "^1.2.0" + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "word-wrap": { @@ -16820,9 +16950,9 @@ } }, "ws": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.2.tgz", - "integrity": "sha512-Q6B6H2oc8QY3llc3cB8kVmQ6pnJWVQbP7Q5algTcIxx7YEpc0oU4NBVHlztA7Ekzfhw2r0rPducMUiCGWKQRzw==" + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==" }, "xmlcreate": { "version": "2.0.3", diff --git a/package.json b/package.json index 23c1981c32..8bbbb1720b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.0.0-alpha.5", + "version": "5.0.0-alpha.28", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { @@ -20,45 +20,46 @@ "license": "BSD-3-Clause", "dependencies": { "@apollographql/graphql-playground-html": "1.6.29", - "@graphql-tools/links": "6.2.5", + "@graphql-tools/links": "8.2.2", + "@apollo/client": "3.5.8", "@graphql-tools/stitch": "6.2.4", "@graphql-tools/utils": "6.2.4", - "@parse/fs-files-adapter": "1.2.0", - "@parse/push-adapter": "3.4.1", + "@parse/fs-files-adapter": "1.2.1", + "@parse/push-adapter": "4.1.0", "apollo-server-express": "2.25.2", "bcryptjs": "2.4.3", - "body-parser": "1.19.0", + "body-parser": "1.19.1", "commander": "5.1.0", "cors": "2.8.5", "deepcopy": "2.1.0", - "express": "4.17.1", - "follow-redirects": "1.14.2", - "graphql": "15.6.0", + "express": "4.17.2", + "follow-redirects": "1.14.8", + "graphql": "15.7.1", "graphql-list-fields": "2.0.2", - "graphql-relay": "0.9.0", - "graphql-tag": "2.12.5", + "graphql-relay": "0.7.0", + "graphql-tag": "2.12.6", "graphql-upload": "11.0.0", "intersect": "1.0.1", "jsonwebtoken": "8.5.1", - "jwks-rsa": "1.12.3", + "jwks-rsa": "2.0.5", "ldapjs": "2.3.1", "lodash": "4.17.21", - "lru-cache": "5.1.1", - "mime": "2.5.2", - "mongodb": "3.6.11", + "lru-cache": "6.0.0", + "mime": "3.0.0", + "mongodb": "4.3.1", "mustache": "4.2.0", - "parse": "3.3.1", + "parse": "3.4.1", "pg-monitor": "1.4.1", - "pg-promise": "10.11.0", + "pg-promise": "10.11.1", "pluralize": "8.0.0", "redis": "3.1.2", "semver": "7.3.5", - "subscriptions-transport-ws": "0.10.0", + "subscriptions-transport-ws": "0.11.0", "tv4": "1.3.0", "uuid": "8.3.2", - "winston": "3.3.3", - "winston-daily-rotate-file": "4.5.5", - "ws": "8.2.2" + "winston": "3.5.1", + "winston-daily-rotate-file": "4.6.0", + "ws": "8.2.3" }, "devDependencies": { "@actions/core": "1.2.6", @@ -75,12 +76,7 @@ "@semantic-release/npm": "7.1.3", "@semantic-release/release-notes-generator": "9.0.3", "all-node-versions": "8.0.0", - "apollo-cache-inmemory": "1.6.6", - "apollo-client": "2.6.10", - "apollo-link": "1.2.14", - "apollo-link-http": "1.5.17", - "apollo-link-ws": "1.0.20", - "apollo-upload-client": "13.0.0", + "apollo-upload-client": "16.0.0", "apollo-utilities": "1.3.4", "babel-eslint": "10.1.0", "bcrypt-nodejs": "0.0.3", @@ -101,7 +97,7 @@ "mock-mail-adapter": "file:spec/dependencies/mock-mail-adapter", "mongodb-runner": "4.8.1", "mongodb-version-list": "1.0.0", - "node-fetch": "2.6.1", + "node-fetch": "3.1.1", "nyc": "15.1.0", "prettier": "2.0.5", "semantic-release": "17.4.6", @@ -122,12 +118,13 @@ "test:mongodb:4.0.27": "npm run test:mongodb --dbversion=4.0.27", "test:mongodb:4.2.17": "npm run test:mongodb --dbversion=4.2.17", "test:mongodb:4.4.10": "npm run test:mongodb --dbversion=4.4.10", + "test:mongodb:5.0.5": "npm run test:mongodb --dbversion=5.0.5", "posttest:mongodb": "mongodb-runner stop", - "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.4.10} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start", - "testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.4.10} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine", + "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start", + "testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine", "test": "npm run testonly", - "posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.4.10} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop", - "coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.4.10} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 nyc jasmine", + "posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop", + "coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 nyc jasmine", "start": "node ./bin/parse-server", "prettier": "prettier --write {src,spec}/{**/*,*}.js", "prepare": "npm run build", @@ -135,7 +132,7 @@ "madge:circular": "node_modules/.bin/madge ./src --circular" }, "engines": { - "node": ">=12.20.0" + "node": ">=12.22.10 <17" }, "bin": { "parse-server": "bin/parse-server" diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 52ff3ac7a8..670aafad38 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -172,6 +172,20 @@ function parseDefaultValue(elt, value, t) { literalValue = t.arrayExpression(array.map((value) => { if (typeof value == 'string') { return t.stringLiteral(value); + } else if (typeof value == 'number') { + return t.numericLiteral(value); + } else if (typeof value == 'object') { + const object = parsers.objectParser(value); + const props = Object.entries(object).map(([k, v]) => { + if (typeof v == 'string') { + return t.objectProperty(t.identifier(k), t.stringLiteral(v)); + } else if (typeof v == 'number') { + return t.objectProperty(t.identifier(k), t.numericLiteral(v)); + } else if (typeof v == 'boolean') { + return t.objectProperty(t.identifier(k), t.booleanLiteral(v)); + } + }); + return t.objectExpression(props); } else { throw new Error('Unable to parse array'); } diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index 208e7834b6..9f0f77896a 100644 --- a/spec/AudienceRouter.spec.js +++ b/spec/AudienceRouter.spec.js @@ -326,22 +326,24 @@ describe('AudiencesRouter', () => { { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }) }, { useMasterKey: true } ).then(audience => { - database.collection('test__Audience').updateOne( - { _id: audience.objectId }, - { - $set: { - times_used: 1, - _last_used: now, - }, - }, - {}, - error => { - expect(error).toEqual(null); + database + .collection('test__Audience') + .updateOne( + { _id: audience.objectId }, + { + $set: { + times_used: 1, + _last_used: now, + }, + } + ) + .then(result => { + expect(result).toBeTruthy(); database .collection('test__Audience') .find({ _id: audience.objectId }) .toArray((error, rows) => { - expect(error).toEqual(null); + expect(error).toEqual(undefined); expect(rows[0]['times_used']).toEqual(1); expect(rows[0]['_last_used']).toEqual(now); Parse._request( @@ -361,8 +363,7 @@ describe('AudiencesRouter', () => { done.fail(error); }); }); - } - ); + }); }); }); diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index d32eba0423..89eeb51231 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -1707,6 +1707,24 @@ describe('Apple Game Center Auth adapter', () => { expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: invalid.com'); } }); + + it('validateAuthData invalid public key http url', async () => { + const authData = { + id: 'G:1965586982', + publicKeyUrl: 'http://static.gc.apple.com/public-key/gc-prod-4.cer', + timestamp: 1565257031287, + signature: '1234', + salt: 'DzqqrQ==', + bundleId: 'cloud.xtralife.gamecenterauth', + }; + + try { + await gcenter.validateAuthData(authData); + fail(); + } catch (e) { + expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: http://static.gc.apple.com/public-key/gc-prod-4.cer'); + } + }); }); describe('phant auth adapter', () => { diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 17968c33d7..4b8df9f9c9 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -39,14 +39,6 @@ describe('Cloud Code', () => { }); }); - it('can load cloud code as a module', async () => { - process.env.npm_package_type = 'module'; - await reconfigureServer({ cloud: './spec/cloud/cloudCodeModuleFile.js' }); - const result = await Parse.Cloud.run('cloudCodeInFile'); - expect(result).toEqual('It is possible to define cloud code in a file.'); - delete process.env.npm_package_type; - }); - it('can create functions', done => { Parse.Cloud.define('hello', () => { return 'Hello world!'; @@ -2007,6 +1999,25 @@ describe('beforeFind hooks', () => { }); }); + it('should have object found with nested relational data query', async () => { + const obj1 = Parse.Object.extend('TestObject'); + const obj2 = Parse.Object.extend('TestObject2'); + let item2 = new obj2(); + item2 = await item2.save(); + let item1 = new obj1(); + const relation = item1.relation('rel'); + relation.add(item2); + item1 = await item1.save(); + Parse.Cloud.beforeFind('TestObject', req => { + const additionalQ = new Parse.Query('TestObject'); + additionalQ.equalTo('rel', item2); + return Parse.Query.and(req.query, additionalQ); + }); + const q = new Parse.Query('TestObject'); + const res = await q.first(); + expect(res.id).toEqual(item1.id); + }); + it('should use the modified exclude query', async () => { Parse.Cloud.beforeFind('MyObject', req => { const q = req.query; @@ -3213,6 +3224,21 @@ describe('afterLogin hook', () => { const query = new Parse.Query(TestObject); await query.find({ context: { a: 'a' } }); }); + + it('beforeFind and afterFind should have access to context while making fetch call', async () => { + Parse.Cloud.beforeFind('TestObject', req => { + expect(req.context.a).toEqual('a'); + expect(req.context.b).toBeUndefined(); + req.context.b = 'b'; + }); + Parse.Cloud.afterFind('TestObject', req => { + expect(req.context.a).toEqual('a'); + expect(req.context.b).toEqual('b'); + }); + const obj = new TestObject(); + await obj.save(); + await obj.fetch({ context: { a: 'a' } }); + }); }); describe('saveFile hooks', () => { @@ -3524,23 +3550,4 @@ describe('sendEmail', () => { 'Failed to send email because no mail adapter is configured for Parse Server.' ); }); - - it('should have object found with nested relational data query', async () => { - const obj1 = Parse.Object.extend('TestObject'); - const obj2 = Parse.Object.extend('TestObject2'); - let item2 = new obj2(); - item2 = await item2.save(); - let item1 = new obj1(); - const relation = item1.relation('rel'); - relation.add(item2); - item1 = await item1.save(); - Parse.Cloud.beforeFind('TestObject', req => { - const additionalQ = new Parse.Query('TestObject'); - additionalQ.equalTo('rel', item2); - return Parse.Query.and(req.query, additionalQ); - }); - const q = new Parse.Query('TestObject'); - const res = await q.first(); - expect(res.id).toEqual(item1.id); - }); }); diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index 996e4c3961..9ea66da6ea 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -3,7 +3,6 @@ const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapte .WinstonLoggerAdapter; const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter') .GridFSBucketAdapter; -const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter; const Config = require('../lib/Config'); const FilesController = require('../lib/Controllers/FilesController').default; const databaseURI = 'mongodb://localhost:27017/parse'; @@ -24,8 +23,8 @@ const mockAdapter = { describe('FilesController', () => { it('should properly expand objects', done => { const config = Config.get(Parse.applicationId); - const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse'); - const filesController = new FilesController(gridStoreAdapter); + const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse'); + const filesController = new FilesController(gridFSAdapter); const result = filesController.expandFilesInObject(config, function () {}); expect(result).toBeUndefined(); @@ -88,19 +87,19 @@ describe('FilesController', () => { it('should add a unique hash to the file name when the preserveFileName option is false', done => { const config = Config.get(Parse.applicationId); - const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse'); - spyOn(gridStoreAdapter, 'createFile'); - gridStoreAdapter.createFile.and.returnValue(Promise.resolve()); + const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse'); + spyOn(gridFSAdapter, 'createFile'); + gridFSAdapter.createFile.and.returnValue(Promise.resolve()); const fileName = 'randomFileName.pdf'; const regexEscapedFileName = fileName.replace(/\./g, '\\$&'); - const filesController = new FilesController(gridStoreAdapter, null, { + const filesController = new FilesController(gridFSAdapter, null, { preserveFileName: false, }); filesController.createFile(config, fileName); - expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1); - expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toMatch( + expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1); + expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toMatch( `^.{32}_${regexEscapedFileName}$` ); @@ -109,42 +108,42 @@ describe('FilesController', () => { it('should not add a unique hash to the file name when the preserveFileName option is true', done => { const config = Config.get(Parse.applicationId); - const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse'); - spyOn(gridStoreAdapter, 'createFile'); - gridStoreAdapter.createFile.and.returnValue(Promise.resolve()); + const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse'); + spyOn(gridFSAdapter, 'createFile'); + gridFSAdapter.createFile.and.returnValue(Promise.resolve()); const fileName = 'randomFileName.pdf'; - const filesController = new FilesController(gridStoreAdapter, null, { + const filesController = new FilesController(gridFSAdapter, null, { preserveFileName: true, }); filesController.createFile(config, fileName); - expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1); - expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName); + expect(gridFSAdapter.createFile).toHaveBeenCalledTimes(1); + expect(gridFSAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName); done(); }); it('should handle adapter without getMetadata', async () => { - const gridStoreAdapter = new GridFSBucketAdapter(databaseURI); - gridStoreAdapter.getMetadata = null; - const filesController = new FilesController(gridStoreAdapter); + const gridFSAdapter = new GridFSBucketAdapter(databaseURI); + gridFSAdapter.getMetadata = null; + const filesController = new FilesController(gridFSAdapter); const result = await filesController.getMetadata(); expect(result).toEqual({}); }); it('should reject slashes in file names', done => { - const gridStoreAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse'); + const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse'); const fileName = 'foo/randomFileName.pdf'; - expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null); + expect(gridFSAdapter.validateFilename(fileName)).not.toBe(null); done(); }); it('should also reject slashes in file names', done => { - const gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse'); + const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse'); const fileName = 'foo/randomFileName.pdf'; - expect(gridStoreAdapter.validateFilename(fileName)).not.toBe(null); + expect(gridFSAdapter.validateFilename(fileName)).not.toBe(null); done(); }); }); diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js index 8431d6d7f5..7ffdced2bb 100644 --- a/spec/GridFSBucketStorageAdapter.spec.js +++ b/spec/GridFSBucketStorageAdapter.spec.js @@ -1,4 +1,3 @@ -const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter; const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter') .GridFSBucketAdapter; const { randomString } = require('../lib/cryptoUtils'); @@ -14,25 +13,13 @@ async function expectMissingFile(gfsAdapter, name) { } } -describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { +describe_only_db('mongo')('GridFSBucket', () => { beforeEach(async () => { - const gsAdapter = new GridStoreAdapter(databaseURI); + const gsAdapter = new GridFSBucketAdapter(databaseURI); const db = await gsAdapter._connect(); await db.dropDatabase(); }); - it('a file created in GridStore should be available in GridFS', async () => { - const gsAdapter = new GridStoreAdapter(databaseURI); - const gfsAdapter = new GridFSBucketAdapter(databaseURI); - await expectMissingFile(gfsAdapter, 'myFileName'); - const originalString = 'abcdefghi'; - await gsAdapter.createFile('myFileName', originalString); - const gsResult = await gsAdapter.getFileData('myFileName'); - expect(gsResult.toString('utf8')).toBe(originalString); - const gfsResult = await gfsAdapter.getFileData('myFileName'); - expect(gfsResult.toString('utf8')).toBe(originalString); - }); - it('should save an encrypted file that can only be decrypted by a GridFS adapter with the encryptionKey', async () => { const unencryptedAdapter = new GridFSBucketAdapter(databaseURI); const encryptedAdapter = new GridFSBucketAdapter( @@ -451,7 +438,7 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { await db.admin().serverStatus(); expect(false).toBe(true); } catch (e) { - expect(e.message).toEqual('topology was destroyed'); + expect(e.message).toEqual('MongoClient must be connected to perform this operation'); } }); }); diff --git a/spec/GridStoreAdapter.spec.js b/spec/GridStoreAdapter.spec.js deleted file mode 100644 index 145b8e0e6d..0000000000 --- a/spec/GridStoreAdapter.spec.js +++ /dev/null @@ -1,111 +0,0 @@ -const MongoClient = require('mongodb').MongoClient; -const GridStore = require('mongodb').GridStore; - -const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter; -const Config = require('../lib/Config'); -const FilesController = require('../lib/Controllers/FilesController').default; - -// Small additional tests to improve overall coverage -describe_only_db('mongo')('GridStoreAdapter', () => { - it('should properly instanciate the GridStore when deleting a file', async done => { - const databaseURI = 'mongodb://localhost:27017/parse'; - const config = Config.get(Parse.applicationId); - const gridStoreAdapter = new GridStoreAdapter(databaseURI); - const db = await gridStoreAdapter._connect(); - await db.dropDatabase(); - const filesController = new FilesController(gridStoreAdapter, Parse.applicationId, {}); - - // save original unlink before redefinition - const originalUnlink = GridStore.prototype.unlink; - - let gridStoreMode; - - // new unlink method that will capture the mode in which GridStore was opened - GridStore.prototype.unlink = function () { - // restore original unlink during first call - GridStore.prototype.unlink = originalUnlink; - - gridStoreMode = this.mode; - - return originalUnlink.call(this); - }; - - filesController - .createFile(config, 'myFilename.txt', 'my file content', 'text/plain') - .then(myFile => { - return MongoClient.connect(databaseURI) - .then(client => { - const database = client.db(client.s.options.dbName); - // Verify the existance of the fs.files document - return database - .collection('fs.files') - .count() - .then(count => { - expect(count).toEqual(1); - return { database, client }; - }); - }) - .then(({ database, client }) => { - // Verify the existance of the fs.files document - return database - .collection('fs.chunks') - .count() - .then(count => { - expect(count).toEqual(1); - return client.close(); - }); - }) - .then(() => { - return filesController.deleteFile(config, myFile.name); - }); - }) - .then(() => { - return MongoClient.connect(databaseURI) - .then(client => { - const database = client.db(client.s.options.dbName); - // Verify the existance of the fs.files document - return database - .collection('fs.files') - .count() - .then(count => { - expect(count).toEqual(0); - return { database, client }; - }); - }) - .then(({ database, client }) => { - // Verify the existance of the fs.files document - return database - .collection('fs.chunks') - .count() - .then(count => { - expect(count).toEqual(0); - return client.close(); - }); - }); - }) - .then(() => { - // Verify that gridStore was opened in read only mode - expect(gridStoreMode).toEqual('r'); - - done(); - }) - .catch(fail); - }); - - it('handleShutdown, close connection', async () => { - const databaseURI = 'mongodb://localhost:27017/parse'; - const gridStoreAdapter = new GridStoreAdapter(databaseURI); - - const db = await gridStoreAdapter._connect(); - const status = await db.admin().serverStatus(); - expect(status.connections.current > 0).toEqual(true); - - await gridStoreAdapter.handleShutdown(); - try { - await db.admin().serverStatus(); - expect(false).toBe(true); - } catch (e) { - expect(e.message).toEqual('topology was destroyed'); - } - }); -}); diff --git a/spec/Idempotency.spec.js b/spec/Idempotency.spec.js index c2ef8665b7..85fa8a65fa 100644 --- a/spec/Idempotency.spec.js +++ b/spec/Idempotency.spec.js @@ -6,11 +6,14 @@ const rest = require('../lib/rest'); const auth = require('../lib/Auth'); const uuid = require('uuid'); -describe_only_db('mongo')('Idempotency', () => { +describe('Idempotency', () => { // Parameters /** Enable TTL expiration simulated by removing entry instead of waiting for MongoDB TTL monitor which runs only every 60s, so it can take up to 119s until entry removal - ain't nobody got time for that */ const SIMULATE_TTL = true; + const ttl = 2; + const maxTimeOut = 4000; + // Helpers async function deleteRequestEntry(reqId) { const config = Config.get(Parse.applicationId); @@ -38,9 +41,10 @@ describe_only_db('mongo')('Idempotency', () => { } await setup({ paths: ['functions/.*', 'jobs/.*', 'classes/.*', 'users', 'installations'], - ttl: 30, + ttl: ttl, }); }); + // Tests it('should enforce idempotency for cloud code function', async () => { let counter = 0; @@ -56,7 +60,7 @@ describe_only_db('mongo')('Idempotency', () => { 'X-Parse-Request-Id': 'abc-123', }, }; - expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(30); + expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(ttl); await request(params); await request(params).then(fail, e => { expect(e.status).toEqual(400); @@ -83,12 +87,35 @@ describe_only_db('mongo')('Idempotency', () => { if (SIMULATE_TTL) { await deleteRequestEntry('abc-123'); } else { - await new Promise(resolve => setTimeout(resolve, 130000)); + await new Promise(resolve => setTimeout(resolve, maxTimeOut)); } await expectAsync(request(params)).toBeResolved(); expect(counter).toBe(2); }); + it_only_db('postgres')('should delete request entry when postgress ttl function is called', async () => { + const client = Config.get(Parse.applicationId).database.adapter._client; + let counter = 0; + Parse.Cloud.define('myFunction', () => { + counter++; + }); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/functions/myFunction', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123', + }, + }; + await expectAsync(request(params)).toBeResolved(); + await expectAsync(request(params)).toBeRejected(); + await new Promise(resolve => setTimeout(resolve, maxTimeOut)); + await client.one('SELECT idempotency_delete_expired_records()'); + await expectAsync(request(params)).toBeResolved(); + expect(counter).toBe(2); + }); + it('should enforce idempotency for cloud code jobs', async () => { let counter = 0; Parse.Cloud.job('myJob', () => { diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index f6d2866417..a31a6134f7 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -1,7 +1,7 @@ 'use strict'; const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; -const { MongoClient } = require('mongodb'); +const { MongoClient, Collection } = require('mongodb'); const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; const request = require('../lib/request'); const Config = require('../lib/Config'); @@ -101,7 +101,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { done.fail('Find succeeded despite taking too long!'); }, err => { - expect(err.name).toEqual('MongoError'); + expect(err.name).toEqual('MongoServerError'); expect(err.code).toEqual(50); expect(err.message).toMatch('operation exceeded time limit'); done(); @@ -283,7 +283,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { await adapter.database.admin().serverStatus(); expect(false).toBe(true); } catch (e) { - expect(e.message).toEqual('topology was destroyed'); + expect(e.message).toEqual('MongoClient must be connected to perform this operation'); } }); @@ -392,8 +392,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const myObject = new Parse.Object('MyObject'); await myObject.save(); - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough(); + spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough(); await request({ method: 'POST', @@ -412,9 +411,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { }); let found = false; - databaseAdapter.database.serverConfig.command.calls.all().forEach(call => { + Collection.prototype.findOneAndUpdate.calls.all().forEach(call => { found = true; - expect(call.args[2].session.transaction.state).not.toBe('NO_TRANSACTION'); + expect(call.args[2].session.transaction.state).toBe('TRANSACTION_COMMITTED'); }); expect(found).toBe(true); }); @@ -423,8 +422,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const myObject = new Parse.Object('MyObject'); await myObject.save(); - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough(); + spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough(); await request({ method: 'POST', @@ -443,9 +441,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { }); let found = false; - databaseAdapter.database.serverConfig.command.calls.all().forEach(call => { + Collection.prototype.findOneAndUpdate.calls.all().forEach(call => { found = true; - expect(call.args[2].session).toBe(undefined); + expect(call.args[2].session).toBeFalsy(); }); expect(found).toBe(true); }); @@ -454,8 +452,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const myObject = new Parse.Object('MyObject'); await myObject.save(); - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough(); + spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough(); await request({ method: 'POST', @@ -473,9 +470,9 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { }); let found = false; - databaseAdapter.database.serverConfig.command.calls.all().forEach(call => { + Collection.prototype.findOneAndUpdate.calls.all().forEach(call => { found = true; - expect(call.args[2].session).toBe(undefined); + expect(call.args[2].session).toBeFalsy(); }); expect(found).toBe(true); }); @@ -484,8 +481,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { const myObject = new Parse.Object('MyObject'); await myObject.save(); - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough(); + spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough(); await request({ method: 'PUT', @@ -495,30 +491,28 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { }); let found = false; - databaseAdapter.database.serverConfig.command.calls.all().forEach(call => { + Collection.prototype.findOneAndUpdate.calls.all().forEach(call => { found = true; - expect(call.args[2].session).toBe(undefined); + expect(call.args[2].session).toBeFalsy(); }); expect(found).toBe(true); }); it('should not use transactions when using SDK insert', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - spyOn(databaseAdapter.database.serverConfig, 'insert').and.callThrough(); + spyOn(Collection.prototype, 'insertOne').and.callThrough(); const myObject = new Parse.Object('MyObject'); await myObject.save(); - const calls = databaseAdapter.database.serverConfig.insert.calls.all(); + const calls = Collection.prototype.insertOne.calls.all(); expect(calls.length).toBeGreaterThan(0); calls.forEach(call => { - expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION'); + expect(call.args[1].session).toBeFalsy(); }); }); it('should not use transactions when using SDK update', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - spyOn(databaseAdapter.database.serverConfig, 'update').and.callThrough(); + spyOn(Collection.prototype, 'findOneAndUpdate').and.callThrough(); const myObject = new Parse.Object('MyObject'); await myObject.save(); @@ -526,26 +520,25 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { myObject.set('myAttribute', 'myValue'); await myObject.save(); - const calls = databaseAdapter.database.serverConfig.update.calls.all(); + const calls = Collection.prototype.findOneAndUpdate.calls.all(); expect(calls.length).toBeGreaterThan(0); calls.forEach(call => { - expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION'); + expect(call.args[2].session).toBeFalsy(); }); }); it('should not use transactions when using SDK delete', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - spyOn(databaseAdapter.database.serverConfig, 'remove').and.callThrough(); + spyOn(Collection.prototype, 'deleteMany').and.callThrough(); const myObject = new Parse.Object('MyObject'); await myObject.save(); await myObject.destroy(); - const calls = databaseAdapter.database.serverConfig.remove.calls.all(); + const calls = Collection.prototype.deleteMany.calls.all(); expect(calls.length).toBeGreaterThan(0); calls.forEach(call => { - expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION'); + expect(call.args[1].session).toBeFalsy(); }); }); }); diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 6834a6b28d..60adb4b7f0 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -4,6 +4,7 @@ const transform = require('../lib/Adapters/Storage/Mongo/MongoTransform'); const dd = require('deep-diff'); const mongodb = require('mongodb'); +const Utils = require('../lib/Utils'); describe('parseObjectToMongoObjectForCreate', () => { it('a basic number', done => { @@ -592,7 +593,7 @@ describe('relativeTimeToDate', () => { describe('In the future', () => { it('should parse valid natural time', () => { const text = 'in 1 year 2 weeks 12 days 10 hours 24 minutes 30 seconds'; - const { result, status, info } = transform.relativeTimeToDate(text, now); + const { result, status, info } = Utils.relativeTimeToDate(text, now); expect(result.toISOString()).toBe('2018-10-22T23:52:46.617Z'); expect(status).toBe('success'); expect(info).toBe('future'); @@ -602,7 +603,7 @@ describe('relativeTimeToDate', () => { describe('In the past', () => { it('should parse valid natural time', () => { const text = '2 days 12 hours 1 minute 12 seconds ago'; - const { result, status, info } = transform.relativeTimeToDate(text, now); + const { result, status, info } = Utils.relativeTimeToDate(text, now); expect(result.toISOString()).toBe('2017-09-24T01:27:04.617Z'); expect(status).toBe('success'); expect(info).toBe('past'); @@ -612,7 +613,7 @@ describe('relativeTimeToDate', () => { describe('From now', () => { it('should equal current time', () => { const text = 'now'; - const { result, status, info } = transform.relativeTimeToDate(text, now); + const { result, status, info } = Utils.relativeTimeToDate(text, now); expect(result.toISOString()).toBe('2017-09-26T13:28:16.617Z'); expect(status).toBe('success'); expect(info).toBe('present'); @@ -621,54 +622,54 @@ describe('relativeTimeToDate', () => { describe('Error cases', () => { it('should error if string is completely gibberish', () => { - expect(transform.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({ + expect(Utils.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({ status: 'error', info: "Time should either start with 'in' or end with 'ago'", }); }); it('should error if string contains neither `ago` nor `in`', () => { - expect(transform.relativeTimeToDate('12 hours 1 minute')).toEqual({ + expect(Utils.relativeTimeToDate('12 hours 1 minute')).toEqual({ status: 'error', info: "Time should either start with 'in' or end with 'ago'", }); }); it('should error if there are missing units or numbers', () => { - expect(transform.relativeTimeToDate('in 12 hours 1')).toEqual({ + expect(Utils.relativeTimeToDate('in 12 hours 1')).toEqual({ status: 'error', info: 'Invalid time string. Dangling unit or number.', }); - expect(transform.relativeTimeToDate('12 hours minute ago')).toEqual({ + expect(Utils.relativeTimeToDate('12 hours minute ago')).toEqual({ status: 'error', info: 'Invalid time string. Dangling unit or number.', }); }); it('should error on floating point numbers', () => { - expect(transform.relativeTimeToDate('in 12.3 hours')).toEqual({ + expect(Utils.relativeTimeToDate('in 12.3 hours')).toEqual({ status: 'error', info: "'12.3' is not an integer.", }); }); it('should error if numbers are invalid', () => { - expect(transform.relativeTimeToDate('12 hours 123a minute ago')).toEqual({ + expect(Utils.relativeTimeToDate('12 hours 123a minute ago')).toEqual({ status: 'error', info: "'123a' is not an integer.", }); }); it('should error on invalid interval units', () => { - expect(transform.relativeTimeToDate('4 score 7 years ago')).toEqual({ + expect(Utils.relativeTimeToDate('4 score 7 years ago')).toEqual({ status: 'error', info: "Invalid interval: 'score'", }); }); it("should error when string contains 'ago' and 'in'", () => { - expect(transform.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({ + expect(Utils.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({ status: 'error', info: "Time cannot have both 'in' and 'ago'", }); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 52427efcc2..32a526132a 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -1,7 +1,7 @@ const http = require('http'); const express = require('express'); const req = require('../lib/request'); -const fetch = require('node-fetch'); +const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args)); const FormData = require('form-data'); const ws = require('ws'); require('./helper'); @@ -9,13 +9,16 @@ const { updateCLP } = require('./support/dev'); const pluralize = require('pluralize'); const { getMainDefinition } = require('apollo-utilities'); -const { ApolloLink, split } = require('apollo-link'); -const { createHttpLink } = require('apollo-link-http'); -const { InMemoryCache } = require('apollo-cache-inmemory'); const { createUploadLink } = require('apollo-upload-client'); const { SubscriptionClient } = require('subscriptions-transport-ws'); -const { WebSocketLink } = require('apollo-link-ws'); -const ApolloClient = require('apollo-client').default; +const { WebSocketLink } = require('@apollo/client/link/ws'); +const { + ApolloClient, + InMemoryCache, + ApolloLink, + split, + createHttpLink, +} = require('@apollo/client/core'); const gql = require('graphql-tag'); const { toGlobalId } = require('graphql-relay'); const { @@ -29,7 +32,7 @@ const { } = require('graphql'); const { ParseServer } = require('../'); const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer'); -const ReadPreference = require('mongodb').ReadPreference; +const { ReadPreference, Collection } = require('mongodb'); const { v4: uuidv4 } = require('uuid'); function handleError(e) { @@ -2284,8 +2287,7 @@ describe('ParseGraphQLServer', () => { expect(nodeResult.data.node2.objectId).toBe(obj2.id); expect(nodeResult.data.node2.someField).toBe('some value 2'); }); - // TODO: (moumouls, davimacedo) Fix flaky test - xit('Id inputs should work either with global id or object id', async () => { + it('Id inputs should work either with global id or object id', async () => { try { await apolloClient.mutate({ mutation: gql` @@ -2592,14 +2594,29 @@ describe('ParseGraphQLServer', () => { .map(value => value.node.someField) .sort() ).toEqual(['some value 22', 'some value 44']); - expect( - findSecondaryObjectsResult.data.secondaryObjects.edges[0].node.id - ).toBeLessThan(findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.id); + // NOTE: Here @davimacedo tried to test RelayID order, but the test is wrong since + // "objectId1" < "objectId2" do not always keep the order when objectId is transformed + // to base64 by Relay + // "SecondaryObject:bBRgmzIRRM" < "SecondaryObject:nTMcuVbATY" true + // base64("SecondaryObject:bBRgmzIRRM"") < base64(""SecondaryObject:nTMcuVbATY"") false + // "U2Vjb25kYXJ5T2JqZWN0OmJCUmdteklSUk0=" < "U2Vjb25kYXJ5T2JqZWN0Om5UTWN1VmJBVFk=" false + const originalIds = [ + getSecondaryObjectsResult.data.secondaryObject2.objectId, + getSecondaryObjectsResult.data.secondaryObject4.objectId, + ]; expect( findSecondaryObjectsResult.data.secondaryObjects.edges[0].node.objectId - ).toBeLessThan( - findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId - ); + ).not.toBe(findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId); + expect( + originalIds.includes( + findSecondaryObjectsResult.data.secondaryObjects.edges[0].node.objectId + ) + ).toBeTrue(); + expect( + originalIds.includes( + findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId + ) + ).toBeTrue(); const createPrimaryObjectResult = await apolloClient.mutate({ mutation: gql` @@ -2760,6 +2777,23 @@ describe('ParseGraphQLServer', () => { handleError(e); } }); + it('Id inputs should work either with global id or object id with objectId higher than 19', async () => { + await reconfigureServer({ objectIdSize: 20 }); + const obj = new Parse.Object('SomeClass'); + await obj.save({ name: 'aname', type: 'robot' }); + const result = await apolloClient.query({ + query: gql` + query getSomeClass($id: ID!) { + someClass(id: $id) { + objectId + id + } + } + `, + variables: { id: obj.id }, + }); + expect(result.data.someClass.objectId).toEqual(obj.id); + }); }); }); @@ -4439,8 +4473,7 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const databaseAdapter = parseServer.config.databaseController.adapter; - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); await apolloClient.query({ query: gql` @@ -4464,13 +4497,13 @@ describe('ParseGraphQLServer', () => { let foundGraphQLClassReadPreference = false; let foundUserClassReadPreference = false; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) { + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) { foundGraphQLClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY); - } else if (call.args[0].ns.collection.indexOf('_User') >= 0) { + expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY); + } else if (call.object.s.namespace.collection.indexOf('_User') >= 0) { foundUserClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY); + expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY); } }); @@ -4486,8 +4519,7 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const databaseAdapter = parseServer.config.databaseController.adapter; - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); await apolloClient.query({ query: gql` @@ -4511,13 +4543,13 @@ describe('ParseGraphQLServer', () => { let foundGraphQLClassReadPreference = false; let foundUserClassReadPreference = false; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) { + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) { foundGraphQLClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY); - } else if (call.args[0].ns.collection.indexOf('_User') >= 0) { + expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY); + } else if (call.object.s.namespace.collection.indexOf('_User') >= 0) { foundUserClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY); + expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY); } }); @@ -4530,8 +4562,7 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const databaseAdapter = parseServer.config.databaseController.adapter; - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); await apolloClient.query({ query: gql` @@ -4558,13 +4589,13 @@ describe('ParseGraphQLServer', () => { let foundGraphQLClassReadPreference = false; let foundUserClassReadPreference = false; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) { + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) { foundGraphQLClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY); - } else if (call.args[0].ns.collection.indexOf('_User') >= 0) { + expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY); + } else if (call.object.s.namespace.collection.indexOf('_User') >= 0) { foundUserClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.NEAREST); + expect(call.args[1].readPreference).toBe(ReadPreference.NEAREST); } }); @@ -5422,8 +5453,7 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const databaseAdapter = parseServer.config.databaseController.adapter; - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); await apolloClient.query({ query: gql` @@ -5448,13 +5478,13 @@ describe('ParseGraphQLServer', () => { let foundGraphQLClassReadPreference = false; let foundUserClassReadPreference = false; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) { + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) { foundGraphQLClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY); - } else if (call.args[0].ns.collection.indexOf('_User') >= 0) { + expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY); + } else if (call.object.s.namespace.collection.indexOf('_User') >= 0) { foundUserClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY); + expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY); } }); @@ -5467,8 +5497,7 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const databaseAdapter = parseServer.config.databaseController.adapter; - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); await apolloClient.query({ query: gql` @@ -5493,13 +5522,13 @@ describe('ParseGraphQLServer', () => { let foundGraphQLClassReadPreference = false; let foundUserClassReadPreference = false; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) { + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) { foundGraphQLClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY); - } else if (call.args[0].ns.collection.indexOf('_User') >= 0) { + expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY); + } else if (call.object.s.namespace.collection.indexOf('_User') >= 0) { foundUserClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY); + expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY); } }); @@ -5512,8 +5541,7 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const databaseAdapter = parseServer.config.databaseController.adapter; - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); await apolloClient.query({ query: gql` @@ -5540,13 +5568,13 @@ describe('ParseGraphQLServer', () => { let foundGraphQLClassReadPreference = false; let foundUserClassReadPreference = false; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) { + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) { foundGraphQLClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY); - } else if (call.args[0].ns.collection.indexOf('_User') >= 0) { + expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY); + } else if (call.object.s.namespace.collection.indexOf('_User') >= 0) { foundUserClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.NEAREST); + expect(call.args[1].readPreference).toBe(ReadPreference.NEAREST); } }); @@ -5560,8 +5588,7 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const databaseAdapter = parseServer.config.databaseController.adapter; - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); await apolloClient.query({ query: gql` @@ -5598,13 +5625,13 @@ describe('ParseGraphQLServer', () => { let foundGraphQLClassReadPreference = false; let foundUserClassReadPreference = false; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) { + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('GraphQLClass') >= 0) { foundGraphQLClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.SECONDARY); - } else if (call.args[0].ns.collection.indexOf('_User') >= 0) { + expect(call.args[1].readPreference).toBe(ReadPreference.SECONDARY); + } else if (call.object.s.namespace.collection.indexOf('_User') >= 0) { foundUserClassReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.NEAREST); + expect(call.args[1].readPreference).toBe(ReadPreference.NEAREST); } }); diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index bf870c92b8..6de0957999 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -4766,7 +4766,7 @@ describe('Parse.Query testing', () => { .catch(done.fail); }); - it_only_db('mongo')('should handle relative times correctly', function (done) { + it('should handle relative times correctly', async () => { const now = Date.now(); const obj1 = new Parse.Object('MyCustomObject', { name: 'obj1', @@ -4777,94 +4777,75 @@ describe('Parse.Query testing', () => { ttl: new Date(now - 2 * 24 * 60 * 60 * 1000), // 2 days ago }); - Parse.Object.saveAll([obj1, obj2]) - .then(() => { - const q = new Parse.Query('MyCustomObject'); - q.greaterThan('ttl', { $relativeTime: 'in 1 day' }); - return q.find({ useMasterKey: true }); - }) - .then(results => { - expect(results.length).toBe(1); - }) - .then(() => { - const q = new Parse.Query('MyCustomObject'); - q.greaterThan('ttl', { $relativeTime: '1 day ago' }); - return q.find({ useMasterKey: true }); - }) - .then(results => { - expect(results.length).toBe(1); - }) - .then(() => { - const q = new Parse.Query('MyCustomObject'); - q.lessThan('ttl', { $relativeTime: '5 days ago' }); - return q.find({ useMasterKey: true }); - }) - .then(results => { - expect(results.length).toBe(0); - }) - .then(() => { - const q = new Parse.Query('MyCustomObject'); - q.greaterThan('ttl', { $relativeTime: '3 days ago' }); - return q.find({ useMasterKey: true }); - }) - .then(results => { - expect(results.length).toBe(2); - }) - .then(() => { - const q = new Parse.Query('MyCustomObject'); - q.greaterThan('ttl', { $relativeTime: 'now' }); - return q.find({ useMasterKey: true }); - }) - .then(results => { - expect(results.length).toBe(1); - }) - .then(() => { - const q = new Parse.Query('MyCustomObject'); - q.greaterThan('ttl', { $relativeTime: 'now' }); - q.lessThan('ttl', { $relativeTime: 'in 1 day' }); - return q.find({ useMasterKey: true }); - }) - .then(results => { - expect(results.length).toBe(0); - }) - .then(() => { - const q = new Parse.Query('MyCustomObject'); - q.greaterThan('ttl', { $relativeTime: '1 year 3 weeks ago' }); - return q.find({ useMasterKey: true }); - }) - .then(results => { - expect(results.length).toBe(2); - }) - .then(done, done.fail); + await Parse.Object.saveAll([obj1, obj2]) + const q1 = new Parse.Query('MyCustomObject'); + q1.greaterThan('ttl', { $relativeTime: 'in 1 day' }); + const results1 = await q1.find({ useMasterKey: true }); + expect(results1.length).toBe(1); + + const q2 = new Parse.Query('MyCustomObject'); + q2.greaterThan('ttl', { $relativeTime: '1 day ago' }); + const results2 = await q2.find({ useMasterKey: true }); + expect(results2.length).toBe(1); + + const q3 = new Parse.Query('MyCustomObject'); + q3.lessThan('ttl', { $relativeTime: '5 days ago' }); + const results3 = await q3.find({ useMasterKey: true }); + expect(results3.length).toBe(0); + + const q4 = new Parse.Query('MyCustomObject'); + q4.greaterThan('ttl', { $relativeTime: '3 days ago' }); + const results4 = await q4.find({ useMasterKey: true }); + expect(results4.length).toBe(2); + + const q5 = new Parse.Query('MyCustomObject'); + q5.greaterThan('ttl', { $relativeTime: 'now' }); + const results5 = await q5.find({ useMasterKey: true }); + expect(results5.length).toBe(1); + + const q6 = new Parse.Query('MyCustomObject'); + q6.greaterThan('ttl', { $relativeTime: 'now' }); + q6.lessThan('ttl', { $relativeTime: 'in 1 day' }); + const results6 = await q6.find({ useMasterKey: true }); + expect(results6.length).toBe(0); + + const q7 = new Parse.Query('MyCustomObject'); + q7.greaterThan('ttl', { $relativeTime: '1 year 3 weeks ago' }); + const results7 = await q7.find({ useMasterKey: true }); + expect(results7.length).toBe(2); }); - it_only_db('mongo')('should error on invalid relative time', function (done) { + it('should error on invalid relative time', async () => { const obj1 = new Parse.Object('MyCustomObject', { name: 'obj1', ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now }); - + await obj1.save({ useMasterKey: true }); const q = new Parse.Query('MyCustomObject'); q.greaterThan('ttl', { $relativeTime: '-12 bananas ago' }); - obj1 - .save({ useMasterKey: true }) - .then(() => q.find({ useMasterKey: true })) - .then(done.fail, () => done()); + try { + await q.find({ useMasterKey: true }); + fail("Should have thrown error"); + } catch(error) { + expect(error.code).toBe(Parse.Error.INVALID_JSON); + } }); - it_only_db('mongo')('should error when using $relativeTime on non-Date field', function (done) { + it('should error when using $relativeTime on non-Date field', async () => { const obj1 = new Parse.Object('MyCustomObject', { name: 'obj1', nonDateField: 'abcd', ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now }); - + await obj1.save({ useMasterKey: true }); const q = new Parse.Query('MyCustomObject'); q.greaterThan('nonDateField', { $relativeTime: '1 day ago' }); - obj1 - .save({ useMasterKey: true }) - .then(() => q.find({ useMasterKey: true })) - .then(done.fail, () => done()); + try { + await q.find({ useMasterKey: true }); + fail("Should have thrown error"); + } catch(error) { + expect(error.code).toBe(Parse.Error.INVALID_JSON); + } }); it('should match complex structure with dot notation when using matchesKeyInQuery', function (done) { diff --git a/spec/ParseServerRESTController.spec.js b/spec/ParseServerRESTController.spec.js index 90fe383257..1b136808b9 100644 --- a/spec/ParseServerRESTController.spec.js +++ b/spec/ParseServerRESTController.spec.js @@ -15,35 +15,18 @@ describe('ParseServerRESTController', () => { ); }); - it('should handle a get request', done => { - RESTController.request('GET', '/classes/MyObject').then( - res => { - expect(res.results.length).toBe(0); - done(); - }, - err => { - console.log(err); - jfail(err); - done(); - } - ); + it('should handle a get request', async () => { + const res = await RESTController.request('GET', '/classes/MyObject'); + expect(res.results.length).toBe(0); }); - it('should handle a get request with full serverURL mount path', done => { - RESTController.request('GET', '/1/classes/MyObject').then( - res => { - expect(res.results.length).toBe(0); - done(); - }, - err => { - jfail(err); - done(); - } - ); + it('should handle a get request with full serverURL mount path', async () => { + const res = await RESTController.request('GET', '/1/classes/MyObject'); + expect(res.results.length).toBe(0); }); - it('should handle a POST batch without transaction', done => { - RESTController.request('POST', 'batch', { + it('should handle a POST batch without transaction', async () => { + const res = await RESTController.request('POST', 'batch', { requests: [ { method: 'GET', @@ -59,20 +42,12 @@ describe('ParseServerRESTController', () => { path: '/classes/MyObject', }, ], - }).then( - res => { - expect(res.length).toBe(3); - done(); - }, - err => { - jfail(err); - done(); - } - ); + }); + expect(res.length).toBe(3); }); - it('should handle a POST batch with transaction=false', done => { - RESTController.request('POST', 'batch', { + it('should handle a POST batch with transaction=false', async () => { + const res = await RESTController.request('POST', 'batch', { requests: [ { method: 'GET', @@ -89,16 +64,8 @@ describe('ParseServerRESTController', () => { }, ], transaction: false, - }).then( - res => { - expect(res.length).toBe(3); - done(); - }, - err => { - jfail(err); - done(); - } - ); + }); + expect(res.length).toBe(3); }); it('should handle response status', async () => { @@ -186,54 +153,43 @@ describe('ParseServerRESTController', () => { } }); - it('should handle a batch request with transaction = true', async done => { - await reconfigureServer(); + it('should handle a batch request with transaction = true', async () => { const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections - myObject - .save() - .then(() => { - return myObject.destroy(); - }) - .then(() => { - spyOn(databaseAdapter, 'createObject').and.callThrough(); - - return RESTController.request('POST', 'batch', { - requests: [ - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value2' }, - }, - ], - transaction: true, - }).then(response => { - expect(response.length).toEqual(2); - expect(response[0].success.objectId).toBeDefined(); - expect(response[0].success.createdAt).toBeDefined(); - expect(response[1].success.objectId).toBeDefined(); - expect(response[1].success.createdAt).toBeDefined(); - const query = new Parse.Query('MyObject'); - return query.find().then(results => { - expect(databaseAdapter.createObject.calls.count() % 2).toBe(0); - for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) { - expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe( - databaseAdapter.createObject.calls.argsFor(i + 1)[3] - ); - } - expect(results.map(result => result.get('key')).sort()).toEqual([ - 'value1', - 'value2', - ]); - done(); - }); - }); - }) - .catch(done.fail); + await myObject.save(); + await myObject.destroy(); + spyOn(databaseAdapter, 'createObject').and.callThrough(); + const response = await RESTController.request('POST', 'batch', { + requests: [ + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value2' }, + }, + ], + transaction: true, + }); + expect(response.length).toEqual(2); + expect(response[0].success.objectId).toBeDefined(); + expect(response[0].success.createdAt).toBeDefined(); + expect(response[1].success.objectId).toBeDefined(); + expect(response[1].success.createdAt).toBeDefined(); + const query = new Parse.Query('MyObject'); + const results = await query.find(); + expect(databaseAdapter.createObject.calls.count() % 2).toBe(0); + for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) { + expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe( + databaseAdapter.createObject.calls.argsFor(i + 1)[3] + ); + } + expect(results.map(result => result.get('key')).sort()).toEqual([ + 'value1', + 'value2', + ]); }); it('should not save anything when one operation fails in a transaction', async () => { @@ -560,21 +516,11 @@ describe('ParseServerRESTController', () => { }); } - it('should handle a POST request', done => { - RESTController.request('POST', '/classes/MyObject', { key: 'value' }) - .then(() => { - return RESTController.request('GET', '/classes/MyObject'); - }) - .then(res => { - expect(res.results.length).toBe(1); - expect(res.results[0].key).toEqual('value'); - done(); - }) - .catch(err => { - console.log(err); - jfail(err); - done(); - }); + it('should handle a POST request', async () => { + await RESTController.request('POST', '/classes/MyObject', { key: 'value' }); + const res = await RESTController.request('GET', '/classes/MyObject'); + expect(res.results.length).toBe(1); + expect(res.results[0].key).toEqual('value'); }); it('should handle a POST request with context', async () => { @@ -593,125 +539,76 @@ describe('ParseServerRESTController', () => { ); }); - it('ensures sessionTokens are properly handled', done => { - let userId; - Parse.User.signUp('user', 'pass') - .then(user => { - userId = user.id; - const sessionToken = user.getSessionToken(); - return RESTController.request('GET', '/users/me', undefined, { - sessionToken, - }); - }) - .then(res => { - // Result is in JSON format - expect(res.objectId).toEqual(userId); - done(); - }) - .catch(err => { - console.log(err); - jfail(err); - done(); - }); + it('ensures sessionTokens are properly handled', async () => { + const user = await Parse.User.signUp('user', 'pass'); + const sessionToken = user.getSessionToken(); + const res = await RESTController.request('GET', '/users/me', undefined, { + sessionToken, + }); + // Result is in JSON format + expect(res.objectId).toEqual(user.id); }); - it('ensures masterKey is properly handled', done => { - let userId; - Parse.User.signUp('user', 'pass') - .then(user => { - userId = user.id; - return Parse.User.logOut().then(() => { - return RESTController.request('GET', '/classes/_User', undefined, { - useMasterKey: true, - }); - }); - }) - .then( - res => { - expect(res.results.length).toBe(1); - expect(res.results[0].objectId).toEqual(userId); - done(); - }, - err => { - jfail(err); - done(); - } - ); + it('ensures masterKey is properly handled', async () => { + const user = await Parse.User.signUp('user', 'pass'); + const userId = user.id; + await Parse.User.logOut(); + const res = await RESTController.request('GET', '/classes/_User', undefined, { + useMasterKey: true, + }); + expect(res.results.length).toBe(1); + expect(res.results[0].objectId).toEqual(userId); }); - it('ensures no user is created when passing an empty username', done => { - RESTController.request('POST', '/classes/_User', { - username: '', - password: 'world', - }).then( - () => { - jfail(new Error('Success callback should not be called when passing an empty username.')); - done(); - }, - err => { - expect(err.code).toBe(Parse.Error.USERNAME_MISSING); - expect(err.message).toBe('bad or missing username'); - done(); - } - ); + it('ensures no user is created when passing an empty username', async () => { + try { + await RESTController.request('POST', '/classes/_User', { + username: '', + password: 'world', + }); + fail('Success callback should not be called when passing an empty username.'); + } catch (err) { + expect(err.code).toBe(Parse.Error.USERNAME_MISSING); + expect(err.message).toBe('bad or missing username'); + } }); - it('ensures no user is created when passing an empty password', done => { - RESTController.request('POST', '/classes/_User', { - username: 'hello', - password: '', - }).then( - () => { - jfail(new Error('Success callback should not be called when passing an empty password.')); - done(); - }, - err => { - expect(err.code).toBe(Parse.Error.PASSWORD_MISSING); - expect(err.message).toBe('password is required'); - done(); - } - ); + it('ensures no user is created when passing an empty password', async () => { + try { + await RESTController.request('POST', '/classes/_User', { + username: 'hello', + password: '', + }); + fail('Success callback should not be called when passing an empty password.'); + } catch (err) { + expect(err.code).toBe(Parse.Error.PASSWORD_MISSING); + expect(err.message).toBe('password is required'); + } }); - it('ensures no session token is created on creating users', done => { - RESTController.request('POST', '/classes/_User', { + it('ensures no session token is created on creating users', async () => { + const user = await RESTController.request('POST', '/classes/_User', { username: 'hello', password: 'world', - }) - .then(user => { - expect(user.sessionToken).toBeUndefined(); - const query = new Parse.Query('_Session'); - return query.find({ useMasterKey: true }); - }) - .then(sessions => { - expect(sessions.length).toBe(0); - done(); - }, done.fail); + }); + expect(user.sessionToken).toBeUndefined(); + const query = new Parse.Query('_Session'); + const sessions = await query.find({ useMasterKey: true }); + expect(sessions.length).toBe(0); }); - it('ensures a session token is created when passing installationId != cloud', done => { - RESTController.request( + it('ensures a session token is created when passing installationId != cloud', async () => { + const user = await RESTController.request( 'POST', '/classes/_User', { username: 'hello', password: 'world' }, { installationId: 'my-installation' } - ) - .then(user => { - expect(user.sessionToken).not.toBeUndefined(); - const query = new Parse.Query('_Session'); - return query.find({ useMasterKey: true }); - }) - .then( - sessions => { - expect(sessions.length).toBe(1); - expect(sessions[0].get('installationId')).toBe('my-installation'); - done(); - }, - err => { - jfail(err); - done(); - } - ); + ); + expect(user.sessionToken).not.toBeUndefined(); + const query = new Parse.Query('_Session'); + const sessions = await query.find({ useMasterKey: true }); + expect(sessions.length).toBe(1); + expect(sessions[0].get('installationId')).toBe('my-installation'); }); it('ensures logIn is saved with installationId', async () => { diff --git a/spec/PostgresInitOptions.spec.js b/spec/PostgresInitOptions.spec.js index 069d5a6437..760240734d 100644 --- a/spec/PostgresInitOptions.spec.js +++ b/spec/PostgresInitOptions.spec.js @@ -4,8 +4,7 @@ const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/Postgre const postgresURI = process.env.PARSE_SERVER_TEST_DATABASE_URI || 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; -const ParseServer = require('../lib/index'); -const express = require('express'); + //public schema const databaseOptions1 = { initOptions: { @@ -24,72 +23,57 @@ const GameScore = Parse.Object.extend({ className: 'GameScore', }); -function createParseServer(options) { - return new Promise((resolve, reject) => { - const parseServer = new ParseServer.default( - Object.assign({}, defaultConfiguration, options, { - serverURL: 'http://localhost:12668/parse', - serverStartComplete: error => { - if (error) { - reject(error); - } else { - expect(Parse.applicationId).toEqual('test'); - const app = express(); - app.use('/parse', parseServer.app); - - const server = app.listen(12668); - Parse.serverURL = 'http://localhost:12668/parse'; - resolve(server); - } - }, - }) - ); - }); -} - describe_only_db('postgres')('Postgres database init options', () => { - let server; - - afterAll(done => { - if (server) { - Parse.serverURL = 'http://localhost:8378/1'; - server.close(done); - } - }); - it('should create server with public schema databaseOptions', done => { + it('should create server with public schema databaseOptions', async () => { const adapter = new PostgresStorageAdapter({ uri: postgresURI, collectionPrefix: 'test_', databaseOptions: databaseOptions1, }); + await reconfigureServer({ + databaseAdapter: adapter, + }); + const score = new GameScore({ + score: 1337, + playerName: 'Sean Plott', + cheatMode: false, + }); + await score.save(); + }); - createParseServer({ databaseAdapter: adapter }) - .then(newServer => { - server = newServer; - const score = new GameScore({ - score: 1337, - playerName: 'Sean Plott', - cheatMode: false, - }); - return score.save(); - }) - .then(async () => { - await reconfigureServer(); - done(); - }, done.fail); + it('should create server using postgresql uri with public schema databaseOptions', async () => { + const postgresURI2 = new URL(postgresURI); + postgresURI2.protocol = 'postgresql:'; + const adapter = new PostgresStorageAdapter({ + uri: postgresURI2.toString(), + collectionPrefix: 'test_', + databaseOptions: databaseOptions1, + }); + await reconfigureServer({ + databaseAdapter: adapter, + }); + const score = new GameScore({ + score: 1337, + playerName: 'Sean Plott', + cheatMode: false, + }); + await score.save(); }); - it('should fail to create server if schema databaseOptions does not exist', done => { + it('should fail to create server if schema databaseOptions does not exist', async () => { const adapter = new PostgresStorageAdapter({ uri: postgresURI, collectionPrefix: 'test_', databaseOptions: databaseOptions2, }); - - createParseServer({ databaseAdapter: adapter }).then(done.fail, async () => { - await reconfigureServer(); - done(); - }); + try { + await reconfigureServer({ + databaseAdapter: adapter, + }); + fail("Should have thrown error"); + } catch(error) { + expect(error).toBeDefined(); + } }); }); diff --git a/spec/PostgresStorageAdapter.spec.js b/spec/PostgresStorageAdapter.spec.js index b042206db2..5673d6bc70 100644 --- a/spec/PostgresStorageAdapter.spec.js +++ b/spec/PostgresStorageAdapter.spec.js @@ -149,6 +149,135 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined); }); + it('$relativeTime should error on $eq', async () => { + const tableName = '_User'; + const schema = { + fields: { + objectId: { type: 'String' }, + username: { type: 'String' }, + email: { type: 'String' }, + emailVerified: { type: 'Boolean' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + authData: { type: 'Object' }, + }, + }; + const client = adapter._client; + await adapter.createTable(tableName, schema); + await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [ + tableName, + 'objectId', + 'username', + 'Bugs', + 'Bunny', + ]); + const database = Config.get(Parse.applicationId).database; + await database.loadSchema({ clearCache: true }); + try { + await database.find( + tableName, + { + createdAt: { + $eq: { + $relativeTime: '12 days ago' + } + } + }, + { } + ); + fail("Should have thrown error"); + } catch(error) { + expect(error.code).toBe(Parse.Error.INVALID_JSON); + } + await dropTable(client, tableName); + }); + + it('$relativeTime should error on $ne', async () => { + const tableName = '_User'; + const schema = { + fields: { + objectId: { type: 'String' }, + username: { type: 'String' }, + email: { type: 'String' }, + emailVerified: { type: 'Boolean' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + authData: { type: 'Object' }, + }, + }; + const client = adapter._client; + await adapter.createTable(tableName, schema); + await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [ + tableName, + 'objectId', + 'username', + 'Bugs', + 'Bunny', + ]); + const database = Config.get(Parse.applicationId).database; + await database.loadSchema({ clearCache: true }); + try { + await database.find( + tableName, + { + createdAt: { + $ne: { + $relativeTime: '12 days ago' + } + } + }, + { } + ); + fail("Should have thrown error"); + } catch(error) { + expect(error.code).toBe(Parse.Error.INVALID_JSON); + } + await dropTable(client, tableName); + }); + + it('$relativeTime should error on $exists', async () => { + const tableName = '_User'; + const schema = { + fields: { + objectId: { type: 'String' }, + username: { type: 'String' }, + email: { type: 'String' }, + emailVerified: { type: 'Boolean' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + authData: { type: 'Object' }, + }, + }; + const client = adapter._client; + await adapter.createTable(tableName, schema); + await client.none('INSERT INTO $1:name ($2:name, $3:name) VALUES ($4, $5)', [ + tableName, + 'objectId', + 'username', + 'Bugs', + 'Bunny', + ]); + const database = Config.get(Parse.applicationId).database; + await database.loadSchema({ clearCache: true }); + try { + await database.find( + tableName, + { + createdAt: { + $exists: { + $relativeTime: '12 days ago' + } + } + }, + { } + ); + fail("Should have thrown error"); + } catch(error) { + expect(error.code).toBe(Parse.Error.INVALID_JSON); + } + await dropTable(client, tableName); + }); + it('should use index for caseInsensitive query using Postgres', async () => { const tableName = '_User'; const schema = { @@ -426,9 +555,20 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { }, classLevelPermissions: undefined, }); - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise(resolve => setTimeout(resolve, 2000)); expect(adapter._onchange).toHaveBeenCalled(); }); + + it('Idempotency class should have function', async () => { + await reconfigureServer(); + const adapter = Config.get('test').database.adapter; + const client = adapter._client; + const qs = "SELECT format('%I.%I(%s)', ns.nspname, p.proname, oidvectortypes(p.proargtypes)) FROM pg_proc p INNER JOIN pg_namespace ns ON (p.pronamespace = ns.oid) WHERE p.proname = 'idempotency_delete_expired_records'"; + const foundFunction = await client.one(qs); + expect(foundFunction.format).toBe("public.idempotency_delete_expired_records()"); + await adapter.deleteIdempotencyFunction(); + await client.none(qs); + }); }); describe_only_db('postgres')('PostgresStorageAdapter shutdown', () => { @@ -438,4 +578,13 @@ describe_only_db('postgres')('PostgresStorageAdapter shutdown', () => { adapter.handleShutdown(); expect(adapter._client.$pool.ending).toEqual(true); }); + + it('handleShutdown, close connection of postgresql uri', () => { + const databaseURI2 = new URL(databaseURI); + databaseURI2.protocol = 'postgresql:'; + const adapter = new PostgresStorageAdapter({ uri: databaseURI2.toString() }); + expect(adapter._client.$pool.ending).toEqual(false); + adapter.handleShutdown(); + expect(adapter._client.$pool.ending).toEqual(true); + }); }); diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index 4626b5e0a7..49613214f5 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -562,11 +562,7 @@ describe('PushController', () => { }); const pushStatusId = await sendPush(payload, {}, config, auth); // it is enqueued so it can take time - await new Promise(resolve => { - setTimeout(() => { - resolve(); - }, 1000); - }); + await sleep(1000); Parse.serverURL = 'http://localhost:8378/1'; // GOOD url const result = await Parse.Push.getPushStatus(pushStatusId); expect(result).toBeDefined(); @@ -767,7 +763,7 @@ describe('PushController', () => { }); }); - it('should not schedule push when not configured', done => { + it('should not schedule push when not configured', async () => { const config = Config.get(Parse.applicationId); const auth = { isMaster: true, @@ -800,33 +796,20 @@ describe('PushController', () => { installations.push(installation); } - reconfigureServer({ + await reconfigureServer({ push: { adapter: pushAdapter }, - }) - .then(() => { - return Parse.Object.saveAll(installations) - .then(() => { - return pushController.sendPush(payload, {}, config, auth); - }) - .then(() => new Promise(resolve => setTimeout(resolve, 300))); - }) - .then(() => { - const query = new Parse.Query('_PushStatus'); - return query.find({ useMasterKey: true }).then(results => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('status')).not.toBe('scheduled'); - done(); - }); - }) - .catch(err => { - console.error(err); - fail('should not fail'); - done(); - }); + }); + await Parse.Object.saveAll(installations); + await pushController.sendPush(payload, {}, config, auth); + await sleep(1000); + const query = new Parse.Query('_PushStatus'); + const results = await query.find({ useMasterKey: true }); + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('status')).not.toBe('scheduled'); }); - it('should schedule push when configured', done => { + it('should schedule push when configured', async () => { const auth = { isMaster: true, }; @@ -866,28 +849,19 @@ describe('PushController', () => { installation.set('deviceType', 'ios'); installations.push(installation); } - reconfigureServer({ + await reconfigureServer({ push: { adapter: pushAdapter }, scheduledPush: true, - }) - .then(() => { - const config = Config.get(Parse.applicationId); - return Parse.Object.saveAll(installations) - .then(() => { - return pushController.sendPush(payload, {}, config, auth); - }) - .then(() => new Promise(resolve => setTimeout(resolve, 300))); - }) - .then(() => { - const query = new Parse.Query('_PushStatus'); - return query.find({ useMasterKey: true }).then(results => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('status')).toBe('scheduled'); - }); - }) - .then(done) - .catch(done.err); + }); + const config = Config.get(Parse.applicationId); + await Parse.Object.saveAll(installations); + await pushController.sendPush(payload, {}, config, auth); + await sleep(1000); + const query = new Parse.Query('_PushStatus'); + const results = await query.find({ useMasterKey: true }); + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('status')).toBe('scheduled'); }); it('should not enqueue push when device token is not set', async () => { diff --git a/spec/ReadPreferenceOption.spec.js b/spec/ReadPreferenceOption.spec.js index f2bc328d99..535c35c8b0 100644 --- a/spec/ReadPreferenceOption.spec.js +++ b/spec/ReadPreferenceOption.spec.js @@ -1,9 +1,8 @@ 'use strict'; const Parse = require('parse/node'); -const ReadPreference = require('mongodb').ReadPreference; +const { ReadPreference, Collection } = require('mongodb'); const request = require('../lib/request'); -const Config = require('../lib/Config'); function waitForReplication() { return new Promise(function (resolve) { @@ -13,8 +12,6 @@ function waitForReplication() { describe_only_db('mongo')('Read preference option', () => { it('should find in primary by default', done => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); @@ -22,7 +19,7 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1]) .then(() => { - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); const query = new Parse.Query('MyObject'); query.equalTo('boolKey', false); @@ -31,10 +28,10 @@ describe_only_db('mongo')('Read preference option', () => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { myObjectReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.PRIMARY); + expect(call.object.s.readPreference.mode).toBe(ReadPreference.PRIMARY); } }); @@ -58,15 +55,13 @@ describe_only_db('mongo')('Read preference option', () => { databaseAdapter: new MongoStorageAdapter(adapterOptions), }); - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); const query = new Parse.Query('MyObject'); query.equalTo('boolKey', false); @@ -76,10 +71,10 @@ describe_only_db('mongo')('Read preference option', () => { expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { myObjectReadPreference = true; - expect(call.args[0].options.readPreference.mode).toBe(ReadPreference.NEAREST); + expect(call.args[1].readPreference).toBe(ReadPreference.NEAREST); } }); @@ -87,15 +82,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference in the beforeFind trigger', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; @@ -110,9 +103,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -120,15 +113,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should check read preference as case insensitive', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'sEcOnDarY'; @@ -144,9 +135,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -154,15 +145,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference in the beforeFind trigger even changing query', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.query.equalTo('boolKey', true); @@ -178,9 +167,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(results[0].get('boolKey')).toBe(true); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -188,15 +177,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference in the beforeFind trigger even returning query', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; @@ -216,9 +203,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(results[0].get('boolKey')).toBe(true); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -226,15 +213,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference in the beforeFind trigger even returning promise', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; @@ -253,9 +238,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(results[0].get('boolKey')).toBe(true); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -263,15 +248,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference to PRIMARY_PREFERRED', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'PRIMARY_PREFERRED'; @@ -286,9 +269,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -296,15 +279,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference to SECONDARY_PREFERRED', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY_PREFERRED'; @@ -319,9 +300,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -329,15 +310,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference to NEAREST', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'NEAREST'; @@ -352,9 +331,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -362,15 +341,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference for GET', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; @@ -383,9 +360,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(result.get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -393,15 +370,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference for GET using API', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; @@ -421,9 +396,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(body.boolKey).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -431,15 +406,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference for GET directly from API', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); await waitForReplication(); const response = await request({ @@ -454,9 +427,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(response.data.boolKey).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -464,15 +437,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference for GET using API through the beforeFind overriding API option', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY_PREFERRED'; @@ -491,9 +462,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(response.data.boolKey).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -501,15 +472,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference for FIND using API through beforeFind trigger', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; @@ -528,9 +497,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(response.data.results.length).toEqual(2); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -538,15 +507,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference for FIND directly from API', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); await waitForReplication(); const response = await request({ @@ -561,9 +528,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(response.data.results.length).toEqual(2); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -571,15 +538,13 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change read preference for FIND using API through the beforeFind overriding API option', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); await Parse.Object.saveAll([obj0, obj1]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY_PREFERRED'; @@ -598,9 +563,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(response.data.results.length).toEqual(2); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -608,15 +573,13 @@ describe_only_db('mongo')('Read preference option', () => { }); xit('should change read preference for count', done => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); Parse.Object.saveAll([obj0, obj1]).then(() => { - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; @@ -631,9 +594,9 @@ describe_only_db('mongo')('Read preference option', () => { expect(result).toBe(1); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); @@ -659,17 +622,16 @@ describe_only_db('mongo')('Read preference option', () => { await waitForReplication(); // Spy on DB adapter - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - spyOn(databaseAdapter.database.serverConfig, 'startSession').and.callThrough(); + spyOn(Collection.prototype, 'aggregate').and.callThrough(); // Query const query = new Parse.Query('MyObject'); const results = await query.aggregate([{ match: { boolKey: false } }]); // Validate expect(results.length).toBe(1); let readPreference = null; - databaseAdapter.database.serverConfig.startSession.calls.all().forEach(call => { - if (call.args[0].owner.ns.indexOf('MyObject') > -1) { - readPreference = call.args[0].owner.operation.readPreference.mode; + Collection.prototype.aggregate.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') > -1) { + readPreference = call.args[1].readPreference; } }); expect(readPreference).toEqual(ReadPreference.SECONDARY); @@ -685,8 +647,7 @@ describe_only_db('mongo')('Read preference option', () => { await waitForReplication(); // Spy on DB adapter - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); // Query const query = new Parse.Query('MyObject'); query.equalTo('boolKey', false); @@ -695,9 +656,9 @@ describe_only_db('mongo')('Read preference option', () => { // Validate expect(results.length).toBe(1); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[1].readPreference; } }); expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); @@ -713,8 +674,7 @@ describe_only_db('mongo')('Read preference option', () => { await waitForReplication(); // Spy on DB adapter - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - spyOn(databaseAdapter.database.serverConfig, 'startSession').and.callThrough(); + spyOn(Collection.prototype, 'aggregate').and.callThrough(); // Query const query = new Parse.Query('MyObject'); query.readPreference('SECONDARY'); @@ -722,17 +682,15 @@ describe_only_db('mongo')('Read preference option', () => { // Validate expect(results.length).toBe(1); let readPreference = null; - databaseAdapter.database.serverConfig.startSession.calls.all().forEach(call => { - if (call.args[0].owner.ns.indexOf('MyObject') > -1) { - readPreference = call.args[0].owner.operation.readPreference.mode; + Collection.prototype.aggregate.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject') > -1) { + readPreference = call.args[1].readPreference; } }); expect(readPreference).toEqual(ReadPreference.SECONDARY); }); it('should find includes in same replica of readPreference by default', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject1'); @@ -743,7 +701,7 @@ describe_only_db('mongo')('Read preference option', () => { obj2.set('myObject1', obj1); await Parse.Object.saveAll([obj0, obj1, obj2]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject2', req => { req.readPreference = 'SECONDARY'; @@ -765,15 +723,15 @@ describe_only_db('mongo')('Read preference option', () => { let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[1].readPreference; } }); @@ -783,8 +741,6 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change includes read preference', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject1'); @@ -795,7 +751,7 @@ describe_only_db('mongo')('Read preference option', () => { obj2.set('myObject1', obj1); await Parse.Object.saveAll([obj0, obj1, obj2]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject2', req => { req.readPreference = 'SECONDARY_PREFERRED'; @@ -818,15 +774,15 @@ describe_only_db('mongo')('Read preference option', () => { let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[1].readPreference; } }); @@ -836,8 +792,6 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change includes read preference when finding through API', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject1'); @@ -848,7 +802,7 @@ describe_only_db('mongo')('Read preference option', () => { obj2.set('myObject1', obj1); await Parse.Object.saveAll([obj0, obj1, obj2]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); await waitForReplication(); const response = await request({ @@ -873,15 +827,15 @@ describe_only_db('mongo')('Read preference option', () => { let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[1].readPreference; } }); @@ -891,8 +845,6 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change includes read preference when getting through API', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject1'); @@ -903,7 +855,7 @@ describe_only_db('mongo')('Read preference option', () => { obj2.set('myObject1', obj1); await Parse.Object.saveAll([obj0, obj1, obj2]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); await waitForReplication(); const response = await request({ @@ -929,15 +881,15 @@ describe_only_db('mongo')('Read preference option', () => { let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[1].readPreference; } }); @@ -947,8 +899,6 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should find subqueries in same replica of readPreference by default', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject1'); @@ -959,7 +909,7 @@ describe_only_db('mongo')('Read preference option', () => { obj2.set('myObject1', obj1); await Parse.Object.saveAll([obj0, obj1, obj2]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject2', req => { req.readPreference = 'SECONDARY'; @@ -982,15 +932,15 @@ describe_only_db('mongo')('Read preference option', () => { let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[1].readPreference; } }); @@ -1000,8 +950,6 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change subqueries read preference when using matchesQuery', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject1'); @@ -1012,7 +960,7 @@ describe_only_db('mongo')('Read preference option', () => { obj2.set('myObject1', obj1); await Parse.Object.saveAll([obj0, obj1, obj2]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject2', req => { req.readPreference = 'SECONDARY_PREFERRED'; @@ -1036,15 +984,15 @@ describe_only_db('mongo')('Read preference option', () => { let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[1].readPreference; } }); @@ -1054,8 +1002,6 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change subqueries read preference when using doesNotMatchQuery', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject1'); @@ -1066,7 +1012,7 @@ describe_only_db('mongo')('Read preference option', () => { obj2.set('myObject1', obj1); await Parse.Object.saveAll([obj0, obj1, obj2]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject2', req => { req.readPreference = 'SECONDARY_PREFERRED'; @@ -1090,15 +1036,15 @@ describe_only_db('mongo')('Read preference option', () => { let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[1].readPreference; } }); @@ -1108,8 +1054,6 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change subqueries read preference when using matchesKeyInQuery and doesNotMatchKeyInQuery', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject1'); @@ -1120,7 +1064,7 @@ describe_only_db('mongo')('Read preference option', () => { obj2.set('myObject1', obj1); await Parse.Object.saveAll([obj0, obj1, obj2]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); Parse.Cloud.beforeFind('MyObject2', req => { req.readPreference = 'SECONDARY_PREFERRED'; @@ -1145,15 +1089,15 @@ describe_only_db('mongo')('Read preference option', () => { let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[1].readPreference; } }); @@ -1163,8 +1107,6 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should change subqueries read preference when using matchesKeyInQuery and doesNotMatchKeyInQuery to find through API', async () => { - const databaseAdapter = Config.get(Parse.applicationId).database.adapter; - const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject1'); @@ -1175,7 +1117,7 @@ describe_only_db('mongo')('Read preference option', () => { obj2.set('myObject1', obj1); await Parse.Object.saveAll([obj0, obj1, obj2]); - spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + spyOn(Collection.prototype, 'find').and.callThrough(); await waitForReplication(); const whereString = JSON.stringify({ @@ -1215,15 +1157,15 @@ describe_only_db('mongo')('Read preference option', () => { let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { - if (call.args[0].ns.collection.indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[0].options.readPreference.mode; + Collection.prototype.find.calls.all().forEach(call => { + if (call.object.s.namespace.collection.indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[1].readPreference; } - if (call.args[0].ns.collection.indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[0].options.readPreference.mode; + if (call.object.s.namespace.collection.indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[1].readPreference; } }); diff --git a/spec/batch.spec.js b/spec/batch.spec.js index 98050254ba..f91f91a0b5 100644 --- a/spec/batch.spec.js +++ b/spec/batch.spec.js @@ -89,10 +89,54 @@ describe('batch', () => { expect(internalURL).toEqual('/classes/Object'); }); - it('should handle a batch request without transaction', async done => { + it('should return the proper url with no url provided', () => { + const originalURL = '/parse/batch'; + const internalURL = batch.makeBatchRoutingPathFunction( + originalURL, + undefined, + publicServerURL + )('/parse/classes/Object'); + + expect(internalURL).toEqual('/classes/Object'); + }); + + it('should return the proper url with no public url provided', () => { + const originalURL = '/parse/batch'; + const internalURL = batch.makeBatchRoutingPathFunction( + originalURL, + serverURLNaked, + undefined + )('/parse/classes/Object'); + + expect(internalURL).toEqual('/classes/Object'); + }); + + it('should return the proper url with bad url provided', () => { + const originalURL = '/parse/batch'; + const internalURL = batch.makeBatchRoutingPathFunction( + originalURL, + 'badurl.com', + publicServerURL + )('/parse/classes/Object'); + + expect(internalURL).toEqual('/classes/Object'); + }); + + it('should return the proper url with bad public url provided', () => { + const originalURL = '/parse/batch'; + const internalURL = batch.makeBatchRoutingPathFunction( + originalURL, + serverURLNaked, + 'badurl.com' + )('/parse/classes/Object'); + + expect(internalURL).toEqual('/classes/Object'); + }); + + it('should handle a batch request without transaction', async () => { spyOn(databaseAdapter, 'createObject').and.callThrough(); - request({ + const response = await request({ method: 'POST', headers: headers, url: 'http://localhost:8378/1/batch', @@ -110,28 +154,25 @@ describe('batch', () => { }, ], }), - }).then(response => { - expect(response.data.length).toEqual(2); - expect(response.data[0].success.objectId).toBeDefined(); - expect(response.data[0].success.createdAt).toBeDefined(); - expect(response.data[1].success.objectId).toBeDefined(); - expect(response.data[1].success.createdAt).toBeDefined(); - const query = new Parse.Query('MyObject'); - query.find().then(results => { - expect(databaseAdapter.createObject.calls.count()).toBe(2); - expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toEqual(null); - expect(databaseAdapter.createObject.calls.argsFor(1)[3]).toEqual(null); - expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); - done(); - }); }); + + expect(response.data.length).toEqual(2); + expect(response.data[0].success.objectId).toBeDefined(); + expect(response.data[0].success.createdAt).toBeDefined(); + expect(response.data[1].success.objectId).toBeDefined(); + expect(response.data[1].success.createdAt).toBeDefined(); + const query = new Parse.Query('MyObject'); + const results = await query.find(); + expect(databaseAdapter.createObject.calls.count()).toBe(2); + expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toEqual(null); + expect(databaseAdapter.createObject.calls.argsFor(1)[3]).toEqual(null); + expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); }); - it('should handle a batch request with transaction = false', async done => { - await reconfigureServer(); + it('should handle a batch request with transaction = false', async () => { spyOn(databaseAdapter, 'createObject').and.callThrough(); - request({ + const response = await request({ method: 'POST', headers: headers, url: 'http://localhost:8378/1/batch', @@ -150,21 +191,18 @@ describe('batch', () => { ], transaction: false, }), - }).then(response => { - expect(response.data.length).toEqual(2); - expect(response.data[0].success.objectId).toBeDefined(); - expect(response.data[0].success.createdAt).toBeDefined(); - expect(response.data[1].success.objectId).toBeDefined(); - expect(response.data[1].success.createdAt).toBeDefined(); - const query = new Parse.Query('MyObject'); - query.find().then(results => { - expect(databaseAdapter.createObject.calls.count()).toBe(2); - expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toEqual(null); - expect(databaseAdapter.createObject.calls.argsFor(1)[3]).toEqual(null); - expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); - done(); - }); }); + expect(response.data.length).toEqual(2); + expect(response.data[0].success.objectId).toBeDefined(); + expect(response.data[0].success.createdAt).toBeDefined(); + expect(response.data[1].success.objectId).toBeDefined(); + expect(response.data[1].success.createdAt).toBeDefined(); + const query = new Parse.Query('MyObject'); + const results = await query.find(); + expect(databaseAdapter.createObject.calls.count()).toBe(2); + expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toEqual(null); + expect(databaseAdapter.createObject.calls.argsFor(1)[3]).toEqual(null); + expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); }); if ( @@ -191,58 +229,48 @@ describe('batch', () => { } }); - it('should handle a batch request with transaction = true', async done => { - await reconfigureServer(); + it('should handle a batch request with transaction = true', async () => { const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections - myObject - .save() - .then(() => { - return myObject.destroy(); - }) - .then(() => { - spyOn(databaseAdapter, 'createObject').and.callThrough(); - - request({ - method: 'POST', - headers: headers, - url: 'http://localhost:8378/1/batch', - body: JSON.stringify({ - requests: [ - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value2' }, - }, - ], - transaction: true, - }), - }).then(response => { - expect(response.data.length).toEqual(2); - expect(response.data[0].success.objectId).toBeDefined(); - expect(response.data[0].success.createdAt).toBeDefined(); - expect(response.data[1].success.objectId).toBeDefined(); - expect(response.data[1].success.createdAt).toBeDefined(); - const query = new Parse.Query('MyObject'); - query.find().then(results => { - expect(databaseAdapter.createObject.calls.count() % 2).toBe(0); - for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) { - expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe( - databaseAdapter.createObject.calls.argsFor(i + 1)[3] - ); - } - expect(results.map(result => result.get('key')).sort()).toEqual([ - 'value1', - 'value2', - ]); - done(); - }); - }); - }); + await myObject.save(); + await myObject.destroy(); + spyOn(databaseAdapter, 'createObject').and.callThrough(); + const response = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/batch', + body: JSON.stringify({ + requests: [ + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value2' }, + }, + ], + transaction: true, + }), + }); + expect(response.data.length).toEqual(2); + expect(response.data[0].success.objectId).toBeDefined(); + expect(response.data[0].success.createdAt).toBeDefined(); + expect(response.data[1].success.objectId).toBeDefined(); + expect(response.data[1].success.createdAt).toBeDefined(); + const query = new Parse.Query('MyObject'); + const results = await query.find(); + expect(databaseAdapter.createObject.calls.count() % 2).toBe(0); + for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) { + expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe( + databaseAdapter.createObject.calls.argsFor(i + 1)[3] + ); + } + expect(results.map(result => result.get('key')).sort()).toEqual([ + 'value1', + 'value2', + ]); }); it('should not save anything when one operation fails in a transaction', async () => { @@ -350,6 +378,7 @@ describe('batch', () => { transaction: true, }), }); + fail(); } catch (error) { expect(error).toBeDefined(); const query = new Parse.Query('MyObject'); diff --git a/spec/cloud/cloudCodeModuleFile.js b/spec/cloud/cloudCodeModuleFile.js deleted file mode 100644 index a62b4fcc24..0000000000 --- a/spec/cloud/cloudCodeModuleFile.js +++ /dev/null @@ -1,3 +0,0 @@ -Parse.Cloud.define('cloudCodeInFile', () => { - return 'It is possible to define cloud code in a file.'; -}); diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 5180b36ca5..9557dd7924 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -5,6 +5,7 @@ const dd = require('deep-diff'); const Config = require('../lib/Config'); const request = require('../lib/request'); const TestUtils = require('../lib/TestUtils'); +const SchemaController = require('../lib/Controllers/SchemaController').SchemaController; let config; @@ -239,6 +240,52 @@ describe('schemas', () => { }); }); + it('ensure refresh cache after creating a class', async done => { + spyOn(SchemaController.prototype, 'reloadData').and.callFake(() => Promise.resolve()); + await request({ + url: 'http://localhost:8378/1/schemas', + method: 'POST', + headers: masterKeyHeaders, + json: true, + body: { + className: 'A', + }, + }); + const response = await request({ + url: 'http://localhost:8378/1/schemas', + method: 'GET', + headers: masterKeyHeaders, + json: true, + }); + const expected = { + results: [ + userSchema, + roleSchema, + { + className: 'A', + fields: { + //Default fields + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + }, + ], + }; + expect( + response.data.results + .sort((s1, s2) => s1.className.localeCompare(s2.className)) + .map(s => { + const withoutIndexes = Object.assign({}, s); + delete withoutIndexes.indexes; + return withoutIndexes; + }) + ).toEqual(expected.results.sort((s1, s2) => s1.className.localeCompare(s2.className))); + done(); + }); + it('responds with a single schema', done => { const obj = hasAllPODobject(); obj.save().then(() => { @@ -1507,6 +1554,46 @@ describe('schemas', () => { }); }); + it('ensure refresh cache after deleting a class', async done => { + config = Config.get('test'); + spyOn(config.schemaCache, 'del').and.callFake(() => {}); + spyOn(SchemaController.prototype, 'reloadData').and.callFake(() => Promise.resolve()); + await request({ + url: 'http://localhost:8378/1/schemas', + method: 'POST', + headers: masterKeyHeaders, + json: true, + body: { + className: 'A', + }, + }); + await request({ + method: 'DELETE', + url: 'http://localhost:8378/1/schemas/A', + headers: masterKeyHeaders, + json: true, + }); + const response = await request({ + url: 'http://localhost:8378/1/schemas', + method: 'GET', + headers: masterKeyHeaders, + json: true, + }); + const expected = { + results: [userSchema, roleSchema], + }; + expect( + response.data.results + .sort((s1, s2) => s1.className.localeCompare(s2.className)) + .map(s => { + const withoutIndexes = Object.assign({}, s); + delete withoutIndexes.indexes; + return withoutIndexes; + }) + ).toEqual(expected.results.sort((s1, s2) => s1.className.localeCompare(s2.className))); + done(); + }); + it('deletes collections including join tables', done => { const obj = new Parse.Object('MyClass'); obj.set('data', 'data'); @@ -1779,6 +1866,34 @@ describe('schemas', () => { }); }); + describe('Nested documents', () => { + beforeAll(async () => { + const testSchema = new Parse.Schema('test_7371'); + testSchema.setCLP({ + create: { ['*']: true }, + update: { ['*']: true }, + addField: {}, + }); + testSchema.addObject('a'); + await testSchema.save(); + }); + + it('addField permission not required for adding a nested property', async () => { + const obj = new Parse.Object('test_7371'); + obj.set('a', {}); + await obj.save(); + obj.set('a.b', 2); + await obj.save(); + }); + it('addField permission not required for modifying a nested property', async () => { + const obj = new Parse.Object('test_7371'); + obj.set('a', { b: 1 }); + await obj.save(); + obj.set('a.b', 2); + await obj.save(); + }); + }); + it('should aceept class-level permission with userid of any length', async done => { await global.reconfigureServer({ customIdSize: 11, diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js new file mode 100644 index 0000000000..1255d64398 --- /dev/null +++ b/spec/vulnerabilities.spec.js @@ -0,0 +1,283 @@ +const request = require('../lib/request'); + +describe('Vulnerabilities', () => { + describe('Object prototype pollution', () => { + it('denies object prototype to be polluted with keyword "constructor"', async () => { + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const response = await request({ + headers: headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/PP', + body: JSON.stringify({ + obj: { + constructor: { + prototype: { + dummy: 0, + }, + }, + }, + }), + }).catch(e => e); + expect(response.status).toBe(400); + const text = JSON.parse(response.text); + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toBe('Prohibited keyword in request data: {"key":"constructor"}.'); + expect(Object.prototype.dummy).toBeUndefined(); + }); + + it('denies object prototype to be polluted with keypath string "constructor"', async () => { + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const objResponse = await request({ + headers: headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/PP', + body: JSON.stringify({ + obj: {}, + }), + }).catch(e => e); + const pollResponse = await request({ + headers: headers, + method: 'PUT', + url: `http://localhost:8378/1/classes/PP/${objResponse.data.objectId}`, + body: JSON.stringify({ + 'obj.constructor.prototype.dummy': { + __op: 'Increment', + amount: 1, + }, + }), + }).catch(e => e); + expect(Object.prototype.dummy).toBeUndefined(); + expect(pollResponse.status).toBe(400); + const text = JSON.parse(pollResponse.text); + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toBe('Prohibited keyword in request data: {"key":"constructor"}.'); + expect(Object.prototype.dummy).toBeUndefined(); + }); + + it('denies object prototype to be polluted with keyword "__proto__"', async () => { + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const response = await request({ + headers: headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/PP', + body: JSON.stringify({ 'obj.__proto__.dummy': 0 }), + }).catch(e => e); + expect(response.status).toBe(400); + const text = JSON.parse(response.text); + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toBe('Prohibited keyword in request data: {"key":"__proto__"}.'); + expect(Object.prototype.dummy).toBeUndefined(); + }); + }); + + describe('Request denylist', () => { + it('denies BSON type code data in write request by default', async () => { + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const params = { + headers: headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/RCE', + body: JSON.stringify({ + obj: { + _bsontype: 'Code', + code: 'delete Object.prototype.evalFunctions', + }, + }), + }; + const response = await request(params).catch(e => e); + expect(response.status).toBe(400); + const text = JSON.parse(response.text); + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toBe( + 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.' + ); + }); + + it('allows BSON type code data in write request with custom denylist', async () => { + await reconfigureServer({ + requestKeywordDenylist: [], + }); + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const params = { + headers: headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/RCE', + body: JSON.stringify({ + obj: { + _bsontype: 'Code', + code: 'delete Object.prototype.evalFunctions', + }, + }), + }; + const response = await request(params).catch(e => e); + expect(response.status).toBe(201); + const text = JSON.parse(response.text); + expect(text.objectId).toBeDefined(); + }); + + it('denies write request with custom denylist of key/value', async () => { + await reconfigureServer({ + requestKeywordDenylist: [{ key: 'a[K]ey', value: 'aValue[123]*' }], + }); + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const params = { + headers: headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/RCE', + body: JSON.stringify({ + obj: { + aKey: 'aValue321', + code: 'delete Object.prototype.evalFunctions', + }, + }), + }; + const response = await request(params).catch(e => e); + expect(response.status).toBe(400); + const text = JSON.parse(response.text); + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toBe( + 'Prohibited keyword in request data: {"key":"a[K]ey","value":"aValue[123]*"}.' + ); + }); + + it('denies write request with custom denylist of nested key/value', async () => { + await reconfigureServer({ + requestKeywordDenylist: [{ key: 'a[K]ey', value: 'aValue[123]*' }], + }); + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const params = { + headers: headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/RCE', + body: JSON.stringify({ + obj: { + nested: { + aKey: 'aValue321', + code: 'delete Object.prototype.evalFunctions', + }, + }, + }), + }; + const response = await request(params).catch(e => e); + expect(response.status).toBe(400); + const text = JSON.parse(response.text); + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toBe( + 'Prohibited keyword in request data: {"key":"a[K]ey","value":"aValue[123]*"}.' + ); + }); + + it('denies write request with custom denylist of key/value in array', async () => { + await reconfigureServer({ + requestKeywordDenylist: [{ key: 'a[K]ey', value: 'aValue[123]*' }], + }); + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const params = { + headers: headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/RCE', + body: JSON.stringify({ + obj: [ + { + aKey: 'aValue321', + code: 'delete Object.prototype.evalFunctions', + }, + ], + }), + }; + const response = await request(params).catch(e => e); + expect(response.status).toBe(400); + const text = JSON.parse(response.text); + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toBe( + 'Prohibited keyword in request data: {"key":"a[K]ey","value":"aValue[123]*"}.' + ); + }); + + it('denies write request with custom denylist of key', async () => { + await reconfigureServer({ + requestKeywordDenylist: [{ key: 'a[K]ey' }], + }); + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const params = { + headers: headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/RCE', + body: JSON.stringify({ + obj: { + aKey: 'aValue321', + code: 'delete Object.prototype.evalFunctions', + }, + }), + }; + const response = await request(params).catch(e => e); + expect(response.status).toBe(400); + const text = JSON.parse(response.text); + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toBe('Prohibited keyword in request data: {"key":"a[K]ey"}.'); + }); + + it('denies write request with custom denylist of value', async () => { + await reconfigureServer({ + requestKeywordDenylist: [{ value: 'aValue[123]*' }], + }); + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const params = { + headers: headers, + method: 'POST', + url: 'http://localhost:8378/1/classes/RCE', + body: JSON.stringify({ + obj: { + aKey: 'aValue321', + code: 'delete Object.prototype.evalFunctions', + }, + }), + }; + const response = await request(params).catch(e => e); + expect(response.status).toBe(400); + const text = JSON.parse(response.text); + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toBe('Prohibited keyword in request data: {"value":"aValue[123]*"}.'); + }); + }); +}); diff --git a/src/Adapters/Auth/gcenter.js b/src/Adapters/Auth/gcenter.js index 090b9fab02..322c2430d1 100644 --- a/src/Adapters/Auth/gcenter.js +++ b/src/Adapters/Auth/gcenter.js @@ -14,20 +14,23 @@ const authData = { const { Parse } = require('parse/node'); const crypto = require('crypto'); const https = require('https'); -const url = require('url'); const cache = {}; // (publicKey -> cert) cache function verifyPublicKeyUrl(publicKeyUrl) { - const parsedUrl = url.parse(publicKeyUrl); - if (parsedUrl.protocol !== 'https:') { + try { + const parsedUrl = new URL(publicKeyUrl); + if (parsedUrl.protocol !== 'https:') { + return false; + } + const hostnameParts = parsedUrl.hostname.split('.'); + const length = hostnameParts.length; + const domainParts = hostnameParts.slice(length - 2, length); + const domain = domainParts.join('.'); + return domain === 'apple.com'; + } catch(error) { return false; } - const hostnameParts = parsedUrl.hostname.split('.'); - const length = hostnameParts.length; - const domainParts = hostnameParts.slice(length - 2, length); - const domain = domainParts.join('.'); - return domain === 'apple.com'; } function convertX509CertToPEM(X509Cert) { diff --git a/src/Adapters/Auth/oauth2.js b/src/Adapters/Auth/oauth2.js index cefe7bdff2..ba1fe7bc4f 100644 --- a/src/Adapters/Auth/oauth2.js +++ b/src/Adapters/Auth/oauth2.js @@ -54,7 +54,6 @@ */ const Parse = require('parse/node').Parse; -const url = require('url'); const querystring = require('querystring'); const httpsRequest = require('./httpsRequest'); @@ -112,7 +111,7 @@ function requestTokenInfo(options, access_token) { if (!options || !options.tokenIntrospectionEndpointUrl) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, MISSING_URL); } - const parsedUrl = url.parse(options.tokenIntrospectionEndpointUrl); + const parsedUrl = new URL(options.tokenIntrospectionEndpointUrl); const postData = querystring.stringify({ token: access_token, }); diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 84876fad6f..06896e73b5 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -1,6 +1,6 @@ /** GridFSBucketAdapter - Stores files in Mongo using GridStore + Stores files in Mongo using GridFS Requires the database adapter to be based on mongoclient @flow weak diff --git a/src/Adapters/Files/GridStoreAdapter.js b/src/Adapters/Files/GridStoreAdapter.js index ca3166af98..39d1ca41c6 100644 --- a/src/Adapters/Files/GridStoreAdapter.js +++ b/src/Adapters/Files/GridStoreAdapter.js @@ -1,181 +1,4 @@ -/** - GridStoreAdapter - Stores files in Mongo using GridStore - Requires the database adapter to be based on mongoclient - (GridStore is deprecated, Please use GridFSBucket instead) - - @flow weak - */ - -// @flow-disable-next -import { MongoClient, GridStore, Db } from 'mongodb'; -import { FilesAdapter, validateFilename } from './FilesAdapter'; -import defaults from '../../defaults'; - -export class GridStoreAdapter extends FilesAdapter { - _databaseURI: string; - _connectionPromise: Promise; - _mongoOptions: Object; - - constructor(mongoDatabaseURI = defaults.DefaultMongoURI, mongoOptions = {}) { - super(); - this._databaseURI = mongoDatabaseURI; - - const defaultMongoOptions = { - useNewUrlParser: true, - useUnifiedTopology: true, - }; - this._mongoOptions = Object.assign(defaultMongoOptions, mongoOptions); - } - - _connect() { - if (!this._connectionPromise) { - this._connectionPromise = MongoClient.connect(this._databaseURI, this._mongoOptions).then( - client => { - this._client = client; - return client.db(client.s.options.dbName); - } - ); - } - return this._connectionPromise; - } - - // For a given config object, filename, and data, store a file - // Returns a promise - createFile(filename: string, data) { - return this._connect() - .then(database => { - const gridStore = new GridStore(database, filename, 'w'); - return gridStore.open(); - }) - .then(gridStore => { - return gridStore.write(data); - }) - .then(gridStore => { - return gridStore.close(); - }); - } - - deleteFile(filename: string) { - return this._connect() - .then(database => { - const gridStore = new GridStore(database, filename, 'r'); - return gridStore.open(); - }) - .then(gridStore => { - return gridStore.unlink(); - }) - .then(gridStore => { - return gridStore.close(); - }); - } - - getFileData(filename: string) { - return this._connect() - .then(database => { - return GridStore.exist(database, filename).then(() => { - const gridStore = new GridStore(database, filename, 'r'); - return gridStore.open(); - }); - }) - .then(gridStore => { - return gridStore.read(); - }); - } - - getFileLocation(config, filename) { - return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename); - } - - async handleFileStream(filename: string, req, res, contentType) { - const stream = await this._connect().then(database => { - return GridStore.exist(database, filename).then(() => { - const gridStore = new GridStore(database, filename, 'r'); - return gridStore.open(); - }); - }); - handleRangeRequest(stream, req, res, contentType); - } - - handleShutdown() { - if (!this._client) { - return Promise.resolve(); - } - return this._client.close(false); - } - - validateFilename(filename) { - return validateFilename(filename); - } -} - -// handleRangeRequest is licensed under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/). -// Author: LEROIB at weightingformypizza (https://weightingformypizza.wordpress.com/2015/06/24/stream-html5-media-content-like-video-audio-from-mongodb-using-express-and-gridstore/). -function handleRangeRequest(stream, req, res, contentType) { - const buffer_size = 1024 * 1024; //1024Kb - // Range request, partial stream the file - const parts = req - .get('Range') - .replace(/bytes=/, '') - .split('-'); - let [start, end] = parts; - const notEnded = !end && end !== 0; - const notStarted = !start && start !== 0; - // No end provided, we want all bytes - if (notEnded) { - end = stream.length - 1; - } - // No start provided, we're reading backwards - if (notStarted) { - start = stream.length - end; - end = start + end - 1; - } - - // Data exceeds the buffer_size, cap - if (end - start >= buffer_size) { - end = start + buffer_size - 1; - } - - const contentLength = end - start + 1; - - res.writeHead(206, { - 'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length, - 'Accept-Ranges': 'bytes', - 'Content-Length': contentLength, - 'Content-Type': contentType, - }); - - stream.seek(start, function () { - // Get gridFile stream - const gridFileStream = stream.stream(true); - let bufferAvail = 0; - let remainingBytesToWrite = contentLength; - let totalBytesWritten = 0; - // Write to response - gridFileStream.on('data', function (data) { - bufferAvail += data.length; - if (bufferAvail > 0) { - // slice returns the same buffer if overflowing - // safe to call in any case - const buffer = data.slice(0, remainingBytesToWrite); - // Write the buffer - res.write(buffer); - // Increment total - totalBytesWritten += buffer.length; - // Decrement remaining - remainingBytesToWrite -= data.length; - // Decrement the available buffer - bufferAvail -= buffer.length; - } - // In case of small slices, all values will be good at that point - // we've written enough, end... - if (totalBytesWritten >= contentLength) { - stream.close(); - res.end(); - this.destroy(); - } - }); - }); -} - -export default GridStoreAdapter; +// Note: GridStore was replaced by GridFSBucketAdapter by default in 2018 by @flovilmart +throw new Error( + 'GridStoreAdapter: GridStore is no longer supported by parse server and mongodb, use GridFSBucketAdapter instead.' +); diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index 78284503fc..20e3eec325 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -177,7 +177,7 @@ class MongoSchemaCollection { insertSchema(schema: any) { return this._collection .insertOne(schema) - .then(result => mongoSchemaToParseSchema(result.ops[0])) + .then(() => mongoSchemaToParseSchema(schema)) .catch(error => { if (error.code === 11000) { //Mongo's duplicate key error diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index c1fe63ba9b..93e23b91c3 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -479,6 +479,7 @@ export class MongoStorageAdapter implements StorageAdapter { const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema); return this._adaptiveCollection(className) .then(collection => collection.insertOne(mongoObject, transactionalSession)) + .then(() => ({ ops: [mongoObject] })) .catch(error => { if (error.code === 11000) { // Duplicate value @@ -517,8 +518,8 @@ export class MongoStorageAdapter implements StorageAdapter { }) .catch(err => this.handleError(err)) .then( - ({ result }) => { - if (result.n === 0) { + ({ deletedCount }) => { + if (deletedCount === 0) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); } return Promise.resolve(); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 5578077778..91ad23fa4a 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -2,6 +2,7 @@ import log from '../../../logger'; import _ from 'lodash'; var mongodb = require('mongodb'); var Parse = require('parse/node').Parse; +const Utils = require('../../../Utils'); const transformKey = (className, fieldName, schema) => { // Check if the schema is known since it's a built-in field. @@ -634,133 +635,6 @@ function transformTopLevelAtom(atom, field) { } } -function relativeTimeToDate(text, now = new Date()) { - text = text.toLowerCase(); - - let parts = text.split(' '); - - // Filter out whitespace - parts = parts.filter(part => part !== ''); - - const future = parts[0] === 'in'; - const past = parts[parts.length - 1] === 'ago'; - - if (!future && !past && text !== 'now') { - return { - status: 'error', - info: "Time should either start with 'in' or end with 'ago'", - }; - } - - if (future && past) { - return { - status: 'error', - info: "Time cannot have both 'in' and 'ago'", - }; - } - - // strip the 'ago' or 'in' - if (future) { - parts = parts.slice(1); - } else { - // past - parts = parts.slice(0, parts.length - 1); - } - - if (parts.length % 2 !== 0 && text !== 'now') { - return { - status: 'error', - info: 'Invalid time string. Dangling unit or number.', - }; - } - - const pairs = []; - while (parts.length) { - pairs.push([parts.shift(), parts.shift()]); - } - - let seconds = 0; - for (const [num, interval] of pairs) { - const val = Number(num); - if (!Number.isInteger(val)) { - return { - status: 'error', - info: `'${num}' is not an integer.`, - }; - } - - switch (interval) { - case 'yr': - case 'yrs': - case 'year': - case 'years': - seconds += val * 31536000; // 365 * 24 * 60 * 60 - break; - - case 'wk': - case 'wks': - case 'week': - case 'weeks': - seconds += val * 604800; // 7 * 24 * 60 * 60 - break; - - case 'd': - case 'day': - case 'days': - seconds += val * 86400; // 24 * 60 * 60 - break; - - case 'hr': - case 'hrs': - case 'hour': - case 'hours': - seconds += val * 3600; // 60 * 60 - break; - - case 'min': - case 'mins': - case 'minute': - case 'minutes': - seconds += val * 60; - break; - - case 'sec': - case 'secs': - case 'second': - case 'seconds': - seconds += val; - break; - - default: - return { - status: 'error', - info: `Invalid interval: '${interval}'`, - }; - } - } - - const milliseconds = seconds * 1000; - if (future) { - return { - status: 'success', - info: 'future', - result: new Date(now.valueOf() + milliseconds), - }; - } else if (past) { - return { - status: 'success', - info: 'past', - result: new Date(now.valueOf() - milliseconds), - }; - } else { - return { - status: 'success', - info: 'present', - result: new Date(now.valueOf()), - }; - } -} - // Transforms a query constraint from REST API format to Mongo format. // A constraint is something with fields like $lt. // If it is not a valid constraint but it could be a valid something @@ -813,7 +687,7 @@ function transformConstraint(constraint, field, count = false) { ); } - const parserResult = relativeTimeToDate(val.$relativeTime); + const parserResult = Utils.relativeTimeToDate(val.$relativeTime); if (parserResult.status === 'success') { answer[key] = parserResult.result; break; @@ -1556,7 +1430,6 @@ module.exports = { transformUpdate, transformWhere, mongoObjectToParseObject, - relativeTimeToDate, transformConstraint, transformPointerString, }; diff --git a/src/Adapters/Storage/Postgres/PostgresConfigParser.js b/src/Adapters/Storage/Postgres/PostgresConfigParser.js index 170e76282a..d86778cf20 100644 --- a/src/Adapters/Storage/Postgres/PostgresConfigParser.js +++ b/src/Adapters/Storage/Postgres/PostgresConfigParser.js @@ -1,18 +1,16 @@ -const url = require('url'); const fs = require('fs'); function getDatabaseOptionsFromURI(uri) { const databaseOptions = {}; - const parsedURI = url.parse(uri); - const queryParams = parseQueryParams(parsedURI.query); - const authParts = parsedURI.auth ? parsedURI.auth.split(':') : []; + const parsedURI = new URL(uri); + const queryParams = parseQueryParams(parsedURI.searchParams.toString()); databaseOptions.host = parsedURI.hostname || 'localhost'; databaseOptions.port = parsedURI.port ? parseInt(parsedURI.port) : 5432; databaseOptions.database = parsedURI.pathname ? parsedURI.pathname.substr(1) : undefined; - databaseOptions.user = authParts.length > 0 ? authParts[0] : ''; - databaseOptions.password = authParts.length > 1 ? authParts[1] : ''; + databaseOptions.user = parsedURI.username; + databaseOptions.password = parsedURI.password; if (queryParams.ssl && queryParams.ssl.toLowerCase() === 'true') { databaseOptions.ssl = true; diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index e6c2bdbb1b..7477270a3d 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -7,12 +7,14 @@ import _ from 'lodash'; // @flow-disable-next import { v4 as uuidv4 } from 'uuid'; import sql from './sql'; +import { StorageAdapter } from '../StorageAdapter'; +import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter'; +const Utils = require('../../../Utils'); const PostgresRelationDoesNotExistError = '42P01'; const PostgresDuplicateRelationError = '42P07'; const PostgresDuplicateColumnError = '42701'; const PostgresMissingColumnError = '42703'; -const PostgresDuplicateObjectError = '42710'; const PostgresUniqueIndexViolationError = '23505'; const logger = require('../../../logger'); @@ -22,9 +24,6 @@ const debug = function (...args: any) { log.debug.apply(log, args); }; -import { StorageAdapter } from '../StorageAdapter'; -import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter'; - const parseTypeToPostgresType = type => { switch (type.type) { case 'String': @@ -374,6 +373,11 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus patterns.push( `(${constraintFieldName} <> $${index} OR ${constraintFieldName} IS NULL)` ); + } else if (typeof fieldValue.$ne === 'object' && fieldValue.$ne.$relativeTime) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + '$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators' + ); } else { patterns.push(`($${index}:name <> $${index + 1} OR $${index}:name IS NULL)`); } @@ -399,6 +403,11 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus if (fieldName.indexOf('.') >= 0) { values.push(fieldValue.$eq); patterns.push(`${transformDotField(fieldName)} = $${index++}`); + } else if (typeof fieldValue.$eq === 'object' && fieldValue.$eq.$relativeTime) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + '$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators' + ); } else { values.push(fieldName, fieldValue.$eq); patterns.push(`$${index}:name = $${index + 1}`); @@ -513,7 +522,12 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus } if (typeof fieldValue.$exists !== 'undefined') { - if (fieldValue.$exists) { + if (typeof fieldValue.$exists === 'object' && fieldValue.$exists.$relativeTime) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + '$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators' + ); + } else if (fieldValue.$exists) { patterns.push(`$${index}:name IS NOT NULL`); } else { patterns.push(`$${index}:name IS NULL`); @@ -757,7 +771,7 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus Object.keys(ParseToPosgresComparator).forEach(cmp => { if (fieldValue[cmp] || fieldValue[cmp] === 0) { const pgComparator = ParseToPosgresComparator[cmp]; - const postgresValue = toPostgresValue(fieldValue[cmp]); + let postgresValue = toPostgresValue(fieldValue[cmp]); let constraintFieldName; if (fieldName.indexOf('.') >= 0) { let castType; @@ -775,6 +789,24 @@ const buildWhereClause = ({ schema, query, index, caseInsensitive }): WhereClaus ? `CAST ((${transformDotField(fieldName)}) AS ${castType})` : transformDotField(fieldName); } else { + if (typeof postgresValue === 'object' && postgresValue.$relativeTime) { + if (schema.fields[fieldName].type !== 'Date') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + '$relativeTime can only be used with Date field' + ); + } + const parserResult = Utils.relativeTimeToDate(postgresValue.$relativeTime); + if (parserResult.status === 'success') { + postgresValue = toPostgresValue(parserResult.result); + } else { + console.error('Error while parsing relative date', parserResult); + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $relativeTime (${postgresValue.$relativeTime}) value. ${parserResult.info}` + ); + } + } constraintFieldName = `$${index++}:name`; values.push(fieldName); } @@ -873,15 +905,7 @@ export class PostgresStorageAdapter implements StorageAdapter { 'CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )' ) .catch(error => { - if ( - error.code === PostgresDuplicateRelationError || - error.code === PostgresUniqueIndexViolationError || - error.code === PostgresDuplicateObjectError - ) { - // Table already exists, must have been created by a different request. Ignore error. - } else { - throw error; - } + throw error; }); } @@ -1807,7 +1831,13 @@ export class PostgresStorageAdapter implements StorageAdapter { if (key === 'ACL') { memo.push('_rperm'); memo.push('_wperm'); - } else if (key.length > 0) { + } else if ( + key.length > 0 && + // Remove selected field not referenced in the schema + // Relation is not a column in postgres + // $score is a Parse special field and is also not a column + ((schema.fields[key] && schema.fields[key].type !== 'Relation') || key === '$score') + ) { memo.push(key); } return memo; @@ -2410,25 +2440,55 @@ export class PostgresStorageAdapter implements StorageAdapter { ? fieldNames.map((fieldName, index) => `lower($${index + 3}:name) varchar_pattern_ops`) : fieldNames.map((fieldName, index) => `$${index + 3}:name`); const qs = `CREATE INDEX IF NOT EXISTS $1:name ON $2:name (${constraintPatterns.join()})`; - await conn.none(qs, [indexNameOptions.name, className, ...fieldNames]).catch(error => { - if ( - error.code === PostgresDuplicateRelationError && - error.message.includes(indexNameOptions.name) - ) { - // Index already exists. Ignore error. - } else if ( - error.code === PostgresUniqueIndexViolationError && - error.message.includes(indexNameOptions.name) - ) { - // Cast the error into the proper parse error - throw new Parse.Error( - Parse.Error.DUPLICATE_VALUE, - 'A duplicate value for a field with unique values was provided' - ); - } else { + const setIdempotencyFunction = options.setIdempotencyFunction !== undefined ? options.setIdempotencyFunction : false; + if (setIdempotencyFunction) { + await this.ensureIdempotencyFunctionExists(options); + } + await conn.none(qs, [indexNameOptions.name, className, ...fieldNames]) + .catch(error => { + if ( + error.code === PostgresDuplicateRelationError && + error.message.includes(indexNameOptions.name) + ) { + // Index already exists. Ignore error. + } else if ( + error.code === PostgresUniqueIndexViolationError && + error.message.includes(indexNameOptions.name) + ) { + // Cast the error into the proper parse error + throw new Parse.Error( + Parse.Error.DUPLICATE_VALUE, + 'A duplicate value for a field with unique values was provided' + ); + } else { + throw error; + } + }); + } + + async deleteIdempotencyFunction( + options?: Object = {} + ): Promise { + const conn = options.conn !== undefined ? options.conn : this._client; + const qs = 'DROP FUNCTION IF EXISTS idempotency_delete_expired_records()'; + return conn + .none(qs) + .catch(error => { throw error; - } - }); + }); + } + + async ensureIdempotencyFunctionExists( + options?: Object = {} + ): Promise { + const conn = options.conn !== undefined ? options.conn : this._client; + const ttlOptions = options.ttl !== undefined ? `${options.ttl} seconds` : '60 seconds'; + const qs = 'CREATE OR REPLACE FUNCTION idempotency_delete_expired_records() RETURNS void LANGUAGE plpgsql AS $$ BEGIN DELETE FROM "_Idempotency" WHERE expire < NOW() - INTERVAL $1; END; $$;'; + return conn + .none(qs, [ttlOptions]) + .catch(error => { + throw error; + }); } } diff --git a/src/Config.js b/src/Config.js index 069b7e2a43..04834d3291 100644 --- a/src/Config.js +++ b/src/Config.js @@ -35,7 +35,7 @@ export class Config { config.applicationId = applicationId; Object.keys(cacheInfo).forEach(key => { if (key == 'databaseController') { - config.database = new DatabaseController(cacheInfo.databaseController.adapter); + config.database = new DatabaseController(cacheInfo.databaseController.adapter, config); } else { config[key] = cacheInfo[key]; } @@ -78,6 +78,7 @@ export class Config { security, enforcePrivateUsers, schema, + requestKeywordDenylist, }) { if (masterKey === readOnlyMasterKey) { throw new Error('masterKey and readOnlyMasterKey should be different'); @@ -116,6 +117,15 @@ export class Config { this.validateSecurityOptions(security); this.validateSchemaOptions(schema); this.validateEnforcePrivateUsers(enforcePrivateUsers); + this.validateRequestKeywordDenylist(requestKeywordDenylist); + } + + static validateRequestKeywordDenylist(requestKeywordDenylist) { + if (requestKeywordDenylist === undefined) { + requestKeywordDenylist = requestKeywordDenylist.default; + } else if (!Array.isArray(requestKeywordDenylist)) { + throw 'Parse Server option requestKeywordDenylist must be an array.'; + } } static validateEnforcePrivateUsers(enforcePrivateUsers) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 20d5c4ba66..f7322fc531 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -14,8 +14,10 @@ import logger from '../logger'; import * as SchemaController from './SchemaController'; import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter'; +import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter'; import SchemaCache from '../Adapters/Cache/SchemaCache'; import type { LoadSchemaOptions } from './types'; +import type { ParseServerOptions } from '../Options'; import type { QueryOptions, FullQueryOptions } from '../Adapters/Storage/StorageAdapter'; function addWriteACL(query, acl) { @@ -257,41 +259,6 @@ const isSpecialUpdateKey = key => { return specialKeysForUpdate.indexOf(key) >= 0; }; -function expandResultOnKeyPath(object, key, value) { - if (key.indexOf('.') < 0) { - object[key] = value[key]; - return object; - } - const path = key.split('.'); - const firstKey = path[0]; - const nextPath = path.slice(1).join('.'); - object[firstKey] = expandResultOnKeyPath(object[firstKey] || {}, nextPath, value[firstKey]); - delete object[key]; - return object; -} - -function sanitizeDatabaseResult(originalObject, result): Promise { - const response = {}; - if (!result) { - return Promise.resolve(response); - } - Object.keys(originalObject).forEach(key => { - const keyUpdate = originalObject[key]; - // determine if that was an op - if ( - keyUpdate && - typeof keyUpdate === 'object' && - keyUpdate.__op && - ['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1 - ) { - // only valid ops that produce an actionable result - // the op may have happend on a keypath - expandResultOnKeyPath(response, key, result); - } - }); - return Promise.resolve(response); -} - function joinTableName(className, key) { return `_Join:${key}:${className}`; } @@ -397,12 +364,15 @@ class DatabaseController { schemaCache: any; schemaPromise: ?Promise; _transactionalSession: ?any; + options: ParseServerOptions; + idempotencyOptions: any; - constructor(adapter: StorageAdapter) { + constructor(adapter: StorageAdapter, options: ParseServerOptions) { this.adapter = adapter; - // We don't want a mutable this.schema, because then you could have - // one request that uses different schemas for different parts of - // it. Instead, use loadSchema to get a schema. + this.options = options || {}; + this.idempotencyOptions = this.options.idempotencyOptions || {}; + // Prevent mutable this.schema, otherwise one request could use + // multiple schemas, so instead use loadSchema to get a schema. this.schemaPromise = null; this._transactionalSession = null; } @@ -643,7 +613,7 @@ class DatabaseController { if (skipSanitization) { return Promise.resolve(result); } - return sanitizeDatabaseResult(originalUpdate, result); + return this._sanitizeDatabaseResult(originalUpdate, result); }); }); } @@ -870,7 +840,7 @@ class DatabaseController { object, relationUpdates ).then(() => { - return sanitizeDatabaseResult(originalObject, result.ops[0]); + return this._sanitizeDatabaseResult(originalObject, result.ops[0]); }); }); }); @@ -894,7 +864,7 @@ class DatabaseController { if (object[field] && object[field].__op && object[field].__op === 'Delete') { return false; } - return schemaFields.indexOf(field) < 0; + return schemaFields.indexOf(getRootFieldName(field)) < 0; }); if (newKeys.length > 0) { // adds a marker that new field is being adding during update @@ -972,9 +942,9 @@ class DatabaseController { }); } if (query['$and']) { - const ors = query['$and']; + const ands = query['$and']; return Promise.all( - ors.map((aQuery, index) => { + ands.map((aQuery, index) => { return this.reduceInRelation(className, aQuery, schema).then(aQuery => { query['$and'][index] = aQuery; }); @@ -1713,9 +1683,7 @@ class DatabaseController { }; await this.loadSchema().then(schema => schema.enforceClassExists('_User')); await this.loadSchema().then(schema => schema.enforceClassExists('_Role')); - if (this.adapter instanceof MongoStorageAdapter) { - await this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency')); - } + await this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency')); await this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']).catch(error => { logger.warn('Unable to ensure uniqueness for usernames: ', error); @@ -1751,18 +1719,28 @@ class DatabaseController { logger.warn('Unable to ensure uniqueness for role name: ', error); throw error; }); - if (this.adapter instanceof MongoStorageAdapter) { - await this.adapter - .ensureUniqueness('_Idempotency', requiredIdempotencyFields, ['reqId']) - .catch(error => { - logger.warn('Unable to ensure uniqueness for idempotency request ID: ', error); - throw error; - }); - await this.adapter - .ensureIndex('_Idempotency', requiredIdempotencyFields, ['expire'], 'ttl', false, { + await this.adapter + .ensureUniqueness('_Idempotency', requiredIdempotencyFields, ['reqId']) + .catch(error => { + logger.warn('Unable to ensure uniqueness for idempotency request ID: ', error); + throw error; + }); + + const isMongoAdapter = this.adapter instanceof MongoStorageAdapter; + const isPostgresAdapter = this.adapter instanceof PostgresStorageAdapter; + if (isMongoAdapter || isPostgresAdapter) { + let options = {}; + if (isMongoAdapter) { + options = { ttl: 0, - }) + }; + } else if (isPostgresAdapter) { + options = this.idempotencyOptions; + options.setIdempotencyFunction = true; + } + await this.adapter + .ensureIndex('_Idempotency', requiredIdempotencyFields, ['expire'], 'ttl', false, options) .catch(error => { logger.warn('Unable to create TTL index for idempotency expire date: ', error); throw error; @@ -1771,6 +1749,60 @@ class DatabaseController { await this.adapter.updateSchemaWithIndexes(); } + _expandResultOnKeyPath(object: any, key: string, value: any): any { + if (key.indexOf('.') < 0) { + object[key] = value[key]; + return object; + } + const path = key.split('.'); + const firstKey = path[0]; + const nextPath = path.slice(1).join('.'); + + // Scan request data for denied keywords + if (this.options && this.options.requestKeywordDenylist) { + // Scan request data for denied keywords + for (const keyword of this.options.requestKeywordDenylist) { + const isMatch = (a, b) => (typeof a === 'string' && new RegExp(a).test(b)) || a === b; + if (isMatch(firstKey, keyword.key)) { + throw new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + `Prohibited keyword in request data: ${JSON.stringify(keyword)}.` + ); + } + } + } + + object[firstKey] = this._expandResultOnKeyPath( + object[firstKey] || {}, + nextPath, + value[firstKey] + ); + delete object[key]; + return object; + } + + _sanitizeDatabaseResult(originalObject: any, result: any): Promise { + const response = {}; + if (!result) { + return Promise.resolve(response); + } + Object.keys(originalObject).forEach(key => { + const keyUpdate = originalObject[key]; + // determine if that was an op + if ( + keyUpdate && + typeof keyUpdate === 'object' && + keyUpdate.__op && + ['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1 + ) { + // only valid ops that produce an actionable result + // the op may have happened on a keypath + this._expandResultOnKeyPath(response, key, result); + } + }); + return Promise.resolve(response); + } + static _validateQuery: any => void; } diff --git a/src/Controllers/LoggerController.js b/src/Controllers/LoggerController.js index 04d3a6d784..8ee492cf4b 100644 --- a/src/Controllers/LoggerController.js +++ b/src/Controllers/LoggerController.js @@ -1,7 +1,6 @@ import { Parse } from 'parse/node'; import AdaptableController from './AdaptableController'; import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter'; -import url from 'url'; const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; const LOG_STRING_TRUNCATE_LENGTH = 1000; @@ -38,15 +37,16 @@ export class LoggerController extends AdaptableController { }); } - maskSensitiveUrl(urlString) { - const urlObj = url.parse(urlString, true); - const query = urlObj.query; + maskSensitiveUrl(path) { + const urlString = 'http://localhost' + path; // prepend dummy string to make a real URL + const urlObj = new URL(urlString); + const query = urlObj.searchParams; let sanitizedQuery = '?'; - for (const key in query) { + for (const [key, value] of query) { if (key !== 'password') { // normal value - sanitizedQuery += key + '=' + query[key] + '&'; + sanitizedQuery += key + '=' + value + '&'; } else { // password value, redact it sanitizedQuery += key + '=' + '********' + '&'; diff --git a/src/Controllers/index.js b/src/Controllers/index.js index 89dc79c232..b2feff0fc2 100644 --- a/src/Controllers/index.js +++ b/src/Controllers/index.js @@ -2,7 +2,6 @@ import authDataManager from '../Adapters/Auth'; import { ParseServerOptions } from '../Options'; import { loadAdapter } from '../Adapters/AdapterLoader'; import defaults from '../defaults'; -import url from 'url'; // Controllers import { LoggerController } from './LoggerController'; import { FilesController } from './FilesController'; @@ -157,7 +156,7 @@ export function getDatabaseController(options: ParseServerOptions): DatabaseCont } else { databaseAdapter = loadAdapter(databaseAdapter); } - return new DatabaseController(databaseAdapter); + return new DatabaseController(databaseAdapter, options); } export function getHooksController( @@ -220,13 +219,14 @@ export function getAuthDataManager(options: ParseServerOptions) { export function getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions) { let protocol; try { - const parsedURI = url.parse(databaseURI); + const parsedURI = new URL(databaseURI); protocol = parsedURI.protocol ? parsedURI.protocol.toLowerCase() : null; } catch (e) { /* */ } switch (protocol) { case 'postgres:': + case 'postgresql:': return new PostgresStorageAdapter({ uri: databaseURI, collectionPrefix, diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index e2cbd17920..1edd704bd6 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -350,6 +350,24 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY', help: 'Read-only key, which has the same capabilities as MasterKey without writes', }, + requestKeywordDenylist: { + env: 'PARSE_SERVER_REQUEST_KEYWORD_DENYLIST', + help: + 'An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.', + action: parsers.arrayParser, + default: [ + { + key: '_bsontype', + value: 'Code', + }, + { + key: 'constructor', + }, + { + key: '__proto__', + }, + ], + }, restAPIKey: { env: 'PARSE_SERVER_REST_API_KEY', help: 'Key for REST calls', diff --git a/src/Options/docs.js b/src/Options/docs.js index c6b2e1abf1..fc0ff3b799 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -64,6 +64,7 @@ * @property {String} publicServerURL Public URL to your parse server with http:// or https://. * @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications * @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes + * @property {RequestKeywordDenylist[]} requestKeywordDenylist An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns. * @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. diff --git a/src/Options/index.js b/src/Options/index.js index 31b9e10d41..3482d88c50 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -14,6 +14,10 @@ type Adapter = string | any | T; type NumberOrBoolean = number | boolean; type NumberOrString = number | string; type ProtectedFields = any; +type RequestKeywordDenylist = { + key: string | any, + value: any, +}; export interface ParseServerOptions { /* Your Parse Application ID @@ -252,6 +256,9 @@ export interface ParseServerOptions { /* Set to true if new users should be created without public read and write access. :DEFAULT: false */ enforcePrivateUsers: ?boolean; + /* An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns. + :DEFAULT: [{"key":"_bsontype","value":"Code"},{"key":"constructor"},{"key":"__proto__"}] */ + requestKeywordDenylist: ?(RequestKeywordDenylist[]); } export interface SecurityOptions { diff --git a/src/ParseServer.js b/src/ParseServer.js index dc2af9e7be..e6b30d1918 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -108,11 +108,7 @@ class ParseServer { if (typeof cloud === 'function') { cloud(Parse); } else if (typeof cloud === 'string') { - if (process.env.npm_package_type === 'module') { - import(path.resolve(process.cwd(), cloud)); - } else { - require(path.resolve(process.cwd(), cloud)); - } + require(path.resolve(process.cwd(), cloud)); } else { throw "argument 'cloud' must either be a string or a function"; } diff --git a/src/ParseServerRESTController.js b/src/ParseServerRESTController.js index 9e765ff3e3..12ee0a67e5 100644 --- a/src/ParseServerRESTController.js +++ b/src/ParseServerRESTController.js @@ -1,7 +1,6 @@ const Config = require('./Config'); const Auth = require('./Auth'); const RESTController = require('parse/lib/node/RESTController'); -const URL = require('url'); const Parse = require('parse/node'); function getSessionToken(options) { @@ -38,9 +37,9 @@ function ParseServerRESTController(applicationId, router) { if (!config) { config = Config.get(applicationId); } - const serverURL = URL.parse(config.serverURL); - if (path.indexOf(serverURL.path) === 0) { - path = path.slice(serverURL.path.length, path.length); + const serverURL = new URL(config.serverURL); + if (path.indexOf(serverURL.pathname) === 0) { + path = path.slice(serverURL.pathname.length, path.length); } if (path[0] !== '/') { diff --git a/src/RestWrite.js b/src/RestWrite.js index a651cf9c6c..8b728731da 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -6,6 +6,7 @@ var SchemaController = require('./Controllers/SchemaController'); var deepcopy = require('deepcopy'); const Auth = require('./Auth'); +const Utils = require('./Utils'); var cryptoUtils = require('./cryptoUtils'); var passwordCrypto = require('./password'); var Parse = require('parse/node'); @@ -61,6 +62,19 @@ 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)}.` + ); + } + } + } + // When the operation is complete, this.response may have several // fields. // response: the actual data to be returned diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 6788d93e5f..7f3e0a84f3 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -83,7 +83,8 @@ export class ClassesRouter extends PromiseRouter { this.className(req), req.params.objectId, options, - req.info.clientSDK + req.info.clientSDK, + req.info.context ) .then(response => { if (!response.results || response.results.length == 0) { diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index 54f73ceacc..1b72b93a68 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -16,7 +16,7 @@ function classNameMismatchResponse(bodyClass, pathClass) { function getAllSchemas(req) { return req.config.database .loadSchema({ clearCache: true }) - .then(schemaController => schemaController.getAllClasses(true)) + .then(schemaController => schemaController.getAllClasses({ clearCache: true })) .then(schemas => ({ response: { results: schemas } })); } diff --git a/src/Utils.js b/src/Utils.js index e78d7ddd6c..399939a152 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -200,6 +200,164 @@ class Utils { } } } + + /** + * Computes the relative date based on a string. + * @param {String} text The string to interpret the date from. + * @param {Date} now The date the string is comparing against. + * @returns {Object} The relative date object. + **/ + static relativeTimeToDate(text, now = new Date()) { + text = text.toLowerCase(); + let parts = text.split(' '); + + // Filter out whitespace + parts = parts.filter(part => part !== ''); + + const future = parts[0] === 'in'; + const past = parts[parts.length - 1] === 'ago'; + + if (!future && !past && text !== 'now') { + return { + status: 'error', + info: "Time should either start with 'in' or end with 'ago'", + }; + } + + if (future && past) { + return { + status: 'error', + info: "Time cannot have both 'in' and 'ago'", + }; + } + + // strip the 'ago' or 'in' + if (future) { + parts = parts.slice(1); + } else { + // past + parts = parts.slice(0, parts.length - 1); + } + + if (parts.length % 2 !== 0 && text !== 'now') { + return { + status: 'error', + info: 'Invalid time string. Dangling unit or number.', + }; + } + + const pairs = []; + while (parts.length) { + pairs.push([parts.shift(), parts.shift()]); + } + + let seconds = 0; + for (const [num, interval] of pairs) { + const val = Number(num); + if (!Number.isInteger(val)) { + return { + status: 'error', + info: `'${num}' is not an integer.`, + }; + } + + switch (interval) { + case 'yr': + case 'yrs': + case 'year': + case 'years': + seconds += val * 31536000; // 365 * 24 * 60 * 60 + break; + + case 'wk': + case 'wks': + case 'week': + case 'weeks': + seconds += val * 604800; // 7 * 24 * 60 * 60 + break; + + case 'd': + case 'day': + case 'days': + seconds += val * 86400; // 24 * 60 * 60 + break; + + case 'hr': + case 'hrs': + case 'hour': + case 'hours': + seconds += val * 3600; // 60 * 60 + break; + + case 'min': + case 'mins': + case 'minute': + case 'minutes': + seconds += val * 60; + break; + + case 'sec': + case 'secs': + case 'second': + case 'seconds': + seconds += val; + break; + + default: + return { + status: 'error', + info: `Invalid interval: '${interval}'`, + }; + } + } + + const milliseconds = seconds * 1000; + if (future) { + return { + status: 'success', + info: 'future', + result: new Date(now.valueOf() + milliseconds), + }; + } else if (past) { + return { + status: 'success', + info: 'past', + result: new Date(now.valueOf() - milliseconds), + }; + } else { + return { + status: 'success', + info: 'present', + result: new Date(now.valueOf()), + }; + } + } + + /** + * Deep-scans an object for a matching key/value definition. + * @param {Object} obj The object to scan. + * @param {String | undefined} key The key to match, or undefined if only the value should be matched. + * @param {any | undefined} value The value to match, or undefined if only the key should be matched. + * @returns {Boolean} True if a match was found, false otherwise. + */ + static objectContainsKeyValue(obj, key, value) { + const isMatch = (a, b) => (typeof a === 'string' && new RegExp(a).test(b)) || a === b; + const isKeyMatch = k => isMatch(key, k); + const isValueMatch = v => isMatch(value, v); + for (const [k, v] of Object.entries(obj)) { + if (key !== undefined && value === undefined && isKeyMatch(k)) { + return true; + } else if (key === undefined && value !== undefined && isValueMatch(v)) { + return true; + } else if (key !== undefined && value !== undefined && isKeyMatch(k) && isValueMatch(v)) { + return true; + } + if (['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(v))) { + return Utils.objectContainsKeyValue(v, key, value); + } + } + return false; + } } module.exports = Utils; diff --git a/src/batch.js b/src/batch.js index 58c23ccab6..0625ef0ecc 100644 --- a/src/batch.js +++ b/src/batch.js @@ -1,5 +1,4 @@ const Parse = require('parse/node').Parse; -const url = require('url'); const path = require('path'); // These methods handle batch requests. const batchPath = '/batch'; @@ -11,11 +10,12 @@ function mountOnto(router) { }); } -function parseURL(URL) { - if (typeof URL === 'string') { - return url.parse(URL); +function parseURL(urlString) { + try { + return new URL(urlString); + } catch(error) { + return undefined; } - return undefined; } function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) { @@ -33,9 +33,9 @@ function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) { return path.posix.join('/', requestPath.slice(apiPrefix.length)); }; - if (serverURL && publicServerURL && serverURL.path != publicServerURL.path) { - const localPath = serverURL.path; - const publicPath = publicServerURL.path; + if (serverURL && publicServerURL && serverURL.pathname != publicServerURL.pathname) { + const localPath = serverURL.pathname; + const publicPath = publicServerURL.pathname; // Override the api prefix apiPrefix = localPath; diff --git a/src/middlewares.js b/src/middlewares.js index 88de107264..e749d0507a 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -6,6 +6,7 @@ import ClientSDK from './ClientSDK'; import defaultLogger from './logger'; import rest from './rest'; import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter'; +import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageAdapter'; export const DEFAULT_ALLOWED_HEADERS = 'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, X-Parse-Request-Id, Content-Type, Pragma, Cache-Control'; @@ -431,7 +432,7 @@ export function promiseEnforceMasterKeyAccess(request) { */ export function promiseEnsureIdempotency(req) { // Enable feature only for MongoDB - if (!(req.config.database.adapter instanceof MongoStorageAdapter)) { + if (!((req.config.database.adapter instanceof MongoStorageAdapter) || (req.config.database.adapter instanceof PostgresStorageAdapter))) { return Promise.resolve(); } // Get parameters