mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0958b15297 | ||
|
|
5d9dbea07b | ||
|
|
f331f6644c | ||
|
|
df029b82eb | ||
|
|
edba550cf6 | ||
|
|
c7775eb0b9 | ||
|
|
0b44820a7d | ||
|
|
b7322064d8 | ||
|
|
ac3f5cd686 | ||
|
|
9fd34fd25e | ||
|
|
7ff3a3813d | ||
|
|
754f5d0505 | ||
|
|
0e8c3c2457 | ||
|
|
423a50133a | ||
|
|
a3d416de4a | ||
|
|
a2dfcc8da8 | ||
|
|
f4734a65c0 |
+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.
|
||||
|
||||
+2
-4
@@ -1,7 +1,7 @@
|
||||
language: node_js
|
||||
dist: trusty
|
||||
node_js:
|
||||
- '4.5'
|
||||
- '4.6'
|
||||
- '6.1'
|
||||
services:
|
||||
- postgresql
|
||||
@@ -12,7 +12,7 @@ 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
|
||||
@@ -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
|
||||
|
||||
+87
-3
@@ -1,5 +1,89 @@
|
||||
## Parse Server Changelog
|
||||
|
||||
### 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)
|
||||
|
||||
#### New features
|
||||
* Add parseFrameURL for masking user-facing pages (#3267), thanks to [Lenart Rudel](https://github.com/lenart)
|
||||
|
||||
#### Bug fixes
|
||||
* Fix Parse-Server to work with winston-daily-rotate-1.4.2 (#3335), thanks to [Arthur Cinader](https://github.com/acinader)
|
||||
|
||||
#### Improvements
|
||||
* Add support for regex string for password policy validatorPattern setting (#3331), thanks to [Bhaskar Reddy Yasa](https://github.com/bhaskaryasa)
|
||||
* LiveQuery should match subobjects with dot notation (#3322), thanks to [David Starke](https://github.com/dstarke)
|
||||
* Reduce time to process high number of installations for push (#3264), thanks to [jeacott1](https://github.com/jeacott1)
|
||||
* Fix trivial typo in error message (#3238), thanks to [Arthur Cinader](https://github.com/acinader)
|
||||
|
||||
### 2.3.1
|
||||
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.0...2.3.1)
|
||||
|
||||
@@ -17,7 +101,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
|
||||
@@ -70,7 +154,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
|
||||
@@ -117,7 +201,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)
|
||||
|
||||
@@ -156,7 +156,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
|
||||
|
||||
@@ -216,6 +216,8 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
||||
* `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
|
||||
|
||||
@@ -281,7 +283,7 @@ var server = ParseServer({
|
||||
passwordPolicy: {
|
||||
// Two optional settings to enforce strong passwords. Either one or both can be specified.
|
||||
// If both are specified, both checks must pass to accept the password
|
||||
// 1. a RegExp representing the pattern to enforce
|
||||
// 1. a RegExp object or a regex string representing the pattern to enforce
|
||||
validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit
|
||||
// 2. a callback function to be invoked to validate the password
|
||||
validatorCallback: (password) => { return validatePassword(password) },
|
||||
|
||||
+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}"
|
||||
|
||||
+29
-27
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "2.3.1",
|
||||
"version": "2.3.7",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -18,49 +18,50 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"bcryptjs": "2.3.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.2",
|
||||
"lodash": "4.17.4",
|
||||
"lru-cache": "4.0.2",
|
||||
"mime": "1.3.4",
|
||||
"mongodb": "2.2.10",
|
||||
"multer": "1.2.0",
|
||||
"mongodb": "2.2.24",
|
||||
"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.2.0",
|
||||
"parse-server-s3-adapter": "1.0.6",
|
||||
"parse-server-simple-mailgun-adapter": "1.0.0",
|
||||
"pg-promise": "5.4.5",
|
||||
"redis": "2.6.3",
|
||||
"request": "2.79.0",
|
||||
"pg-promise": "5.6.2",
|
||||
"redis": "2.6.5",
|
||||
"request": "2.81.0",
|
||||
"semver": "5.2.0",
|
||||
"tv4": "1.2.7",
|
||||
"winston": "2.3.0",
|
||||
"winston-daily-rotate-file": "1.4.0",
|
||||
"ws": "1.1.1"
|
||||
"winston": "2.3.1",
|
||||
"winston-daily-rotate-file": "1.4.5",
|
||||
"ws": "2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "6.18.0",
|
||||
"babel-core": "6.18.2",
|
||||
|
||||
"babel-cli": "6.23.0",
|
||||
"babel-core": "6.23.1",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-plugin-syntax-flow": "6.13.0",
|
||||
"babel-plugin-transform-flow-strip-types": "6.18.0",
|
||||
"babel-preset-es2015": "6.14.0",
|
||||
"babel-preset-stage-0": "6.5.0",
|
||||
"babel-register": "6.18.0",
|
||||
"babel-plugin-transform-flow-strip-types": "6.22.0",
|
||||
"babel-preset-es2015": "6.22.0",
|
||||
"babel-preset-stage-0": "6.22.0",
|
||||
"babel-register": "6.23.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"cross-env": "3.1.3",
|
||||
"cross-env": "3.1.4",
|
||||
"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"
|
||||
@@ -68,7 +69,7 @@
|
||||
"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",
|
||||
@@ -78,12 +79,13 @@
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.5"
|
||||
"node": ">=4.6"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "./bin/parse-server"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bcrypt": "1.0.1"
|
||||
"bcrypt": "1.0.2",
|
||||
"uws": "^0.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
+1
-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"')
|
||||
|
||||
+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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
@@ -147,14 +147,14 @@ describe("Password Policy: ", () => {
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
passwordPolicy: {
|
||||
validatorPattern: "abc" // string is not a valid setting
|
||||
validatorPattern: 1234 // number is not a valid setting
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
}).then(() => {
|
||||
fail('passwordPolicy.validatorPattern type test failed');
|
||||
done();
|
||||
}).catch(err => {
|
||||
expect(err).toEqual('passwordPolicy.validatorPattern must be a RegExp.');
|
||||
expect(err).toEqual('passwordPolicy.validatorPattern must be a regex string or RegExp object.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -197,6 +197,50 @@ describe("Password Policy: ", () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('signup should fail if password does not conform to the policy enforced using validatorPattern string', (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("less");
|
||||
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.code).toEqual(142);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
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({
|
||||
@@ -231,6 +275,40 @@ describe("Password Policy: ", () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('signup should succeed if password conforms to the policy enforced using validatorPattern string', (done) => {
|
||||
const user = new Parse.User();
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
passwordPolicy: {
|
||||
validatorPattern: "[!@#$]+" // password should contain at least one special char
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
}).then(() => {
|
||||
user.setUsername("user1");
|
||||
user.setPassword("p@sswrod");
|
||||
user.set('email', 'user1@parse.com');
|
||||
user.signUp().then(() => {
|
||||
Parse.User.logOut().then(() => {
|
||||
Parse.User.logIn("user1", "p@sswrod").then(function () {
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
fail("Should be able to login");
|
||||
done();
|
||||
});
|
||||
}).catch((error) => {
|
||||
jfail(error);
|
||||
fail('logout should have succeeded');
|
||||
done();
|
||||
});
|
||||
}).catch((error) => {
|
||||
jfail(error);
|
||||
fail('Signup should have succeeded as password conforms to the policy.');
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('signup should fail if password does not conform to the policy enforced using validatorCallback', (done) => {
|
||||
const user = new Parse.User();
|
||||
reconfigureServer({
|
||||
@@ -409,12 +487,12 @@ describe("Password Policy: ", () => {
|
||||
fail("should login with new password");
|
||||
done();
|
||||
});
|
||||
}).catch((error)=> {
|
||||
}).catch((error) => {
|
||||
jfail(error);
|
||||
fail("Failed to POST request password reset");
|
||||
done();
|
||||
});
|
||||
}).catch((error)=> {
|
||||
}).catch((error) => {
|
||||
jfail(error);
|
||||
fail("Failed to get the reset link");
|
||||
done();
|
||||
@@ -839,7 +917,7 @@ describe("Password Policy: ", () => {
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
passwordPolicy: {
|
||||
maxPasswordAge: 0.5/(24*60*60) // 0.5 sec
|
||||
maxPasswordAge: 0.5 / (24 * 60 * 60) // 0.5 sec
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
}).then(() => {
|
||||
@@ -880,7 +958,7 @@ describe("Password Policy: ", () => {
|
||||
reconfigureServer({
|
||||
appName: 'passwordPolicy',
|
||||
passwordPolicy: {
|
||||
maxPasswordAge: 0.5/(24*60*60) // 0.5 sec
|
||||
maxPasswordAge: 0.5 / (24 * 60 * 60) // 0.5 sec
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
}).then(() => {
|
||||
@@ -979,7 +1057,7 @@ describe("Password Policy: ", () => {
|
||||
appName: 'passwordPolicy',
|
||||
emailAdapter: emailAdapter,
|
||||
passwordPolicy: {
|
||||
maxPasswordAge: 0.5/(24*60*60) // 0.5 sec
|
||||
maxPasswordAge: 0.5 / (24 * 60 * 60) // 0.5 sec
|
||||
},
|
||||
publicServerURL: "http://localhost:8378/1"
|
||||
}).then(() => {
|
||||
|
||||
+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(() => {
|
||||
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);
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -412,4 +412,66 @@ describe('matchesQuery', function() {
|
||||
expect(matchesQuery(caltrainStation, q)).toBe(false);
|
||||
expect(matchesQuery(santaClara, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches on subobjects with dot notation', function() {
|
||||
var message = {
|
||||
id: new Id('Message', 'O1'),
|
||||
text: "content",
|
||||
status: {x: "read", y: "delivered"}
|
||||
};
|
||||
|
||||
var q = new Parse.Query('Message');
|
||||
q.equalTo("status.x", "read");
|
||||
expect(matchesQuery(message, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.equalTo("status.z", "read");
|
||||
expect(matchesQuery(message, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.equalTo("status.x", "delivered");
|
||||
expect(matchesQuery(message, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.notEqualTo("status.x", "read");
|
||||
expect(matchesQuery(message, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.notEqualTo("status.z", "read");
|
||||
expect(matchesQuery(message, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.notEqualTo("status.x", "delivered");
|
||||
expect(matchesQuery(message, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.exists("status.x");
|
||||
expect(matchesQuery(message, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.exists("status.z");
|
||||
expect(matchesQuery(message, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.exists("nonexistent.x");
|
||||
expect(matchesQuery(message, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.doesNotExist("status.x");
|
||||
expect(matchesQuery(message, q)).toBe(false);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.doesNotExist("status.z");
|
||||
expect(matchesQuery(message, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.doesNotExist("nonexistent.z");
|
||||
expect(matchesQuery(message, q)).toBe(true);
|
||||
|
||||
q = new Parse.Query('Message');
|
||||
q.equalTo("status.x", "read");
|
||||
q.doesNotExist("status.y");
|
||||
expect(matchesQuery(message, q)).toBe(false);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
var RedisCacheAdapter = require('../src/Adapters/Cache/RedisCacheAdapter').default;
|
||||
|
||||
describe('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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
var UserController = require('../src/Controllers/UserController').UserController;
|
||||
var emailAdapter = require('./MockEmailAdapter')
|
||||
var AppCache = require('../src/cache').AppCache;
|
||||
|
||||
describe('UserController', () => {
|
||||
var user = {
|
||||
_email_verify_token: 'testToken',
|
||||
username: 'testUser',
|
||||
email: 'test@example.com'
|
||||
}
|
||||
|
||||
describe('sendVerificationEmail', () => {
|
||||
describe('parseFrameURL not provided', () => {
|
||||
it('uses publicServerURL', (done) => {
|
||||
|
||||
AppCache.put(defaultConfiguration.appId, Object.assign({}, defaultConfiguration, {
|
||||
publicServerURL: 'http://www.example.com',
|
||||
customPages: {
|
||||
parseFrameURL: undefined
|
||||
}
|
||||
}))
|
||||
|
||||
emailAdapter.sendVerificationEmail = (options) => {
|
||||
expect(options.link).toEqual('http://www.example.com/apps/test/verify_email?token=testToken&username=testUser')
|
||||
done()
|
||||
}
|
||||
|
||||
var userController = new UserController(emailAdapter, 'test', {
|
||||
verifyUserEmails: true
|
||||
})
|
||||
|
||||
userController.sendVerificationEmail(user)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseFrameURL provided', () => {
|
||||
it('uses parseFrameURL and includes the destination in the link parameter', (done) => {
|
||||
|
||||
AppCache.put(defaultConfiguration.appId, Object.assign({}, defaultConfiguration, {
|
||||
publicServerURL: 'http://www.example.com',
|
||||
customPages: {
|
||||
parseFrameURL: 'http://someother.example.com/handle-parse-iframe'
|
||||
}
|
||||
}))
|
||||
|
||||
emailAdapter.sendVerificationEmail = (options) => {
|
||||
expect(options.link).toEqual('http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser')
|
||||
done()
|
||||
}
|
||||
|
||||
var userController = new UserController(emailAdapter, 'test', {
|
||||
verifyUserEmails: true
|
||||
})
|
||||
|
||||
userController.sendVerificationEmail(user)
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
@@ -12,7 +12,8 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
|
||||
invalidLink: "myInvalidLink",
|
||||
verifyEmailSuccess: "myVerifyEmailSuccess",
|
||||
choosePassword: "myChoosePassword",
|
||||
passwordResetSuccess: "myPasswordResetSuccess"
|
||||
passwordResetSuccess: "myPasswordResetSuccess",
|
||||
parseFrameURL: "http://example.com/handle-parse-iframe"
|
||||
},
|
||||
publicServerURL: "https://my.public.server.com/1"
|
||||
})
|
||||
@@ -22,6 +23,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
|
||||
expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess");
|
||||
expect(config.choosePasswordURL).toEqual("myChoosePassword");
|
||||
expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess");
|
||||
expect(config.parseFrameURL).toEqual("http://example.com/handle-parse-iframe");
|
||||
expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email");
|
||||
expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset");
|
||||
done();
|
||||
@@ -381,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();
|
||||
});
|
||||
})
|
||||
@@ -412,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();
|
||||
});
|
||||
})
|
||||
@@ -440,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();
|
||||
});
|
||||
})
|
||||
|
||||
+2
-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);
|
||||
|
||||
|
||||
+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)
|
||||
|
||||
@@ -36,7 +36,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 +118,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 +162,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 +172,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 +193,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 +210,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 = '';
|
||||
|
||||
@@ -12,7 +12,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,12 +21,12 @@ function updateTransports(options) {
|
||||
Object.assign({}, {
|
||||
filename: 'parse-server.info',
|
||||
name: 'parse-server',
|
||||
}, options));
|
||||
}, options, { timestamp: true }));
|
||||
transports['parse-server-error'] = new (DailyRotateFile)(
|
||||
Object.assign({}, {
|
||||
filename: 'parse-server.err',
|
||||
name: 'parse-server-error',
|
||||
}, options, { level: 'error'}));
|
||||
}, options, { level: 'error', timestamp: true }));
|
||||
}
|
||||
|
||||
transports.console = new (winston.transports.Console)(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
import { createClient } from './PostgresClient';
|
||||
import Parse from 'parse/node';
|
||||
import _ from 'lodash';
|
||||
import sql from './sql';
|
||||
|
||||
const PostgresRelationDoesNotExistError = '42P01';
|
||||
const PostgresDuplicateRelationError = '42P07';
|
||||
@@ -11,7 +12,7 @@ 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 +197,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 +278,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 +306,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 +324,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 +338,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 +358,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;
|
||||
}
|
||||
@@ -490,11 +491,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 +619,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 +700,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 +763,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 +849,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 +927,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 +1016,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 +1043,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);
|
||||
}
|
||||
@@ -1141,41 +1160,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 +1265,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$;
|
||||
+18
-5
@@ -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;
|
||||
@@ -127,10 +130,16 @@ export class Config {
|
||||
throw 'passwordPolicy.resetTokenValidityDuration must be a positive number';
|
||||
}
|
||||
|
||||
if(passwordPolicy.validatorPattern && !(passwordPolicy.validatorPattern instanceof RegExp)) {
|
||||
throw 'passwordPolicy.validatorPattern must be a RegExp.';
|
||||
if(passwordPolicy.validatorPattern){
|
||||
if(typeof(passwordPolicy.validatorPattern) === 'string') {
|
||||
passwordPolicy.validatorPattern = new RegExp(passwordPolicy.validatorPattern);
|
||||
}
|
||||
else if(!(passwordPolicy.validatorPattern instanceof RegExp)){
|
||||
throw 'passwordPolicy.validatorPattern must be a regex string or RegExp object.';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(passwordPolicy.validatorCallback && typeof passwordPolicy.validatorCallback !== 'function') {
|
||||
throw 'passwordPolicy.validatorCallback must be a function.';
|
||||
}
|
||||
@@ -201,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() {
|
||||
@@ -217,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() {
|
||||
@@ -240,6 +249,10 @@ export class Config {
|
||||
return this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html`;
|
||||
}
|
||||
|
||||
get parseFrameURL() {
|
||||
return this.customPages.parseFrameURL;
|
||||
}
|
||||
|
||||
get verifyEmailURL() {
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`;
|
||||
}
|
||||
|
||||
@@ -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,10 +145,10 @@ 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 availabe');
|
||||
'Logger adapter is not available');
|
||||
}
|
||||
if (typeof this.adapter.query !== 'function') {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -119,7 +119,8 @@ export class UserController extends AdaptableController {
|
||||
// We may need to fetch the user in case of update email
|
||||
this.getUserIfNeeded(user).then((user) => {
|
||||
const username = encodeURIComponent(user.username);
|
||||
const link = `${this.config.verifyEmailURL}?token=${token}&username=${username}`;
|
||||
|
||||
const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config);
|
||||
const options = {
|
||||
appName: this.config.appName,
|
||||
link: link,
|
||||
@@ -153,8 +154,8 @@ export class UserController extends AdaptableController {
|
||||
.then(user => {
|
||||
const token = encodeURIComponent(user._perishable_token);
|
||||
const username = encodeURIComponent(user.username);
|
||||
const link = `${this.config.requestResetPasswordURL}?token=${token}&username=${username}`
|
||||
|
||||
const link = buildEmailLink(this.config.requestResetPasswordURL, username, token, this.config);
|
||||
const options = {
|
||||
appName: this.config.appName,
|
||||
link: link,
|
||||
@@ -215,4 +216,16 @@ function updateUserPassword(userId, password, config) {
|
||||
});
|
||||
}
|
||||
|
||||
function buildEmailLink(destination, username, token, config) {
|
||||
const usernameAndToken = `token=${token}&username=${username}`
|
||||
|
||||
if (config.parseFrameURL) {
|
||||
const destinationWithoutHost = destination.replace(config.publicServerURL, '');
|
||||
|
||||
return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`;
|
||||
} else {
|
||||
return `${destination}?${usernameAndToken}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UserController;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -133,6 +133,13 @@ function matchesKeyConstraints(object, key, constraints) {
|
||||
if (constraints === null) {
|
||||
return false;
|
||||
}
|
||||
if(key.indexOf(".") >= 0){
|
||||
// Key references a subobject
|
||||
var keyComponents = key.split(".");
|
||||
var subObjectKey = keyComponents[0];
|
||||
var keyRemainder = keyComponents.slice(1).join(".");
|
||||
return matchesKeyConstraints(object[subObjectKey] || {}, keyRemainder, constraints);
|
||||
}
|
||||
var i;
|
||||
if (key === '$or') {
|
||||
for (i = 0; i < constraints.length; i++) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
@@ -191,7 +191,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
return Promise.resolve(success);
|
||||
}
|
||||
|
||||
handleResetRequest(req) {
|
||||
_throwOnBadEmailConfig(req) {
|
||||
try {
|
||||
Config.validateEmailConfiguration({
|
||||
emailAdapter: req.config.userController.adapter,
|
||||
@@ -202,11 +202,16 @@ export class UsersRouter extends ClassesRouter {
|
||||
} catch (e) {
|
||||
if (typeof e === 'string') {
|
||||
// Maybe we need a Bad Configuration error, but the SDKs won't understand it. For now, Internal Server Error.
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'An appName, publicServerURL, and emailAdapter are required for password reset functionality.');
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleResetRequest(req) {
|
||||
this._throwOnBadEmailConfig(req);
|
||||
|
||||
const { email } = req.body;
|
||||
if (!email) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_MISSING, "you must provide an email");
|
||||
@@ -228,6 +233,33 @@ export class UsersRouter extends ClassesRouter {
|
||||
});
|
||||
}
|
||||
|
||||
handleVerificationEmailRequest(req) {
|
||||
this._throwOnBadEmailConfig(req);
|
||||
|
||||
const { email } = req.body;
|
||||
if (!email) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
|
||||
}
|
||||
if (typeof email !== 'string') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string');
|
||||
}
|
||||
|
||||
return req.config.database.find('_User', { email: email }).then((results) => {
|
||||
if (!results.length || results.length < 1) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`);
|
||||
}
|
||||
const user = results[0];
|
||||
|
||||
if (user.emailVerified) {
|
||||
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `Email ${email} is already verified.`);
|
||||
}
|
||||
|
||||
const userController = req.config.userController;
|
||||
userController.sendVerificationEmail(user);
|
||||
return { response: {} };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/users', req => { return this.handleFind(req); });
|
||||
@@ -238,7 +270,8 @@ export class UsersRouter extends ClassesRouter {
|
||||
this.route('DELETE', '/users/:objectId', req => { return this.handleDelete(req); });
|
||||
this.route('GET', '/login', req => { return this.handleLogIn(req); });
|
||||
this.route('POST', '/logout', req => { return this.handleLogOut(req); });
|
||||
this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); })
|
||||
this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); });
|
||||
this.route('POST', '/verificationEmailRequest', req => { return this.handleVerificationEmailRequest(req); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+55
-25
@@ -4,15 +4,25 @@ import { logger } from './logger';
|
||||
const PUSH_STATUS_COLLECTION = '_PushStatus';
|
||||
const JOB_STATUS_COLLECTION = '_JobStatus';
|
||||
|
||||
const incrementOp = function(object = {}, key, amount = 1) {
|
||||
if (!object[key]) {
|
||||
object[key] = {__op: 'Increment', amount: amount}
|
||||
} else {
|
||||
object[key].amount += amount;
|
||||
}
|
||||
return object[key];
|
||||
}
|
||||
|
||||
export function flatten(array) {
|
||||
return array.reduce((memo, element) => {
|
||||
if (Array.isArray(element)) {
|
||||
memo = memo.concat(flatten(element));
|
||||
var flattened = [];
|
||||
for(var i = 0; i < array.length; i++) {
|
||||
if(Array.isArray(array[i])) {
|
||||
flattened = flattened.concat(flatten(array[i]));
|
||||
} else {
|
||||
memo = memo.concat(element);
|
||||
flattened.push(array[i]);
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
}
|
||||
return flattened;
|
||||
}
|
||||
|
||||
function statusHandler(className, database) {
|
||||
@@ -93,10 +103,9 @@ export function jobStatusHandler(config) {
|
||||
});
|
||||
}
|
||||
|
||||
export function pushStatusHandler(config) {
|
||||
export function pushStatusHandler(config, objectId = newObjectId()) {
|
||||
|
||||
let pushStatus;
|
||||
const objectId = newObjectId();
|
||||
const database = config.database;
|
||||
const handler = statusHandler(PUSH_STATUS_COLLECTION, database);
|
||||
const setInitial = function(body = {}, where, options = {source: 'rest'}) {
|
||||
@@ -135,18 +144,17 @@ export function pushStatusHandler(config) {
|
||||
});
|
||||
}
|
||||
|
||||
const setRunning = function(installations) {
|
||||
logger.verbose('sending push to %d installations', installations.length);
|
||||
const setRunning = function(count) {
|
||||
logger.verbose(`_PushStatus ${objectId}: sending push to %d installations`, count);
|
||||
return handler.update({status:"pending", objectId: objectId},
|
||||
{status: "running", updatedAt: new Date() });
|
||||
{status: "running", updatedAt: new Date(), count });
|
||||
}
|
||||
|
||||
const complete = function(results) {
|
||||
const trackSent = function(results) {
|
||||
const update = {
|
||||
status: 'succeeded',
|
||||
updatedAt: new Date(),
|
||||
numSent: 0,
|
||||
numFailed: 0,
|
||||
numFailed: 0
|
||||
};
|
||||
if (Array.isArray(results)) {
|
||||
results = flatten(results);
|
||||
@@ -156,23 +164,44 @@ export function pushStatusHandler(config) {
|
||||
return memo;
|
||||
}
|
||||
const deviceType = result.device.deviceType;
|
||||
if (result.transmitted)
|
||||
{
|
||||
const key = result.transmitted ? `sentPerType.${deviceType}` : `failedPerType.${deviceType}`;
|
||||
memo[key] = incrementOp(memo, key);
|
||||
if (result.transmitted) {
|
||||
memo.numSent++;
|
||||
memo.sentPerType = memo.sentPerType || {};
|
||||
memo.sentPerType[deviceType] = memo.sentPerType[deviceType] || 0;
|
||||
memo.sentPerType[deviceType]++;
|
||||
} else {
|
||||
memo.numFailed++;
|
||||
memo.failedPerType = memo.failedPerType || {};
|
||||
memo.failedPerType[deviceType] = memo.failedPerType[deviceType] || 0;
|
||||
memo.failedPerType[deviceType]++;
|
||||
}
|
||||
return memo;
|
||||
}, update);
|
||||
incrementOp(update, 'count', -results.length);
|
||||
}
|
||||
logger.verbose('sent push! %d success, %d failures', update.numSent, update.numFailed);
|
||||
return handler.update({status:"running", objectId }, update);
|
||||
|
||||
logger.verbose(`_PushStatus ${objectId}: sent push! %d success, %d failures`, update.numSent, update.numFailed);
|
||||
|
||||
['numSent', 'numFailed'].forEach((key) => {
|
||||
if (update[key] > 0) {
|
||||
update[key] = {
|
||||
__op: 'Increment',
|
||||
amount: update[key]
|
||||
};
|
||||
} else {
|
||||
delete update[key];
|
||||
}
|
||||
});
|
||||
|
||||
return handler.update({ objectId }, update).then((res) => {
|
||||
if (res && res.count === 0) {
|
||||
return this.complete();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const complete = function() {
|
||||
return handler.update({ objectId }, {
|
||||
status: 'succeeded',
|
||||
count: {__op: 'Delete'},
|
||||
updatedAt: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
const fail = function(err) {
|
||||
@@ -181,7 +210,7 @@ export function pushStatusHandler(config) {
|
||||
status: 'failed',
|
||||
updatedAt: new Date()
|
||||
}
|
||||
logger.info('warning: error while sending push', err);
|
||||
logger.warn(`_PushStatus ${objectId}: error while sending push`, err);
|
||||
return handler.update({ objectId }, update);
|
||||
}
|
||||
|
||||
@@ -189,6 +218,7 @@ export function pushStatusHandler(config) {
|
||||
objectId,
|
||||
setInitial,
|
||||
setRunning,
|
||||
trackSent,
|
||||
complete,
|
||||
fail
|
||||
})
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ function parseURL(URL) {
|
||||
|
||||
function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) {
|
||||
serverURL = serverURL ? parseURL(serverURL) : undefined;
|
||||
publicServerURL = publicServerURL ? parseURL(publicServerURL): undefined;
|
||||
publicServerURL = publicServerURL ? parseURL(publicServerURL) : undefined;
|
||||
|
||||
const apiPrefixLength = originalUrl.length - batchPath.length;
|
||||
let apiPrefix = originalUrl.slice(0, apiPrefixLength);
|
||||
|
||||
@@ -178,7 +178,7 @@ export default {
|
||||
},
|
||||
"userSensitiveFields": {
|
||||
help: "Personally identifiable information fields in the user table the should be removed for non-authorized users.",
|
||||
default: "email"
|
||||
default: ["email"]
|
||||
},
|
||||
"sessionLength": {
|
||||
env: "PARSE_SERVER_SESSION_LENGTH",
|
||||
@@ -249,4 +249,7 @@ export default {
|
||||
help: "Live query server configuration options (will start the liveQuery server)",
|
||||
action: objectParser
|
||||
},
|
||||
"middleware": {
|
||||
help: "middleware for express server, can be string or function"
|
||||
}
|
||||
};
|
||||
|
||||
+15
-3
@@ -5,6 +5,7 @@ import definitions from './definitions/parse-server';
|
||||
import cluster from 'cluster';
|
||||
import os from 'os';
|
||||
import runner from './utils/runner';
|
||||
const path = require("path");
|
||||
|
||||
const help = function(){
|
||||
console.log(' Get Started guide:');
|
||||
@@ -30,9 +31,20 @@ const help = function(){
|
||||
|
||||
function startServer(options, callback) {
|
||||
const app = express();
|
||||
if (options.middleware) {
|
||||
let middleware;
|
||||
if (typeof options.middleware == 'function') {
|
||||
middleware = options.middleware;
|
||||
} if (typeof options.middleware == 'string') {
|
||||
middleware = require(path.resolve(process.cwd(), options.middleware));
|
||||
} else {
|
||||
throw "middleware should be a string or a function";
|
||||
}
|
||||
app.use(middleware);
|
||||
}
|
||||
|
||||
const api = new ParseServer(options);
|
||||
const sockets = {};
|
||||
|
||||
app.use(options.mountPath, api);
|
||||
|
||||
const server = app.listen(options.port, options.host, callback);
|
||||
@@ -122,14 +134,14 @@ runner({
|
||||
});
|
||||
} else {
|
||||
startServer(options, () => {
|
||||
console.log('['+process.pid+'] parse-server running on '+options.serverURL);
|
||||
console.log('[' + process.pid + '] parse-server running on ' + options.serverURL);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
startServer(options, () => {
|
||||
logOptions();
|
||||
console.log('');
|
||||
console.log('['+process.pid+'] parse-server running on '+options.serverURL);
|
||||
console.log('[' + process.pid + '] parse-server running on ' + options.serverURL);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ export function randomHexString(size: number): string {
|
||||
if (size % 2 !== 0) {
|
||||
throw new Error('randomHexString size must be divisible by 2.')
|
||||
}
|
||||
return randomBytes(size/2).toString('hex');
|
||||
return randomBytes(size / 2).toString('hex');
|
||||
}
|
||||
|
||||
// Returns a new random alphanumeric string of the given size.
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import RedisCacheAdapter from './Adapters/Cache/RedisCacheAdapter'
|
||||
import * as TestUtils from './TestUtils';
|
||||
import { useExternal } from './deprecated';
|
||||
import { getLogger } from './logger';
|
||||
import { PushWorker } from './Push/PushWorker';
|
||||
|
||||
// Factory function
|
||||
const _ParseServer = function(options) {
|
||||
@@ -23,4 +24,4 @@ Object.defineProperty(module.exports, 'logger', {
|
||||
});
|
||||
|
||||
export default ParseServer;
|
||||
export { S3Adapter, GCSAdapter, FileSystemAdapter, InMemoryCacheAdapter, NullCacheAdapter, RedisCacheAdapter, TestUtils, _ParseServer as ParseServer };
|
||||
export { S3Adapter, GCSAdapter, FileSystemAdapter, InMemoryCacheAdapter, NullCacheAdapter, RedisCacheAdapter, TestUtils, PushWorker, _ParseServer as ParseServer };
|
||||
|
||||
+19
-16
@@ -1,9 +1,9 @@
|
||||
import AppCache from './cache';
|
||||
import log from './logger';
|
||||
import Parse from 'parse/node';
|
||||
import auth from './Auth';
|
||||
import Config from './Config';
|
||||
import ClientSDK from './ClientSDK';
|
||||
import AppCache from './cache';
|
||||
import log from './logger';
|
||||
import Parse from 'parse/node';
|
||||
import auth from './Auth';
|
||||
import Config from './Config';
|
||||
import ClientSDK from './ClientSDK';
|
||||
|
||||
// Checks that the request is authorized for this app and checks user
|
||||
// auth too.
|
||||
@@ -122,10 +122,10 @@ export function handleParseHeaders(req, res, next) {
|
||||
// to preserve original behavior.
|
||||
const keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"];
|
||||
const oneKeyConfigured = keys.some(function(key) {
|
||||
return req.config[key];
|
||||
return req.config[key] !== undefined;
|
||||
});
|
||||
const oneKeyMatches = keys.some(function(key){
|
||||
return req.config[key] && info[key] == req.config[key];
|
||||
return req.config[key] !== undefined && info[key] === req.config[key];
|
||||
});
|
||||
|
||||
if (oneKeyConfigured && !oneKeyMatches) {
|
||||
@@ -233,10 +233,8 @@ export function allowMethodOverride(req, res, next) {
|
||||
}
|
||||
|
||||
export function handleParseErrors(err, req, res, next) {
|
||||
// TODO: Add logging as those errors won't make it to the PromiseRouter
|
||||
if (err instanceof Parse.Error) {
|
||||
var httpStatus;
|
||||
|
||||
let httpStatus;
|
||||
// TODO: fill out this mapping
|
||||
switch (err.code) {
|
||||
case Parse.Error.INTERNAL_SERVER_ERROR:
|
||||
@@ -250,17 +248,22 @@ export function handleParseErrors(err, req, res, next) {
|
||||
}
|
||||
|
||||
res.status(httpStatus);
|
||||
res.json({code: err.code, error: err.message});
|
||||
res.json({ code: err.code, error: err.message });
|
||||
log.error(err.message, err);
|
||||
} else if (err.status && err.message) {
|
||||
res.status(err.status);
|
||||
res.json({error: err.message});
|
||||
res.json({ error: err.message });
|
||||
next(err);
|
||||
} else {
|
||||
log.error('Uncaught internal server error.', err, err.stack);
|
||||
res.status(500);
|
||||
res.json({code: Parse.Error.INTERNAL_SERVER_ERROR,
|
||||
message: 'Internal server error.'});
|
||||
res.json({
|
||||
code: Parse.Error.INTERNAL_SERVER_ERROR,
|
||||
message: 'Internal server error.'
|
||||
});
|
||||
next(err);
|
||||
}
|
||||
next(err);
|
||||
|
||||
}
|
||||
|
||||
export function enforceMasterKeyAccess(req, res, next) {
|
||||
|
||||
+16
-7
@@ -14,6 +14,16 @@ var RestQuery = require('./RestQuery');
|
||||
var RestWrite = require('./RestWrite');
|
||||
var triggers = require('./triggers');
|
||||
|
||||
function checkTriggers(className, config, types) {
|
||||
return types.some((triggerType) => {
|
||||
return triggers.getTrigger(className, triggers.Types[triggerType], config.applicationId);
|
||||
});
|
||||
}
|
||||
|
||||
function checkLiveQuery(className, config) {
|
||||
return config.liveQueryController && config.liveQueryController.hasLiveQuery(className)
|
||||
}
|
||||
|
||||
// Returns a promise for an object with optional keys 'results' and 'count'.
|
||||
function find(config, auth, className, restWhere, restOptions, clientSDK) {
|
||||
enforceRoleSecurity('find', className, auth);
|
||||
@@ -49,10 +59,9 @@ function del(config, auth, className, objectId) {
|
||||
var inflatedObject;
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
if (triggers.getTrigger(className, triggers.Types.beforeDelete, config.applicationId) ||
|
||||
triggers.getTrigger(className, triggers.Types.afterDelete, config.applicationId) ||
|
||||
(config.liveQueryController && config.liveQueryController.hasLiveQuery(className)) ||
|
||||
className == '_Session') {
|
||||
const hasTriggers = checkTriggers(className, config, ['beforeDelete', 'afterDelete']);
|
||||
const hasLiveQuery = checkLiveQuery(className, config);
|
||||
if (hasTriggers || hasLiveQuery || className == '_Session') {
|
||||
return find(config, Auth.master(config), className, {objectId: objectId})
|
||||
.then((response) => {
|
||||
if (response && response.results && response.results.length) {
|
||||
@@ -108,9 +117,9 @@ function update(config, auth, className, objectId, restObject, clientSDK) {
|
||||
enforceRoleSecurity('update', className, auth);
|
||||
|
||||
return Promise.resolve().then(() => {
|
||||
if (triggers.getTrigger(className, triggers.Types.beforeSave, config.applicationId) ||
|
||||
triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId) ||
|
||||
(config.liveQueryController && config.liveQueryController.hasLiveQuery(className))) {
|
||||
const hasTriggers = checkTriggers(className, config, ['beforeSave', 'afterSave']);
|
||||
const hasLiveQuery = checkLiveQuery(className, config);
|
||||
if (hasTriggers || hasLiveQuery) {
|
||||
return find(config, Auth.master(config), className, {objectId: objectId});
|
||||
}
|
||||
return Promise.resolve({});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user