mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
302a0dda73 | ||
|
|
e7a45cecb7 | ||
|
|
906fb27389 | ||
|
|
f3f81b692b | ||
|
|
52828683ba | ||
|
|
0add7c0c50 | ||
|
|
1bc74f4aa3 | ||
|
|
c2e3092385 | ||
|
|
79806c5eba | ||
|
|
48115349d4 | ||
|
|
87085b35b6 | ||
|
|
65a8a1fc56 | ||
|
|
f9ecebde3b | ||
|
|
e01b417d3f | ||
|
|
a54f4d43fa | ||
|
|
7a43faa49c | ||
|
|
abf6828a7f | ||
|
|
2436a85de6 | ||
|
|
1f9a4edacd | ||
|
|
6a2b972566 | ||
|
|
f3765e0b8e | ||
|
|
eeb3a7028d | ||
|
|
2533a8cdb3 | ||
|
|
ea94ae73f6 | ||
|
|
1ec70b299b | ||
|
|
45efe2f29c | ||
|
|
5e29336962 | ||
|
|
82e0a59139 | ||
|
|
51b519355a | ||
|
|
08e8a428b6 | ||
|
|
63bd333715 | ||
|
|
11071dd015 | ||
|
|
9b879dd371 | ||
|
|
eb1b84b556 | ||
|
|
80ec61f7ce | ||
|
|
9f1077c9d3 | ||
|
|
593a7a6264 | ||
|
|
4addf9dfa9 | ||
|
|
2547fd08ae | ||
|
|
581f919d1a | ||
|
|
0b1a61cd85 | ||
|
|
943b84b13f | ||
|
|
70f60267c5 | ||
|
|
f0f04d6351 | ||
|
|
1d8edd73c3 | ||
|
|
8d802b9461 | ||
|
|
271608bc82 | ||
|
|
5c5ef0313b | ||
|
|
29fec01a42 | ||
|
|
92b699920e | ||
|
|
9bfa0c60c4 | ||
|
|
0181fb51b3 | ||
|
|
e6006e800e | ||
|
|
a45d1848db | ||
|
|
6381e15e6d | ||
|
|
73195627c0 | ||
|
|
053bbefda9 | ||
|
|
032d5e55ca | ||
|
|
fe5947ce4c | ||
|
|
0e900cbefd | ||
|
|
41358d2226 | ||
|
|
73260897cd | ||
|
|
d3e6c0dea3 | ||
|
|
b4e27e1160 | ||
|
|
79b0c790f0 | ||
|
|
64f608ec9f | ||
|
|
67260b5fe0 | ||
|
|
411daf4d41 | ||
|
|
7a3c9cd33a | ||
|
|
193e5a4278 | ||
|
|
6ae0675010 | ||
|
|
a520ea0243 | ||
|
|
58bdeeee2d | ||
|
|
fc7f9f76f5 | ||
|
|
ef8d534679 | ||
|
|
1c9852e53f | ||
|
|
3f0de0c2ed | ||
|
|
eccdeccb59 | ||
|
|
f16f8423db | ||
|
|
bd60a5c1d1 | ||
|
|
40b9f953ca | ||
|
|
15eafd4dcc | ||
|
|
11cde12245 | ||
|
|
5861996cb0 | ||
|
|
ca70ad0141 | ||
|
|
5a22df13a5 | ||
|
|
5b7144176c | ||
|
|
4979503f63 | ||
|
|
e32a37c2ec | ||
|
|
f7f17cdb23 | ||
|
|
ecf422b2d9 | ||
|
|
49dc8df1de | ||
|
|
c3c5f06efd | ||
|
|
2a454fec35 | ||
|
|
01b4c4e0cb | ||
|
|
3714d68aaf | ||
|
|
77daa3ca18 | ||
|
|
0b574f04a9 | ||
|
|
a89a30182e | ||
|
|
96a9588a6e | ||
|
|
66cdfaed4b | ||
|
|
9262147a6f | ||
|
|
8b4e9ca637 | ||
|
|
2160d10990 | ||
|
|
e7f2995143 | ||
|
|
d5940b17b6 | ||
|
|
f864141663 | ||
|
|
711db9ccd2 | ||
|
|
6a6829ce80 | ||
|
|
d67be492f5 | ||
|
|
ea45f69af6 | ||
|
|
43c3d8e2a2 | ||
|
|
d8a1a614e8 | ||
|
|
553db0ada5 | ||
|
|
388a59d816 | ||
|
|
de4c1ee418 | ||
|
|
cb15c0b616 | ||
|
|
8dd864a033 | ||
|
|
c26db2248f | ||
|
|
b2a3479dbf | ||
|
|
deedf7b370 | ||
|
|
5f849ca662 | ||
|
|
0279b5e5f3 | ||
|
|
4cb6e7d209 | ||
|
|
56bb505df2 | ||
|
|
ceb5a1b5f0 |
+3
-2
@@ -1,2 +1,3 @@
|
||||
lib/*
|
||||
coverage/*
|
||||
lib
|
||||
coverage
|
||||
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@
|
||||
"eol-last": 2,
|
||||
"space-in-parens": ["error", "never"],
|
||||
"no-multiple-empty-lines": 1,
|
||||
"prefer-const": "error"
|
||||
"prefer-const": "error",
|
||||
"space-infix-ops": "error"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,28 @@
|
||||
Please read the following instructions carefully.
|
||||
We use GitHub Issues for bugs.
|
||||
|
||||
Check out https://github.com/ParsePlatform/parse-server/issues/1271 for an ideal bug report.
|
||||
The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly!
|
||||
If you have a non-bug question, ask on Stack Overflow or Server Fault:
|
||||
- https://stackoverflow.com/questions/tagged/parse.com
|
||||
- https://serverfault.com/tags/parse
|
||||
|
||||
Many members of the community use Stack Overflow and Server Fault to ask questions.
|
||||
Read through the existing questions or ask your own!
|
||||
- Stack Overflow: http://stackoverflow.com/questions/tagged/parse.com
|
||||
- Server Fault: https://serverfault.com/tags/parse
|
||||
You may also search through existing issues before opening a new one: https://github.com/ParsePlatform/Parse-Server/issues?utf8=%E2%9C%93&q=is%3Aissue
|
||||
|
||||
For database migration help, please file a bug report at https://parse.com/help#report
|
||||
|
||||
Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!
|
||||
|
||||
- [ ] You've met the prerequisites: https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites.
|
||||
|
||||
- [ ] You're running the latest version of Parse Server: https://github.com/ParsePlatform/parse-server/releases
|
||||
|
||||
- [ ] You've searched through existing issues: https://github.com/ParsePlatform/Parse-Server/issues?utf8=%E2%9C%93&q=is%3Aissue Chances are that your issue has been reported or resolved before.
|
||||
|
||||
- [ ] You have filled out every section below. Issues without sufficient information are more likely to be closed.
|
||||
|
||||
--
|
||||
--- Please use this template. If you don't use this template, your issue may be closed without comment. ---
|
||||
|
||||
### Issue Description
|
||||
|
||||
[DELETE EVERYTHING ABOVE THIS LINE BEFORE SUBMITTING YOUR ISSUE]
|
||||
|
||||
Describe your issue in as much detail as possible.
|
||||
|
||||
[FILL THIS OUT]
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
Please include a detailed list of steps that reproduce the issue. Include curl commands when applicable.
|
||||
|
||||
1. [FILL THIS OUT]
|
||||
2. [FILL THIS OUT]
|
||||
3. [FILL THIS OUT]
|
||||
|
||||
#### Expected Results
|
||||
|
||||
[FILL THIS OUT]
|
||||
What you expected to happen.
|
||||
|
||||
#### Actual Outcome
|
||||
|
||||
[FILL THIS OUT]
|
||||
What is happening instead.
|
||||
|
||||
### Environment Setup
|
||||
|
||||
@@ -62,6 +40,4 @@ Please include a detailed list of steps that reproduce the issue. Include curl c
|
||||
|
||||
### Logs/Trace
|
||||
|
||||
You can turn on additional logging by configuring VERBOSE=1 in your environment.
|
||||
|
||||
[FILL THIS OUT]
|
||||
Include all relevant logs. You can turn on additional logging by configuring VERBOSE=1 in your environment.
|
||||
|
||||
@@ -13,6 +13,7 @@ lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
instrumentation:
|
||||
excludes: ["**/spec/**"]
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"reporter": [
|
||||
"lcov",
|
||||
"text-summary"
|
||||
],
|
||||
"exclude": [
|
||||
"**/spec/**",
|
||||
"lib/"
|
||||
]
|
||||
}
|
||||
|
||||
+3
-5
@@ -1,7 +1,7 @@
|
||||
language: node_js
|
||||
dist: trusty
|
||||
node_js:
|
||||
- '4.5'
|
||||
- '4.6'
|
||||
- '6.1'
|
||||
services:
|
||||
- postgresql
|
||||
@@ -12,13 +12,13 @@ addons:
|
||||
apt_packages:
|
||||
- postgresql-9.5-postgis-2.3
|
||||
before_script:
|
||||
- ls -al "$HOME/.mongodb/versions"
|
||||
- node -e 'require("./lib/index.js")'
|
||||
- psql -c 'create database parse_server_postgres_adapter_test_database;' -U postgres
|
||||
- psql -c 'CREATE EXTENSION postgis;' -U postgres -d parse_server_postgres_adapter_test_database
|
||||
- psql -c 'CREATE EXTENSION postgis_topology;' -U postgres -d parse_server_postgres_adapter_test_database
|
||||
env:
|
||||
global:
|
||||
- COVERAGE_OPTION='./node_modules/.bin/istanbul cover'
|
||||
- COVERAGE_OPTION='./node_modules/.bin/nyc'
|
||||
matrix:
|
||||
- MONGODB_VERSION=3.0.8
|
||||
- MONGODB_VERSION=3.2.6
|
||||
@@ -26,8 +26,6 @@ env:
|
||||
- PARSE_SERVER_TEST_CACHE=redis
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- env: PARSE_SERVER_TEST_DB=postgres
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
+101
-3
@@ -1,5 +1,103 @@
|
||||
## Parse Server Changelog
|
||||
|
||||
### 2.3.8
|
||||
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.7...2.3.8)
|
||||
|
||||
#### New Features
|
||||
* Support for PG-Promise options, thanks to [ren dong](https://github.com/rendongsc)
|
||||
|
||||
#### Improvements
|
||||
* Improves support for graceful shutdown, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Improves configuration validation for Twitter Authentication, thanks to [Benjamin Wilson Friedman](https://github.com/montymxb)
|
||||
|
||||
#### Bug Fixes
|
||||
* Fixes issue affecting GeoPoint __type with Postgres, thanks to [zhoul-HS](https://github.com/zhoul-HS)
|
||||
* Prevent user creation if username or password is empty, thanks to [Wissam Abirached](https://github.com/wabirached)
|
||||
|
||||
#### Dependency Updates:
|
||||
* [cross-env@4.0.0 ](https://www.npmjs.com/package/cross-env)
|
||||
* [ws@2.2.3](https://www.npmjs.com/package/ws)
|
||||
* [babel-core@6.24.0](https://www.npmjs.com/package/babel-core)
|
||||
* [uws@0.14.0](https://www.npmjs.com/package/uws)
|
||||
* [babel-preset-es2015@6.24.0](https://www.npmjs.com/package/babel-preset-es2015)
|
||||
* [babel-plugin-syntax-flow@6.18.0](https://www.npmjs.com/package/babel-plugin-syntax-flow)
|
||||
* [babel-cli@6.24.0](https://www.npmjs.com/package/babel-cli)
|
||||
* [babel-register@6.24.0](https://www.npmjs.com/package/babel-register)
|
||||
* [winston-daily-rotate-file@1.4.6](https://www.npmjs.com/package/winston-daily-rotate-file)
|
||||
* [mongodb@2.2.25](https://www.npmjs.com/package/mongodb)
|
||||
* [redis@2.7.0](https://www.npmjs.com/package/redis)
|
||||
* [pg-promise@5.6.4](https://www.npmjs.com/package/pg-promise)
|
||||
* [parse-server-push-adapter@1.3.0](https://www.npmjs.com/package/parse-server-push-adapter)
|
||||
|
||||
### 2.3.7
|
||||
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.6...2.3.7)
|
||||
|
||||
#### New Features
|
||||
* New endpoint to resend verification email, thanks to [Xy Ziemba](https://github.com/xyziemba)
|
||||
|
||||
#### Improvements
|
||||
* Add TTL option for Redis Cache Adapter, thanks to [Ryan Foster](https://github.com/f0ster)
|
||||
* Update Postgres Storage Adapter, thanks to [Vitaly Tomilov](https://github.com/vitaly-t)
|
||||
|
||||
#### Bug Fixes
|
||||
* Add index on Role.name, fixes (#3579), thanks to [Natan Rolnik](https://github.com/natanrolnik)
|
||||
* Fix default value of userSensitiveFields, fixes (#3593), thanks to [Arthur Cinader](https://github.com/acinader)
|
||||
|
||||
#### Dependency Updates:
|
||||
* [body-parser@1.17.1](https://www.npmjs.com/package/body-parser)
|
||||
* [express@4.15.2](https://www.npmjs.com/package/express)
|
||||
* [request@2.81.0](https://www.npmjs.com/package/request)
|
||||
* [winston-daily-rotate-file@1.4.5](https://www.npmjs.com/package/winston-daily-rotate-file)
|
||||
* [ws@2.2.0](https://www.npmjs.com/package/ws)
|
||||
|
||||
|
||||
### 2.3.6
|
||||
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.5...2.3.6)
|
||||
|
||||
#### Improvements
|
||||
* Adds support for injecting a middleware for instumentation in the CLI, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Alleviate mongodb bug with $or queries [SERVER-13732](https://jira.mongodb.org/browse/SERVER-13732), thanks to [Jack Wearden](https://github.com/NotBobTheBuilder)
|
||||
|
||||
#### Bug Fixes
|
||||
* Fix issue affecting password policy and empty passwords, thanks to [Bhaskar Reddy Yasa](https://github.com/bhaskaryasa)
|
||||
* Fix issue when logging url in non string objects, thanks to [Paulo Vítor S Reis](https://github.com/paulovitin)
|
||||
|
||||
#### Dependencies updates:
|
||||
* [ws@2.1.0](https://npmjs.com/package/ws)
|
||||
* [uws@0.13.0](https://npmjs.com/package/uws)
|
||||
* [pg-promise@5.6.2](https://npmjs.com/package/pg-promise)
|
||||
|
||||
|
||||
### 2.3.5
|
||||
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.3...2.3.5)
|
||||
|
||||
#### Bug Fixes
|
||||
* Allow empty client key
|
||||
(#3497), thanks to [Arthur Cinader](https://github.com/acinader)
|
||||
* Fix LiveQuery unsafe user
|
||||
(#3525), thanks to [David Starke](https://github.com/dstarke)
|
||||
* Use `flushdb` instead of `flushall` in RedisCacheAdapter
|
||||
(#3523), thanks to [Jeremy Louie](https://github.com/JeremyPlease)
|
||||
* Fix saving GeoPoints and Files in `_GlobalConfig` (Make sure we don't treat
|
||||
dot notation keys as topLevel atoms)
|
||||
(#3531), thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
|
||||
### 2.3.3
|
||||
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.2...2.3.3)
|
||||
|
||||
#### Breaking Changes
|
||||
* **Minimum Node engine bumped to 4.6** (#3480), thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
|
||||
#### Bug Fixes
|
||||
* Add logging on failure to create file (#3424), thanks to [Arthur Cinader](https://github.com/acinader)
|
||||
* Log Parse Errors so they are intelligible (#3431), thanks to [Arthur Cinader](https://github.com/acinader)
|
||||
* MongoDB $or Queries avoid SERVER-13732 bug (#3476), thanks to [Jack Wearden](https://github.com/NotBobTheBuilder)
|
||||
* Mongo object to Parse object date serialization - avoid re-serialization of iso of type Date (#3389), thanks to [nodechefMatt](https://github.com/nodechefMatt)
|
||||
|
||||
#### Improvements
|
||||
* Ground preparations for push scalability (#3080), thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Use uWS as optional dependency for ws server (#3231), thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
|
||||
### 2.3.2
|
||||
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.1...2.3.2)
|
||||
|
||||
@@ -32,7 +130,7 @@ This release addresses only that issue.
|
||||
|
||||
#### New Features
|
||||
* Adds ability to restrict access through Class Level Permissions to only authenticated users [see docs](http://parseplatform.github.io/docs/ios/guide/#requires-authentication-permission-requires-parse-server---230)
|
||||
* Adds ability to strip sensitive data from _User responses, strips emails by default, thanks to [Arthur Cinader](https://github.com/acinader)
|
||||
* Adds ability to strip sensitive data from `_User` responses, strips emails by default, thanks to [Arthur Cinader](https://github.com/acinader)
|
||||
* Adds password history support for password policies, thanks to [Bhaskar Reddy Yasa](https://github.com/bhaskaryasa)
|
||||
|
||||
#### Improvements
|
||||
@@ -85,7 +183,7 @@ Postgres support requires v9.5
|
||||
* Better Postgres support, thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
|
||||
* Logs the function name when failing (#2963), thanks to [Michael Helvey](https://github.com/michaelhelvey)
|
||||
* CLI: forces closing the connections with SIGINT/SIGTERM (#2964), thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
|
||||
* Reduce the number of calls to the _SCHEMA table (#2912), thanks to [Steven Shipton](https://github.com/steven-supersolid)
|
||||
* Reduce the number of calls to the `_SCHEMA` table (#2912), thanks to [Steven Shipton](https://github.com/steven-supersolid)
|
||||
* LiveQuery: Support for Role ACL's, thanks to [Aaron Blondeau](https://github.com/aaron-blondeau-dose)
|
||||
|
||||
#### Bug Fixes
|
||||
@@ -132,7 +230,7 @@ Postgres support requires v9.5
|
||||
### 2.2.20
|
||||
|
||||
* New: Adds CloudCode handler for `beforeFind`, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* New: RedisCacheAdapter for syncing schema, role and user caches across servers, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* New: RedisCacheAdapter for syncing schema, role and user caches across servers, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* New: Latest master build available at `ParsePlatform/parse-server#latest`, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Fix: Better support for upgradeToRevocableSession with missing session token, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Fix: Removes babel-polyfill runtime dependency, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
|
||||
@@ -28,3 +28,7 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
-----
|
||||
|
||||
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
|
||||
|
||||
@@ -31,3 +31,7 @@ necessarily infringed by the Software standing alone.
|
||||
A "Patent Assertion" is any lawsuit or other action alleging direct, indirect,
|
||||
or contributory infringement or inducement to infringe any patent, including a
|
||||
cross-claim or counterclaim.
|
||||
|
||||
-----
|
||||
|
||||
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||

|
||||
|
||||
[](https://travis-ci.org/ParsePlatform/parse-server)
|
||||
[](https://codecov.io/github/ParsePlatform/parse-server?branch=master)
|
||||
[](https://travis-ci.org/parse-community/parse-server)
|
||||
[](https://codecov.io/github/parse-community/parse-server?branch=master)
|
||||
[](https://www.npmjs.com/package/parse-server)
|
||||
|
||||
[](https://gitter.im/ParsePlatform/Chat)
|
||||
@@ -24,12 +24,14 @@ $ npm install -g parse-server mongodb-runner
|
||||
$ mongodb-runner start
|
||||
$ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://localhost/test
|
||||
```
|
||||
***Note:*** *If installation with* `-g` *fails due to permission problems* (`npm ERR! code 'EACCES'`), *please refer to [this link](https://docs.npmjs.com/getting-started/fixing-npm-permissions).*
|
||||
|
||||
|
||||
### Inside a Docker container
|
||||
```
|
||||
$ docker build --tag parse-server .
|
||||
$ docker run --name my-mongo -d mongo
|
||||
$ docker run --name my-parse-server --link my-mongo:mongo parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test
|
||||
$ docker run --name my-parse-server --link my-mongo:mongo -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test
|
||||
```
|
||||
|
||||
You can use any arbitrary string as your application id and master key. These will be used by your clients to authenticate with the Parse Server.
|
||||
@@ -40,7 +42,7 @@ That's it! You are now running a standalone version of Parse Server on your mach
|
||||
|
||||
### Saving your first object
|
||||
|
||||
Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](https://parse.com/docs/rest/guide), but you can easily do the same using any of the [Parse SDKs](https://parseplatform.github.io/#sdks). Run the following:
|
||||
Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](https://docs.parseplatform.org/rest/guide), but you can easily do the same using any of the [Parse SDKs](https://parseplatform.github.io/#sdks). Run the following:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
@@ -102,19 +104,19 @@ $ curl -X GET \
|
||||
|
||||
```
|
||||
|
||||
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](https://parse.com/docs).
|
||||
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](https://docs.parseplatform.org).
|
||||
|
||||
### Connect your app to Parse Server
|
||||
|
||||
Parse provides SDKs for all the major platforms. Refer to the Parse Server guide to [learn how to connect your app to Parse Server](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#using-parse-sdks-with-parse-server).
|
||||
Parse provides SDKs for all the major platforms. Refer to the Parse Server guide to [learn how to connect your app to Parse Server](https://github.com/parse-community/parse-server/wiki/Parse-Server-Guide#using-parse-sdks-with-parse-server).
|
||||
|
||||
## Running Parse Server elsewhere
|
||||
|
||||
Once you have a better understanding of how the project works, please refer to the [Parse Server wiki](https://github.com/ParsePlatform/parse-server/wiki) for in-depth guides to deploy Parse Server to major infrastructure providers. Read on to learn more about additional ways of running Parse Server.
|
||||
Once you have a better understanding of how the project works, please refer to the [Parse Server wiki](https://github.com/parse-community/parse-server/wiki) for in-depth guides to deploy Parse Server to major infrastructure providers. Read on to learn more about additional ways of running Parse Server.
|
||||
|
||||
### Parse Server Sample Application
|
||||
|
||||
We have provided a basic [Node.js application](https://github.com/ParsePlatform/parse-server-example) that uses the Parse Server module on Express and can be easily deployed to various infrastructure providers:
|
||||
We have provided a basic [Node.js application](https://github.com/parse-community/parse-server-example) that uses the Parse Server module on Express and can be easily deployed to various infrastructure providers:
|
||||
|
||||
* [Heroku and mLab](https://devcenter.heroku.com/articles/deploying-a-parse-server-to-heroku)
|
||||
* [AWS and Elastic Beanstalk](http://mobile.awsblog.com/post/TxCD57GZLM2JR/How-to-set-up-Parse-Server-on-AWS-using-AWS-Elastic-Beanstalk)
|
||||
@@ -125,6 +127,7 @@ We have provided a basic [Node.js application](https://github.com/ParsePlatform/
|
||||
* [Pivotal Web Services](https://github.com/cf-platform-eng/pws-parse-server)
|
||||
* [Back4app](http://blog.back4app.com/2016/03/01/quick-wizard-migration/)
|
||||
* [Gomix](https://gomix.com/#!/project/parse-server)
|
||||
* [Flynn](https://flynn.io/blog/parse-apps-on-flynn)
|
||||
|
||||
### Parse Server + Express
|
||||
|
||||
@@ -156,7 +159,7 @@ For a full list of available options, run `parse-server --help`.
|
||||
|
||||
## Logging
|
||||
|
||||
Parse Server will, by default, will log:
|
||||
Parse Server will, by default, log:
|
||||
* to the console
|
||||
* daily rotating files as new line delimited JSON
|
||||
|
||||
@@ -170,11 +173,7 @@ Logs are also be viewable in Parse Dashboard.
|
||||
|
||||
# Documentation
|
||||
|
||||
The full documentation for Parse Server is available in the [wiki](https://github.com/ParsePlatform/parse-server/wiki). The [Parse Server guide](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide) is a good place to get started. If you're interested in developing for Parse Server, the [Development guide](https://github.com/ParsePlatform/parse-server/wiki/Development-Guide) will help you get set up.
|
||||
|
||||
## Migrating an Existing Parse App
|
||||
|
||||
The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. There are a few areas where Parse Server does not provide compatibility with the hosted version of Parse. Learn more in the [Migration guide](https://parse.com/migration).
|
||||
The full documentation for Parse Server is available in the [wiki](https://github.com/parse-community/parse-server/wiki). The [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/) is a good place to get started. If you're interested in developing for Parse Server, the [Development guide](http://docs.parseplatform.org/parse-server/guide/#development-guide) will help you get set up.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -190,7 +189,7 @@ For the full list of available options, run `parse-server --help`.
|
||||
* `port` - The default port is 1337, specify this parameter to use a different port.
|
||||
* `serverURL` - URL to your Parse Server (don't forget to specify http:// or https://). This URL will be used when making requests to Parse Server from Cloud Code.
|
||||
* `cloud` - The absolute path to your cloud code `main.js` file.
|
||||
* `push` - Configuration options for APNS and GCM push. See the [Push Notifications wiki entry](https://github.com/ParsePlatform/parse-server/wiki/Push).
|
||||
* `push` - Configuration options for APNS and GCM push. See the [Push Notifications quick start](http://docs.parseplatform.org/parse-server/guide/#push-notifications_push-notifications-quick-start).
|
||||
|
||||
#### Client key options
|
||||
|
||||
@@ -206,17 +205,18 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
||||
* `fileKey` - For migrated apps, this is necessary to provide access to files already hosted on Parse.
|
||||
* `allowClientClassCreation` - Set to false to disable client class creation. Defaults to true.
|
||||
* `enableAnonymousUsers` - Set to false to disable anonymous users. Defaults to true.
|
||||
* `auth` - Used to configure support for [3rd party authentication](https://github.com/ParsePlatform/parse-server/wiki/OAuth--and-Custom-Authentication).
|
||||
* `auth` - Used to configure support for [3rd party authentication](http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication).
|
||||
* `facebookAppIds` - An array of valid Facebook application IDs that users may authenticate with.
|
||||
* `mountPath` - Mount path for the server. Defaults to `/parse`.
|
||||
* `filesAdapter` - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js)).
|
||||
* `filesAdapter` - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/parse-community/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js)).
|
||||
* `maxUploadSize` - Max file size for uploads. Defaults to 20 MB.
|
||||
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)).
|
||||
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/parse-community/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)).
|
||||
* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year).
|
||||
* `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.
|
||||
* `accountLockout` - Lock account when a malicious user is attempting to determine an account password by trial and error.
|
||||
* `passwordPolicy` - Optional password policy rules to enforce.
|
||||
* `customPages` - A hash with urls to override email verification links, password reset links and specify frame url for masking user-facing pages. Available keys: `parseFrameURL`, `invalidLink`, `choosePassword`, `passwordResetSuccess`, `verifyEmailSuccess`.
|
||||
* `middleware` - (CLI only), a module name, function that is an express middleware. When using the CLI, the express app will load it just **before** mounting parse-server on the mount path. This option is useful for injecting a monitoring middleware.
|
||||
|
||||
##### Logging
|
||||
|
||||
@@ -336,19 +336,19 @@ Parse Server allows developers to choose from several options when hosting files
|
||||
* `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/)
|
||||
|
||||
`GridStoreAdapter` 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 wiki](https://github.com/ParsePlatform/parse-server/wiki/Configuring-File-Adapters).
|
||||
`GridStoreAdapter` 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).
|
||||
|
||||
# Support
|
||||
|
||||
For implementation related questions or technical support, please refer to the [Stack Overflow](http://stackoverflow.com/questions/tagged/parse.com) and [Server Fault](https://serverfault.com/tags/parse) communities.
|
||||
|
||||
If you believe you've found an issue with Parse Server, make sure these boxes are checked before [reporting an issue](https://github.com/ParsePlatform/parse-server/issues):
|
||||
If you believe you've found an issue with Parse Server, make sure these boxes are checked before [reporting an issue](https://github.com/parse-community/parse-server/issues):
|
||||
|
||||
- [ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
|
||||
- [ ] You've met the [prerequisites](http://docs.parseplatform.org/parse-server/guide/#prerequisites).
|
||||
|
||||
- [ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
|
||||
- [ ] You're running the [latest version](https://github.com/parse-community/parse-server/releases) of Parse Server.
|
||||
|
||||
- [ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
|
||||
- [ ] You've searched through [existing issues](https://github.com/parse-community/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
|
||||
|
||||
# Want to ride the bleeding edge?
|
||||
|
||||
@@ -365,3 +365,7 @@ npm install parseplatform/parse-server.git#latest
|
||||
# Contributing
|
||||
|
||||
We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md).
|
||||
|
||||
-----
|
||||
|
||||
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
|
||||
|
||||
+2
-2
@@ -95,10 +95,10 @@ confirm 'Y' 'Do you want to continue? (Y/n): '
|
||||
check_node
|
||||
check_npm
|
||||
|
||||
printf "Setting up parse-server in %s" "${INSTALL_DIR}\n"
|
||||
printf "Setting up parse-server in %s\n" "${INSTALL_DIR}"
|
||||
|
||||
if [ -d "${INSTALL_DIR}" ]; then
|
||||
echo "{CHECK} ${INSTALL_DIR} exists"
|
||||
echo "${CHECK} ${INSTALL_DIR} exists"
|
||||
else
|
||||
mkdir -p "${INSTALL_DIR}"
|
||||
echo "${CHECK} Created ${INSTALL_DIR}"
|
||||
|
||||
+33
-32
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "2.3.2",
|
||||
"version": "2.3.8",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -18,72 +18,73 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"bcryptjs": "2.4.0",
|
||||
"body-parser": "1.15.2",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.17.1",
|
||||
"commander": "2.9.0",
|
||||
"deepcopy": "0.6.3",
|
||||
"express": "4.14.0",
|
||||
"express": "4.15.2",
|
||||
"intersect": "1.0.1",
|
||||
"lodash": "4.17.4",
|
||||
"lru-cache": "4.0.2",
|
||||
"mime": "1.3.4",
|
||||
"mongodb": "2.2.10",
|
||||
"multer": "1.2.1",
|
||||
"mongodb": "2.2.25",
|
||||
"multer": "1.3.0",
|
||||
"parse": "1.9.2",
|
||||
"parse-server-fs-adapter": "1.0.1",
|
||||
"parse-server-push-adapter": "1.1.0",
|
||||
"parse-server-push-adapter": "1.3.0",
|
||||
"parse-server-s3-adapter": "1.0.6",
|
||||
"parse-server-simple-mailgun-adapter": "1.0.0",
|
||||
"pg-promise": "5.5.0",
|
||||
"redis": "2.6.3",
|
||||
"request": "2.79.0",
|
||||
"pg-promise": "5.6.4",
|
||||
"redis": "2.7.1",
|
||||
"request": "2.81.0",
|
||||
"semver": "5.2.0",
|
||||
"tv4": "1.2.7",
|
||||
"winston": "2.3.0",
|
||||
"winston-daily-rotate-file": "1.4.2",
|
||||
"ws": "1.1.1"
|
||||
"tv4": "1.3.0",
|
||||
"winston": "2.3.1",
|
||||
"winston-daily-rotate-file": "1.4.6",
|
||||
"ws": "2.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "6.18.0",
|
||||
"babel-core": "6.21.0",
|
||||
"babel-cli": "6.24.0",
|
||||
"babel-core": "6.24.0",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-plugin-syntax-flow": "6.13.0",
|
||||
"babel-plugin-transform-flow-strip-types": "6.21.0",
|
||||
"babel-preset-es2015": "6.14.0",
|
||||
"babel-preset-stage-0": "6.5.0",
|
||||
"babel-register": "6.18.0",
|
||||
"babel-plugin-syntax-flow": "6.18.0",
|
||||
"babel-plugin-transform-flow-strip-types": "6.22.0",
|
||||
"babel-preset-es2015": "6.24.0",
|
||||
"babel-preset-stage-0": "6.22.0",
|
||||
"babel-register": "6.24.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"cross-env": "3.1.4",
|
||||
"cross-env": "4.0.0",
|
||||
"deep-diff": "0.3.4",
|
||||
"eslint": "^3.10.2",
|
||||
"eslint": "^3.16.1",
|
||||
"eslint-plugin-flowtype": "^2.25.0",
|
||||
"gaze": "1.1.1",
|
||||
"istanbul": "1.0.0-alpha.1",
|
||||
"jasmine": "2.5.2",
|
||||
"jasmine-spec-reporter": "^2.7.0",
|
||||
"jasmine": "2.5.3",
|
||||
"jasmine-spec-reporter": "^3.1.0",
|
||||
"mongodb-runner": "3.4.0",
|
||||
"nodemon": "1.11.0",
|
||||
"request-promise": "4.1.1"
|
||||
"nyc": "^10.1.2",
|
||||
"request-promise": "4.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm run build && node bin/dev",
|
||||
"lint": "eslint --cache ./",
|
||||
"build": "babel src/ -d lib/",
|
||||
"build": "babel src/ -d lib/ --copy-files",
|
||||
"pretest": "npm run lint",
|
||||
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 $COVERAGE_OPTION jasmine",
|
||||
"test:win": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 jasmine",
|
||||
"coverage": "cross-env COVERAGE_OPTION='./node_modules/.bin/istanbul cover' npm test",
|
||||
"coverage:win": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 node ./node_modules/istanbul/lib/cli.js cover ./node_modules/jasmine/bin/jasmine.js",
|
||||
"coverage": "cross-env COVERAGE_OPTION='./node_modules/.bin/nyc' npm test",
|
||||
"coverage:win": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 node ./node_modules/.bin/nyc ./node_modules/jasmine/bin/jasmine.js",
|
||||
"start": "node ./bin/parse-server",
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.5"
|
||||
"node": ">=4.6"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "./bin/parse-server"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bcrypt": "1.0.2"
|
||||
"bcrypt": "1.0.2",
|
||||
"uws": "^0.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"notEqual": true,
|
||||
"it_exclude_dbs": true,
|
||||
"describe_only_db": true,
|
||||
"describe_only": true,
|
||||
"on_db": true,
|
||||
"defaultConfiguration": true,
|
||||
"expectSuccess": true,
|
||||
|
||||
@@ -3,6 +3,7 @@ var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
|
||||
var FilesAdapter = require("parse-server-fs-adapter").default;
|
||||
var S3Adapter = require("parse-server-s3-adapter").default;
|
||||
var ParsePushAdapter = require("parse-server-push-adapter").default;
|
||||
const Config = require('../src/Config');
|
||||
|
||||
describe("AdapterLoader", ()=>{
|
||||
|
||||
@@ -118,6 +119,30 @@ describe("AdapterLoader", ()=>{
|
||||
done();
|
||||
});
|
||||
|
||||
it("should load custom push adapter from string (#3544)", (done) => {
|
||||
var adapterPath = require('path').resolve("./spec/MockPushAdapter");
|
||||
var options = {
|
||||
ios: {
|
||||
bundleId: 'bundle.id'
|
||||
}
|
||||
}
|
||||
const pushAdapterOptions = {
|
||||
adapter: adapterPath,
|
||||
options
|
||||
};
|
||||
expect(() => {
|
||||
reconfigureServer({
|
||||
push: pushAdapterOptions,
|
||||
}).then(() => {
|
||||
const config = new Config(Parse.applicationId);
|
||||
const pushAdapter = config.pushWorker.adapter;
|
||||
expect(pushAdapter.getValidPushTypes()).toEqual(['ios']);
|
||||
expect(pushAdapter.options).toEqual(pushAdapterOptions);
|
||||
done();
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should load S3Adapter from direct passing", (done) => {
|
||||
var s3Adapter = new S3Adapter("key", "secret", "bucket")
|
||||
expect(() => {
|
||||
|
||||
@@ -6,8 +6,8 @@ var path = require('path');
|
||||
|
||||
describe('AuthenticationProviers', function() {
|
||||
["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter", "janrainengage", "janraincapture", "vkontakte"].map(function(providerName){
|
||||
it("Should validate structure of "+providerName, (done) => {
|
||||
var provider = require("../src/Adapters/Auth/"+providerName);
|
||||
it("Should validate structure of " + providerName, (done) => {
|
||||
var provider = require("../src/Adapters/Auth/" + providerName);
|
||||
jequal(typeof provider.validateAuthData, "function");
|
||||
jequal(typeof provider.validateAppId, "function");
|
||||
jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor);
|
||||
|
||||
@@ -1361,7 +1361,7 @@ describe('afterFind hooks', () => {
|
||||
Parse.Cloud.afterFind('MyObject', (req, res) => {
|
||||
const filteredResults = [];
|
||||
for(let i = 0 ; i < req.objects.length ; i++){
|
||||
if(req.objects[i].get("secretField")==="SSID1") {
|
||||
if(req.objects[i].get("secretField") === "SSID1") {
|
||||
filteredResults.push(req.objects[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,8 +156,9 @@ describe("Cloud Code Logger", () => {
|
||||
.save()
|
||||
.then(
|
||||
() => done.fail('this is not supposed to succeed'),
|
||||
() => logController.getLogs({ from: Date.now() - 500, size: 1000 })
|
||||
() => new Promise(resolve => setTimeout(resolve, 100))
|
||||
)
|
||||
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
|
||||
.then(logs => {
|
||||
const log = logs[1]; // 0 is the 'uh oh!' from rejection...
|
||||
expect(log.level).toEqual('error');
|
||||
@@ -194,7 +195,7 @@ describe("Cloud Code Logger", () => {
|
||||
Parse.Cloud.run('aFunction', { foo: 'bar' })
|
||||
.then(null, () => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
|
||||
.then(logs => {
|
||||
const log = logs[1];
|
||||
const log = logs[2];
|
||||
expect(log.level).toEqual('error');
|
||||
expect(log.message).toMatch(
|
||||
/Failed running cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Error: {"code":141,"message":"it failed!"}/);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
var DatabaseController = require('../src/Controllers/DatabaseController.js');
|
||||
var validateQuery = DatabaseController._validateQuery;
|
||||
|
||||
describe('DatabaseController', function() {
|
||||
|
||||
describe('validateQuery', function() {
|
||||
|
||||
it('should restructure simple cases of SERVER-13732', (done) => {
|
||||
var query = {$or: [{a: 1}, {a: 2}], _rperm: {$in: ['a', 'b']}, foo: 3};
|
||||
validateQuery(query);
|
||||
expect(query).toEqual({$or: [{a: 1, _rperm: {$in: ['a', 'b']}, foo: 3},
|
||||
{a: 2, _rperm: {$in: ['a', 'b']}, foo: 3}]});
|
||||
done();
|
||||
});
|
||||
|
||||
it('should reject invalid queries', (done) => {
|
||||
expect(() => validateQuery({$or: {'a': 1}})).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should accept valid queries', (done) => {
|
||||
expect(() => validateQuery({$or: [{'a': 1}, {'b': 2}]})).not.toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const request = require('request');
|
||||
const requestp = require('request-promise');
|
||||
const Config = require('../src/Config');
|
||||
|
||||
describe("Email Verification Token Expiration: ", () => {
|
||||
@@ -482,6 +483,257 @@ describe("Email Verification Token Expiration: ", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should send a new verification email when a resend is requested and the user is UNVERIFIED', done => {
|
||||
var user = new Parse.User();
|
||||
var sendEmailOptions;
|
||||
var sendVerificationEmailCallCount = 0;
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
sendVerificationEmailCallCount++;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})
|
||||
.then(() => {
|
||||
user.setUsername('resends_verification_token');
|
||||
user.setPassword('expiringToken');
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
expect(sendVerificationEmailCallCount).toBe(1);
|
||||
|
||||
return requestp.post({
|
||||
uri: 'http://localhost:8378/1/verificationEmailRequest',
|
||||
body: {
|
||||
email: 'user@parse.com'
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false // this promise is only rejected if the call itself failed
|
||||
})
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(sendVerificationEmailCallCount).toBe(2);
|
||||
expect(sendEmailOptions).toBeDefined();
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
jfail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not send a new verification email when a resend is requested and the user is VERIFIED', done => {
|
||||
var user = new Parse.User();
|
||||
var sendEmailOptions;
|
||||
var sendVerificationEmailCallCount = 0;
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
sendVerificationEmailCallCount++;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})
|
||||
.then(() => {
|
||||
user.setUsername('no_new_verification_token_once_verified');
|
||||
user.setPassword('expiringToken');
|
||||
user.set('email', 'user@parse.com');
|
||||
return user.signUp();
|
||||
})
|
||||
.then(() => {
|
||||
return requestp.get({
|
||||
url: sendEmailOptions.link,
|
||||
followRedirect: false,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false
|
||||
})
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toEqual(302);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
expect(sendVerificationEmailCallCount).toBe(1);
|
||||
|
||||
return requestp.post({
|
||||
uri: 'http://localhost:8378/1/verificationEmailRequest',
|
||||
body: {
|
||||
email: 'user@parse.com'
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false // this promise is only rejected if the call itself failed
|
||||
})
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(sendVerificationEmailCallCount).toBe(1);
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
jfail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not send a new verification email if this user does not exist', done => {
|
||||
var sendEmailOptions;
|
||||
var sendVerificationEmailCallCount = 0;
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
sendVerificationEmailCallCount++;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})
|
||||
.then(() => {
|
||||
return requestp.post({
|
||||
uri: 'http://localhost:8378/1/verificationEmailRequest',
|
||||
body: {
|
||||
email: 'user@parse.com'
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(sendVerificationEmailCallCount).toBe(0);
|
||||
expect(sendEmailOptions).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
jfail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if no email is supplied', done => {
|
||||
var sendEmailOptions;
|
||||
var sendVerificationEmailCallCount = 0;
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
sendVerificationEmailCallCount++;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})
|
||||
.then(() => {
|
||||
request.post({
|
||||
uri: 'http://localhost:8378/1/verificationEmailRequest',
|
||||
body: {},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false
|
||||
}, (err, response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body.code).toBe(Parse.Error.EMAIL_MISSING);
|
||||
expect(response.body.error).toBe('you must provide an email');
|
||||
expect(sendVerificationEmailCallCount).toBe(0);
|
||||
expect(sendEmailOptions).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
jfail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if email is not a string', done => {
|
||||
var sendEmailOptions;
|
||||
var sendVerificationEmailCallCount = 0;
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: options => {
|
||||
sendEmailOptions = options;
|
||||
sendVerificationEmailCallCount++;
|
||||
},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {}
|
||||
}
|
||||
reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1'
|
||||
})
|
||||
.then(() => {
|
||||
request.post({
|
||||
uri: 'http://localhost:8378/1/verificationEmailRequest',
|
||||
body: {email: 3},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
simple: false
|
||||
}, (err, response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body.code).toBe(Parse.Error.INVALID_EMAIL_ADDRESS);
|
||||
expect(response.body.error).toBe('you must provide a valid email string');
|
||||
expect(sendVerificationEmailCallCount).toBe(0);
|
||||
expect(sendEmailOptions).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
jfail(error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('client should not see the _email_verify_token_expires_at field', done => {
|
||||
var user = new Parse.User();
|
||||
var sendEmailOptions;
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
|
||||
var Config = require("../src/Config");
|
||||
var FilesController = require('../src/Controllers/FilesController').default;
|
||||
const LoggerController = require('../src/Controllers/LoggerController').LoggerController;
|
||||
const WinstonLoggerAdapter = require('../src/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter;
|
||||
const GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
|
||||
const Config = require("../src/Config");
|
||||
const FilesController = require('../src/Controllers/FilesController').default;
|
||||
|
||||
const mockAdapter = {
|
||||
createFile: () => {
|
||||
return Parse.Promise.reject(new Error('it failed'));
|
||||
},
|
||||
deleteFile: () => { },
|
||||
getFileData: () => { },
|
||||
getFileLocation: () => 'xyz'
|
||||
}
|
||||
|
||||
// Small additional tests to improve overall coverage
|
||||
describe("FilesController",() =>{
|
||||
@@ -26,5 +36,30 @@ describe("FilesController",() =>{
|
||||
expect(anObject.aFile.url).toEqual("http://an.url");
|
||||
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should create a server log on failure', done => {
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
reconfigureServer({ filesAdapter: mockAdapter })
|
||||
.then(() => new Promise(resolve => setTimeout(resolve, 1000)))
|
||||
.then(() => new Parse.File("yolo.txt", [1,2,3], "text/plain").save())
|
||||
.then(
|
||||
() => done.fail('should not succeed'),
|
||||
() => setImmediate(() => Parse.Promise.as('done'))
|
||||
)
|
||||
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
|
||||
.then((logs) => {
|
||||
// we get two logs here: 1. the source of the failure to save the file
|
||||
// and 2 the message that will be sent back to the client.
|
||||
const log1 = logs.pop();
|
||||
expect(log1.level).toBe('error');
|
||||
expect(log1.message).toBe('it failed');
|
||||
const log2 = logs.pop();
|
||||
expect(log2.level).toBe('error');
|
||||
expect(log2.code).toBe(130);
|
||||
expect(log2.message).toBe('Could not store file.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+10
-10
@@ -6,7 +6,7 @@ var httpRequest = require("../src/cloud-code/httpRequest"),
|
||||
express = require("express");
|
||||
|
||||
var port = 13371;
|
||||
var httpRequestServer = "http://localhost:"+port;
|
||||
var httpRequestServer = "http://localhost:" + port;
|
||||
|
||||
var app = express();
|
||||
app.use(bodyParser.json({ 'type': '*/*' }));
|
||||
@@ -39,7 +39,7 @@ app.listen(13371);
|
||||
describe("httpRequest", () => {
|
||||
it("should do /hello", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/hello"
|
||||
url: httpRequestServer + "/hello"
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(200);
|
||||
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
|
||||
@@ -55,7 +55,7 @@ describe("httpRequest", () => {
|
||||
it("should do /hello with callback and promises", (done) => {
|
||||
var calls = 0;
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/hello",
|
||||
url: httpRequestServer + "/hello",
|
||||
success: function() { calls++; },
|
||||
error: function() { calls++; }
|
||||
}).then(function(httpResponse){
|
||||
@@ -74,7 +74,7 @@ describe("httpRequest", () => {
|
||||
it("should do not follow redirects by default", (done) => {
|
||||
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/301"
|
||||
url: httpRequestServer + "/301"
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(301);
|
||||
done();
|
||||
@@ -87,7 +87,7 @@ describe("httpRequest", () => {
|
||||
it("should follow redirects when set", (done) => {
|
||||
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/301",
|
||||
url: httpRequestServer + "/301",
|
||||
followRedirects: true
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(200);
|
||||
@@ -104,7 +104,7 @@ describe("httpRequest", () => {
|
||||
it("should fail on 404", (done) => {
|
||||
var calls = 0;
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/404",
|
||||
url: httpRequestServer + "/404",
|
||||
success: function() {
|
||||
calls++;
|
||||
fail("should not succeed");
|
||||
@@ -124,7 +124,7 @@ describe("httpRequest", () => {
|
||||
|
||||
it("should fail on 404", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/404",
|
||||
url: httpRequestServer + "/404",
|
||||
}).then(function(){
|
||||
fail("should not succeed");
|
||||
done();
|
||||
@@ -141,7 +141,7 @@ describe("httpRequest", () => {
|
||||
var calls = 0;
|
||||
httpRequest({
|
||||
method: "POST",
|
||||
url: httpRequestServer+"/echo",
|
||||
url: httpRequestServer + "/echo",
|
||||
body: {
|
||||
foo: "bar"
|
||||
},
|
||||
@@ -218,7 +218,7 @@ describe("httpRequest", () => {
|
||||
|
||||
it("should params object to query string", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/qs",
|
||||
url: httpRequestServer + "/qs",
|
||||
params: {
|
||||
foo: "bar"
|
||||
}
|
||||
@@ -234,7 +234,7 @@ describe("httpRequest", () => {
|
||||
|
||||
it("should params string to query string", (done) => {
|
||||
httpRequest({
|
||||
url: httpRequestServer+"/qs",
|
||||
url: httpRequestServer + "/qs",
|
||||
params: "foo=bar&foo2=bar2"
|
||||
}).then(function(httpResponse){
|
||||
expect(httpResponse.status).toBe(200);
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ describe('Logger', () => {
|
||||
logging.logger.info('hi', {key: 'value'});
|
||||
expect(process.stdout.write).toHaveBeenCalled();
|
||||
var firstLog = process.stdout.write.calls.first().args[0];
|
||||
expect(firstLog).toEqual(JSON.stringify({key: 'value', level: 'info', message: 'hi' })+'\n');
|
||||
expect(firstLog).toEqual(JSON.stringify({key: 'value', level: 'info', message: 'hi' }) + '\n');
|
||||
return reconfigureServer({
|
||||
jsonLogs: false
|
||||
});
|
||||
|
||||
@@ -79,6 +79,19 @@ describe('middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when client key supplied but empty', (done) => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
clientKey: '',
|
||||
masterKey: 'masterKey',
|
||||
restAPIKey: 'restAPIKey'
|
||||
});
|
||||
fakeReq.headers['x-parse-client-key'] = '';
|
||||
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
|
||||
expect(fakeRes.status).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when no keys are configured and none supplied', (done) => {
|
||||
AppCache.put(fakeReq.body._ApplicationId, {
|
||||
masterKey: 'masterKey'
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = function(options) {
|
||||
return {
|
||||
options: options,
|
||||
send: function() {},
|
||||
getValidPushTypes: function() {
|
||||
return Object.keys(options.options);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -293,6 +293,32 @@ describe('parseObjectToMongoObjectForCreate', () => {
|
||||
expect(output.double).toBe(Number.MAX_VALUE);
|
||||
done();
|
||||
});
|
||||
|
||||
it('Date object where iso attribute is of type Date', (done) => {
|
||||
var input = {
|
||||
ts : { __type: 'Date', iso: new Date('2017-01-18T00:00:00.000Z') }
|
||||
}
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields : {
|
||||
ts : { type : 'Date' }
|
||||
}
|
||||
});
|
||||
expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z');
|
||||
done();
|
||||
});
|
||||
|
||||
it('Date object where iso attribute is of type String', (done) => {
|
||||
var input = {
|
||||
ts : { __type: 'Date', iso: '2017-01-18T00:00:00.000Z' }
|
||||
}
|
||||
var output = transform.mongoObjectToParseObject(null, input, {
|
||||
fields : {
|
||||
ts : { type : 'Date' }
|
||||
}
|
||||
});
|
||||
expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformUpdate', () => {
|
||||
|
||||
+11
-1
@@ -64,7 +64,7 @@ describe('OAuth', function() {
|
||||
var req = oauthClient.buildRequest(method, path, {"query": "param"});
|
||||
|
||||
jequal(req.host, options.host);
|
||||
jequal(req.path, "/"+path+"?query=param");
|
||||
jequal(req.path, "/" + path + "?query=param");
|
||||
jequal(req.method, "GET");
|
||||
jequal(req.headers['Content-Type'], 'application/x-www-form-urlencoded');
|
||||
jequal(req.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="wNkyEkDE%2F0JZ2idmqyrgHdvC0rs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"')
|
||||
@@ -133,4 +133,14 @@ describe('OAuth', function() {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("Should fail with missing options", (done) => {
|
||||
var options = undefined;
|
||||
try {
|
||||
new OAuth(options);
|
||||
} catch (error) {
|
||||
jequal(error.message, 'No options passed to OAuth');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+34
-9
@@ -2,22 +2,32 @@
|
||||
|
||||
const request = require('request');
|
||||
|
||||
const delayPromise = (delay) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, delay);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Parse.Push', () => {
|
||||
var setup = function() {
|
||||
var sendToInstallationSpy = jasmine.createSpy();
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
var badge = body.data.badge;
|
||||
const promises = installations.map((installation) => {
|
||||
sendToInstallationSpy(installation);
|
||||
|
||||
if (installation.deviceType == "ios") {
|
||||
expect(installation.badge).toEqual(badge);
|
||||
expect(installation.originalBadge+1).toEqual(installation.badge);
|
||||
expect(installation.originalBadge + 1).toEqual(installation.badge);
|
||||
} else {
|
||||
expect(installation.badge).toBeUndefined();
|
||||
}
|
||||
return Promise.resolve({
|
||||
err: null,
|
||||
deviceType: installation.deviceType,
|
||||
result: true
|
||||
device: installation,
|
||||
transmitted: true
|
||||
})
|
||||
});
|
||||
return Promise.all(promises);
|
||||
@@ -39,21 +49,29 @@ describe('Parse.Push', () => {
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("installationId", "installation_" + installations.length);
|
||||
installation.set("deviceToken","device_token_" + installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
return Parse.Object.saveAll(installations);
|
||||
}).catch((err) => {
|
||||
})
|
||||
.then(() => {
|
||||
return {
|
||||
sendToInstallationSpy,
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
throw err;
|
||||
})
|
||||
}
|
||||
|
||||
it('should properly send push', (done) => {
|
||||
return setup().then(() => {
|
||||
return setup().then(({ sendToInstallationSpy }) => {
|
||||
return Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
@@ -63,8 +81,13 @@ describe('Parse.Push', () => {
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true})
|
||||
})
|
||||
.then(() => {
|
||||
.then(() => {
|
||||
return delayPromise(500);
|
||||
})
|
||||
.then(() => {
|
||||
expect(sendToInstallationSpy.calls.count()).toEqual(10);
|
||||
})
|
||||
}).then(() => {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
@@ -83,6 +106,8 @@ describe('Parse.Push', () => {
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true})
|
||||
}).then(() => {
|
||||
return delayPromise(500);
|
||||
}).then(() => {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
|
||||
@@ -755,14 +755,12 @@ describe('Parse.ACL', () => {
|
||||
user2.set("password", "burger");
|
||||
return user2.signUp();
|
||||
}).then(() => {
|
||||
console.log(user2.getSessionToken());
|
||||
return object.destroy({sessionToken: user2.getSessionToken() });
|
||||
}).then((res) => {
|
||||
console.log(res);
|
||||
}).then(() => {
|
||||
fail('should not be able to destroy the object');
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
expect(err).not.toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -825,7 +825,7 @@ describe('miscellaneous', function() {
|
||||
};
|
||||
request.put({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/GameScore/'+obj.id,
|
||||
url: 'http://localhost:8378/1/classes/GameScore/' + obj.id,
|
||||
body: JSON.stringify({
|
||||
a: 'b',
|
||||
c: {"__op":"Increment","amount":2},
|
||||
@@ -1241,7 +1241,7 @@ describe('miscellaneous', function() {
|
||||
amount: amount
|
||||
}
|
||||
},
|
||||
url: 'http://localhost:8378/1/classes/AnObject/'+object.id
|
||||
url: 'http://localhost:8378/1/classes/AnObject/' + object.id
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
request.put(options, (err, res, body) => {
|
||||
|
||||
@@ -26,6 +26,29 @@ describe('Parse.GeoPoint testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('has the correct __type field in the json response', done => {
|
||||
var point = new Parse.GeoPoint(44.0, -11.0);
|
||||
var obj = new TestObject();
|
||||
obj.set('location', point);
|
||||
obj.set('name', 'Zhoul')
|
||||
obj.save(null, {
|
||||
success: (obj) => {
|
||||
console.log(obj);
|
||||
Parse.Cloud.httpRequest({
|
||||
url: 'http://localhost:8378/1/classes/TestObject/' + obj.id,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test'
|
||||
}
|
||||
}).then(response => {
|
||||
equal(response.data.location.__type, 'GeoPoint');
|
||||
done();
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
it('geo point exception two fields', (done) => {
|
||||
var point = new Parse.GeoPoint(20, 20);
|
||||
var obj = new TestObject();
|
||||
|
||||
@@ -56,6 +56,49 @@ describe('a GlobalConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can add and retrive files', (done) => {
|
||||
request.put({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
body : { params: { file: { __type: 'File', name: 'name', url: 'http://url' } } },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key' : 'test'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(body.result).toEqual(true);
|
||||
Parse.Config.get().then((res) => {
|
||||
const file = res.get('file');
|
||||
expect(file.name()).toBe('name');
|
||||
expect(file.url()).toBe('http://url');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can add and retrive Geopoints', (done) => {
|
||||
const geopoint = new Parse.GeoPoint(10,-20);
|
||||
request.put({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
body : { params: { point: geopoint.toJSON() } },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key' : 'test'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(body.result).toEqual(true);
|
||||
Parse.Config.get().then((res) => {
|
||||
const point = res.get('point');
|
||||
expect(point.latitude).toBe(10);
|
||||
expect(point.longitude).toBe(-20);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('properly handles delete op', (done) => {
|
||||
request.put({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
|
||||
+32
-31
@@ -7,7 +7,7 @@ var express = require("express");
|
||||
var bodyParser = require('body-parser');
|
||||
|
||||
var port = 12345;
|
||||
var hookServerURL = "http://localhost:"+port;
|
||||
var hookServerURL = "http://localhost:" + port;
|
||||
const AppCache = require('../src/cache').AppCache;
|
||||
|
||||
var app = express();
|
||||
@@ -129,7 +129,7 @@ describe('Hooks', () => {
|
||||
});
|
||||
|
||||
it("should fail to register hooks without Master Key", (done) => {
|
||||
request.post(Parse.serverURL+"/hooks/functions", {
|
||||
request.post(Parse.serverURL + "/hooks/functions", {
|
||||
headers: {
|
||||
"X-Parse-Application-Id": Parse.applicationId,
|
||||
"X-Parse-REST-API-Key": Parse.restKey,
|
||||
@@ -143,7 +143,9 @@ describe('Hooks', () => {
|
||||
});
|
||||
|
||||
it("should fail trying to create two times the same function", (done) => {
|
||||
Parse.Hooks.createFunction("my_new_function", "http://url.com").then(() => {
|
||||
Parse.Hooks.createFunction("my_new_function", "http://url.com")
|
||||
.then(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||
.then(() => {
|
||||
return Parse.Hooks.createFunction("my_new_function", "http://url.com")
|
||||
}, () => {
|
||||
fail("should create a new function");
|
||||
@@ -254,7 +256,7 @@ describe('Hooks', () => {
|
||||
});
|
||||
|
||||
it("should fail trying to create a malformed function (REST)", (done) => {
|
||||
request.post(Parse.serverURL+"/hooks/functions", {
|
||||
request.post(Parse.serverURL + "/hooks/functions", {
|
||||
headers: {
|
||||
"X-Parse-Application-Id": Parse.applicationId,
|
||||
"X-Parse-Master-Key": Parse.masterKey,
|
||||
@@ -272,18 +274,18 @@ describe('Hooks', () => {
|
||||
it("should create hooks and properly preload them", (done) => {
|
||||
|
||||
var promises = [];
|
||||
for (var i = 0; i<5; i++) {
|
||||
promises.push(Parse.Hooks.createTrigger("MyClass"+i, "beforeSave", "http://url.com/beforeSave/"+i));
|
||||
promises.push(Parse.Hooks.createFunction("AFunction"+i, "http://url.com/function"+i));
|
||||
for (var i = 0; i < 5; i++) {
|
||||
promises.push(Parse.Hooks.createTrigger("MyClass" + i, "beforeSave", "http://url.com/beforeSave/" + i));
|
||||
promises.push(Parse.Hooks.createFunction("AFunction" + i, "http://url.com/function" + i));
|
||||
}
|
||||
|
||||
Parse.Promise.when(promises).then(function(){
|
||||
for (var i=0; i<5; i++) {
|
||||
for (var i = 0; i < 5; i++) {
|
||||
// Delete everything from memory, as the server just started
|
||||
triggers.removeTrigger("beforeSave", "MyClass"+i, Parse.applicationId);
|
||||
triggers.removeFunction("AFunction"+i, Parse.applicationId);
|
||||
expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).toBeUndefined();
|
||||
expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).toBeUndefined();
|
||||
triggers.removeTrigger("beforeSave", "MyClass" + i, Parse.applicationId);
|
||||
triggers.removeFunction("AFunction" + i, Parse.applicationId);
|
||||
expect(triggers.getTrigger("MyClass" + i, "beforeSave", Parse.applicationId)).toBeUndefined();
|
||||
expect(triggers.getFunction("AFunction" + i, Parse.applicationId)).toBeUndefined();
|
||||
}
|
||||
const hooksController = new HooksController(Parse.applicationId, AppCache.get('test').databaseController);
|
||||
return hooksController.load()
|
||||
@@ -292,9 +294,9 @@ describe('Hooks', () => {
|
||||
fail('Should properly create all hooks');
|
||||
done();
|
||||
}).then(function() {
|
||||
for (var i=0; i<5; i++) {
|
||||
expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).not.toBeUndefined();
|
||||
expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).not.toBeUndefined();
|
||||
for (var i = 0; i < 5; i++) {
|
||||
expect(triggers.getTrigger("MyClass" + i, "beforeSave", Parse.applicationId)).not.toBeUndefined();
|
||||
expect(triggers.getFunction("AFunction" + i, Parse.applicationId)).not.toBeUndefined();
|
||||
}
|
||||
done();
|
||||
}, (err) => {
|
||||
@@ -310,7 +312,7 @@ describe('Hooks', () => {
|
||||
res.json({success:"OK!"});
|
||||
});
|
||||
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunction").then(function(){
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL + "/SomeFunction").then(function(){
|
||||
return Parse.Cloud.run("SOME_TEST_FUNCTION")
|
||||
}, (err) => {
|
||||
jfail(err);
|
||||
@@ -332,7 +334,7 @@ describe('Hooks', () => {
|
||||
res.json({error: {code: 1337, error: "hacking that one!"}});
|
||||
});
|
||||
// The function is deleted as the DB is dropped between calls
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunctionError").then(function(){
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL + "/SomeFunctionError").then(function(){
|
||||
return Parse.Cloud.run("SOME_TEST_FUNCTION")
|
||||
}, (err) => {
|
||||
jfail(err);
|
||||
@@ -362,7 +364,7 @@ describe('Hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/ExpectingKey").then(function(){
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL + "/ExpectingKey").then(function(){
|
||||
return Parse.Cloud.run("SOME_TEST_FUNCTION")
|
||||
}, (err) => {
|
||||
jfail(err);
|
||||
@@ -389,7 +391,7 @@ describe('Hooks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/ExpectingKeyAlso").then(function(){
|
||||
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL + "/ExpectingKeyAlso").then(function(){
|
||||
return Parse.Cloud.run("SOME_TEST_FUNCTION")
|
||||
}, (err) => {
|
||||
jfail(err);
|
||||
@@ -421,8 +423,8 @@ describe('Hooks', () => {
|
||||
// But this should override the key upon return
|
||||
res.json({success: object});
|
||||
});
|
||||
// The function is delete as the DB is dropped between calls
|
||||
Parse.Hooks.createTrigger("SomeRandomObject", "beforeSave" ,hookServerURL+"/BeforeSaveSome").then(function(){
|
||||
// The function is deleted as the DB is dropped between calls
|
||||
Parse.Hooks.createTrigger("SomeRandomObject", "beforeSave", hookServerURL + "/BeforeSaveSome").then(function () {
|
||||
const obj = new Parse.Object("SomeRandomObject");
|
||||
return obj.save();
|
||||
}).then(function(res) {
|
||||
@@ -444,7 +446,7 @@ describe('Hooks', () => {
|
||||
object.set('hello', "world");
|
||||
res.json({success: object});
|
||||
});
|
||||
Parse.Hooks.createTrigger("SomeRandomObject2", "beforeSave" ,hookServerURL+"/BeforeSaveSome2").then(function(){
|
||||
Parse.Hooks.createTrigger("SomeRandomObject2", "beforeSave", hookServerURL + "/BeforeSaveSome2").then(function(){
|
||||
const obj = new Parse.Object("SomeRandomObject2");
|
||||
return obj.save();
|
||||
}).then(function(res) {
|
||||
@@ -470,20 +472,19 @@ describe('Hooks', () => {
|
||||
res.json({success: {}});
|
||||
})
|
||||
});
|
||||
// The function is delete as the DB is dropped between calls
|
||||
Parse.Hooks.createTrigger("SomeRandomObject", "afterSave" ,hookServerURL+"/AfterSaveSome").then(function(){
|
||||
// The function is deleted as the DB is dropped between calls
|
||||
Parse.Hooks.createTrigger("SomeRandomObject", "afterSave", hookServerURL + "/AfterSaveSome").then(function(){
|
||||
const obj = new Parse.Object("SomeRandomObject");
|
||||
return obj.save();
|
||||
}).then(function() {
|
||||
var promise = new Parse.Promise();
|
||||
// Wait a bit here as it's an after save
|
||||
setTimeout(function(){
|
||||
// Wait a bit here as it's an after save
|
||||
setTimeout(() => {
|
||||
expect(triggerCount).toBe(1);
|
||||
var q = new Parse.Query("AnotherObject");
|
||||
q.get(newObjectId).then(function(r){
|
||||
promise.resolve(r);
|
||||
});
|
||||
}, 300)
|
||||
new Parse.Query("AnotherObject")
|
||||
.get(newObjectId)
|
||||
.then((r) => promise.resolve(r));
|
||||
}, 500);
|
||||
return promise;
|
||||
}).then(function(res){
|
||||
expect(res.get("foo")).toEqual("bar");
|
||||
|
||||
@@ -12,6 +12,10 @@ let config;
|
||||
let database;
|
||||
const defaultColumns = require('../src/Controllers/SchemaController').defaultColumns;
|
||||
|
||||
const delay = function delay(delay) {
|
||||
return new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
const installationSchema = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation) };
|
||||
|
||||
describe('Installations', () => {
|
||||
@@ -542,12 +546,14 @@ describe('Installations', () => {
|
||||
};
|
||||
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||
})
|
||||
.then(() => delay(100))
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId1}, {}))
|
||||
.then((results) => {
|
||||
expect(results.length).toEqual(1);
|
||||
firstObject = results[0];
|
||||
return database.adapter.find('_Installation', installationSchema, {installationId: installId2}, {});
|
||||
})
|
||||
.then(() => delay(100))
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId2}, {}))
|
||||
.then(results => {
|
||||
expect(results.length).toEqual(1);
|
||||
secondObject = results[0];
|
||||
@@ -558,6 +564,7 @@ describe('Installations', () => {
|
||||
};
|
||||
return rest.update(config, auth.nobody(config), '_Installation', secondObject.objectId, input);
|
||||
})
|
||||
.then(() => delay(100))
|
||||
.then(() => database.adapter.find('_Installation', installationSchema, {objectId: firstObject.objectId}, {}))
|
||||
.then(results => {
|
||||
// The first object should have been deleted
|
||||
|
||||
@@ -1390,7 +1390,7 @@ describe('Parse.Object testing', () => {
|
||||
}
|
||||
equal(itemsAgain.length, numItems, "Should get the array back");
|
||||
itemsAgain.forEach(function(item, i) {
|
||||
var newValue = i*2;
|
||||
var newValue = i * 2;
|
||||
item.set("x", newValue);
|
||||
});
|
||||
return Parse.Object.saveAll(itemsAgain);
|
||||
@@ -1400,7 +1400,7 @@ describe('Parse.Object testing', () => {
|
||||
equal(fetchedItemsAgain.length, numItems,
|
||||
"Number of items fetched should not change");
|
||||
fetchedItemsAgain.forEach(function(item, i) {
|
||||
equal(item.get("x"), i*2);
|
||||
equal(item.get("x"), i * 2);
|
||||
});
|
||||
done();
|
||||
});
|
||||
@@ -1457,7 +1457,7 @@ describe('Parse.Object testing', () => {
|
||||
}
|
||||
equal(itemsAgain.length, numItems, "Should get the array back");
|
||||
itemsAgain.forEach(function(item, i) {
|
||||
var newValue = i*2;
|
||||
var newValue = i * 2;
|
||||
item.set("x", newValue);
|
||||
});
|
||||
return Parse.Object.saveAll(itemsAgain);
|
||||
@@ -1467,7 +1467,7 @@ describe('Parse.Object testing', () => {
|
||||
equal(fetchedItemsAgain.length, numItems,
|
||||
"Number of items fetched should not change");
|
||||
fetchedItemsAgain.forEach(function(item, i) {
|
||||
equal(item.get("x"), i*2);
|
||||
equal(item.get("x"), i * 2);
|
||||
});
|
||||
done();
|
||||
},
|
||||
@@ -1581,7 +1581,7 @@ describe('Parse.Object testing', () => {
|
||||
return;
|
||||
}
|
||||
itemsAgain.forEach(function(item, i) {
|
||||
item.set("x", i*2);
|
||||
item.set("x", i * 2);
|
||||
});
|
||||
return Parse.Object.saveAll(itemsAgain);
|
||||
}).then(function() {
|
||||
@@ -1619,7 +1619,7 @@ describe('Parse.Object testing', () => {
|
||||
return;
|
||||
}
|
||||
itemsAgain.forEach(function(item, i) {
|
||||
item.set("x", i*2);
|
||||
item.set("x", i * 2);
|
||||
});
|
||||
return Parse.Object.saveAll(itemsAgain);
|
||||
}).then(function() {
|
||||
|
||||
@@ -1631,8 +1631,9 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
expect(total).toBe(0);
|
||||
done()
|
||||
}, () => {
|
||||
}, (e) => {
|
||||
fail('should not fail');
|
||||
fail(JSON.stringify(e));
|
||||
done();
|
||||
})
|
||||
});
|
||||
@@ -1652,8 +1653,8 @@ describe('Parse.Query testing', () => {
|
||||
}
|
||||
Parse.Object.saveAll(objects).then(() => {
|
||||
const object = new Parse.Object("AContainer");
|
||||
for (var i=0; i<objects.length; i++) {
|
||||
if (i%2 == 0) {
|
||||
for (var i = 0; i < objects.length; i++) {
|
||||
if (i % 2 == 0) {
|
||||
objects[i].id = 'randomThing'
|
||||
} else {
|
||||
total += objects[i].get('key');
|
||||
@@ -2401,7 +2402,7 @@ describe('Parse.Query testing', () => {
|
||||
it('query match on array with multiple objects', (done) => {
|
||||
var target1 = {__type: 'Pointer', className: 'TestObject', objectId: 'abc'};
|
||||
var target2 = {__type: 'Pointer', className: 'TestObject', objectId: '123'};
|
||||
var obj= new Parse.Object('TestObject');
|
||||
var obj = new Parse.Object('TestObject');
|
||||
obj.set('someObjs', [target1, target2]);
|
||||
obj.save().then(() => {
|
||||
var query = new Parse.Query('TestObject');
|
||||
|
||||
@@ -575,6 +575,9 @@ describe('Parse.Relation testing', () => {
|
||||
expect(result.get('key').get('even')).toBe(false);
|
||||
});
|
||||
done();
|
||||
}, (e) => {
|
||||
fail(JSON.stringify(e));
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -613,6 +616,9 @@ describe('Parse.Relation testing', () => {
|
||||
done();
|
||||
}
|
||||
}));
|
||||
}, (e) => {
|
||||
fail(JSON.stringify(e));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -653,6 +659,9 @@ describe('Parse.Relation testing', () => {
|
||||
done();
|
||||
}
|
||||
}));
|
||||
}, (e) => {
|
||||
fail(JSON.stringify(e));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+20
-2
@@ -124,7 +124,7 @@ describe('Parse Role testing', () => {
|
||||
expect(roles.length).toEqual(4);
|
||||
|
||||
allRoles.forEach(function(name) {
|
||||
expect(roles.indexOf("role:"+name)).not.toBe(-1);
|
||||
expect(roles.indexOf("role:" + name)).not.toBe(-1);
|
||||
});
|
||||
|
||||
// 1 Query for the initial setup
|
||||
@@ -165,7 +165,7 @@ describe('Parse Role testing', () => {
|
||||
}).then((roles) => {
|
||||
expect(roles.length).toEqual(3);
|
||||
rolesNames.forEach((name) => {
|
||||
expect(roles.indexOf('role:'+name)).not.toBe(-1);
|
||||
expect(roles.indexOf('role:' + name)).not.toBe(-1);
|
||||
});
|
||||
done();
|
||||
}, function(){
|
||||
@@ -192,6 +192,24 @@ describe('Parse Role testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Different _Role objects cannot have the same name.", (done) => {
|
||||
const roleName = "MyRole";
|
||||
let aUser;
|
||||
createTestUser().then((user) => {
|
||||
aUser = user;
|
||||
return createRole(roleName, null, aUser);
|
||||
}).then((firstRole) => {
|
||||
expect(firstRole.getName()).toEqual(roleName);
|
||||
return createRole(roleName, null, aUser);
|
||||
}).then(() => {
|
||||
fail("_Role cannot have the same name as another role");
|
||||
done();
|
||||
}, (error) => {
|
||||
expect(error.code).toEqual(137);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("Should properly resolve roles", (done) => {
|
||||
const admin = new Parse.Role("Admin", new Parse.ACL());
|
||||
const moderator = new Parse.Role("Moderator", new Parse.ACL());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const ParseServerRESTController = require('../src/ParseServerRESTController').ParseServerRESTController;
|
||||
const ParseServer = require('../src/ParseServer').default;
|
||||
const Parse = require('parse/node').Parse;
|
||||
|
||||
let RESTController;
|
||||
|
||||
describe('ParseServerRESTController', () => {
|
||||
@@ -103,6 +105,28 @@ describe('ParseServerRESTController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures no user is created when passing an empty username', (done) => {
|
||||
RESTController.request("POST", "/classes/_User", {username: "", password: "world"}).then(() => {
|
||||
jfail(new Error('Success callback should not be called when passing an empty username.'));
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(Parse.Error.USERNAME_MISSING);
|
||||
expect(err.message).toBe('bad or missing username');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures no user is created when passing an empty password', (done) => {
|
||||
RESTController.request("POST", "/classes/_User", {username: "hello", password: ""}).then(() => {
|
||||
jfail(new Error('Success callback should not be called when passing an empty password.'));
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(Parse.Error.PASSWORD_MISSING);
|
||||
expect(err.message).toBe('password is required');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures no session token is created on creating users', (done) => {
|
||||
RESTController.request("POST", "/classes/_User", {username: "hello", password: "world"}).then((user) => {
|
||||
expect(user.sessionToken).toBeUndefined();
|
||||
|
||||
@@ -1678,7 +1678,7 @@ describe('Parse.User testing', () => {
|
||||
const userId = model.id;
|
||||
Parse.User.logOut().then(() => {
|
||||
request.post({
|
||||
url:Parse.serverURL+'/classes/_User',
|
||||
url:Parse.serverURL + '/classes/_User',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest'
|
||||
@@ -1688,7 +1688,7 @@ describe('Parse.User testing', () => {
|
||||
// make sure the location header is properly set
|
||||
expect(userId).not.toBeUndefined();
|
||||
expect(body.objectId).toEqual(userId);
|
||||
expect(res.headers.location).toEqual(Parse.serverURL+'/users/'+userId);
|
||||
expect(res.headers.location).toEqual(Parse.serverURL + '/users/' + userId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,9 @@ describe('ParseWebSocketServer', function() {
|
||||
|
||||
it('can handle connect event when ws is open', function(done) {
|
||||
var onConnectCallback = jasmine.createSpy('onConnectCallback');
|
||||
var parseWebSocketServer = new ParseWebSocketServer({}, onConnectCallback, 5).server;
|
||||
var http = require('http');
|
||||
var server = http.createServer();
|
||||
var parseWebSocketServer = new ParseWebSocketServer(server, onConnectCallback, 5).server;
|
||||
var ws = {
|
||||
readyState: 0,
|
||||
OPEN: 0,
|
||||
@@ -27,6 +29,7 @@ describe('ParseWebSocketServer', function() {
|
||||
// Make sure we ping to the client
|
||||
setTimeout(function() {
|
||||
expect(ws.ping).toHaveBeenCalled();
|
||||
server.close();
|
||||
done();
|
||||
}, 10)
|
||||
});
|
||||
|
||||
@@ -219,6 +219,28 @@ describe("Password Policy: ", () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('signup should fail if password is empty', (done) => {
|
||||
const user = new Parse.User();
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
passwordPolicy: {
|
||||
validatorPattern: "^.{8,}" // password should contain at least 8 char
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
}).then(() => {
|
||||
user.setUsername("user1");
|
||||
user.setPassword("");
|
||||
user.set('email', 'user1@parse.com');
|
||||
user.signUp().then(() => {
|
||||
fail('Should have failed as password does not conform to the policy.');
|
||||
done();
|
||||
}).catch((error) => {
|
||||
expect(error.message).toEqual('Cannot sign up user with an empty password.');
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('signup should succeed if password conforms to the policy enforced using validatorPattern', (done) => {
|
||||
const user = new Parse.User();
|
||||
reconfigureServer({
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
const Parse = require('parse/node').Parse;
|
||||
const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter');
|
||||
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
|
||||
|
||||
//public schema
|
||||
const databaseOptions1 = {
|
||||
initOptions: {
|
||||
connect: function (client, dc, isFresh) {
|
||||
if (isFresh) {
|
||||
client.query('SET search_path = public');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//not exists schema
|
||||
const databaseOptions2 = {
|
||||
initOptions: {
|
||||
connect: function (client, dc, isFresh) {
|
||||
if (isFresh) {
|
||||
client.query('SET search_path = not_exists_schema');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const GameScore = Parse.Object.extend({
|
||||
className: "GameScore"
|
||||
});
|
||||
|
||||
describe('Postgres database init options', () => {
|
||||
it('create server with public schema databaseOptions,shoud be ok', (done) => {
|
||||
reconfigureServer({
|
||||
databaseAdapter: new PostgresStorageAdapter({
|
||||
uri: postgresURI, collectionPrefix: 'test_',
|
||||
databaseOptions: databaseOptions1
|
||||
})
|
||||
}).then(done, fail);
|
||||
});
|
||||
|
||||
it("save new GameScore in public schema", function (done) {
|
||||
var score = new GameScore({ "score": 1337, "playerName": "Sean Plott", "cheatMode": false });
|
||||
score.save().then(done, fail);
|
||||
});
|
||||
|
||||
it('create server with not exists schema databaseOptions,shoud be fail', (done) => {
|
||||
reconfigureServer({
|
||||
databaseAdapter: new PostgresStorageAdapter({
|
||||
uri: postgresURI, collectionPrefix: 'test_',
|
||||
databaseOptions: databaseOptions2
|
||||
})
|
||||
}).then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual('42P01');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
+134
-106
@@ -2,6 +2,7 @@
|
||||
var PushController = require('../src/Controllers/PushController').PushController;
|
||||
var StatusHandler = require('../src/StatusHandler');
|
||||
var Config = require('../src/Config');
|
||||
var validatePushType = require('../src/Push/utils').validatePushType;
|
||||
|
||||
const successfulTransmissions = function(body, installations) {
|
||||
|
||||
@@ -35,7 +36,7 @@ describe('PushController', () => {
|
||||
var validPushTypes = ['ios', 'android'];
|
||||
|
||||
expect(function(){
|
||||
PushController.validatePushType(where, validPushTypes);
|
||||
validatePushType(where, validPushTypes);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
@@ -48,7 +49,7 @@ describe('PushController', () => {
|
||||
var validPushTypes = ['ios', 'android'];
|
||||
|
||||
expect(function(){
|
||||
PushController.validatePushType(where, validPushTypes);
|
||||
validatePushType(where, validPushTypes);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
@@ -63,7 +64,7 @@ describe('PushController', () => {
|
||||
var validPushTypes = ['ios', 'android'];
|
||||
|
||||
expect(function(){
|
||||
PushController.validatePushType(where, validPushTypes);
|
||||
validatePushType(where, validPushTypes);
|
||||
}).not.toThrow();
|
||||
done();
|
||||
});
|
||||
@@ -76,7 +77,7 @@ describe('PushController', () => {
|
||||
var validPushTypes = ['ios', 'android'];
|
||||
|
||||
expect(function(){
|
||||
PushController.validatePushType(where, validPushTypes);
|
||||
validatePushType(where, validPushTypes);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
@@ -89,7 +90,7 @@ describe('PushController', () => {
|
||||
var validPushTypes = ['ios', 'android'];
|
||||
|
||||
expect(function(){
|
||||
PushController.validatePushType(where, validPushTypes);
|
||||
validatePushType(where, validPushTypes);
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
@@ -131,37 +132,13 @@ describe('PushController', () => {
|
||||
});
|
||||
|
||||
it('properly increment badges', (done) => {
|
||||
|
||||
var payload = {data:{
|
||||
alert: "Hello World!",
|
||||
badge: "Increment",
|
||||
}}
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
const installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
while(installations.length != 15) {
|
||||
const installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("deviceType", "android");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
var badge = body.data.badge;
|
||||
installations.forEach((installation) => {
|
||||
if (installation.deviceType == "ios") {
|
||||
expect(installation.badge).toEqual(badge);
|
||||
expect(installation.originalBadge+1).toEqual(installation.badge);
|
||||
expect(installation.originalBadge + 1).toEqual(installation.badge);
|
||||
} else {
|
||||
expect(installation.badge).toBeUndefined();
|
||||
}
|
||||
@@ -172,14 +149,39 @@ describe('PushController', () => {
|
||||
return ["ios", "android"];
|
||||
}
|
||||
}
|
||||
var payload = {data:{
|
||||
alert: "Hello World!",
|
||||
badge: "Increment",
|
||||
}}
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
const installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_" + installations.length);
|
||||
installation.set("deviceToken","device_token_" + installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
while(installations.length != 15) {
|
||||
const installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_" + installations.length);
|
||||
installation.set("deviceToken","device_token_" + installations.length)
|
||||
installation.set("deviceType", "android");
|
||||
installations.push(installation);
|
||||
}
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
|
||||
Parse.Object.saveAll(installations).then(() => {
|
||||
var pushController = new PushController();
|
||||
reconfigureServer({
|
||||
push: { adapter: pushAdapter }
|
||||
}).then(() => {
|
||||
return Parse.Object.saveAll(installations)
|
||||
}).then(() => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then(() => {
|
||||
done();
|
||||
@@ -187,26 +189,10 @@ describe('PushController', () => {
|
||||
jfail(err);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('properly set badges to 1', (done) => {
|
||||
|
||||
var payload = {data: {
|
||||
alert: "Hello World!",
|
||||
badge: 1,
|
||||
}}
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
var pushAdapter = {
|
||||
send: function(body, installations) {
|
||||
var badge = body.data.badge;
|
||||
@@ -221,13 +207,32 @@ describe('PushController', () => {
|
||||
}
|
||||
}
|
||||
|
||||
var payload = {data: {
|
||||
alert: "Hello World!",
|
||||
badge: 1,
|
||||
}}
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_" + installations.length);
|
||||
installation.set("deviceToken","device_token_" + installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
|
||||
Parse.Object.saveAll(installations).then(() => {
|
||||
var pushController = new PushController();
|
||||
reconfigureServer({
|
||||
push: { adapter: pushAdapter }
|
||||
}).then(() => {
|
||||
return Parse.Object.saveAll(installations)
|
||||
}).then(() => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then(() => {
|
||||
done();
|
||||
@@ -235,7 +240,6 @@ describe('PushController', () => {
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('properly set badges to 1 with complex query #2903 #3022', (done) => {
|
||||
@@ -249,8 +253,8 @@ describe('PushController', () => {
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("installationId", "installation_" + installations.length);
|
||||
installation.set("deviceToken","device_token_" + installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
@@ -276,9 +280,14 @@ describe('PushController', () => {
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
|
||||
Parse.Object.saveAll(installations).then((installations) => {
|
||||
var pushController = new PushController();
|
||||
reconfigureServer({
|
||||
push: {
|
||||
adapter: pushAdapter
|
||||
}
|
||||
}).then(() => {
|
||||
return Parse.Object.saveAll(installations)
|
||||
}).then((installations) => {
|
||||
const objectIds = installations.map(installation => {
|
||||
return installation.id;
|
||||
})
|
||||
@@ -286,6 +295,10 @@ describe('PushController', () => {
|
||||
objectId: {'$in': objectIds.slice(0, 5)}
|
||||
}
|
||||
return pushController.sendPush(payload, where, config, auth);
|
||||
}).then(() => {
|
||||
return new Promise((res) => {
|
||||
setTimeout(res, 300);
|
||||
});
|
||||
}).then(() => {
|
||||
expect(matchedInstallationsCount).toBe(5);
|
||||
const query = new Parse.Query(Parse.Installation);
|
||||
@@ -305,8 +318,8 @@ describe('PushController', () => {
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
const installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("installationId", "installation_" + installations.length);
|
||||
installation.set("deviceToken","device_token_" + installations.length)
|
||||
installation.set("badge", installations.length);
|
||||
installation.set("originalBadge", installations.length);
|
||||
installation.set("deviceType", "ios");
|
||||
@@ -315,8 +328,8 @@ describe('PushController', () => {
|
||||
|
||||
while(installations.length != 15) {
|
||||
const installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_"+installations.length);
|
||||
installation.set("deviceToken","device_token_"+installations.length)
|
||||
installation.set("installationId", "installation_" + installations.length);
|
||||
installation.set("deviceToken","device_token_" + installations.length)
|
||||
installation.set("deviceType", "android");
|
||||
installations.push(installation);
|
||||
}
|
||||
@@ -338,46 +351,50 @@ describe('PushController', () => {
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
|
||||
Parse.Object.saveAll(installations).then(() => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
var pushController = new PushController();
|
||||
reconfigureServer({
|
||||
push: { adapter: pushAdapter }
|
||||
}).then(() => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
}).then(() => {
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find({useMasterKey: true});
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
const result = results[0];
|
||||
expect(result.createdAt instanceof Date).toBe(true);
|
||||
expect(result.updatedAt instanceof Date).toBe(true);
|
||||
expect(result.id.length).toBe(10);
|
||||
expect(result.get('source')).toEqual('rest');
|
||||
expect(result.get('query')).toEqual(JSON.stringify({}));
|
||||
expect(typeof result.get('payload')).toEqual("string");
|
||||
expect(JSON.parse(result.get('payload'))).toEqual(payload.data);
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
expect(result.get('numSent')).toEqual(10);
|
||||
expect(result.get('sentPerType')).toEqual({
|
||||
'ios': 10 // 10 ios
|
||||
});
|
||||
expect(result.get('numFailed')).toEqual(5);
|
||||
expect(result.get('failedPerType')).toEqual({
|
||||
'android': 5 // android
|
||||
});
|
||||
return Parse.Object.saveAll(installations)
|
||||
})
|
||||
.then(() => {
|
||||
return pushController.sendPush(payload, {}, config, auth);
|
||||
}).then(() => {
|
||||
// it is enqueued so it can take time
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
}).then(() => {
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find({useMasterKey: true});
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
const result = results[0];
|
||||
expect(result.createdAt instanceof Date).toBe(true);
|
||||
expect(result.updatedAt instanceof Date).toBe(true);
|
||||
expect(result.id.length).toBe(10);
|
||||
expect(result.get('source')).toEqual('rest');
|
||||
expect(result.get('query')).toEqual(JSON.stringify({}));
|
||||
expect(typeof result.get('payload')).toEqual("string");
|
||||
expect(JSON.parse(result.get('payload'))).toEqual(payload.data);
|
||||
expect(result.get('status')).toEqual('succeeded');
|
||||
expect(result.get('numSent')).toEqual(10);
|
||||
expect(result.get('sentPerType')).toEqual({
|
||||
'ios': 10 // 10 ios
|
||||
});
|
||||
expect(result.get('numFailed')).toEqual(5);
|
||||
expect(result.get('failedPerType')).toEqual({
|
||||
'android': 5 // android
|
||||
});
|
||||
// Try to get it without masterKey
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
|
||||
const query = new Parse.Query('_PushStatus');
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
expect(results.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly report failures in _PushStatus', (done) => {
|
||||
@@ -404,8 +421,12 @@ describe('PushController', () => {
|
||||
var auth = {
|
||||
isMaster: true
|
||||
}
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
|
||||
pushController.sendPush(payload, where, config, auth).then(() => {
|
||||
var pushController = new PushController();
|
||||
reconfigureServer({
|
||||
push: { adapter: pushAdapter }
|
||||
}).then(() => {
|
||||
return pushController.sendPush(payload, where, config, auth)
|
||||
}).then(() => {
|
||||
fail('should not succeed');
|
||||
done();
|
||||
}).catch(() => {
|
||||
@@ -416,7 +437,7 @@ describe('PushController', () => {
|
||||
expect(pushStatus.get('status')).toBe('failed');
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('should support full RESTQuery for increment', (done) => {
|
||||
@@ -433,7 +454,6 @@ describe('PushController', () => {
|
||||
return ["ios"];
|
||||
}
|
||||
}
|
||||
|
||||
var config = new Config(Parse.applicationId);
|
||||
var auth = {
|
||||
isMaster: true
|
||||
@@ -450,8 +470,12 @@ describe('PushController', () => {
|
||||
}
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
|
||||
pushController.sendPush(payload, where, config, auth).then(() => {
|
||||
var pushController = new PushController();
|
||||
reconfigureServer({
|
||||
push: { adapter: pushAdapter }
|
||||
}).then(() => {
|
||||
return pushController.sendPush(payload, where, config, auth);
|
||||
}).then(() => {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
@@ -491,8 +515,12 @@ describe('PushController', () => {
|
||||
}
|
||||
}
|
||||
|
||||
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
|
||||
pushController.sendPush(payload, where, config, auth).then(() => {
|
||||
var pushController = new PushController();
|
||||
reconfigureServer({
|
||||
push: { adapter: pushAdapter }
|
||||
}).then(() => {
|
||||
return pushController.sendPush(payload, where, config, auth)
|
||||
}).then(() => {
|
||||
done();
|
||||
}).catch(() => {
|
||||
fail('should not fail');
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('PushRouter', () => {
|
||||
|
||||
it('sends a push through REST', (done) => {
|
||||
request.post({
|
||||
url: Parse.serverURL+"/push",
|
||||
url: Parse.serverURL + "/push",
|
||||
json: true,
|
||||
body: {
|
||||
'channels': {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
var PushWorker = require('../src').PushWorker;
|
||||
var Config = require('../src/Config');
|
||||
|
||||
describe('PushWorker', () => {
|
||||
it('should run with small batch', (done) => {
|
||||
const batchSize = 3;
|
||||
var sendCount = 0;
|
||||
reconfigureServer({
|
||||
push: {
|
||||
queueOptions: {
|
||||
disablePushWorker: true,
|
||||
batchSize
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
expect(new Config('test').pushWorker).toBeUndefined();
|
||||
new PushWorker({
|
||||
send: (body, installations) => {
|
||||
expect(installations.length <= batchSize).toBe(true);
|
||||
sendCount += installations.length;
|
||||
return Promise.resolve();
|
||||
},
|
||||
getValidPushTypes: function() {
|
||||
return ['ios', 'android']
|
||||
}
|
||||
});
|
||||
var installations = [];
|
||||
while(installations.length != 10) {
|
||||
var installation = new Parse.Object("_Installation");
|
||||
installation.set("installationId", "installation_" + installations.length);
|
||||
installation.set("deviceToken","device_token_" + installations.length)
|
||||
installation.set("badge", 1);
|
||||
installation.set("deviceType", "ios");
|
||||
installations.push(installation);
|
||||
}
|
||||
return Parse.Object.saveAll(installations);
|
||||
}).then(() => {
|
||||
return Parse.Push.send({
|
||||
where: {
|
||||
deviceType: 'ios'
|
||||
},
|
||||
data: {
|
||||
alert: 'Hello world!'
|
||||
}
|
||||
}, {useMasterKey: true})
|
||||
}).then(() => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
}).then(() => {
|
||||
expect(sendCount).toBe(10);
|
||||
done();
|
||||
}).catch(err => {
|
||||
jfail(err);
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
var RedisCacheAdapter = require('../src/Adapters/Cache/RedisCacheAdapter').default;
|
||||
/*
|
||||
To run this test part of the complete suite
|
||||
set PARSE_SERVER_TEST_CACHE='redis'
|
||||
and make sure a redis server is available on the default port
|
||||
*/
|
||||
describe_only(() => {
|
||||
return process.env.PARSE_SERVER_TEST_CACHE === 'redis';
|
||||
})('RedisCacheAdapter', function() {
|
||||
var KEY = 'hello';
|
||||
var VALUE = 'world';
|
||||
|
||||
function wait(sleep) {
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, sleep);
|
||||
})
|
||||
}
|
||||
|
||||
it('should get/set/clear', (done) => {
|
||||
var cache = new RedisCacheAdapter({
|
||||
ttl: NaN
|
||||
});
|
||||
|
||||
cache.put(KEY, VALUE)
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(VALUE))
|
||||
.then(() => cache.clear())
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(null))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('should expire after ttl', (done) => {
|
||||
var cache = new RedisCacheAdapter(null, 1);
|
||||
|
||||
cache.put(KEY, VALUE)
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(VALUE))
|
||||
.then(wait.bind(null, 2))
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(null))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
it('should find un-expired records', (done) => {
|
||||
var cache = new RedisCacheAdapter(null, 5);
|
||||
|
||||
cache.put(KEY, VALUE)
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).toEqual(VALUE))
|
||||
.then(wait.bind(null, 1))
|
||||
.then(() => cache.get(KEY))
|
||||
.then((value) => expect(value).not.toEqual(null))
|
||||
.then(done);
|
||||
});
|
||||
});
|
||||
@@ -71,7 +71,7 @@ describe_only_db('mongo')('revocable sessions', () => {
|
||||
|
||||
it('should not upgrade bad legacy session token', done => {
|
||||
rp.post({
|
||||
url: Parse.serverURL+'/upgradeToRevocableSession',
|
||||
url: Parse.serverURL + '/upgradeToRevocableSession',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Rest-API-Key': 'rest',
|
||||
@@ -92,7 +92,7 @@ describe_only_db('mongo')('revocable sessions', () => {
|
||||
|
||||
it('should not crash without session token #2720', done => {
|
||||
rp.post({
|
||||
url: Parse.serverURL+'/upgradeToRevocableSession',
|
||||
url: Parse.serverURL + '/upgradeToRevocableSession',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Rest-API-Key': 'rest'
|
||||
|
||||
+2
-1
@@ -494,7 +494,8 @@ describe('SchemaController', () => {
|
||||
|
||||
it('creates non-custom classes which include relation field', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('_Role', {}))
|
||||
//as `_Role` is always created by default, we only get it here
|
||||
.then(schema => schema.getOneSchema('_Role'))
|
||||
.then(actualSchema => {
|
||||
const expectedSchema = {
|
||||
className: '_Role',
|
||||
|
||||
@@ -4,13 +4,14 @@ describe('SessionTokenCache', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
var Parse = require('parse/node');
|
||||
// Mock parse
|
||||
var mockUser = {
|
||||
become: jasmine.createSpy('become').and.returnValue(Parse.Promise.as({
|
||||
id: 'userId'
|
||||
}))
|
||||
}
|
||||
jasmine.mockLibrary('parse/node', 'User', mockUser);
|
||||
|
||||
spyOn(Parse, "Query").and.returnValue({
|
||||
first: jasmine.createSpy("first").and.returnValue(Parse.Promise.as(new Parse.Object("_Session", {
|
||||
user: new Parse.User({id:"userId"})
|
||||
}))),
|
||||
equalTo: function(){}
|
||||
})
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -46,7 +47,4 @@ describe('SessionTokenCache', function() {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
jasmine.restoreLibrary('parse/node', 'User');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('Twitter Auth', () => {
|
||||
consumer_key: 'hello'
|
||||
}, {
|
||||
consumer_key: 'world'
|
||||
}]).consumer_key).toEqual('hello')
|
||||
}]).consumer_key).toEqual('hello');
|
||||
|
||||
// Multiple options, consumer_key not found
|
||||
expect(function(){
|
||||
@@ -47,4 +47,18 @@ describe('Twitter Auth', () => {
|
||||
consumer_key: 'hello'
|
||||
}).consumer_key).toEqual('hello');
|
||||
});
|
||||
|
||||
it("Should fail with missing options", (done) => {
|
||||
try {
|
||||
twitter.validateAuthData({
|
||||
consumer_key: 'key',
|
||||
consumer_secret: 'secret',
|
||||
auth_token: 'token',
|
||||
auth_token_secret: 'secret'
|
||||
}, undefined);
|
||||
} catch (error) {
|
||||
jequal(error.message, 'Twitter auth configuration missing');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -383,7 +383,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
|
||||
fail('sending password reset email should not have succeeded');
|
||||
done();
|
||||
}, error => {
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.')
|
||||
done();
|
||||
});
|
||||
})
|
||||
@@ -414,7 +414,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
|
||||
fail('sending password reset email should not have succeeded');
|
||||
done();
|
||||
}, error => {
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.')
|
||||
done();
|
||||
});
|
||||
})
|
||||
@@ -442,7 +442,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
|
||||
fail('sending password reset email should not have succeeded');
|
||||
done();
|
||||
}, error => {
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
|
||||
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.')
|
||||
done();
|
||||
});
|
||||
})
|
||||
|
||||
+10
-2
@@ -1,6 +1,6 @@
|
||||
"use strict"
|
||||
// Sets up a Parse API server for testing.
|
||||
const SpecReporter = require('jasmine-spec-reporter');
|
||||
const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 5000;
|
||||
|
||||
@@ -167,7 +167,7 @@ Parse.serverURL = 'http://localhost:' + port + '/1';
|
||||
Parse.Promise.disableAPlusCompliant();
|
||||
|
||||
// 10 minutes timeout
|
||||
beforeAll(startDB, 10*60*1000);
|
||||
beforeAll(startDB, 10 * 60 * 1000);
|
||||
|
||||
afterAll(stopDB);
|
||||
|
||||
@@ -419,6 +419,14 @@ global.describe_only_db = db => {
|
||||
}
|
||||
}
|
||||
|
||||
global.describe_only = (validator) =>{
|
||||
if (validator()) {
|
||||
return describe;
|
||||
} else {
|
||||
return xdescribe;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var libraryCache = {};
|
||||
jasmine.mockLibrary = function(library, name, mock) {
|
||||
|
||||
+1
-1
@@ -374,7 +374,7 @@ describe('rest create', () => {
|
||||
|
||||
var session = r.results[0];
|
||||
var actual = new Date(session.expiresAt.iso);
|
||||
var expected = new Date(now.getTime() + (sessionLength*1000));
|
||||
var expected = new Date(now.getTime() + (sessionLength * 1000));
|
||||
|
||||
expect(actual.getFullYear()).toEqual(expected.getFullYear());
|
||||
expect(actual.getMonth()).toEqual(expected.getMonth());
|
||||
|
||||
+21
-4
@@ -102,6 +102,20 @@ const userSchema = {
|
||||
"classLevelPermissions": defaultClassLevelPermissions,
|
||||
}
|
||||
|
||||
const roleSchema = {
|
||||
"className": "_Role",
|
||||
"fields": {
|
||||
"objectId": {"type": "String"},
|
||||
"createdAt": {"type": "Date"},
|
||||
"updatedAt": {"type": "Date"},
|
||||
"ACL": {"type": "ACL"},
|
||||
"name": {"type":"String"},
|
||||
"users": {"type":"Relation", "targetClass":"_User"},
|
||||
"roles": {"type":"Relation", "targetClass":"_Role"}
|
||||
},
|
||||
"classLevelPermissions": defaultClassLevelPermissions,
|
||||
}
|
||||
|
||||
var noAuthHeaders = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
};
|
||||
@@ -166,7 +180,10 @@ describe('schemas', () => {
|
||||
json: true,
|
||||
headers: masterKeyHeaders,
|
||||
}, (error, response, body) => {
|
||||
expect(dd(body.results, [userSchema])).toEqual();
|
||||
var expected = {
|
||||
results: [userSchema,roleSchema]
|
||||
};
|
||||
expect(dd(body.results.sort((s1, s2) => s1.className > s2.className), expected.results.sort((s1, s2) => s1.className > s2.className))).toEqual(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -186,9 +203,9 @@ describe('schemas', () => {
|
||||
headers: masterKeyHeaders,
|
||||
}, (error, response, body) => {
|
||||
var expected = {
|
||||
results: [userSchema,plainOldDataSchema,pointersAndRelationsSchema]
|
||||
results: [userSchema,roleSchema,plainOldDataSchema,pointersAndRelationsSchema]
|
||||
};
|
||||
expect(dd(body, expected)).toEqual(undefined);
|
||||
expect(dd(body.results.sort((s1, s2) => s1.className > s2.className), expected.results.sort((s1, s2) => s1.className > s2.className))).toEqual(undefined);
|
||||
done();
|
||||
})
|
||||
});
|
||||
@@ -1230,7 +1247,7 @@ describe('schemas', () => {
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
op({
|
||||
url: 'http://localhost:8378/1/schemas/'+className,
|
||||
url: 'http://localhost:8378/1/schemas/' + className,
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
body: {
|
||||
|
||||
@@ -98,7 +98,7 @@ export class AccountLockout {
|
||||
const now = new Date();
|
||||
|
||||
const updateFields = {
|
||||
_account_lockout_expires_at: Parse._encode(new Date(now.getTime() + this._config.accountLockout.duration*60*1000))
|
||||
_account_lockout_expires_at: Parse._encode(new Date(now.getTime() + this._config.accountLockout.duration * 60 * 1000))
|
||||
};
|
||||
|
||||
this._config.database.update('_User', query, updateFields)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
var https = require('https'),
|
||||
crypto = require('crypto');
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
var OAuth = function(options) {
|
||||
if(!options) {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'No options passed to OAuth');
|
||||
}
|
||||
this.consumer_key = options.consumer_key;
|
||||
this.consumer_secret = options.consumer_secret;
|
||||
this.auth_token = options.auth_token;
|
||||
@@ -36,7 +40,7 @@ OAuth.prototype.send = function(method, path, params, body){
|
||||
|
||||
OAuth.prototype.buildRequest = function(method, path, params, body) {
|
||||
if (path.indexOf("/") != 0) {
|
||||
path = "/"+path;
|
||||
path = "/" + path;
|
||||
}
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
path += "?" + OAuth.buildParameterString(params);
|
||||
@@ -118,7 +122,7 @@ OAuth.nonce = function(){
|
||||
var text = "";
|
||||
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for(var i=0; i < 30; i++)
|
||||
for(var i = 0; i < 30; i++)
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
|
||||
return text;
|
||||
@@ -162,7 +166,7 @@ OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_to
|
||||
oauth_parameters.oauth_nonce = OAuth.nonce();
|
||||
}
|
||||
if (!oauth_parameters.oauth_timestamp) {
|
||||
oauth_parameters.oauth_timestamp = Math.floor(new Date().getTime()/1000);
|
||||
oauth_parameters.oauth_timestamp = Math.floor(new Date().getTime() / 1000);
|
||||
}
|
||||
if (!oauth_parameters.oauth_signature_method) {
|
||||
oauth_parameters.oauth_signature_method = OAuth.signatureMethod;
|
||||
@@ -172,7 +176,7 @@ OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_to
|
||||
}
|
||||
|
||||
if(!auth_token_secret){
|
||||
auth_token_secret="";
|
||||
auth_token_secret = "";
|
||||
}
|
||||
// Force GET method if unset
|
||||
if (!request.method) {
|
||||
@@ -193,7 +197,7 @@ OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_to
|
||||
var parameterString = OAuth.buildParameterString(signatureParams);
|
||||
|
||||
// Build the signature string
|
||||
var url = "https://"+request.host+""+request.path;
|
||||
var url = "https://" + request.host + "" + request.path;
|
||||
|
||||
var signatureString = OAuth.buildSignatureString(request.method, url, parameterString);
|
||||
// Hash the signature string
|
||||
@@ -210,7 +214,7 @@ OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_to
|
||||
// Set the authorization header
|
||||
var authHeader = Object.keys(oauth_parameters).sort().map(function(key){
|
||||
var value = oauth_parameters[key];
|
||||
return key+'="'+value+'"';
|
||||
return key + '="' + value + '"';
|
||||
}).join(", ")
|
||||
|
||||
request.headers.Authorization = 'OAuth ' + authHeader;
|
||||
|
||||
@@ -27,7 +27,7 @@ function request(path, access_token) {
|
||||
host: 'api.github.com',
|
||||
path: '/' + path,
|
||||
headers: {
|
||||
'Authorization': 'bearer '+access_token,
|
||||
'Authorization': 'bearer ' + access_token,
|
||||
'User-Agent': 'parse-server'
|
||||
}
|
||||
}, function(res) {
|
||||
|
||||
@@ -3,7 +3,7 @@ var https = require('https');
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
function validateIdToken(id, token) {
|
||||
return request("tokeninfo?id_token="+token)
|
||||
return request("tokeninfo?id_token=" + token)
|
||||
.then((response) => {
|
||||
if (response && (response.sub == id || response.user_id == id)) {
|
||||
return;
|
||||
@@ -15,7 +15,7 @@ function validateIdToken(id, token) {
|
||||
}
|
||||
|
||||
function validateAuthToken(id, token) {
|
||||
return request("tokeninfo?access_token="+token)
|
||||
return request("tokeninfo?access_token=" + token)
|
||||
.then((response) => {
|
||||
if (response && (response.sub == id || response.user_id == id)) {
|
||||
return;
|
||||
|
||||
@@ -4,7 +4,7 @@ var Parse = require('parse/node').Parse;
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return request("users/self/?access_token="+authData.access_token)
|
||||
return request("users/self/?access_token=" + authData.access_token)
|
||||
.then((response) => {
|
||||
if (response && response.data && response.data.id == authData.id) {
|
||||
return;
|
||||
|
||||
@@ -27,7 +27,7 @@ function request(path, access_token) {
|
||||
host: 'api.meetup.com',
|
||||
path: '/2/' + path,
|
||||
headers: {
|
||||
'Authorization': 'bearer '+access_token
|
||||
'Authorization': 'bearer ' + access_token
|
||||
}
|
||||
}, function(res) {
|
||||
var data = '';
|
||||
|
||||
@@ -26,12 +26,12 @@ function graphRequest(path) {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', function () {
|
||||
var starPos=data.indexOf("(");
|
||||
var endPos=data.indexOf(")");
|
||||
if(starPos==-1||endPos==-1){
|
||||
var starPos = data.indexOf("(");
|
||||
var endPos = data.indexOf(")");
|
||||
if(starPos == -1 || endPos == -1){
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.');
|
||||
}
|
||||
data=data.substring(starPos+1,endPos-1);
|
||||
data = data.substring(starPos + 1,endPos - 1);
|
||||
data = JSON.parse(data);
|
||||
resolve(data);
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ function request(path, access_token) {
|
||||
host: 'api.spotify.com',
|
||||
path: '/v1/' + path,
|
||||
headers: {
|
||||
'Authorization': 'Bearer '+access_token
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
}, function(res) {
|
||||
var data = '';
|
||||
|
||||
@@ -5,6 +5,9 @@ var logger = require('../../logger').default;
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData, options) {
|
||||
if(!options) {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Twitter auth configuration missing');
|
||||
}
|
||||
options = handleMultipleConfigurations(authData, options);
|
||||
var client = new OAuth(options);
|
||||
client.host = "api.twitter.com";
|
||||
@@ -12,7 +15,7 @@ function validateAuthData(authData, options) {
|
||||
client.auth_token_secret = authData.auth_token_secret;
|
||||
|
||||
return client.get("/1.1/account/verify_credentials.json").then((data) => {
|
||||
if (data && data.id_str == ''+authData.id) {
|
||||
if (data && data.id_str == '' + authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
|
||||
@@ -4,7 +4,7 @@ var Parse = require('parse/node').Parse;
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return graphRequest('auth?access_token=' + authData.access_token +'&openid=' +authData.id).then(function (data) {
|
||||
return graphRequest('auth?access_token=' + authData.access_token + '&openid=' + authData.id).then(function (data) {
|
||||
if (data.errcode == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ function debug() {
|
||||
|
||||
export class RedisCacheAdapter {
|
||||
|
||||
constructor(ctx) {
|
||||
this.client = redis.createClient(ctx);
|
||||
constructor(redisCtx, ttl = DEFAULT_REDIS_TTL) {
|
||||
this.client = redis.createClient(redisCtx);
|
||||
this.p = Promise.resolve();
|
||||
this.ttl = ttl;
|
||||
}
|
||||
|
||||
get(key) {
|
||||
@@ -30,7 +31,7 @@ export class RedisCacheAdapter {
|
||||
return this.p;
|
||||
}
|
||||
|
||||
put(key, value, ttl = DEFAULT_REDIS_TTL) {
|
||||
put(key, value, ttl = this.ttl) {
|
||||
value = JSON.stringify(value);
|
||||
debug('put', key, value, ttl);
|
||||
if (ttl === 0) {
|
||||
@@ -71,7 +72,7 @@ export class RedisCacheAdapter {
|
||||
debug('clear');
|
||||
this.p = this.p.then(() => {
|
||||
return new Promise((resolve) => {
|
||||
this.client.flushall(function() {
|
||||
this.client.flushdb(function() {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import events from 'events';
|
||||
|
||||
const emitter = new events.EventEmitter();
|
||||
const subscriptions = new Map();
|
||||
|
||||
function unsubscribe(channel: string) {
|
||||
if (!subscriptions.has(channel)) {
|
||||
//console.log('No channel to unsub from');
|
||||
return;
|
||||
}
|
||||
//console.log('unsub ', channel);
|
||||
emitter.removeListener(channel, subscriptions.get(channel));
|
||||
subscriptions.delete(channel);
|
||||
}
|
||||
|
||||
class Publisher {
|
||||
emitter: any;
|
||||
|
||||
constructor(emitter: any) {
|
||||
this.emitter = emitter;
|
||||
}
|
||||
|
||||
publish(channel: string, message: string): void {
|
||||
this.emitter.emit(channel, message);
|
||||
}
|
||||
}
|
||||
|
||||
class Consumer extends events.EventEmitter {
|
||||
emitter: any;
|
||||
|
||||
constructor(emitter: any) {
|
||||
super();
|
||||
this.emitter = emitter;
|
||||
}
|
||||
|
||||
subscribe(channel: string): void {
|
||||
unsubscribe(channel);
|
||||
const handler = (message) => {
|
||||
this.emit('message', channel, message);
|
||||
}
|
||||
subscriptions.set(channel, handler);
|
||||
this.emitter.on(channel, handler);
|
||||
}
|
||||
|
||||
unsubscribe(channel: string): void {
|
||||
unsubscribe(channel);
|
||||
}
|
||||
}
|
||||
|
||||
function createPublisher(): any {
|
||||
return new Publisher(emitter);
|
||||
}
|
||||
|
||||
function createSubscriber(): any {
|
||||
return new Consumer(emitter);
|
||||
}
|
||||
|
||||
const EventEmitterMQ = {
|
||||
createPublisher,
|
||||
createSubscriber
|
||||
}
|
||||
|
||||
export {
|
||||
EventEmitterMQ
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// @flow
|
||||
/*eslint no-unused-vars: "off"*/
|
||||
// Push Adapter
|
||||
//
|
||||
@@ -11,13 +12,15 @@
|
||||
// android push and APNS for ios push.
|
||||
|
||||
export class PushAdapter {
|
||||
send(devices, installations, pushStatus) { }
|
||||
send(body: any, installations: any[], pushStatus: any): ?Promise<*> {}
|
||||
|
||||
/**
|
||||
* Get an array of valid push types.
|
||||
* @returns {Array} An array of valid push types
|
||||
*/
|
||||
getValidPushTypes() {}
|
||||
getValidPushTypes(): string[] {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export default PushAdapter;
|
||||
|
||||
@@ -97,6 +97,17 @@ export class MongoStorageAdapter {
|
||||
|
||||
// MaxTimeMS is not a global MongoDB client option, it is applied per operation.
|
||||
this._maxTimeMS = mongoOptions.maxTimeMS;
|
||||
process.on('SIGTERM', this.handleShutdown(this));
|
||||
process.on('SIGINT', this.handleShutdown(this));
|
||||
}
|
||||
|
||||
handleShutdown(storageAdapter) {
|
||||
return () => {
|
||||
if (!storageAdapter.database) {
|
||||
return;
|
||||
}
|
||||
storageAdapter.database.close(false);
|
||||
}
|
||||
}
|
||||
|
||||
connect() {
|
||||
|
||||
@@ -89,6 +89,9 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
|
||||
if (timeField && (typeof value === 'string')) {
|
||||
value = new Date(value);
|
||||
}
|
||||
if (restKey.indexOf('.') > 0) {
|
||||
return {key, value: restValue}
|
||||
}
|
||||
return {key, value};
|
||||
}
|
||||
|
||||
@@ -98,7 +101,7 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
|
||||
return {key, value};
|
||||
}
|
||||
|
||||
// Handle update operators
|
||||
// Handle update operators
|
||||
if (typeof restValue === 'object' && '__op' in restValue) {
|
||||
return {key, value: transformUpdateOperator(restValue, false)};
|
||||
}
|
||||
@@ -109,7 +112,7 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
|
||||
}
|
||||
|
||||
const transformInteriorValue = restValue => {
|
||||
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$')|| key.includes('.'))) {
|
||||
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
|
||||
}
|
||||
// Handle atomic values
|
||||
@@ -726,6 +729,11 @@ const nestedMongoObjectToNestedParseObject = mongoObject => {
|
||||
return BytesCoder.databaseToJSON(mongoObject);
|
||||
}
|
||||
|
||||
if (mongoObject.hasOwnProperty('__type') && mongoObject.__type == 'Date' && mongoObject.iso instanceof Date) {
|
||||
mongoObject.iso = mongoObject.iso.toJSON();
|
||||
return mongoObject;
|
||||
}
|
||||
|
||||
return _.mapValues(mongoObject, nestedMongoObjectToNestedParseObject);
|
||||
default:
|
||||
throw 'unknown js type';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const pgp = require('pg-promise')();
|
||||
|
||||
const parser = require('./PostgresConfigParser');
|
||||
|
||||
export function createClient(uri, databaseOptions) {
|
||||
@@ -13,11 +13,13 @@ export function createClient(uri, databaseOptions) {
|
||||
dbOptions[key] = databaseOptions[key];
|
||||
}
|
||||
|
||||
const initOptions = dbOptions.initOptions || {};
|
||||
const pgp = require('pg-promise')(initOptions);
|
||||
const client = pgp(dbOptions);
|
||||
|
||||
if (dbOptions.pgOptions) {
|
||||
for (const key in dbOptions.pgOptions) {
|
||||
client.pg.defaults[key] = dbOptions.pgOptions[key];
|
||||
pgp.pg.defaults[key] = dbOptions.pgOptions[key];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { createClient } from './PostgresClient';
|
||||
import Parse from 'parse/node';
|
||||
import _ from 'lodash';
|
||||
import sql from './sql';
|
||||
|
||||
const PostgresRelationDoesNotExistError = '42P01';
|
||||
const PostgresDuplicateRelationError = '42P07';
|
||||
const PostgresDuplicateColumnError = '42701';
|
||||
const PostgresDuplicateObjectError = '42710';
|
||||
const PostgresUniqueIndexViolationError = '23505';
|
||||
const PostgresTransactionAbortedError = '25P02';
|
||||
const logger = require('../../../logger');
|
||||
|
||||
const debug = function(){
|
||||
let args = [...arguments];
|
||||
args = ['PG: '+arguments[0]].concat(args.slice(1, args.length));
|
||||
args = ['PG: ' + arguments[0]].concat(args.slice(1, args.length));
|
||||
const log = logger.getLogger();
|
||||
log.debug.apply(log, args);
|
||||
}
|
||||
@@ -196,8 +198,8 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
}
|
||||
return `'${cmpt}'`;
|
||||
});
|
||||
let name = components.slice(0, components.length-1).join('->');
|
||||
name+='->>'+components[components.length-1];
|
||||
let name = components.slice(0, components.length - 1).join('->');
|
||||
name += '->>' + components[components.length - 1];
|
||||
patterns.push(`${name} = '${fieldValue}'`);
|
||||
} else if (typeof fieldValue === 'string') {
|
||||
patterns.push(`$${index}:name = $${index + 1}`);
|
||||
@@ -277,7 +279,7 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
if (baseArray.length > 0) {
|
||||
const not = notIn ? ' NOT ' : '';
|
||||
if (isArrayField) {
|
||||
patterns.push(`${not} array_contains($${index}:name, $${index+1})`);
|
||||
patterns.push(`${not} array_contains($${index}:name, $${index + 1})`);
|
||||
values.push(fieldName, JSON.stringify(baseArray));
|
||||
index += 2;
|
||||
} else {
|
||||
@@ -305,9 +307,9 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
}
|
||||
|
||||
if (Array.isArray(fieldValue.$all) && isArrayField) {
|
||||
patterns.push(`array_contains_all($${index}:name, $${index+1}::jsonb)`);
|
||||
patterns.push(`array_contains_all($${index}:name, $${index + 1}::jsonb)`);
|
||||
values.push(fieldName, JSON.stringify(fieldValue.$all));
|
||||
index+=2;
|
||||
index += 2;
|
||||
}
|
||||
|
||||
if (typeof fieldValue.$exists !== 'undefined') {
|
||||
@@ -323,9 +325,9 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
if (fieldValue.$nearSphere) {
|
||||
const point = fieldValue.$nearSphere;
|
||||
const distance = fieldValue.$maxDistance;
|
||||
const distanceInKM = distance*6371*1000;
|
||||
patterns.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index+1}, $${index+2})::geometry) <= $${index+3}`);
|
||||
sorts.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index+1}, $${index+2})::geometry) ASC`)
|
||||
const distanceInKM = distance * 6371 * 1000;
|
||||
patterns.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index + 1}, $${index + 2})::geometry) <= $${index + 3}`);
|
||||
sorts.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index + 1}, $${index + 2})::geometry) ASC`)
|
||||
values.push(fieldName, point.longitude, point.latitude, distanceInKM);
|
||||
index += 4;
|
||||
}
|
||||
@@ -337,7 +339,7 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
const right = box[1].longitude;
|
||||
const top = box[1].latitude;
|
||||
|
||||
patterns.push(`$${index}:name::point <@ $${index+1}::box`);
|
||||
patterns.push(`$${index}:name::point <@ $${index + 1}::box`);
|
||||
values.push(fieldName, `((${left}, ${bottom}), (${right}, ${top}))`);
|
||||
index += 2;
|
||||
}
|
||||
@@ -357,7 +359,7 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
|
||||
regex = processRegexPattern(regex);
|
||||
|
||||
patterns.push(`$${index}:name ${operator} '$${index+1}:raw'`);
|
||||
patterns.push(`$${index}:name ${operator} '$${index + 1}:raw'`);
|
||||
values.push(fieldName, regex);
|
||||
index += 2;
|
||||
}
|
||||
@@ -415,7 +417,9 @@ export class PostgresStorageAdapter {
|
||||
conn = conn || this._client;
|
||||
return conn.none('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) {
|
||||
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;
|
||||
@@ -490,11 +494,11 @@ export class PostgresStorageAdapter {
|
||||
}
|
||||
valuesArray.push(fieldName);
|
||||
valuesArray.push(parseTypeToPostgresType(parseType));
|
||||
patternsArray.push(`$${index}:name $${index+1}:raw`);
|
||||
patternsArray.push(`$${index}:name $${index + 1}:raw`);
|
||||
if (fieldName === 'objectId') {
|
||||
patternsArray.push(`PRIMARY KEY ($${index}:name)`)
|
||||
}
|
||||
index = index+2;
|
||||
index = index + 2;
|
||||
});
|
||||
const qs = `CREATE TABLE IF NOT EXISTS $1:name (${patternsArray.join(',')})`;
|
||||
const values = [className, ...valuesArray];
|
||||
@@ -618,7 +622,7 @@ export class PostgresStorageAdapter {
|
||||
|
||||
const values = [className, ...fieldNames];
|
||||
const columns = fieldNames.map((name, idx) => {
|
||||
return `$${idx+2}:name`;
|
||||
return `$${idx + 2}:name`;
|
||||
}).join(',');
|
||||
|
||||
const doBatch = (t) => {
|
||||
@@ -699,8 +703,8 @@ export class PostgresStorageAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldName === '_account_lockout_expires_at'||
|
||||
fieldName === '_perishable_token_expires_at'||
|
||||
if (fieldName === '_account_lockout_expires_at' ||
|
||||
fieldName === '_perishable_token_expires_at' ||
|
||||
fieldName === '_password_changed_at') {
|
||||
if (object[fieldName]) {
|
||||
valuesArray.push(object[fieldName].iso);
|
||||
@@ -762,7 +766,7 @@ export class PostgresStorageAdapter {
|
||||
const value = geoPoints[key];
|
||||
valuesArray.push(value.longitude, value.latitude);
|
||||
const l = valuesArray.length + columnsArray.length;
|
||||
return `POINT($${l}, $${l+1})`;
|
||||
return `POINT($${l}, $${l + 1})`;
|
||||
});
|
||||
|
||||
const columnsPattern = columnsArray.map((col, index) => `$${index + 2}:name`).join(',');
|
||||
@@ -848,11 +852,11 @@ export class PostgresStorageAdapter {
|
||||
}
|
||||
const lastKey = `$${index}:name`;
|
||||
const fieldNameIndex = index;
|
||||
index+=1;
|
||||
index += 1;
|
||||
values.push(fieldName);
|
||||
const update = Object.keys(fieldValue).reduce((lastKey, key) => {
|
||||
const str = generate(lastKey, `$${index}::text`, `$${index+1}::jsonb`)
|
||||
index+=2;
|
||||
const str = generate(lastKey, `$${index}::text`, `$${index + 1}::jsonb`)
|
||||
index += 2;
|
||||
let value = fieldValue[key];
|
||||
if (value) {
|
||||
if (value.__op === 'Delete') {
|
||||
@@ -926,16 +930,34 @@ export class PostgresStorageAdapter {
|
||||
} else if (typeof fieldValue === 'object'
|
||||
&& schema.fields[fieldName]
|
||||
&& schema.fields[fieldName].type === 'Object') {
|
||||
// Gather keys to increment
|
||||
const keysToIncrement = Object.keys(originalUpdate).filter(k => {
|
||||
// choose top level fields that have a delete operation set
|
||||
return originalUpdate[k].__op === 'Increment' && k.split('.').length === 2 && k.split(".")[0] === fieldName;
|
||||
}).map(k => k.split('.')[1]);
|
||||
|
||||
let incrementPatterns = '';
|
||||
if (keysToIncrement.length > 0) {
|
||||
incrementPatterns = ' || ' + keysToIncrement.map((c) => {
|
||||
const amount = fieldValue[c].amount;
|
||||
return `CONCAT('{"${c}":', COALESCE($${index}:name->>'${c}','0')::int + ${amount}, '}')::jsonb`;
|
||||
}).join(' || ');
|
||||
// Strip the keys
|
||||
keysToIncrement.forEach((key) => {
|
||||
delete fieldValue[key];
|
||||
});
|
||||
}
|
||||
|
||||
const keysToDelete = Object.keys(originalUpdate).filter(k => {
|
||||
// choose top level fields that have a delete operation set
|
||||
return originalUpdate[k].__op === 'Delete' && k.split('.').length === 2
|
||||
return originalUpdate[k].__op === 'Delete' && k.split('.').length === 2 && k.split(".")[0] === fieldName;
|
||||
}).map(k => k.split('.')[1]);
|
||||
|
||||
const deletePatterns = keysToDelete.reduce((p, c, i) => {
|
||||
return p + ` - '$${index + 1 + i}:value'`;
|
||||
}, '');
|
||||
|
||||
updatePatterns.push(`$${index}:name = ( COALESCE($${index}:name, '{}'::jsonb) ${deletePatterns} || $${index + 1 + keysToDelete.length}::jsonb )`);
|
||||
updatePatterns.push(`$${index}:name = ( COALESCE($${index}:name, '{}'::jsonb) ${deletePatterns} ${incrementPatterns} || $${index + 1 + keysToDelete.length}::jsonb )`);
|
||||
|
||||
values.push(fieldName, ...keysToDelete, JSON.stringify(fieldValue));
|
||||
index += 2 + keysToDelete.length;
|
||||
@@ -997,7 +1019,7 @@ export class PostgresStorageAdapter {
|
||||
if (hasLimit) {
|
||||
values.push(limit);
|
||||
}
|
||||
const skipPattern = hasSkip ? `OFFSET $${values.length+1}` : '';
|
||||
const skipPattern = hasSkip ? `OFFSET $${values.length + 1}` : '';
|
||||
if (hasSkip) {
|
||||
values.push(skip);
|
||||
}
|
||||
@@ -1024,7 +1046,7 @@ export class PostgresStorageAdapter {
|
||||
return key.length > 0;
|
||||
});
|
||||
columns = keys.map((key, index) => {
|
||||
return `$${index+values.length+1}:name`;
|
||||
return `$${index + values.length + 1}:name`;
|
||||
}).join(',');
|
||||
values = values.concat(keys);
|
||||
}
|
||||
@@ -1052,6 +1074,7 @@ export class PostgresStorageAdapter {
|
||||
}
|
||||
if (object[fieldName] && schema.fields[fieldName].type === 'GeoPoint') {
|
||||
object[fieldName] = {
|
||||
__type: "GeoPoint",
|
||||
latitude: object[fieldName].y,
|
||||
longitude: object[fieldName].x
|
||||
}
|
||||
@@ -1141,41 +1164,35 @@ export class PostgresStorageAdapter {
|
||||
}
|
||||
|
||||
performInitialization({ VolatileClassesSchemas }) {
|
||||
const now = new Date().getTime();
|
||||
debug('performInitialization');
|
||||
let promises = VolatileClassesSchemas.map((schema) => {
|
||||
return this.createTable(schema.className, schema).catch((err) =>{
|
||||
const promises = VolatileClassesSchemas.map((schema) => {
|
||||
return this.createTable(schema.className, schema).catch((err) => {
|
||||
if (err.code === PostgresDuplicateRelationError || err.code === Parse.Error.INVALID_CLASS_NAME) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
/* eslint-disable no-console */
|
||||
promises = promises.concat([
|
||||
this._client.any(json_object_set_key).catch((err) => {
|
||||
console.error(err);
|
||||
}),
|
||||
this._client.any(array_add).catch((err) => {
|
||||
console.error(err);
|
||||
}),
|
||||
this._client.any(array_add_unique).catch((err) => {
|
||||
console.error(err);
|
||||
}),
|
||||
this._client.any(array_remove).catch((err) => {
|
||||
console.error(err);
|
||||
}),
|
||||
this._client.any(array_contains_all).catch((err) => {
|
||||
console.error(err);
|
||||
}),
|
||||
this._client.any(array_contains).catch((err) => {
|
||||
console.error(err);
|
||||
return Promise.all(promises)
|
||||
.then(() => {
|
||||
return this._client.tx(t => {
|
||||
return t.batch([
|
||||
t.none(sql.misc.jsonObjectSetKeys),
|
||||
t.none(sql.array.add),
|
||||
t.none(sql.array.addUnique),
|
||||
t.none(sql.array.remove),
|
||||
t.none(sql.array.containsAll),
|
||||
t.none(sql.array.contains)
|
||||
]);
|
||||
});
|
||||
})
|
||||
]);
|
||||
/* eslint-enable no-console */
|
||||
return Promise.all(promises).then(() => {
|
||||
debug(`initialzationDone in ${new Date().getTime() - now}`);
|
||||
}, () => {});
|
||||
.then(data => {
|
||||
debug(`initializationDone in ${data.duration}`);
|
||||
})
|
||||
.catch(error => {
|
||||
/* eslint-disable no-console */
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1252,84 +1269,5 @@ function literalizeRegexPart(s) {
|
||||
);
|
||||
}
|
||||
|
||||
// Function to set a key on a nested JSON document
|
||||
const json_object_set_key = 'CREATE OR REPLACE FUNCTION "json_object_set_key"(\
|
||||
"json" jsonb,\
|
||||
"key_to_set" TEXT,\
|
||||
"value_to_set" anyelement\
|
||||
)\
|
||||
RETURNS jsonb \
|
||||
LANGUAGE sql \
|
||||
IMMUTABLE \
|
||||
STRICT \
|
||||
AS $function$\
|
||||
SELECT concat(\'{\', string_agg(to_json("key") || \':\' || "value", \',\'), \'}\')::jsonb\
|
||||
FROM (SELECT *\
|
||||
FROM jsonb_each("json")\
|
||||
WHERE "key" <> "key_to_set"\
|
||||
UNION ALL\
|
||||
SELECT "key_to_set", to_json("value_to_set")::jsonb) AS "fields"\
|
||||
$function$;'
|
||||
|
||||
const array_add = `CREATE OR REPLACE FUNCTION "array_add"(
|
||||
"array" jsonb,
|
||||
"values" jsonb
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT array_to_json(ARRAY(SELECT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT jsonb_array_elements("values")))))::jsonb;
|
||||
$function$;`;
|
||||
|
||||
const array_add_unique = `CREATE OR REPLACE FUNCTION "array_add_unique"(
|
||||
"array" jsonb,
|
||||
"values" jsonb
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT array_to_json(ARRAY(SELECT DISTINCT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT DISTINCT jsonb_array_elements("values")))))::jsonb;
|
||||
$function$;`;
|
||||
|
||||
const array_remove = `CREATE OR REPLACE FUNCTION "array_remove"(
|
||||
"array" jsonb,
|
||||
"values" jsonb
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT array_to_json(ARRAY(SELECT * FROM jsonb_array_elements("array") as elt WHERE elt NOT IN (SELECT * FROM (SELECT jsonb_array_elements("values")) AS sub)))::jsonb;
|
||||
$function$;`;
|
||||
|
||||
const array_contains_all = `CREATE OR REPLACE FUNCTION "array_contains_all"(
|
||||
"array" jsonb,
|
||||
"values" jsonb
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES ;
|
||||
$function$;`;
|
||||
|
||||
const array_contains = `CREATE OR REPLACE FUNCTION "array_contains"(
|
||||
"array" jsonb,
|
||||
"values" jsonb
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT RES.CNT >= 1 FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES ;
|
||||
$function$;`;
|
||||
|
||||
export default PostgresStorageAdapter;
|
||||
module.exports = PostgresStorageAdapter; // Required for tests
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE OR REPLACE FUNCTION array_add_unique(
|
||||
"array" jsonb,
|
||||
"values" jsonb
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT array_to_json(ARRAY(SELECT DISTINCT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT DISTINCT jsonb_array_elements("values")))))::jsonb;
|
||||
$function$;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE OR REPLACE FUNCTION array_add(
|
||||
"array" jsonb,
|
||||
"values" jsonb
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT array_to_json(ARRAY(SELECT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT jsonb_array_elements("values")))))::jsonb;
|
||||
$function$;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE OR REPLACE FUNCTION array_contains_all(
|
||||
"array" jsonb,
|
||||
"values" jsonb
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES;
|
||||
$function$;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE OR REPLACE FUNCTION array_contains(
|
||||
"array" jsonb,
|
||||
"values" jsonb
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT RES.CNT >= 1 FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES;
|
||||
$function$;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE OR REPLACE FUNCTION array_remove(
|
||||
"array" jsonb,
|
||||
"values" jsonb
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT array_to_json(ARRAY(SELECT * FROM jsonb_array_elements("array") as elt WHERE elt NOT IN (SELECT * FROM (SELECT jsonb_array_elements("values")) AS sub)))::jsonb;
|
||||
$function$;
|
||||
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
var QueryFile = require('pg-promise').QueryFile;
|
||||
var path = require('path');
|
||||
|
||||
module.exports = {
|
||||
array: {
|
||||
add: sql('array/add.sql'),
|
||||
addUnique: sql('array/add-unique.sql'),
|
||||
contains: sql('array/contains.sql'),
|
||||
containsAll: sql('array/contains-all.sql'),
|
||||
remove: sql('array/remove.sql')
|
||||
},
|
||||
misc: {
|
||||
jsonObjectSetKeys: sql('misc/json-object-set-keys.sql')
|
||||
}
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// Helper for linking to external query files;
|
||||
function sql(file) {
|
||||
|
||||
var fullPath = path.join(__dirname, file); // generating full path;
|
||||
|
||||
var qf = new QueryFile(fullPath, {minify: true});
|
||||
|
||||
if (qf.error) {
|
||||
throw qf.error;
|
||||
}
|
||||
|
||||
return qf;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Function to set a key on a nested JSON document
|
||||
|
||||
CREATE OR REPLACE FUNCTION json_object_set_key(
|
||||
"json" jsonb,
|
||||
key_to_set TEXT,
|
||||
value_to_set anyelement
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT concat('{', string_agg(to_json("key") || ':' || "value", ','), '}')::jsonb
|
||||
FROM (SELECT *
|
||||
FROM jsonb_each("json")
|
||||
WHERE key <> key_to_set
|
||||
UNION ALL
|
||||
SELECT key_to_set, to_json("value_to_set")::jsonb) AS fields
|
||||
$function$;
|
||||
+6
-3
@@ -11,7 +11,7 @@ function removeTrailingSlash(str) {
|
||||
return str;
|
||||
}
|
||||
if (str.endsWith("/")) {
|
||||
str = str.substr(0, str.length-1);
|
||||
str = str.substr(0, str.length - 1);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
@@ -58,6 +58,9 @@ export class Config {
|
||||
this.hooksController = cacheInfo.hooksController;
|
||||
this.filesController = cacheInfo.filesController;
|
||||
this.pushController = cacheInfo.pushController;
|
||||
this.pushControllerQueue = cacheInfo.pushControllerQueue;
|
||||
this.pushWorker = cacheInfo.pushWorker;
|
||||
this.hasPushSupport = cacheInfo.hasPushSupport;
|
||||
this.loggerController = cacheInfo.loggerController;
|
||||
this.userController = cacheInfo.userController;
|
||||
this.authDataManager = cacheInfo.authDataManager;
|
||||
@@ -207,7 +210,7 @@ export class Config {
|
||||
return undefined;
|
||||
}
|
||||
var now = new Date();
|
||||
return new Date(now.getTime() + (this.emailVerifyTokenValidityDuration*1000));
|
||||
return new Date(now.getTime() + (this.emailVerifyTokenValidityDuration * 1000));
|
||||
}
|
||||
|
||||
generatePasswordResetTokenExpiresAt() {
|
||||
@@ -223,7 +226,7 @@ export class Config {
|
||||
return undefined;
|
||||
}
|
||||
var now = new Date();
|
||||
return new Date(now.getTime() + (this.sessionLength*1000));
|
||||
return new Date(now.getTime() + (this.sessionLength * 1000));
|
||||
}
|
||||
|
||||
get invalidLinkURL() {
|
||||
|
||||
@@ -38,11 +38,15 @@ export class AdaptableController {
|
||||
}
|
||||
|
||||
validateAdapter(adapter) {
|
||||
AdaptableController.validateAdapter(adapter, this);
|
||||
}
|
||||
|
||||
static validateAdapter(adapter, self, ExpectedType) {
|
||||
if (!adapter) {
|
||||
throw new Error(this.constructor.name+" requires an adapter");
|
||||
throw new Error(this.constructor.name + " requires an adapter");
|
||||
}
|
||||
|
||||
const Type = this.expectedAdapterType();
|
||||
const Type = ExpectedType || self.expectedAdapterType();
|
||||
// Allow skipping for testing
|
||||
if (!Type) {
|
||||
return;
|
||||
|
||||
@@ -18,7 +18,7 @@ function addWriteACL(query, acl) {
|
||||
function addReadACL(query, acl) {
|
||||
const newQuery = _.cloneDeep(query);
|
||||
//Can't be any existing '_rperm' query, we don't allow client queries on that, no need to $and
|
||||
newQuery._rperm = { "$in" : [null, "*", ...acl]};
|
||||
newQuery._rperm = {"$in": [null, "*", ...acl]};
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,30 @@ const validateQuery = query => {
|
||||
if (query.$or) {
|
||||
if (query.$or instanceof Array) {
|
||||
query.$or.forEach(validateQuery);
|
||||
|
||||
/* In MongoDB, $or queries which are not alone at the top level of the
|
||||
* query can not make efficient use of indexes due to a long standing
|
||||
* bug known as SERVER-13732.
|
||||
*
|
||||
* This block restructures queries in which $or is not the sole top
|
||||
* level element by moving all other top-level predicates inside every
|
||||
* subdocument of the $or predicate, allowing MongoDB's query planner
|
||||
* to make full use of the most relevant indexes.
|
||||
*
|
||||
* EG: {$or: [{a: 1}, {a: 2}], b: 2}
|
||||
* Becomes: {$or: [{a: 1, b: 2}, {a: 2, b: 2}]}
|
||||
*
|
||||
* https://jira.mongodb.org/browse/SERVER-13732
|
||||
*/
|
||||
Object.keys(query).forEach(key => {
|
||||
const noCollisions = !query.$or.some(subq => subq.hasOwnProperty(key))
|
||||
if (key != '$or' && noCollisions) {
|
||||
query.$or.forEach(subquery => {
|
||||
subquery[key] = query[key];
|
||||
});
|
||||
delete query[key];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.');
|
||||
}
|
||||
@@ -885,31 +909,43 @@ DatabaseController.prototype.addPointerPermissions = function(schema, className,
|
||||
// have a Parse app without it having a _User collection.
|
||||
DatabaseController.prototype.performInitialization = function() {
|
||||
const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } };
|
||||
const requiredRoleFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._Role } };
|
||||
|
||||
const userClassPromise = this.loadSchema()
|
||||
.then(schema => schema.enforceClassExists('_User'))
|
||||
const roleClassPromise = this.loadSchema()
|
||||
.then(schema => schema.enforceClassExists('_Role'))
|
||||
|
||||
const usernameUniqueness = userClassPromise
|
||||
.then(() => this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']))
|
||||
.catch(error => {
|
||||
logger.warn('Unable to ensure uniqueness for usernames: ', error);
|
||||
return Promise.reject(error);
|
||||
throw error;
|
||||
});
|
||||
|
||||
const emailUniqueness = userClassPromise
|
||||
.then(() => this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']))
|
||||
.catch(error => {
|
||||
logger.warn('Unable to ensure uniqueness for user email addresses: ', error);
|
||||
return Promise.reject(error);
|
||||
throw error;
|
||||
});
|
||||
|
||||
const roleUniqueness = roleClassPromise
|
||||
.then(() => this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']))
|
||||
.catch(error => {
|
||||
logger.warn('Unable to ensure uniqueness for role name: ', error);
|
||||
throw error;
|
||||
});
|
||||
|
||||
// Create tables for volatile classes
|
||||
const adapterInit = this.adapter.performInitialization({ VolatileClassesSchemas: SchemaController.VolatileClassesSchemas });
|
||||
return Promise.all([usernameUniqueness, emailUniqueness, adapterInit]);
|
||||
return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness, adapterInit]);
|
||||
}
|
||||
|
||||
function joinTableName(className, key) {
|
||||
return `_Join:${key}:${className}`;
|
||||
}
|
||||
|
||||
// Expose validateQuery for tests
|
||||
DatabaseController._validateQuery = validateQuery;
|
||||
module.exports = DatabaseController;
|
||||
|
||||
@@ -9,6 +9,8 @@ const DefaultHooksCollectionName = "_Hooks";
|
||||
|
||||
export class HooksController {
|
||||
_applicationId:string;
|
||||
_webhookKey:string;
|
||||
database: any;
|
||||
|
||||
constructor(applicationId:string, databaseController, webhookKey) {
|
||||
this._applicationId = applicationId;
|
||||
|
||||
@@ -41,7 +41,18 @@ export class LoggerController extends AdaptableController {
|
||||
|
||||
// check the url
|
||||
if (e.url) {
|
||||
e.url = this.maskSensitiveUrl(e.url);
|
||||
// for strings
|
||||
if (typeof e.url === 'string') {
|
||||
e.url = this.maskSensitiveUrl(e.url);
|
||||
} else if (Array.isArray(e.url)) { // for strings in array
|
||||
e.url = e.url.map(item => {
|
||||
if (typeof item === 'string') {
|
||||
return this.maskSensitiveUrl(item);
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (e.body) {
|
||||
@@ -134,7 +145,7 @@ export class LoggerController extends AdaptableController {
|
||||
// until (optional) End time for the search. Defaults to current time.
|
||||
// order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”.
|
||||
// size (optional) Number of rows returned by search. Defaults to 10
|
||||
getLogs(options= {}) {
|
||||
getLogs(options = {}) {
|
||||
if (!this.adapter) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Logger adapter is not available');
|
||||
|
||||
@@ -1,54 +1,17 @@
|
||||
import { Parse } from 'parse/node';
|
||||
import rest from '../rest';
|
||||
import AdaptableController from './AdaptableController';
|
||||
import { PushAdapter } from '../Adapters/Push/PushAdapter';
|
||||
import deepcopy from 'deepcopy';
|
||||
import RestQuery from '../RestQuery';
|
||||
import RestWrite from '../RestWrite';
|
||||
import { master } from '../Auth';
|
||||
import { pushStatusHandler } from '../StatusHandler';
|
||||
|
||||
const UNSUPPORTED_BADGE_KEY = "unsupported";
|
||||
|
||||
export class PushController extends AdaptableController {
|
||||
|
||||
/**
|
||||
* Check whether the deviceType parameter in qury condition is valid or not.
|
||||
* @param {Object} where A query condition
|
||||
* @param {Array} validPushTypes An array of valid push types(string)
|
||||
*/
|
||||
static validatePushType(where = {}, validPushTypes = []) {
|
||||
var deviceTypeField = where.deviceType || {};
|
||||
var deviceTypes = [];
|
||||
if (typeof deviceTypeField === 'string') {
|
||||
deviceTypes.push(deviceTypeField);
|
||||
} else if (Array.isArray(deviceTypeField['$in'])) {
|
||||
deviceTypes.concat(deviceTypeField['$in']);
|
||||
}
|
||||
for (var i = 0; i < deviceTypes.length; i++) {
|
||||
var deviceType = deviceTypes[i];
|
||||
if (validPushTypes.indexOf(deviceType) < 0) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
deviceType + ' is not supported push type.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get pushIsAvailable() {
|
||||
return !!this.adapter;
|
||||
}
|
||||
export class PushController {
|
||||
|
||||
sendPush(body = {}, where = {}, config, auth, onPushStatusSaved = () => {}) {
|
||||
var pushAdapter = this.adapter;
|
||||
if (!this.pushIsAvailable) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Push adapter is not available');
|
||||
}
|
||||
if (!this.options) {
|
||||
if (!config.hasPushSupport) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Missing push configuration');
|
||||
}
|
||||
PushController.validatePushType(where, pushAdapter.getValidPushTypes());
|
||||
// Replace the expiration_time with a valid Unix epoch milliseconds time
|
||||
body['expiration_time'] = PushController.getExpirationTime(body);
|
||||
// TODO: If the req can pass the checking, we return immediately instead of waiting
|
||||
@@ -86,15 +49,7 @@ export class PushController extends AdaptableController {
|
||||
onPushStatusSaved(pushStatus.objectId);
|
||||
return badgeUpdate();
|
||||
}).then(() => {
|
||||
return rest.find(config, auth, '_Installation', where);
|
||||
}).then((response) => {
|
||||
if (!response.results) {
|
||||
return Promise.reject({error: 'PushController: no results in query'})
|
||||
}
|
||||
pushStatus.setRunning(response.results);
|
||||
return this.sendToAdapter(body, response.results, pushStatus, config);
|
||||
}).then((results) => {
|
||||
return pushStatus.complete(results);
|
||||
return config.pushControllerQueue.enqueue(body, where, config, auth, pushStatus);
|
||||
}).catch((err) => {
|
||||
return pushStatus.fail(err).then(() => {
|
||||
throw err;
|
||||
@@ -102,34 +57,6 @@ export class PushController extends AdaptableController {
|
||||
});
|
||||
}
|
||||
|
||||
sendToAdapter(body, installations, pushStatus) {
|
||||
if (body.data && body.data.badge && typeof body.data.badge == 'string' && body.data.badge.toLowerCase() == "increment") {
|
||||
// Collect the badges to reduce the # of calls
|
||||
const badgeInstallationsMap = installations.reduce((map, installation) => {
|
||||
let badge = installation.badge;
|
||||
if (installation.deviceType != "ios") {
|
||||
badge = UNSUPPORTED_BADGE_KEY;
|
||||
}
|
||||
map[badge+''] = map[badge+''] || [];
|
||||
map[badge+''].push(installation);
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
// Map the on the badges count and return the send result
|
||||
const promises = Object.keys(badgeInstallationsMap).map((badge) => {
|
||||
const payload = deepcopy(body);
|
||||
if (badge == UNSUPPORTED_BADGE_KEY) {
|
||||
delete payload.data.badge;
|
||||
} else {
|
||||
payload.data.badge = parseInt(badge);
|
||||
}
|
||||
return this.adapter.send(payload, badgeInstallationsMap[badge], pushStatus.objectId);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
return this.adapter.send(body, installations, pushStatus.objectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get expiration time from the request body.
|
||||
* @param {Object} request A request object
|
||||
@@ -157,10 +84,6 @@ export class PushController extends AdaptableController {
|
||||
}
|
||||
return expirationTime.valueOf();
|
||||
}
|
||||
|
||||
expectedAdapterType() {
|
||||
return PushAdapter;
|
||||
}
|
||||
}
|
||||
|
||||
export default PushController;
|
||||
|
||||
@@ -21,10 +21,10 @@ export default class SchemaCache {
|
||||
}
|
||||
|
||||
put(key, value) {
|
||||
return this.cache.get(this.prefix+ALL_KEYS).then((allKeys) => {
|
||||
return this.cache.get(this.prefix + ALL_KEYS).then((allKeys) => {
|
||||
allKeys = allKeys || {};
|
||||
allKeys[key] = true;
|
||||
return Promise.all([this.cache.put(this.prefix+ALL_KEYS, allKeys, this.ttl), this.cache.put(key, value, this.ttl)]);
|
||||
return Promise.all([this.cache.put(this.prefix + ALL_KEYS, allKeys, this.ttl), this.cache.put(key, value, this.ttl)]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,32 +32,32 @@ export default class SchemaCache {
|
||||
if (!this.ttl) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.cache.get(this.prefix+MAIN_SCHEMA);
|
||||
return this.cache.get(this.prefix + MAIN_SCHEMA);
|
||||
}
|
||||
|
||||
setAllClasses(schema) {
|
||||
if (!this.ttl) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.put(this.prefix+MAIN_SCHEMA, schema);
|
||||
return this.put(this.prefix + MAIN_SCHEMA, schema);
|
||||
}
|
||||
|
||||
setOneSchema(className, schema) {
|
||||
if (!this.ttl) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.put(this.prefix+className, schema);
|
||||
return this.put(this.prefix + className, schema);
|
||||
}
|
||||
|
||||
getOneSchema(className) {
|
||||
if (!this.ttl) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.cache.get(this.prefix+className).then((schema) => {
|
||||
return this.cache.get(this.prefix + className).then((schema) => {
|
||||
if (schema) {
|
||||
return Promise.resolve(schema);
|
||||
}
|
||||
return this.cache.get(this.prefix+MAIN_SCHEMA).then((cachedSchemas) => {
|
||||
return this.cache.get(this.prefix + MAIN_SCHEMA).then((cachedSchemas) => {
|
||||
cachedSchemas = cachedSchemas || [];
|
||||
schema = cachedSchemas.find((cachedSchema) => {
|
||||
return cachedSchema.className === className;
|
||||
@@ -72,7 +72,7 @@ export default class SchemaCache {
|
||||
|
||||
clear() {
|
||||
// That clears all caches...
|
||||
return this.cache.get(this.prefix+ALL_KEYS).then((allKeys) => {
|
||||
return this.cache.get(this.prefix + ALL_KEYS).then((allKeys) => {
|
||||
if (!allKeys) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ const defaultColumns = Object.freeze({
|
||||
"errorMessage": {type:'Object'},
|
||||
"sentPerType": {type:'Object'},
|
||||
"failedPerType":{type:'Object'},
|
||||
"count": {type:'Number'}
|
||||
},
|
||||
_JobStatus: {
|
||||
"jobName": {type: 'String'},
|
||||
@@ -333,25 +334,29 @@ export default class SchemaController {
|
||||
if (this.reloadDataPromise && !options.clearCache) {
|
||||
return this.reloadDataPromise;
|
||||
}
|
||||
this.data = {};
|
||||
this.perms = {};
|
||||
this.reloadDataPromise = promise.then(() => {
|
||||
return this.getAllClasses(options);
|
||||
})
|
||||
.then(allSchemas => {
|
||||
const data = {};
|
||||
const perms = {};
|
||||
allSchemas.forEach(schema => {
|
||||
this.data[schema.className] = injectDefaultSchema(schema).fields;
|
||||
this.perms[schema.className] = schema.classLevelPermissions;
|
||||
data[schema.className] = injectDefaultSchema(schema).fields;
|
||||
perms[schema.className] = schema.classLevelPermissions;
|
||||
});
|
||||
|
||||
// Inject the in-memory classes
|
||||
volatileClasses.forEach(className => {
|
||||
const schema = injectDefaultSchema({ className });
|
||||
this.data[className] = schema.fields;
|
||||
this.perms[className] = schema.classLevelPermissions;
|
||||
data[className] = schema.fields;
|
||||
perms[className] = schema.classLevelPermissions;
|
||||
});
|
||||
this.data = data;
|
||||
this.perms = perms;
|
||||
delete this.reloadDataPromise;
|
||||
}, (err) => {
|
||||
this.data = {};
|
||||
this.perms = {};
|
||||
delete this.reloadDataPromise;
|
||||
throw err;
|
||||
});
|
||||
@@ -742,7 +747,7 @@ export default class SchemaController {
|
||||
if (missingColumns.length > 0) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INCORRECT_TYPE,
|
||||
missingColumns[0]+' is required.');
|
||||
missingColumns[0] + ' is required.');
|
||||
}
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
@@ -947,7 +952,7 @@ function getObjectType(obj) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid "+obj.__type);
|
||||
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid " + obj.__type);
|
||||
}
|
||||
if (obj['$ne']) {
|
||||
return getObjectType(obj['$ne']);
|
||||
|
||||
@@ -37,7 +37,6 @@ class ParseLiveQueryServer {
|
||||
|
||||
// Initialize Parse
|
||||
Parse.Object.disableSingleInstance();
|
||||
Parse.User.enableUnsafeCurrentUser();
|
||||
|
||||
const serverURL = config.serverURL || Parse.serverURL;
|
||||
Parse.serverURL = serverURL;
|
||||
@@ -363,7 +362,7 @@ class ParseLiveQueryServer {
|
||||
// Then get the user's roles
|
||||
var rolesQuery = new Parse.Query(Parse.Role);
|
||||
rolesQuery.equalTo("users", user);
|
||||
return rolesQuery.find();
|
||||
return rolesQuery.find({useMasterKey:true});
|
||||
}).
|
||||
then((roles) => {
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import logger from '../logger';
|
||||
|
||||
const typeMap = new Map([['disconnect', 'close']]);
|
||||
const getWS = function() {
|
||||
try {
|
||||
return require('uws');
|
||||
} catch(e) {
|
||||
return require('ws');
|
||||
}
|
||||
}
|
||||
|
||||
export class ParseWebSocketServer {
|
||||
server: Object;
|
||||
|
||||
constructor(server: any, onConnect: Function, websocketTimeout: number = 10 * 1000) {
|
||||
const WebSocketServer = require('ws').Server;
|
||||
const WebSocketServer = getWS().Server;
|
||||
const wss = new WebSocketServer({ server: server });
|
||||
wss.on('listening', () => {
|
||||
logger.info('Parse LiveQuery Server starts running');
|
||||
|
||||
@@ -2,10 +2,21 @@ import Parse from 'parse/node';
|
||||
import LRU from 'lru-cache';
|
||||
import logger from '../logger';
|
||||
|
||||
function userForSessionToken(sessionToken){
|
||||
var q = new Parse.Query("_Session");
|
||||
q.equalTo("sessionToken", sessionToken);
|
||||
return q.first({useMasterKey:true}).then(function(session){
|
||||
if(!session){
|
||||
return Parse.Promise.error("No session found for session token");
|
||||
}
|
||||
return session.get("user");
|
||||
});
|
||||
}
|
||||
|
||||
class SessionTokenCache {
|
||||
cache: Object;
|
||||
|
||||
constructor(timeout: number = 30 * 24 * 60 *60 * 1000, maxSize: number = 10000) {
|
||||
constructor(timeout: number = 30 * 24 * 60 * 60 * 1000, maxSize: number = 10000) {
|
||||
this.cache = new LRU({
|
||||
max: maxSize,
|
||||
maxAge: timeout
|
||||
@@ -21,7 +32,7 @@ class SessionTokenCache {
|
||||
logger.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken);
|
||||
return Parse.Promise.as(userId);
|
||||
}
|
||||
return Parse.User.become(sessionToken).then((user) => {
|
||||
return userForSessionToken(sessionToken).then((user) => {
|
||||
logger.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken);
|
||||
const userId = user.id;
|
||||
this.cache.set(sessionToken, userId);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { loadAdapter } from './Adapters/AdapterLoader';
|
||||
import {
|
||||
EventEmitterMQ
|
||||
} from './Adapters/MessageQueue/EventEmitterMQ';
|
||||
|
||||
const ParseMessageQueue = {};
|
||||
|
||||
ParseMessageQueue.createPublisher = function(config: any): any {
|
||||
const adapter = loadAdapter(config.messageQueueAdapter, EventEmitterMQ, config);
|
||||
if (typeof adapter.createPublisher !== 'function') {
|
||||
throw 'pubSubAdapter should have createPublisher()';
|
||||
}
|
||||
return adapter.createPublisher(config);
|
||||
}
|
||||
|
||||
ParseMessageQueue.createSubscriber = function(config: any): void {
|
||||
const adapter = loadAdapter(config.messageQueueAdapter, EventEmitterMQ, config)
|
||||
if (typeof adapter.createSubscriber !== 'function') {
|
||||
throw 'messageQueueAdapter should have createSubscriber()';
|
||||
}
|
||||
return adapter.createSubscriber(config);
|
||||
}
|
||||
|
||||
export {
|
||||
ParseMessageQueue
|
||||
}
|
||||
+26
-4
@@ -39,6 +39,8 @@ import { LogsRouter } from './Routers/LogsRouter';
|
||||
import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer';
|
||||
import { PublicAPIRouter } from './Routers/PublicAPIRouter';
|
||||
import { PushController } from './Controllers/PushController';
|
||||
import { PushQueue } from './Push/PushQueue';
|
||||
import { PushWorker } from './Push/PushWorker';
|
||||
import { PushRouter } from './Routers/PushRouter';
|
||||
import { CloudCodeRouter } from './Routers/CloudCodeRouter';
|
||||
import { RolesRouter } from './Routers/RolesRouter';
|
||||
@@ -168,11 +170,28 @@ class ParseServer {
|
||||
});
|
||||
const filesController = new FilesController(filesControllerAdapter, appId);
|
||||
|
||||
const pushOptions = Object.assign({}, push);
|
||||
const pushQueueOptions = pushOptions.queueOptions || {};
|
||||
if (pushOptions.queueOptions) {
|
||||
delete pushOptions.queueOptions;
|
||||
}
|
||||
// Pass the push options too as it works with the default
|
||||
const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push || {});
|
||||
// We pass the options and the base class for the adapter,
|
||||
const pushAdapter = loadAdapter(pushOptions && pushOptions.adapter, ParsePushAdapter, pushOptions);
|
||||
// We pass the options and the base class for the adatper,
|
||||
// Note that passing an instance would work too
|
||||
const pushController = new PushController(pushControllerAdapter, appId, push);
|
||||
const pushController = new PushController();
|
||||
|
||||
const hasPushSupport = pushAdapter && push;
|
||||
|
||||
const {
|
||||
disablePushWorker
|
||||
} = pushQueueOptions;
|
||||
|
||||
const pushControllerQueue = new PushQueue(pushQueueOptions);
|
||||
let pushWorker;
|
||||
if (!disablePushWorker) {
|
||||
pushWorker = new PushWorker(pushAdapter, pushQueueOptions);
|
||||
}
|
||||
|
||||
const emailControllerAdapter = loadAdapter(emailAdapter);
|
||||
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
|
||||
@@ -237,7 +256,10 @@ class ParseServer {
|
||||
databaseController,
|
||||
schemaCacheTTL,
|
||||
enableSingleSchemaCache,
|
||||
userSensitiveFields
|
||||
userSensitiveFields,
|
||||
pushWorker,
|
||||
pushControllerQueue,
|
||||
hasPushSupport
|
||||
});
|
||||
|
||||
Config.validate(AppCache.get(appId));
|
||||
|
||||
@@ -174,7 +174,7 @@ function makeExpressHandler(appId, promiseHandler) {
|
||||
// Override the default expressjs response
|
||||
// as it double encodes %encoded chars in URL
|
||||
if (!result.response) {
|
||||
res.send('Found. Redirecting to '+result.location);
|
||||
res.send('Found. Redirecting to ' + result.location);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ParseMessageQueue } from '../ParseMessageQueue';
|
||||
import rest from '../rest';
|
||||
import { isPushIncrementing } from './utils';
|
||||
|
||||
const PUSH_CHANNEL = 'parse-server-push';
|
||||
const DEFAULT_BATCH_SIZE = 100;
|
||||
|
||||
export class PushQueue {
|
||||
parsePublisher: Object;
|
||||
channel: String;
|
||||
batchSize: Number;
|
||||
|
||||
// config object of the publisher, right now it only contains the redisURL,
|
||||
// but we may extend it later.
|
||||
constructor(config: any = {}) {
|
||||
this.channel = config.channel || PUSH_CHANNEL;
|
||||
this.batchSize = config.batchSize || DEFAULT_BATCH_SIZE;
|
||||
this.parsePublisher = ParseMessageQueue.createPublisher(config);
|
||||
}
|
||||
|
||||
static defaultPushChannel() {
|
||||
return PUSH_CHANNEL;
|
||||
}
|
||||
|
||||
enqueue(body, where, config, auth, pushStatus) {
|
||||
const limit = this.batchSize;
|
||||
// Order by badge (because the payload is badge dependant)
|
||||
// and createdAt to fix the order
|
||||
const order = isPushIncrementing(body) ? 'badge,createdAt' : 'createdAt';
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
return rest.find(config,
|
||||
auth,
|
||||
'_Installation',
|
||||
where,
|
||||
{limit: 0, count: true});
|
||||
}).then(({results, count}) => {
|
||||
if (!results) {
|
||||
return Promise.reject({error: 'PushController: no results in query'})
|
||||
}
|
||||
pushStatus.setRunning(count);
|
||||
let skip = 0;
|
||||
while (skip < count) {
|
||||
const query = { where,
|
||||
limit,
|
||||
skip,
|
||||
order };
|
||||
|
||||
const pushWorkItem = {
|
||||
body,
|
||||
query,
|
||||
pushStatus: { objectId: pushStatus.objectId },
|
||||
applicationId: config.applicationId
|
||||
}
|
||||
this.parsePublisher.publish(this.channel, JSON.stringify(pushWorkItem));
|
||||
skip += limit;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// @flow
|
||||
import deepcopy from 'deepcopy';
|
||||
import AdaptableController from '../Controllers/AdaptableController';
|
||||
import { master } from '../Auth';
|
||||
import Config from '../Config';
|
||||
import { PushAdapter } from '../Adapters/Push/PushAdapter';
|
||||
import rest from '../rest';
|
||||
import { pushStatusHandler } from '../StatusHandler';
|
||||
import { isPushIncrementing } from './utils';
|
||||
import { ParseMessageQueue } from '../ParseMessageQueue';
|
||||
import { PushQueue } from './PushQueue';
|
||||
|
||||
const UNSUPPORTED_BADGE_KEY = "unsupported";
|
||||
|
||||
function groupByBadge(installations) {
|
||||
return installations.reduce((map, installation) => {
|
||||
let badge = installation.badge + '';
|
||||
if (installation.deviceType != "ios") {
|
||||
badge = UNSUPPORTED_BADGE_KEY;
|
||||
}
|
||||
map[badge] = map[badge] || [];
|
||||
map[badge].push(installation);
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export class PushWorker {
|
||||
subscriber: ?any;
|
||||
adapter: any;
|
||||
channel: string;
|
||||
|
||||
constructor(pushAdapter: PushAdapter, subscriberConfig: any = {}) {
|
||||
AdaptableController.validateAdapter(pushAdapter, this, PushAdapter);
|
||||
this.adapter = pushAdapter;
|
||||
|
||||
this.channel = subscriberConfig.channel || PushQueue.defaultPushChannel();
|
||||
this.subscriber = ParseMessageQueue.createSubscriber(subscriberConfig);
|
||||
if (this.subscriber) {
|
||||
const subscriber = this.subscriber;
|
||||
subscriber.subscribe(this.channel);
|
||||
subscriber.on('message', (channel, messageStr) => {
|
||||
const workItem = JSON.parse(messageStr);
|
||||
this.run(workItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribe(): void {
|
||||
if (this.subscriber) {
|
||||
this.subscriber.unsubscribe(this.channel);
|
||||
}
|
||||
}
|
||||
|
||||
run({ body, query, pushStatus, applicationId }: any): Promise<*> {
|
||||
const config = new Config(applicationId);
|
||||
const auth = master(config);
|
||||
const where = query.where;
|
||||
delete query.where;
|
||||
return rest.find(config, auth, '_Installation', where, query).then(({results}) => {
|
||||
if (results.length == 0) {
|
||||
return;
|
||||
}
|
||||
return this.sendToAdapter(body, results, pushStatus, config);
|
||||
}, err => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
sendToAdapter(body: any, installations: any[], pushStatus: any, config: Config): Promise<*> {
|
||||
pushStatus = pushStatusHandler(config, pushStatus.objectId);
|
||||
if (!isPushIncrementing(body)) {
|
||||
return this.adapter.send(body, installations, pushStatus.objectId).then((results) => {
|
||||
return pushStatus.trackSent(results);
|
||||
});
|
||||
}
|
||||
|
||||
// Collect the badges to reduce the # of calls
|
||||
const badgeInstallationsMap = groupByBadge(installations);
|
||||
|
||||
// Map the on the badges count and return the send result
|
||||
const promises = Object.keys(badgeInstallationsMap).map((badge) => {
|
||||
const payload = deepcopy(body);
|
||||
if (badge == UNSUPPORTED_BADGE_KEY) {
|
||||
delete payload.data.badge;
|
||||
} else {
|
||||
payload.data.badge = parseInt(badge);
|
||||
}
|
||||
const installations = badgeInstallationsMap[badge];
|
||||
return this.sendToAdapter(payload, installations, pushStatus, config);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
export default PushWorker;
|
||||
@@ -0,0 +1,30 @@
|
||||
import Parse from 'parse/node';
|
||||
|
||||
export function isPushIncrementing(body) {
|
||||
return body.data &&
|
||||
body.data.badge &&
|
||||
typeof body.data.badge == 'string' &&
|
||||
body.data.badge.toLowerCase() == "increment"
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the deviceType parameter in qury condition is valid or not.
|
||||
* @param {Object} where A query condition
|
||||
* @param {Array} validPushTypes An array of valid push types(string)
|
||||
*/
|
||||
export function validatePushType(where = {}, validPushTypes = []) {
|
||||
var deviceTypeField = where.deviceType || {};
|
||||
var deviceTypes = [];
|
||||
if (typeof deviceTypeField === 'string') {
|
||||
deviceTypes.push(deviceTypeField);
|
||||
} else if (Array.isArray(deviceTypeField['$in'])) {
|
||||
deviceTypes.concat(deviceTypeField['$in']);
|
||||
}
|
||||
for (var i = 0; i < deviceTypes.length; i++) {
|
||||
var deviceType = deviceTypes[i];
|
||||
if (validPushTypes.indexOf(deviceType) < 0) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
deviceType + ' is not supported push type.');
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -92,15 +92,15 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
|
||||
break;
|
||||
case 'order':
|
||||
var fields = restOptions.order.split(',');
|
||||
var sortMap = {};
|
||||
for (var field of fields) {
|
||||
this.findOptions.sort = fields.reduce((sortMap, field) => {
|
||||
field = field.trim();
|
||||
if (field[0] == '-') {
|
||||
sortMap[field.slice(1)] = -1;
|
||||
} else {
|
||||
sortMap[field] = 1;
|
||||
}
|
||||
}
|
||||
this.findOptions.sort = sortMap;
|
||||
return sortMap;
|
||||
}, {});
|
||||
break;
|
||||
case 'include': {
|
||||
const paths = restOptions.include.split(',');
|
||||
@@ -110,7 +110,7 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
|
||||
// reduce to create all paths
|
||||
// ([a,b,c] -> {a: true, 'a.b': true, 'a.b.c': true})
|
||||
return path.split('.').reduce((memo, path, index, parts) => {
|
||||
memo[parts.slice(0, index+1).join('.')] = true;
|
||||
memo[parts.slice(0, index + 1).join('.')] = true;
|
||||
return memo;
|
||||
}, memo);
|
||||
}, {});
|
||||
@@ -552,8 +552,8 @@ function includePath(config, auth, response, path, restOptions = {}) {
|
||||
const keys = new Set(restOptions.keys.split(','));
|
||||
const keySet = Array.from(keys).reduce((set, key) => {
|
||||
const keyPath = key.split('.');
|
||||
let i=0;
|
||||
for (i; i<path.length; i++) {
|
||||
let i = 0;
|
||||
for (i; i < path.length; i++) {
|
||||
if (path[i] != keyPath[i]) {
|
||||
return set;
|
||||
}
|
||||
|
||||
+3
-3
@@ -204,11 +204,11 @@ RestWrite.prototype.validateAuthData = function() {
|
||||
}
|
||||
|
||||
if (!this.query && !this.data.authData) {
|
||||
if (typeof this.data.username !== 'string') {
|
||||
if (typeof this.data.username !== 'string' || _.isEmpty(this.data.username)) {
|
||||
throw new Parse.Error(Parse.Error.USERNAME_MISSING,
|
||||
'bad or missing username');
|
||||
}
|
||||
if (typeof this.data.password !== 'string') {
|
||||
if (typeof this.data.password !== 'string' || _.isEmpty(this.data.password)) {
|
||||
throw new Parse.Error(Parse.Error.PASSWORD_MISSING,
|
||||
'password is required');
|
||||
}
|
||||
@@ -366,7 +366,7 @@ RestWrite.prototype.transformUser = function() {
|
||||
|
||||
return promise.then(() => {
|
||||
// Transform the password
|
||||
if (!this.data.password) {
|
||||
if (this.data.password === undefined) { // ignore only if undefined. should proceed if empty ('')
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ export class FeaturesRouter extends PromiseRouter {
|
||||
from: true,
|
||||
},
|
||||
push: {
|
||||
immediatePush: req.config.pushController.pushIsAvailable,
|
||||
immediatePush: req.config.hasPushSupport,
|
||||
scheduledPush: false,
|
||||
storedPushData: req.config.pushController.pushIsAvailable,
|
||||
storedPushData: req.config.hasPushSupport,
|
||||
pushAudiences: false,
|
||||
},
|
||||
schemas: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as Middlewares from '../middlewares';
|
||||
import Parse from 'parse/node';
|
||||
import Config from '../Config';
|
||||
import mime from 'mime';
|
||||
import logger from '../logger';
|
||||
|
||||
export class FilesRouter {
|
||||
|
||||
@@ -87,7 +88,8 @@ export class FilesRouter {
|
||||
res.status(201);
|
||||
res.set('Location', result.url);
|
||||
res.json(result);
|
||||
}).catch(() => {
|
||||
}).catch((e) => {
|
||||
logger.error(e.message, e);
|
||||
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Could not store file.'));
|
||||
});
|
||||
}
|
||||
@@ -137,7 +139,7 @@ function handleFileStream(stream, req, res, contentType) {
|
||||
}
|
||||
|
||||
if (!partialend) {
|
||||
if (((stream.length-1) - start) < (buffer_size)) {
|
||||
if (((stream.length - 1) - start) < (buffer_size)) {
|
||||
end = stream.length - 1;
|
||||
}else{
|
||||
end = start + (buffer_size);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user