Compare commits

..
94 Commits
Author SHA1 Message Date
Fosco Marotto b2819df7e5 Merge pull request #1264 from ParsePlatform/fosco.224
Changelog and update to 2.2.4
2016-03-29 14:36:26 -08:00
Fosco Marotto 5dc5ef239e Merge pull request #1265 from drew-gross/ignore-thrown-message
Fix test issue cause by env variables
2016-03-29 14:36:07 -08:00
Fosco Marotto f25bcc490a Updated changelog 2016-03-29 15:31:36 -07:00
Florent Vilmart aa5ebd49f0 Merge pull request #1199 from yuzeh/fix-authData-null-value
Clean authData of null values on _User update
2016-03-29 18:28:17 -04:00
Fosco Marotto 106ac95622 Fix adapter imports 2016-03-29 15:27:43 -07:00
Fosco Marotto 1f4393cd4d Changelog and update to 2.2.4 2016-03-29 15:10:55 -07:00
Fosco Marotto e361ccf5a5 Merge pull request #1263 from drew-gross/fix-adapters
Fix adapter imports
2016-03-29 14:08:46 -08:00
Drew Gross 342a3a92fe Fix adapter imports 2016-03-29 15:05:03 -07:00
Dan Huang 507fa54570 Merge branch 'master' into fix-authData-null-value 2016-03-29 14:43:19 -07:00
yuzeh 1b8e613dfb Clean authData of null values on _User update
Adds a step to the RestWrite#execute chain: it cleans the response
authData object of null values.

For example, this:

{"authData": {"anonymous": null}, "updatedAt", ...}

will be transformed to this:

{"updatedAt", ...}

And this:

{"authData": {"anonymous": null, "twitter": ...}, "updatedAt", ...}

will be transformed to this:

{"authData": {"twitter": ...}, "updatedAt", ...}

Fixing this issue will fix anonymous user upgrades from the Android SDK.
2016-03-29 14:42:37 -07:00
Florent Vilmart f259a3b17e Supports increment as well as Increment 2016-03-29 14:42:37 -07:00
Fosco Marotto 6be067cff5 Merge pull request #1261 from ParsePlatform/fosco.223
Changelog and update to 2.2.3
2016-03-29 13:02:24 -08:00
Fosco Marotto 73f8bbc2b3 Changelog and update to 2.2.3 2016-03-29 13:49:23 -07:00
Drew 337d3c297a Merge pull request #1248 from ParsePlatform/flovilmart.incrementBadge
Supports increment as well as Increment
2016-03-29 12:49:48 -07:00
Florent Vilmart cb4b036947 Supports increment as well as Increment 2016-03-29 15:39:58 -04:00
Drew ef08dcf76c Merge pull request #1234 from ParsePlatform/flovilmart.winston
Better logging with winston
2016-03-29 10:54:36 -07:00
Drew 9dd860948a Merge pull request #1253 from DroidsOnRoids/master
Fixed bug with invalid email verification link on email update.
2016-03-29 10:41:49 -07:00
Krzysztof Zielonka 6dc7aa87c6 Fixed bug with 'undefined' token in email verification link 2016-03-29 19:02:19 +02:00
Florent Vilmart 592334756b Merge pull request #1235 from drew-gross/config-out-of-experimental
Tested with the dashboard, it now works great.
2016-03-28 21:58:11 -04:00
Florent Vilmart 247a06f399 use console.error on EADDRINUSE 2016-03-28 21:57:22 -04:00
Florent Vilmart 74b847f1d1 Merge pull request #1204 from ParsePlatform/flovilmart.flatteningPushAdapterResults
Improve flattening of results from pushAdapter
2016-03-28 21:56:42 -04:00
Drew Gross 75f49b4d1a Tested with the dashboard, it now works great. 2016-03-28 18:43:06 -07:00
Florent Vilmart 5c1fe3a325 Better logging with winston 2016-03-28 20:16:58 -04:00
Florent Vilmart cd05e3bd19 Merge pull request #1210 from framp/master
Make GlobalConfig work like parse.com
2016-03-26 23:57:48 -04:00
Federico Rampazzo 236c7d15c2 Refactor code 2016-03-27 04:32:58 +01:00
Federico Rampazzo 6b9fd16273 Added merging in Config with scoped set 2016-03-27 03:46:36 +01:00
Federico Rampazzo 94b10de7b8 Fixed config upsert implementation to handle nested object and __op:Delete 2016-03-27 00:32:57 +00:00
Florent Vilmart aeda714b87 Improve flattening of results from pushAdapter 2016-03-26 11:02:26 -04:00
Federico Rampazzo 114f7d1b58 Make global config visible from parse-dashboard when it's enabled 2016-03-26 11:15:17 +00:00
Florent Vilmart d47a756f80 Merge pull request #1195 from ParsePlatform/flovilmart.moduleParseServerPush
Push adapters are provided by external packages
2016-03-25 17:37:18 -04:00
Drew 6bea950b32 Merge pull request #1164 from drew-gross/push-enable
Mark push as enabled in serverInfo endpoint
2016-03-25 13:54:22 -07:00
Drew 1796ae3b7c Close code block in readme 2016-03-25 11:44:01 -07:00
Florent Vilmart 8731337677 removes onesignal 2016-03-25 14:20:12 -04:00
Florent Vilmart f2f7c8bad3 ParsePushAdapter is a package 2016-03-25 13:36:52 -04:00
Florent Vilmart e3cb473903 Merge pull request #1185 from ParsePlatform/flovilmart.arrayPointersInQuery
fixes problem affecting finding array pointers
2016-03-25 08:24:23 -04:00
Florent Vilmart ebfbe5794c Merge pull request #1188 from drew-gross/fix-flaky-test
Fix flaky test
2016-03-25 08:24:09 -04:00
Drew Gross 7cdf85b367 Use getter 2016-03-24 20:59:50 -07:00
Drew Gross b8b86264f9 Fix flaky test 2016-03-24 19:48:41 -07:00
Drew Gross ec35c39724 Fix bad rebase 2016-03-24 19:39:54 -07:00
Drew Gross 19e6293638 Mark push as enabled in serverInfo endpoint 2016-03-24 19:32:51 -07:00
Drew 7738d33399 Merge pull request #1144 from drew-gross/document-email-adapter
Document email adapter
2016-03-24 19:31:45 -07:00
Florent Vilmart 4ff52de01c fixes problem affecting finding array pointers 2016-03-24 20:42:06 -04:00
Drew Gross 27a3198d81 Fix imports 2016-03-24 15:08:44 -07:00
Drew Gross b4ee31322e Fix tests 2016-03-24 14:42:45 -07:00
Drew Gross 3e45e04cf1 Use version 1.0.0 of mailgun adapter 2016-03-24 14:42:45 -07:00
Drew Gross 958d77207a Fix typo 2016-03-24 14:42:10 -07:00
Drew Gross 259de3f104 Fix readme to note move of mailgun adapter 2016-03-24 14:42:10 -07:00
Drew Gross 152c7e88b8 Move mailgun adapter to it's own repo 2016-03-24 14:42:10 -07:00
Drew Gross f28b719b83 Fix tests 2016-03-24 14:41:13 -07:00
Drew Gross 2f00a02936 Document email adapter 2016-03-24 14:41:13 -07:00
Florent Vilmart f4ad4c1af5 Merge pull request #1172 from ParsePlatform/flovilmart.externalAdapters
Moves Files adapters to external packages
2016-03-24 14:46:15 -04:00
Florent Vilmart 83f5207d44 remove inaccurate comment 2016-03-24 14:15:00 -04:00
Drew 82ebba4db8 Merge pull request #1133 from carmenlau/reset-password-fix
Reset password fix
2016-03-24 10:35:02 -07:00
Florent Vilmart 3d3ed53c5f Updates test that is failing cuz of instanceof
- Something weird is happening here, instanceof is not giving the right value
2016-03-24 00:57:49 -04:00
Florent Vilmart 1e45ac753a Moves Files adapters to external packages 2016-03-24 00:48:34 -04:00
Carmen 603bf97c0b Add test case for checking _perishable_token, it should be unset after password reset. #951 2016-03-24 10:22:14 +08:00
Carmen 120c6feb61 Unset _perishable_token after reset password, instead of setting it to null. #951 2016-03-24 10:22:14 +08:00
Carmen b3c5e836d5 Clear reset password token after reset password. _perishable_token is not a parse field, cannot clear it through rest. Update it separately. #951 2016-03-24 10:22:14 +08:00
Carmen 53e152e975 Instead of executing write directly, reuse rest.update to fix request object in user before save is empty when reset password. #951 2016-03-24 10:22:14 +08:00
Fosco Marotto ee8f85baef Merge pull request #1169 from ParsePlatform/fosco.222
Changelog and update to 2.2.2
2016-03-23 16:57:53 -08:00
Fosco Marotto 7a73d74869 Changelog and update to 2.2.2 2016-03-23 17:57:12 -07:00
Mengyan Wang 047c8e71ff Merge pull request #1168 from ParsePlatform/wangmengyan.fix_gcm_payload
Add pushId back to GCM payload
2016-03-23 17:47:07 -07:00
wangmengyan95 cbd1b878b9 Add pushId back to GCM payload 2016-03-23 17:39:27 -07:00
Drew 24146753b9 Merge pull request #1166 from ParsePlatform/flovilmart.MoveParseServer
moves ParseServer to it's own file
2016-03-23 16:45:31 -07:00
Florent Vilmart ea211e658f moves ParseServer to it's own file 2016-03-23 19:05:56 -04:00
Florent Vilmart 16b6ec4879 Merge pull request #1153 from ParsePlatform/flovilmart.fixBrokenParseServerClass
Mounts createLiveQueryServer, fix babel induced problem
2016-03-23 12:49:41 -04:00
Florent Vilmart ddb76a8650 Mounts createLiveQueryServer, fix babel induced problem 2016-03-23 09:19:09 -04:00
Florent Vilmart f5fdedfbc7 Merge pull request #1138 from ParsePlatform/flovilmart.bootstrap
Adds bootstrap.sh
2016-03-23 07:52:17 -04:00
Florent Vilmart 3ce634bd82 Merge pull request #1142 from ParsePlatform/flovilmart.fixes1136
do not override username
2016-03-23 07:52:04 -04:00
Florent Vilmart b30805b47b do not override username 2016-03-22 21:17:53 -04:00
Fosco Marotto 0e662190b3 Merge pull request #1139 from ParsePlatform/drew-gross-patch-2
Update README.md - remove deploy buttons - replace with community links
2016-03-22 16:55:30 -08:00
Drew 7d6f359434 Update README.md 2016-03-22 17:17:37 -07:00
Florent Vilmart 0a4cc606c6 Adds curl test command 2016-03-22 20:10:13 -04:00
Fosco Marotto 8124abab34 Merge pull request #1137 from ParsePlatform/fosco.221
Changelog and update to 2.2.1
2016-03-22 15:56:03 -08:00
Florent Vilmart b7a723fa7e Adds bootstrap.sh 2016-03-22 18:59:41 -04:00
Fosco Marotto 4e39975239 Changelog and update to 2.2.1 2016-03-22 15:37:38 -07:00
Drew e506784741 Merge pull request #1132 from ParsePlatform/flovilmart.hotfixCLPAssignments
Generates default CLP, freezes objects
2016-03-22 14:39:48 -07:00
Florent Vilmart 5d19c4ab45 Generates default CLP, freezes objects 2016-03-22 08:12:58 -04:00
Florent Vilmart cc6c3f46fe Merge pull request #1128 from drew-gross/emable-schema-editor
Enabled CLP editing
2016-03-22 07:41:47 -04:00
Drew Gross ffbca83aa3 Enabled CLP editing 2016-03-21 16:20:12 -07:00
Florent Vilmart 4bd32c8a4b Merge pull request #980 from ParsePlatform/flovilmart.ParseServerClass
ParseServer is a class
2016-03-21 08:43:37 -04:00
Florent Vilmart 260cc21b67 ParseServer is a class 2016-03-21 08:32:22 -04:00
Florent Vilmart fb57d91b83 Merge pull request #1110 from ParsePlatform/flovilmart.missingSessionInstallationId
Properly sets installationId on creating session with 3rd party auth
2016-03-21 08:21:47 -04:00
Florent Vilmart 414af99644 Merge pull request #1114 from ParsePlatform/flovilmart.addUniqueObject
Adds support for plain object in $add, $addUnique, $remove
2016-03-21 08:21:30 -04:00
Florent Vilmart 134b6de2b1 Merge pull request #1098 from dtsolis/dtsolis.fsadapter-proper
Add FileSystemAdapter file adapter
2016-03-21 08:20:27 -04:00
Florent Vilmart a31baa4ebd differentiate signup and login createdWith.action on _Session 2016-03-20 15:58:17 -04:00
Florent Vilmart 3603b82eac Properly sets installationId on creating session with 3rd party auth 2016-03-20 15:58:16 -04:00
Florent Vilmart 937dde592f Adds support for plain object in $add, $addUnique, $remove 2016-03-20 15:57:54 -04:00
Fosco Marotto 24f0fffb41 Merge pull request #1111 from ParsePlatform/flovilmart.reduceConnectionCount
Reduces the number of connections to mongo created
2016-03-20 11:56:25 -08:00
Tsolis Dimitris Sotiris e4e7e42c65 Add FileSystemAdapter file adapter 2016-03-20 21:35:42 +02:00
Florent Vilmart 44add9d6ac Reduces the number of connections to mongo created 2016-03-20 10:34:55 -04:00
Florent Vilmart c39ba3bdc7 Merge pull request #1102 from ParsePlatform/flovilmart.vendorCoverage
Ignores uncovered mongodbURL
2016-03-19 15:47:25 -04:00
Florent Vilmart 13a33a9497 forces mongodb-runner 3.1.15 as 3.1.16 is problematic 2016-03-19 14:58:24 -04:00
Florent Vilmart 46c66667c1 Ignores uncovered mongodbURL 2016-03-19 13:27:37 -04:00
60 changed files with 1397 additions and 2817 deletions
+4
View File
@@ -1,5 +1,6 @@
# Logs
logs
test_logs
*.log
# Runtime data
@@ -43,3 +44,6 @@ lib/
# Mac DS_Store files
.DS_Store
# Folder created by FileSystemAdapter
/files
+40
View File
@@ -1,5 +1,45 @@
## Parse Server Changelog
### 2.2.4 (3/29/2016)
* Hotfix: fixed imports issue for S3Adapter, GCSAdapter, FileSystemAdapter [\#1263](https://github.com/ParsePlatform/parse-server/pull/1263) ([drew-gross](https://github.com/drew-gross)
* Fix: Clean null authData values on _User update [\#1199](https://github.com/ParsePlatform/parse-server/pull/1199) ([yuzeh](https://github.com/yuzeh))
### 2.2.3 (3/29/2016)
* Fixed bug with invalid email verification link on email update. [\#1253](https://github.com/ParsePlatform/parse-server/pull/1253) ([kzielonka](https://github.com/kzielonka))
* Badge update supports increment as well as Increment [\#1248](https://github.com/ParsePlatform/parse-server/pull/1248) ([flovilmart](https://github.com/flovilmart))
* Config/Push Tested with the dashboard. [\#1235](https://github.com/ParsePlatform/parse-server/pull/1235) ([drew-gross](https://github.com/drew-gross))
* Better logging with winston [\#1234](https://github.com/ParsePlatform/parse-server/pull/1234) ([flovilmart](https://github.com/flovilmart))
* Make GlobalConfig work like parse.com [\#1210](https://github.com/ParsePlatform/parse-server/pull/1210) ([framp](https://github.com/framp))
* Improve flattening of results from pushAdapter [\#1204](https://github.com/ParsePlatform/parse-server/pull/1204) ([flovilmart](https://github.com/flovilmart))
* Push adapters are provided by external packages [\#1195](https://github.com/ParsePlatform/parse-server/pull/1195) ([flovilmart](https://github.com/flovilmart))
* Fix flaky test [\#1188](https://github.com/ParsePlatform/parse-server/pull/1188) ([drew-gross](https://github.com/drew-gross))
* Fixes problem affecting finding array pointers [\#1185](https://github.com/ParsePlatform/parse-server/pull/1185) ([flovilmart](https://github.com/flovilmart))
* Moves Files adapters to external packages [\#1172](https://github.com/ParsePlatform/parse-server/pull/1172) ([flovilmart](https://github.com/flovilmart))
* Mark push as enabled in serverInfo endpoint [\#1164](https://github.com/ParsePlatform/parse-server/pull/1164) ([drew-gross](https://github.com/drew-gross))
* Document email adapter [\#1144](https://github.com/ParsePlatform/parse-server/pull/1144) ([drew-gross](https://github.com/drew-gross))
* Reset password fix [\#1133](https://github.com/ParsePlatform/parse-server/pull/1133) ([carmenlau](https://github.com/carmenlau))
### 2.2.2 (3/23/2016)
* Important Fix: Mounts createLiveQueryServer, fix babel induced problem [\#1153](https://github.com/ParsePlatform/parse-server/pull/1153) (flovilmart)
* Move ParseServer to it's own file [\#1166](https://github.com/ParsePlatform/parse-server/pull/1166) (flovilmart)
* Update README.md - remove deploy buttons - replace with community links [\#1139](https://github.com/ParsePlatform/parse-server/pull/1139) (drew-gross)
* Adds bootstrap.sh [\#1138](https://github.com/ParsePlatform/parse-server/pull/1138) (flovilmart)
* Fix: Do not override username [\#1142](https://github.com/ParsePlatform/parse-server/pull/1142) (flovilmart)
* Fix: Add pushId back to GCM payload [\#1168](https://github.com/ParsePlatform/parse-server/pull/1168) (wangmengyan95)
### 2.2.1 (3/22/2016)
* New: Add FileSystemAdapter file adapter [\#1098](https://github.com/ParsePlatform/parse-server/pull/1098) (dtsolis)
* New: Enabled CLP editing [\#1128](https://github.com/ParsePlatform/parse-server/pull/1128) (drew-gross)
* Improvement: Reduces the number of connections to mongo created [\#1111](https://github.com/ParsePlatform/parse-server/pull/1111) (flovilmart)
* Improvement: Make ParseServer a class [\#980](https://github.com/ParsePlatform/parse-server/pull/980) (flovilmart)
* Fix: Adds support for plain object in $add, $addUnique, $remove [\#1114](https://github.com/ParsePlatform/parse-server/pull/1114) (flovilmart)
* Fix: Generates default CLP, freezes objects [\#1132](https://github.com/ParsePlatform/parse-server/pull/1132) (flovilmart)
* Fix: Properly sets installationId on creating session with 3rd party auth [\#1110](https://github.com/ParsePlatform/parse-server/pull/1110) (flovilmart)
### 2.2.0 (3/18/2016)
* New Feature: Real-time functionality with Live Queries! [\#1092](https://github.com/ParsePlatform/parse-server/pull/1092) (wangmengyan95)
+40 -2
View File
@@ -102,9 +102,16 @@ Once you have a better understanding of how the project works, please refer to t
### Parse Server Sample Application
We have provided a basic [Node.js application](https://github.com/ParsePlatform/parse-server-example) that uses the Parse Server module on Express and can be easily deployed using any of the following buttons:
We have provided a basic [Node.js application](https://github.com/ParsePlatform/parse-server-example) that uses the Parse Server module on Express and can be easily deployed to various infrastructure providers:
<a title="Deploy to AWS" href="https://console.aws.amazon.com/elasticbeanstalk/home?region=us-west-2#/newApplication?applicationName=ParseServer&solutionStackName=Node.js&tierName=WebServer&sourceBundleUrl=https://s3.amazonaws.com/elasticbeanstalk-samples-us-east-1/eb-parse-server-sample/parse-server-example.zip" target="_blank"><img src="http://d0.awsstatic.com/product-marketing/Elastic%20Beanstalk/deploy-to-aws.png" height="32"></a> <a title="Deploy to Heroku" href="https://heroku.com/deploy?template=https://github.com/parseplatform/parse-server-example" target="_blank"><img src="https://www.herokucdn.com/deploy/button.png"></a> <a title="Deploy to Azure" href="https://azuredeploy.net/?repository=https://github.com/parseplatform/parse-server-example" target="_blank"><img src="http://azuredeploy.net/deploybutton.png"></a>
* [Heroku and mLab](https://github.com/ParsePlatform/parse-server/wiki/Deploying-Parse-Server#deploying-to-heroku-and-mLab)
* [AWS and Elastic Beanstalk](http://mobile.awsblog.com/post/TxCD57GZLM2JR/How-to-set-up-Parse-Server-on-AWS-using-AWS-Elastic-Beanstalk)
* [Digital Ocean](https://www.digitalocean.com/community/tutorials/how-to-run-parse-server-on-ubuntu-14-04)
* [NodeChef](https://nodechef.com/blog/post/6/migrate-from-parse-to-nodechef%E2%80%99s-managed-parse-server)
* [Google App Engine](https://medium.com/@justinbeckwith/deploying-parse-server-to-google-app-engine-6bc0b7451d50)
* [Microsoft Azure](https://azure.microsoft.com/en-us/blog/azure-welcomes-parse-developers/)
* [Pivotal Web Services](https://github.com/cf-platform-eng/pws-parse-server)
* [Back4app](http://blog.back4app.com/2016/03/01/quick-wizard-migration/)
### Parse Server + Express
@@ -180,6 +187,37 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)).
* `databaseAdapter` - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`). Defaults to `MongoStorageAdapter`.
##### Email verification and password reset
Verifying user email addresses and enabling password reset via email requries an email adapter. As part of the `parse-server` package we provide an adapter for sending email through Mailgun. To use it, sign up for Mailgun, and add this to your initialization code:
```js
var server = ParseServer({
...otherOptions,
// Enable email verification
verifyUserEmails: true,
// The public URL of your app.
// This will appear in the link that is used to verify email addresses and reset passwords.
publicServerURL: 'https://example.com',
// Your apps name. This will appear in the subject and body of the emails that are sent.
appName: 'Parse App',
// The email adapter
emailAdapter: {
module: 'parse-server-simple-mailgun-adapter',
options: {
// The address that your emails come from
fromAddress: 'parse@example.com',
// Your domain from mailgun.com
domain: 'example.com',
// Your API key from mailgun.com
apiKey: 'key-mykey',
}
}
});
```
You can also use other email adapters contributed by the community such as [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter).
### Using environment variables to configure Parse Server
You may configure the Parse Server using environment variables:
Executable
+163
View File
@@ -0,0 +1,163 @@
#!/bin/sh
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
BOLD='\033[1m'
CHECK="${GREEN}\xE2\x9C\x93${NC}"
DEFAULT_MONGODB_URI='mongodb://localhost:127.0.0.1:27017/parse'
confirm() {
DEFAULT=$1;
shift
printf "$@"
read -r YN
if [ "$YN" = "" ] && [ "$DEFAULT" = 'N' ]; then
exit 1
elif [ "$YN" != "" ] && [ "$YN" != "y" ] && [ "$YN" != "Y" ]; then
echo 'Bye Bye!'
exit 1
fi
}
genstring() {
local l=$1
[ "$l" == "" ] && l=40
LC_ALL=C tr -dc A-Za-z0-9 < /dev/urandom | head -c ${l}
}
check_node() {
node=`which node 2>&1`
ret=$?
if [ $ret -eq 0 ] && [ -x "$node" ]; then
echo "${CHECK} node:" $(node -v)
(exit 0)
else
echo "parse-server cannot be installed without Node.js." >&2
exit 1
fi
}
check_npm() {
npm=`which npm 2>&1`
ret=$?
if [ $ret -eq 0 ] && [ -x "$npm" ]; then
echo "${CHECK} npm:" $(npm -v)
(exit 0)
else
echo "parse-server cannot be installed without npm." >&2
exit 1
fi
}
echo ''
echo 'This will setup parse-server in the current directory'
confirm 'Y' 'Do you want to continue? (Y/n): '
check_node
check_npm
echo "Setting up parse-server in $PWD"
if [ -f './package.json' ]; then
echo "\n${RED}package.json exists${NC}"
confirm 'N' "Do you want to continue? \n${RED}this will erase your configuration${NC} (y/N): "
fi
if [ -f 'config.json' ]; then
echo "\n${RED}config.json exists${NC}"
confirm 'N' "Do you want to continue? \n${RED}this will erase your configuration${NC} (y/N): "
fi
APP_NAME=''
i=0
while [ "$APP_NAME" = "" ]
do
[[ $i != 0 ]] && printf "${RED}An application name is required!${NC}\n"
printf 'Enter your Application Name: '
read -r APP_NAME
i=$(($i+1))
done
printf 'Enter your appId (leave empty to generate): '
read -r APP_ID
[[ $APP_ID = '' ]] && APP_ID=$(genstring) && printf "\n$APP_ID\n\n"
printf 'Enter your masterKey (leave empty to generate): '
read -r MASTER_KEY
[[ $MASTER_KEY = '' ]] && MASTER_KEY=$(genstring) && printf "\n$MASTER_KEY\n\n"
printf "Enter your mongodbURI (%s): " $DEFAULT_MONGODB_URI
read -r MONGODB_URI
[[ $MONGODB_URI = '' ]] && MONGODB_URI="$DEFAULT_MONGODB_URI"
cat > ./config.json << EOF
{
"appId": "$APP_ID",
"masterKey": "$MASTER_KEY",
"appName": "$APP_NAME",
"cloud": "./cloud/main",
"mongodbURI": "$MONGODB_URI"
}
EOF
echo "${CHECK} Created config.json"
# Make a proper npm app name
NPM_APP_NAME=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
cat > ./package.json << EOF
{
"name": "$NPM_APP_NAME",
"scripts": {
"start": "parse-server ./config.json"
},
"dependencies": {
"parse-server": "^2.0.0"
}
}
EOF
echo "${CHECK} Created package.json"
if [ -d "./cloud/" ]; then
echo "${CHECK} cloud/ exists"
else
mkdir -p ./cloud
echo "${CHECK} Created cloud/"
fi
if [ -e "./cloud/main.js" ]; then
echo "${CHECK} cloud/main.js exists"
else
echo "${CHECK} Created cloud/main.js"
cat > ./cloud/main.js << EOF
// Cloud Code entry point
EOF
fi
if [ -d "./public/" ]; then
echo "${CHECK} public/ exists"
else
mkdir -p ./public
echo "${CHECK} Created public/"
fi
echo "\n${CHECK} running npm install\n"
npm install
CURL_CMD=$(cat << EOF
curl -X POST -H 'X-Parse-Application-Id: ${APP_ID}' \\
-H 'Content-Type: application/json' \\
-d '{"foo":"bar"}' http://localhost:1337/parse/classes/TestObject
EOF)
echo "\n${CHECK} Happy Parsing!\n\n"
echo "${CHECK} Make sure you have ${BOLD}mongo${NC} listening on ${BOLD}${MONGODB_URI}${NC}"
echo "${CHECK} start parse-server by running ${BOLD}npm start${NC}"
echo "${CHECK} Test your setup with:\n\n${CURL_CMD}\n"
+9 -7
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "2.2.0",
"version": "2.2.4",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -18,8 +18,6 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"apn": "^1.7.5",
"aws-sdk": "~2.2.33",
"babel-polyfill": "^6.5.0",
"babel-runtime": "^6.5.0",
"bcrypt-nodejs": "0.0.3",
@@ -28,18 +26,22 @@
"commander": "^2.9.0",
"deepcopy": "^0.6.1",
"express": "^4.13.4",
"gcloud": "^0.28.0",
"lru-cache": "^4.0.0",
"mailgun-js": "^0.7.7",
"mime": "^1.3.4",
"mongodb": "~2.1.0",
"multer": "^1.1.0",
"node-gcm": "^0.14.0",
"parse": "^1.7.0",
"parse": "^1.8.0",
"parse-server-fs-adapter": "^1.0.0",
"parse-server-gcs-adapter": "^1.0.0",
"parse-server-push-adapter": "^1.0.0",
"parse-server-s3-adapter": "^1.0.0",
"parse-server-simple-mailgun-adapter": "^1.0.0",
"redis": "^2.5.0-1",
"request": "^2.65.0",
"tv4": "^1.2.7",
"winston": "^2.1.1",
"winston-daily-rotate-file": "^1.0.1",
"ws": "^1.0.1"
},
"devDependencies": {
@@ -57,7 +59,7 @@
"flow-bin": "^0.22.0",
"gaze": "^0.5.2",
"jasmine": "^2.3.2",
"mongodb-runner": "^3.1.15",
"mongodb-runner": "3.1.15",
"nodemon": "^1.8.1"
},
"scripts": {
-307
View File
@@ -1,307 +0,0 @@
var APNS = require('../src/APNS');
describe('APNS', () => {
it('can initialize with single cert', (done) => {
var args = {
cert: 'prodCert.pem',
key: 'prodKey.pem',
production: true,
bundleId: 'bundleId'
}
var apns = new APNS(args);
expect(apns.conns.length).toBe(1);
var apnsConnection = apns.conns[0];
expect(apnsConnection.index).toBe(0);
expect(apnsConnection.bundleId).toBe(args.bundleId);
// TODO: Remove this checking onec we inject APNS
var prodApnsOptions = apnsConnection.options;
expect(prodApnsOptions.cert).toBe(args.cert);
expect(prodApnsOptions.key).toBe(args.key);
expect(prodApnsOptions.production).toBe(args.production);
done();
});
it('can initialize with multiple certs', (done) => {
var args = [
{
cert: 'devCert.pem',
key: 'devKey.pem',
production: false,
bundleId: 'bundleId'
},
{
cert: 'prodCert.pem',
key: 'prodKey.pem',
production: true,
bundleId: 'bundleIdAgain'
}
]
var apns = new APNS(args);
expect(apns.conns.length).toBe(2);
var devApnsConnection = apns.conns[1];
expect(devApnsConnection.index).toBe(1);
var devApnsOptions = devApnsConnection.options;
expect(devApnsOptions.cert).toBe(args[0].cert);
expect(devApnsOptions.key).toBe(args[0].key);
expect(devApnsOptions.production).toBe(args[0].production);
expect(devApnsConnection.bundleId).toBe(args[0].bundleId);
var prodApnsConnection = apns.conns[0];
expect(prodApnsConnection.index).toBe(0);
// TODO: Remove this checking onec we inject APNS
var prodApnsOptions = prodApnsConnection.options;
expect(prodApnsOptions.cert).toBe(args[1].cert);
expect(prodApnsOptions.key).toBe(args[1].key);
expect(prodApnsOptions.production).toBe(args[1].production);
expect(prodApnsOptions.bundleId).toBe(args[1].bundleId);
done();
});
it('can generate APNS notification', (done) => {
//Mock request data
var data = {
'alert': 'alert',
'badge': 100,
'sound': 'test',
'content-available': 1,
'category': 'INVITE_CATEGORY',
'key': 'value',
'keyAgain': 'valueAgain'
};
var expirationTime = 1454571491354
var notification = APNS.generateNotification(data, expirationTime);
expect(notification.alert).toEqual(data.alert);
expect(notification.badge).toEqual(data.badge);
expect(notification.sound).toEqual(data.sound);
expect(notification.contentAvailable).toEqual(1);
expect(notification.category).toEqual(data.category);
expect(notification.payload).toEqual({
'key': 'value',
'keyAgain': 'valueAgain'
});
expect(notification.expiry).toEqual(expirationTime);
done();
});
it('can choose conns for device without appIdentifier', (done) => {
// Mock conns
var conns = [
{
bundleId: 'bundleId'
},
{
bundleId: 'bundleIdAgain'
}
];
// Mock device
var device = {};
var qualifiedConns = APNS.chooseConns(conns, device);
expect(qualifiedConns).toEqual([0, 1]);
done();
});
it('can choose conns for device with valid appIdentifier', (done) => {
// Mock conns
var conns = [
{
bundleId: 'bundleId'
},
{
bundleId: 'bundleIdAgain'
}
];
// Mock device
var device = {
appIdentifier: 'bundleId'
};
var qualifiedConns = APNS.chooseConns(conns, device);
expect(qualifiedConns).toEqual([0]);
done();
});
it('can choose conns for device with invalid appIdentifier', (done) => {
// Mock conns
var conns = [
{
bundleId: 'bundleId'
},
{
bundleId: 'bundleIdAgain'
}
];
// Mock device
var device = {
appIdentifier: 'invalid'
};
var qualifiedConns = APNS.chooseConns(conns, device);
expect(qualifiedConns).toEqual([]);
done();
});
it('can handle transmission error when notification is not in cache or device is missing', (done) => {
// Mock conns
var conns = [];
var errorCode = 1;
var notification = undefined;
var device = {};
APNS.handleTransmissionError(conns, errorCode, notification, device);
var notification = {};
var device = undefined;
APNS.handleTransmissionError(conns, errorCode, notification, device);
done();
});
it('can handle transmission error when there are other qualified conns', (done) => {
// Mock conns
var conns = [
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId2'
},
];
var errorCode = 1;
var notification = {};
var apnDevice = {
connIndex: 0,
appIdentifier: 'bundleId1'
};
APNS.handleTransmissionError(conns, errorCode, notification, apnDevice);
expect(conns[0].pushNotification).not.toHaveBeenCalled();
expect(conns[1].pushNotification).toHaveBeenCalled();
expect(conns[2].pushNotification).not.toHaveBeenCalled();
done();
});
it('can handle transmission error when there is no other qualified conns', (done) => {
// Mock conns
var conns = [
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId2'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
}
];
var errorCode = 1;
var notification = {};
var apnDevice = {
connIndex: 2,
appIdentifier: 'bundleId1'
};
APNS.handleTransmissionError(conns, errorCode, notification, apnDevice);
expect(conns[0].pushNotification).not.toHaveBeenCalled();
expect(conns[1].pushNotification).not.toHaveBeenCalled();
expect(conns[2].pushNotification).not.toHaveBeenCalled();
expect(conns[3].pushNotification).not.toHaveBeenCalled();
expect(conns[4].pushNotification).toHaveBeenCalled();
done();
});
it('can handle transmission error when device has no appIdentifier', (done) => {
// Mock conns
var conns = [
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId1'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId2'
},
{
pushNotification: jasmine.createSpy('pushNotification'),
bundleId: 'bundleId3'
},
];
var errorCode = 1;
var notification = {};
var apnDevice = {
connIndex: 1,
};
APNS.handleTransmissionError(conns, errorCode, notification, apnDevice);
expect(conns[0].pushNotification).not.toHaveBeenCalled();
expect(conns[1].pushNotification).not.toHaveBeenCalled();
expect(conns[2].pushNotification).toHaveBeenCalled();
done();
});
it('can send APNS notification', (done) => {
var args = {
cert: 'prodCert.pem',
key: 'prodKey.pem',
production: true,
bundleId: 'bundleId'
}
var apns = new APNS(args);
var conn = {
pushNotification: jasmine.createSpy('send'),
bundleId: 'bundleId'
};
apns.conns = [ conn ];
// Mock data
var expirationTime = 1454571491354
var data = {
'expiration_time': expirationTime,
'data': {
'alert': 'alert'
}
}
// Mock devices
var devices = [
{
deviceToken: '112233',
appIdentifier: 'bundleId'
}
];
var promise = apns.send(data, devices);
expect(conn.pushNotification).toHaveBeenCalled();
var args = conn.pushNotification.calls.first().args;
var notification = args[0];
expect(notification.alert).toEqual(data.data.alert);
expect(notification.expiry).toEqual(data['expiration_time']);
var apnDevice = args[1]
expect(apnDevice.connIndex).toEqual(0);
expect(apnDevice.appIdentifier).toEqual('bundleId');
done();
});
});
+9 -5
View File
@@ -1,9 +1,9 @@
var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
var ParsePushAdapter = require("../src/Adapters/Push/ParsePushAdapter");
var S3Adapter = require("../src/Adapters/Files/S3Adapter").default;
var GCSAdapter = require("../src/Adapters/Files/GCSAdapter").default;
var FilesAdapter = require("parse-server-fs-adapter").default;
var S3Adapter = require("parse-server-s3-adapter").default;
var GCSAdapter = require("parse-server-gcs-adapter").default;
var ParsePushAdapter = require("parse-server-push-adapter").default;
describe("AdapterLoader", ()=>{
@@ -38,7 +38,11 @@ describe("AdapterLoader", ()=>{
adapter: adapterPath
});
expect(adapter instanceof FilesAdapter).toBe(true);
expect(typeof adapter).toBe('object');
expect(typeof adapter.createFile).toBe('function');
expect(typeof adapter.deleteFile).toBe('function');
expect(typeof adapter.getFileData).toBe('function');
expect(typeof adapter.getFileLocation).toBe('function');
done();
});
+1 -31
View File
@@ -1,35 +1,10 @@
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
var Parse = require('parse/node').Parse;
var request = require('request');
var fs = require('fs');
var LOGS_FOLDER = './test_logs/';
var deleteFolderRecursive = function(path) {
if( fs.existsSync(path) ) {
fs.readdirSync(path).forEach(function(file,index){
var curPath = path + "/" + file;
if(fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
}
};
describe('info logs', () => {
afterEach((done) => {
deleteFolderRecursive(LOGS_FOLDER);
done();
});
it("Verify INFO logs", (done) => {
var fileLoggerAdapter = new FileLoggerAdapter({
logsFolder: LOGS_FOLDER
});
var fileLoggerAdapter = new FileLoggerAdapter();
fileLoggerAdapter.info('testing info logs', () => {
fileLoggerAdapter.query({
size: 1,
@@ -49,11 +24,6 @@ describe('info logs', () => {
describe('error logs', () => {
afterEach((done) => {
deleteFolderRecursive(LOGS_FOLDER);
done();
});
it("Verify ERROR logs", (done) => {
var fileLoggerAdapter = new FileLoggerAdapter();
fileLoggerAdapter.error('testing error logs', () => {
+19 -41
View File
@@ -1,52 +1,30 @@
var FilesController = require('../src/Controllers/FilesController').FilesController;
var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
var S3Adapter = require("../src/Adapters/Files/S3Adapter").S3Adapter;
var GCSAdapter = require("../src/Adapters/Files/GCSAdapter").GCSAdapter;
var Config = require("../src/Config");
var FCTestFactory = require("./FilesControllerTestFactory");
var FilesController = require('../src/Controllers/FilesController').default;
// Small additional tests to improve overall coverage
describe("FilesController",()=>{
describe("FilesController",() =>{
it("should properly expand objects", (done) => {
// Test the grid store adapter
var gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse');
FCTestFactory.testAdapter("GridStoreAdapter", gridStoreAdapter);
var config = new Config(Parse.applicationId);
var gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse');
var filesController = new FilesController(gridStoreAdapter)
var result = filesController.expandFilesInObject(config, function(){});
if (process.env.S3_ACCESS_KEY && process.env.S3_SECRET_KEY) {
expect(result).toBeUndefined();
// Test the S3 Adapter
var s3Adapter = new S3Adapter(process.env.S3_ACCESS_KEY, process.env.S3_SECRET_KEY, 'parse.server.tests');
var fullFile = {
type: '__type',
url: "http://an.url"
}
FCTestFactory.testAdapter("S3Adapter",s3Adapter);
var anObject = {
aFile: fullFile
}
filesController.expandFilesInObject(config, anObject);
expect(anObject.aFile.url).toEqual("http://an.url");
// Test S3 with direct access
var s3DirectAccessAdapter = new S3Adapter(process.env.S3_ACCESS_KEY, process.env.S3_SECRET_KEY, 'parse.server.tests', {
directAccess: true
});
FCTestFactory.testAdapter("S3AdapterDirect", s3DirectAccessAdapter);
} else if (!process.env.TRAVIS) {
console.log("set S3_ACCESS_KEY and S3_SECRET_KEY to test S3Adapter")
}
if (process.env.GCP_PROJECT_ID && process.env.GCP_KEYFILE_PATH && process.env.GCS_BUCKET) {
// Test the GCS Adapter
var gcsAdapter = new GCSAdapter(process.env.GCP_PROJECT_ID, process.env.GCP_KEYFILE_PATH, process.env.GCS_BUCKET);
FCTestFactory.testAdapter("GCSAdapter", gcsAdapter);
// Test GCS with direct access
var gcsDirectAccessAdapter = new GCSAdapter(process.env.GCP_PROJECT_ID, process.env.GCP_KEYFILE_PATH, process.env.GCS_BUCKET, {
directAccess: true
});
FCTestFactory.testAdapter("GCSAdapterDirect", gcsDirectAccessAdapter);
} else if (!process.env.TRAVIS) {
console.log("set GCP_PROJECT_ID, GCP_KEYFILE_PATH, and GCS_BUCKET to test GCSAdapter")
}
done();
})
});
-72
View File
@@ -1,72 +0,0 @@
var FilesController = require('../src/Controllers/FilesController').FilesController;
var Config = require("../src/Config");
var testAdapter = function(name, adapter) {
// Small additional tests to improve overall coverage
var config = new Config(Parse.applicationId);
var filesController = new FilesController(adapter);
describe("FilesController with "+name,()=>{
it("should properly expand objects", (done) => {
var result = filesController.expandFilesInObject(config, function(){});
expect(result).toBeUndefined();
var fullFile = {
type: '__type',
url: "http://an.url"
}
var anObject = {
aFile: fullFile
}
filesController.expandFilesInObject(config, anObject);
expect(anObject.aFile.url).toEqual("http://an.url");
done();
})
it("should properly create, read, delete files", (done) => {
var filename;
filesController.createFile(config, "file.txt", "hello world").then( (result) => {
ok(result.url);
ok(result.name);
filename = result.name;
expect(result.name.match(/file.txt/)).not.toBe(null);
return filesController.getFileData(config, filename);
}, (err) => {
fail("The adapter should create the file");
console.error(err);
done();
}).then((result) => {
expect(result instanceof Buffer).toBe(true);
expect(result.toString('utf-8')).toEqual("hello world");
return filesController.deleteFile(config, filename);
}, (err) => {
fail("The adapter should get the file");
console.error(err);
done();
}).then((result) => {
filesController.getFileData(config, filename).then((res) => {
fail("the file should be deleted");
done();
}, (err) => {
done();
});
}, (err) => {
fail("The adapter should delete the file");
console.error(err);
done();
});
}, 5000); // longer tests
});
}
module.exports = {
testAdapter: testAdapter
}
-191
View File
@@ -1,191 +0,0 @@
var GCM = require('../src/GCM');
describe('GCM', () => {
it('can initialize', (done) => {
var args = {
apiKey: 'apiKey'
};
var gcm = new GCM(args);
expect(gcm.sender.key).toBe(args.apiKey);
done();
});
it('can throw on initializing with invalid args', (done) => {
var args = 123
expect(function() {
new GCM(args);
}).toThrow();
done();
});
it('can generate GCM Payload without expiration time', (done) => {
//Mock request data
var data = {
'alert': 'alert'
};
var timeStamp = 1454538822113;
var timeStampISOStr = new Date(timeStamp).toISOString();
var payload = GCM.generateGCMPayload(data, timeStamp);
expect(payload.priority).toEqual('normal');
expect(payload.timeToLive).toEqual(undefined);
var dataFromPayload = payload.data;
expect(dataFromPayload.time).toEqual(timeStampISOStr);
var dataFromUser = JSON.parse(dataFromPayload.data);
expect(dataFromUser).toEqual(data);
done();
});
it('can generate GCM Payload with valid expiration time', (done) => {
//Mock request data
var data = {
'alert': 'alert'
};
var timeStamp = 1454538822113;
var timeStampISOStr = new Date(timeStamp).toISOString();
var expirationTime = 1454538922113
var payload = GCM.generateGCMPayload(data, timeStamp, expirationTime);
expect(payload.priority).toEqual('normal');
expect(payload.timeToLive).toEqual(Math.floor((expirationTime - timeStamp) / 1000));
var dataFromPayload = payload.data;
expect(dataFromPayload.time).toEqual(timeStampISOStr);
var dataFromUser = JSON.parse(dataFromPayload.data);
expect(dataFromUser).toEqual(data);
done();
});
it('can generate GCM Payload with too early expiration time', (done) => {
//Mock request data
var data = {
'alert': 'alert'
};
var timeStamp = 1454538822113;
var timeStampISOStr = new Date(timeStamp).toISOString();
var expirationTime = 1454538822112;
var payload = GCM.generateGCMPayload(data, timeStamp, expirationTime);
expect(payload.priority).toEqual('normal');
expect(payload.timeToLive).toEqual(0);
var dataFromPayload = payload.data;
expect(dataFromPayload.time).toEqual(timeStampISOStr);
var dataFromUser = JSON.parse(dataFromPayload.data);
expect(dataFromUser).toEqual(data);
done();
});
it('can generate GCM Payload with too late expiration time', (done) => {
//Mock request data
var data = {
'alert': 'alert'
};
var timeStamp = 1454538822113;
var timeStampISOStr = new Date(timeStamp).toISOString();
var expirationTime = 2454538822113;
var payload = GCM.generateGCMPayload(data, timeStamp, expirationTime);
expect(payload.priority).toEqual('normal');
// Four week in second
expect(payload.timeToLive).toEqual(4 * 7 * 24 * 60 * 60);
var dataFromPayload = payload.data;
expect(dataFromPayload.time).toEqual(timeStampISOStr);
var dataFromUser = JSON.parse(dataFromPayload.data);
expect(dataFromUser).toEqual(data);
done();
});
it('can send GCM request', (done) => {
var gcm = new GCM({
apiKey: 'apiKey'
});
// Mock gcm sender
var sender = {
send: jasmine.createSpy('send')
};
gcm.sender = sender;
// Mock data
var expirationTime = 2454538822113;
var data = {
'expiration_time': expirationTime,
'data': {
'alert': 'alert'
}
}
// Mock devices
var devices = [
{
deviceToken: 'token'
}
];
gcm.send(data, devices);
expect(sender.send).toHaveBeenCalled();
var args = sender.send.calls.first().args;
// It is too hard to verify message of gcm library, we just verify tokens and retry times
expect(args[1].registrationTokens).toEqual(['token']);
expect(args[2]).toEqual(5);
done();
});
it('can send GCM request', (done) => {
var gcm = new GCM({
apiKey: 'apiKey'
});
// Mock data
var expirationTime = 2454538822113;
var data = {
'expiration_time': expirationTime,
'data': {
'alert': 'alert'
}
}
// Mock devices
var devices = [
{
deviceToken: 'token'
},
{
deviceToken: 'token2'
},
{
deviceToken: 'token3'
},
{
deviceToken: 'token4'
}
];
gcm.send(data, devices).then((response) => {
expect(Array.isArray(response)).toBe(true);
expect(response.length).toEqual(devices.length);
expect(response.length).toEqual(4);
response.forEach((res, index) => {
expect(res.transmitted).toEqual(false);
expect(res.device).toEqual(devices[index]);
})
done();
})
});
it('can slice devices', (done) => {
// Mock devices
var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)];
var chunkDevices = GCM.sliceDevices(devices, 3);
expect(chunkDevices).toEqual([
[makeDevice(1), makeDevice(2), makeDevice(3)],
[makeDevice(4)]
]);
done();
});
function makeDevice(deviceToken) {
return {
deviceToken: deviceToken
};
}
});
+20 -14
View File
@@ -10,12 +10,16 @@ describe('LoggerController', () => {
expect(() => {
loggerController.getLogs(query).then(function(res) {
expect(res.length).toBe(0);
expect(res.length).not.toBe(0);
done();
}).catch((err) => {
console.error(err);
fail("should not fail");
done();
})
}).not.toThrow();
});
it('properly validates dateTimes', (done) => {
expect(LoggerController.validDateTime()).toBe(null);
expect(LoggerController.validDateTime("String")).toBe(null);
@@ -23,23 +27,23 @@ describe('LoggerController', () => {
expect(LoggerController.validDateTime("2016-01-01Z00:00:00").getTime()).toBe(1451606400000);
done();
});
it('can set the proper default values', (done) => {
// Make mock request
var result = LoggerController.parseOptions();
expect(result.size).toEqual(10);
expect(result.order).toEqual('desc');
expect(result.level).toEqual('info');
done();
});
it('can process a query witout throwing', (done) => {
// Make mock request
var query = {
from: "2016-01-01Z00:00:00",
until: "2016-01-01Z00:00:00",
size: 5,
size: 5,
order: 'asc',
level: 'error'
};
@@ -51,16 +55,16 @@ describe('LoggerController', () => {
expect(result.size).toEqual(5);
expect(result.order).toEqual('asc');
expect(result.level).toEqual('error');
done();
});
it('can check process a query witout throwing', (done) => {
// Make mock request
var query = {
from: "2015-01-01",
until: "2016-01-01",
size: 5,
from: "2016-01-01",
until: "2016-01-30",
size: 5,
order: 'desc',
level: 'error'
};
@@ -71,13 +75,15 @@ describe('LoggerController', () => {
loggerController.getLogs(query).then(function(res) {
expect(res.length).toBe(0);
done();
}).catch((err) => {
console.error(err);
fail("should not fail");
done();
})
}).not.toThrow();
});
it('should throw without an adapter', (done) => {
it('should throw without an adapter', (done) => {
expect(() => {
var loggerController = new LoggerController();
}).toThrow();
+12 -4
View File
@@ -204,13 +204,11 @@ describe('OAuth', function() {
myoauth: getMockMyOauthProvider().authData
}
};
var headers = {'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json' }
var options = {
headers: {'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': 'yolo',
'Content-Type': 'application/json' },
url: 'http://localhost:8378/1/users',
body: JSON.stringify(jsonBody)
@@ -224,9 +222,19 @@ describe('OAuth', function() {
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
ok(b.sessionToken);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
done();
var sessionToken = b.sessionToken;
var q = new Parse.Query("_Session");
q.equalTo('sessionToken', sessionToken);
q.first({useMasterKey: true}).then((res) => {
expect(res.get("installationId")).toEqual('yolo');
done();
}).fail((err) => {
fail('should not fail fetching the session');
done();
})
});
});
-243
View File
@@ -1,243 +0,0 @@
'use strict';
var OneSignalPushAdapter = require('../src/Adapters/Push/OneSignalPushAdapter');
var classifyInstallations = require('../src/Adapters/Push/PushAdapterUtils').classifyInstallations;
// Make mock config
var pushConfig = {
oneSignalAppId:"APP ID",
oneSignalApiKey:"API KEY"
};
describe('OneSignalPushAdapter', () => {
it('can be initialized', (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var senderMap = oneSignalPushAdapter.senderMap;
expect(senderMap.ios instanceof Function).toBe(true);
expect(senderMap.android instanceof Function).toBe(true);
done();
});
it('cannot be initialized if options are missing', (done) => {
expect(() => {
new OneSignalPushAdapter();
}).toThrow("Trying to initialize OneSignalPushAdapter without oneSignalAppId or oneSignalApiKey");
done();
});
it('can get valid push types', (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
expect(oneSignalPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
done();
});
it('can classify installation', (done) => {
// Mock installations
var validPushTypes = ['ios', 'android'];
var installations = [
{
deviceType: 'android',
deviceToken: 'androidToken'
},
{
deviceType: 'ios',
deviceToken: 'iosToken'
},
{
deviceType: 'win',
deviceToken: 'winToken'
},
{
deviceType: 'android',
deviceToken: undefined
}
];
var deviceMap = OneSignalPushAdapter.classifyInstallations(installations, validPushTypes);
expect(deviceMap['android']).toEqual([makeDevice('androidToken')]);
expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]);
expect(deviceMap['win']).toBe(undefined);
done();
});
it('can send push notifications', (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
// Mock android ios senders
var androidSender = jasmine.createSpy('send')
var iosSender = jasmine.createSpy('send')
var senderMap = {
ios: iosSender,
android: androidSender
};
oneSignalPushAdapter.senderMap = senderMap;
// Mock installations
var installations = [
{
deviceType: 'android',
deviceToken: 'androidToken'
},
{
deviceType: 'ios',
deviceToken: 'iosToken'
},
{
deviceType: 'win',
deviceToken: 'winToken'
},
{
deviceType: 'android',
deviceToken: undefined
}
];
var data = {};
oneSignalPushAdapter.send(data, installations);
// Check android sender
expect(androidSender).toHaveBeenCalled();
var args = androidSender.calls.first().args;
expect(args[0]).toEqual(data);
expect(args[1]).toEqual([
makeDevice('androidToken')
]);
// Check ios sender
expect(iosSender).toHaveBeenCalled();
args = iosSender.calls.first().args;
expect(args[0]).toEqual(data);
expect(args[1]).toEqual([
makeDevice('iosToken')
]);
done();
});
it("can send iOS notifications", (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
oneSignalPushAdapter.sendToAPNS({'data':{
'badge': 1,
'alert': "Example content",
'sound': "Example sound",
'content-available': 1,
'misc-data': 'Example Data'
}},[{'deviceToken':'iosToken1'},{'deviceToken':'iosToken2'}])
expect(sendToOneSignal).toHaveBeenCalled();
var args = sendToOneSignal.calls.first().args;
expect(args[0]).toEqual({
'ios_badgeType':'SetTo',
'ios_badgeCount':1,
'contents': { 'en':'Example content'},
'ios_sound': 'Example sound',
'content_available':true,
'data':{'misc-data':'Example Data'},
'include_ios_tokens':['iosToken1','iosToken2']
})
done();
});
it("can send Android notifications", (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
oneSignalPushAdapter.sendToGCM({'data':{
'title': 'Example title',
'alert': 'Example content',
'misc-data': 'Example Data'
}},[{'deviceToken':'androidToken1'},{'deviceToken':'androidToken2'}])
expect(sendToOneSignal).toHaveBeenCalled();
var args = sendToOneSignal.calls.first().args;
expect(args[0]).toEqual({
'contents': { 'en':'Example content'},
'title': {'en':'Example title'},
'data':{'misc-data':'Example Data'},
'include_android_reg_ids': ['androidToken1','androidToken2']
})
done();
});
it("can post the correct data", (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var write = jasmine.createSpy('write');
oneSignalPushAdapter.https = {
'request': function(a,b) {
return {
'end':function(){},
'on':function(a,b){},
'write':write
}
}
};
var installations = [
{
deviceType: 'android',
deviceToken: 'androidToken'
},
{
deviceType: 'ios',
deviceToken: 'iosToken'
},
{
deviceType: 'win',
deviceToken: 'winToken'
},
{
deviceType: 'android',
deviceToken: undefined
}
];
oneSignalPushAdapter.send({'data':{
'title': 'Example title',
'alert': 'Example content',
'content-available':1,
'misc-data': 'Example Data'
}}, installations);
expect(write).toHaveBeenCalled();
// iOS
let args = write.calls.first().args;
expect(args[0]).toEqual(JSON.stringify({
'contents': { 'en':'Example content'},
'content_available':true,
'data':{'title':'Example title','misc-data':'Example Data'},
'include_ios_tokens':['iosToken'],
'app_id':'APP ID'
}));
// Android
args = write.calls.mostRecent().args;
expect(args[0]).toEqual(JSON.stringify({
'contents': { 'en':'Example content'},
'title': {'en':'Example title'},
'data':{"content-available":1,'misc-data':'Example Data'},
'include_android_reg_ids':['androidToken'],
'app_id':'APP ID'
}));
done();
});
function makeDevice(deviceToken, appIdentifier) {
return {
deviceToken: deviceToken,
appIdentifier: appIdentifier
};
}
});
+62 -32
View File
@@ -1,28 +1,32 @@
'use strict';
describe('Parse.Push', () => {
it('should properly send push', (done) => {
var pushAdapter = {
send: function(body, installations) {
var badge = body.data.badge;
let promises = installations.map((installation) => {
if (installation.deviceType == "ios") {
expect(installation.badge).toEqual(badge);
expect(installation.originalBadge+1).toEqual(installation.badge);
} else {
expect(installation.badge).toBeUndefined();
}
return Promise.resolve({
err: null,
deviceType: installation.deviceType,
result: true
})
});
return Promise.all(promises)
},
getValidPushTypes: function() {
return ["ios", "android"];
}
var setup = function() {
var pushAdapter = {
send: function(body, installations) {
var badge = body.data.badge;
let promises = installations.map((installation) => {
if (installation.deviceType == "ios") {
expect(installation.badge).toEqual(badge);
expect(installation.originalBadge+1).toEqual(installation.badge);
} else {
expect(installation.badge).toBeUndefined();
}
return Promise.resolve({
err: null,
deviceType: installation.deviceType,
result: true
})
});
return Promise.all(promises);
},
getValidPushTypes: function() {
return ["ios", "android"];
}
}
setServerConfiguration({
appId: Parse.applicationId,
masterKey: Parse.masterKey,
@@ -31,6 +35,7 @@ describe('Parse.Push', () => {
adapter: pushAdapter
}
});
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
@@ -41,21 +46,46 @@ describe('Parse.Push', () => {
installation.set("deviceType", "ios");
installations.push(installation);
}
Parse.Object.saveAll(installations).then(() => {
return Parse.Object.saveAll(installations);
}
it('should properly send push', (done) => {
return setup().then(() => {
return Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'Increment',
alert: 'Hello world!'
}
}, {useMasterKey: true});
where: {
deviceType: 'ios'
},
data: {
badge: 'Increment',
alert: 'Hello world!'
}
}, {useMasterKey: true})
})
.then(() => {
done();
}, (err) => {
console.error(err);
console.error();
fail('should not fail sending push')
done();
});
});
it('should properly send push with lowercaseIncrement', (done) => {
return setup().then(() => {
return Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'increment',
alert: 'Hello world!'
}
}, {useMasterKey: true})
}).then(() => {
done();
}, (err) => {
console.error();
fail('should not fail sending push')
done();
});
});
+48 -5
View File
@@ -600,6 +600,49 @@ describe('Parse.Object testing', () => {
});
});
it("addUnique with object", function(done) {
var x1 = new Parse.Object('X');
x1.set('stuff', [ 1, {'hello': 'world'}, {'foo': 'bar'}]);
x1.save().then(() => {
var objectId = x1.id;
var x2 = new Parse.Object('X', {objectId: objectId});
x2.addUnique('stuff', {'hello': 'world'});
x2.addUnique('stuff', {'bar': 'baz'});
expect(x2.get('stuff')).toEqual([{'hello': 'world'}, {'bar': 'baz'}]);
return x2.save();
}).then(() => {
var query = new Parse.Query('X');
return query.get(x1.id);
}).then((x3) => {
expect(x3.get('stuff')).toEqual([1, {'hello': 'world'}, {'foo': 'bar'}, {'bar': 'baz'}]);
done();
}, (error) => {
fail(error);
done();
});
});
it("removes with object", function(done) {
var x1 = new Parse.Object('X');
x1.set('stuff', [ 1, {'hello': 'world'}, {'foo': 'bar'}]);
x1.save().then(() => {
var objectId = x1.id;
var x2 = new Parse.Object('X', {objectId: objectId});
x2.remove('stuff', {'hello': 'world'});
expect(x2.get('stuff')).toEqual([]);
return x2.save();
}).then(() => {
var query = new Parse.Query('X');
return query.get(x1.id);
}).then((x3) => {
expect(x3.get('stuff')).toEqual([1, {'foo': 'bar'}]);
done();
}, (error) => {
fail(error);
done();
});
});
it("dirty attributes", function(done) {
var object = new Parse.Object("TestObject");
object.set("cat", "good");
@@ -1794,14 +1837,14 @@ describe('Parse.Object testing', () => {
done();
});
});
it('should have undefined includes when object is missing', (done) => {
let obj1 = new Parse.Object("AnObject");
let obj2 = new Parse.Object("AnObject");
Parse.Object.saveAll([obj1, obj2]).then(() => {
obj1.set("obj", obj2);
// Save the pointer, delete the pointee
// Save the pointer, delete the pointee
return obj1.save().then(() => { return obj2.destroy() });
}).then(() => {
let query = new Parse.Query("AnObject");
@@ -1823,7 +1866,7 @@ describe('Parse.Object testing', () => {
done();
})
});
it('should have undefined includes when object is missing on deeper path', (done) => {
let obj1 = new Parse.Object("AnObject");
let obj2 = new Parse.Object("AnObject");
@@ -1831,7 +1874,7 @@ describe('Parse.Object testing', () => {
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
obj1.set("obj", obj2);
obj2.set("obj", obj3);
// Save the pointer, delete the pointee
// Save the pointer, delete the pointee
return Parse.Object.saveAll([obj1, obj2]).then(() => { return obj3.destroy() });
}).then(() => {
let query = new Parse.Query("AnObject");
-150
View File
@@ -1,150 +0,0 @@
var ParsePushAdapter = require('../src/Adapters/Push/ParsePushAdapter');
var APNS = require('../src/APNS');
var GCM = require('../src/GCM');
describe('ParsePushAdapter', () => {
it('can be initialized', (done) => {
// Make mock config
var pushConfig = {
android: {
senderId: 'senderId',
apiKey: 'apiKey'
},
ios: [
{
cert: 'prodCert.pem',
key: 'prodKey.pem',
production: true,
bundleId: 'bundleId'
},
{
cert: 'devCert.pem',
key: 'devKey.pem',
production: false,
bundleId: 'bundleIdAgain'
}
]
};
var parsePushAdapter = new ParsePushAdapter(pushConfig);
// Check ios
var iosSender = parsePushAdapter.senderMap['ios'];
expect(iosSender instanceof APNS).toBe(true);
// Check android
var androidSender = parsePushAdapter.senderMap['android'];
expect(androidSender instanceof GCM).toBe(true);
done();
});
it('can throw on initializing with unsupported push type', (done) => {
// Make mock config
var pushConfig = {
win: {
senderId: 'senderId',
apiKey: 'apiKey'
}
};
expect(function() {
new ParsePushAdapter(pushConfig);
}).toThrow();
done();
});
it('can get valid push types', (done) => {
var parsePushAdapter = new ParsePushAdapter();
expect(parsePushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
done();
});
it('can classify installation', (done) => {
// Mock installations
var validPushTypes = ['ios', 'android'];
var installations = [
{
deviceType: 'android',
deviceToken: 'androidToken'
},
{
deviceType: 'ios',
deviceToken: 'iosToken'
},
{
deviceType: 'win',
deviceToken: 'winToken'
},
{
deviceType: 'android',
deviceToken: undefined
}
];
var deviceMap = ParsePushAdapter.classifyInstallations(installations, validPushTypes);
expect(deviceMap['android']).toEqual([makeDevice('androidToken')]);
expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]);
expect(deviceMap['win']).toBe(undefined);
done();
});
it('can send push notifications', (done) => {
var parsePushAdapter = new ParsePushAdapter();
// Mock android ios senders
var androidSender = {
send: jasmine.createSpy('send')
};
var iosSender = {
send: jasmine.createSpy('send')
};
var senderMap = {
ios: iosSender,
android: androidSender
};
parsePushAdapter.senderMap = senderMap;
// Mock installations
var installations = [
{
deviceType: 'android',
deviceToken: 'androidToken'
},
{
deviceType: 'ios',
deviceToken: 'iosToken'
},
{
deviceType: 'win',
deviceToken: 'winToken'
},
{
deviceType: 'android',
deviceToken: undefined
}
];
var data = {};
parsePushAdapter.send(data, installations);
// Check android sender
expect(androidSender.send).toHaveBeenCalled();
var args = androidSender.send.calls.first().args;
expect(args[0]).toEqual(data);
expect(args[1]).toEqual([
makeDevice('androidToken')
]);
// Check ios sender
expect(iosSender.send).toHaveBeenCalled();
args = iosSender.send.calls.first().args;
expect(args[0]).toEqual(data);
expect(args[1]).toEqual([
makeDevice('iosToken')
]);
done();
});
function makeDevice(deviceToken, appIdentifier) {
return {
deviceToken: deviceToken,
appIdentifier: appIdentifier
};
}
});
+39
View File
@@ -2170,4 +2170,43 @@ describe('Parse.Query testing', () => {
});
it('should find objects with array of pointers', (done) => {
var objects = [];
while(objects.length != 5) {
var object = new Parse.Object('ContainedObject');
object.set('index', objects.length);
objects.push(object);
}
Parse.Object.saveAll(objects).then((objects) => {
var container = new Parse.Object('Container');
var pointers = objects.map((obj) => {
return {
__type: 'Pointer',
className: 'ContainedObject',
objectId: obj.id
}
})
container.set('objects', pointers);
let container2 = new Parse.Object('Container');
container2.set('objects', pointers.slice(2, 3));
return Parse.Object.saveAll([container, container2]);
}).then(() => {
let inQuery = new Parse.Query('ContainedObject');
inQuery.greaterThanOrEqualTo('index', 1);
let query = new Parse.Query('Container');
query.matchesQuery('objects', inQuery);
return query.find();
}).then((results) => {
if (results) {
expect(results.length).toBe(2);
}
done();
}).fail((err) => {
console.error(err);
fail('should not fail');
done();
})
})
});
+88 -1
View File
@@ -1768,9 +1768,37 @@ describe('Parse.User testing', () => {
});
});
it('user get session from token', (done) => {
it('user get session from token on signup', (done) => {
Parse.Promise.as().then(() => {
return Parse.User.signUp("finn", "human", { foo: "bar" });
}).then((user) => {
request.get({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Session-Token': user.getSessionToken(),
'X-Parse-REST-API-Key': 'rest'
},
url: 'http://localhost:8378/1/sessions/me',
}, (error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(typeof b.sessionToken).toEqual('string');
expect(typeof b.createdWith).toEqual('object');
expect(b.createdWith.action).toEqual('signup');
expect(typeof b.user).toEqual('object');
expect(b.user.objectId).toEqual(user.id);
done();
});
});
});
it('user get session from token on login', (done) => {
Parse.Promise.as().then(() => {
return Parse.User.signUp("finn", "human", { foo: "bar" });
}).then((user) => {
return Parse.User.logOut().then(() => {
return Parse.User.logIn("finn", "human");
})
}).then((user) => {
request.get({
headers: {
@@ -2033,6 +2061,65 @@ describe('Parse.User testing', () => {
})
});
// https://github.com/ParsePlatform/parse-server/issues/1198
it('should cleanup null authData keys ParseUser update', (done) => {
Parse.Cloud.beforeSave('_User', (req, res) => {
req.object.set('foo', 'bar');
res.success();
});
// Simulate anonymous user save
new Promise((resolve, reject) => {
request.post({
url: 'http://localhost:8378/1/classes/_User',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
}, (err, res, body) => {
if (err) {
reject(err);
} else {
resolve(body);
}
});
}).then((user) => {
// Simulate registration
return new Promise((resolve, reject) => {
request.put({
url: 'http://localhost:8378/1/classes/_User/' + user.objectId,
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Session-Token': user.sessionToken,
'X-Parse-REST-API-Key': 'rest',
},
json: {
authData: {anonymous: null},
user: 'user',
password: 'password',
}
}, (err, res, body) => {
if (err) {
reject(err);
} else {
resolve(body);
}
});
});
}).then((user) => {
expect(typeof user).toEqual('object');
expect(user.authData).toBeUndefined();
Parse.Cloud._removeHook('Triggers', 'beforeSave', '_User');
done();
}).catch((err) => {
fail('no request should fail: ' + JSON.stringify(err));
Parse.Cloud._removeHook('Triggers', 'beforeSave', '_User');
done();
});
});
it('should aftersave with full object', (done) => {
var hit = 0;
Parse.Cloud.afterSave('_User', (req, res) => {
+5 -1
View File
@@ -1,6 +1,6 @@
"use strict";
var PushController = require('../src/Controllers/PushController').PushController;
var pushStatusHandler = require('../src/pushStatusHandler');
var Config = require('../src/Config');
const successfulTransmissions = function(body, installations) {
@@ -357,4 +357,8 @@ describe('PushController', () => {
});
});
it('should flatten', () => {
var res = pushStatusHandler.flatten([1, [2], [[3, 4], 5], [[[6]]]])
expect(res).toEqual([1,2,3,4,5,6]);
})
});
+1 -1
View File
@@ -151,7 +151,7 @@ describe('matchesQuery', function() {
q = new Parse.Query('Person');
q.equalTo('birthday', day);
expect(matchesQuery(obj, q)).toBe(true);
q.equalTo('birthday', new Date());
q.equalTo('birthday', new Date(1990, 1));
expect(matchesQuery(obj, q)).toBe(false);
q = new Parse.Query('Person');
+40 -1
View File
@@ -146,7 +146,46 @@ describe('rest create', () => {
done();
});
});
it('handles anonymous user signup and upgrade to new user', (done) => {
var data1 = {
authData: {
anonymous: {
id: '00000000-0000-0000-0000-000000000001'
}
}
};
var updatedData = {
authData: { anonymous: null },
username: 'hello',
password: 'world'
}
var username1;
var objectId;
rest.create(config, auth.nobody(config), '_User', data1)
.then((r) => {
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
objectId = r.response.objectId;
return auth.getAuthForSessionToken({config, sessionToken: r.response.sessionToken })
}).then((sessionAuth) => {
return rest.update(config, sessionAuth, '_User', objectId, updatedData);
}).then((r) => {
return Parse.User.logOut().then(() => {
return Parse.User.logIn('hello', 'world');
})
}).then((r) => {
expect(r.id).toEqual(objectId);
expect(r.get('username')).toEqual('hello');
done();
}).catch((err) => {
fail('should not fail')
done();
})
});
it('handles no anonymous users config', (done) => {
var NoAnnonConfig = Object.assign({}, config);
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(false);
+64 -2
View File
@@ -156,7 +156,61 @@ describe("Email Verification", () => {
return user.fetch();
}).then(() => {
expect(user.get('emailVerified')).toEqual(false);
// Wait as on update emai, we need to fetch the username
// Wait as on update email, we need to fetch the username
setTimeout(function(){
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
done();
}, 200);
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
});
it('does send a validation email with valid verification link when updating the email', done => {
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
spyOn(emailAdapter, 'sendVerificationEmail').and.callFake((options) => {
expect(options.link).not.toBeNull();
expect(options.link).not.toMatch(/token=undefined/);
Promise.resolve();
});
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
user.fetch()
.then((user) => {
user.set("email", "cool_guy@parse.com");
return user.save();
}).then((user) => {
return user.fetch();
}).then(() => {
expect(user.get('emailVerified')).toEqual(false);
// Wait as on update email, we need to fetch the username
setTimeout(function(){
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
done();
@@ -573,7 +627,15 @@ describe("Password Reset", () => {
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html');
Parse.User.logIn("zxcv", "hello").then(function(user){
done();
let config = new Config('test');
config.database.adaptiveCollection('_User')
.then(coll => coll.find({ 'username': 'zxcv' }, { limit: 1 }))
.then((results) => {
// _perishable_token should be unset after reset password
expect(results.length).toEqual(1);
expect(results[0]['_perishable_token']).toEqual(undefined);
done();
});
}, (err) => {
console.error(err);
fail("should login with new password");
+1 -25
View File
@@ -1,33 +1,9 @@
'use strict';
var features = require('../src/features');
const request = require("request");
describe('features', () => {
it('set and get features', (done) => {
features.setFeature('push', {
testOption1: true,
testOption2: false
});
var _features = features.getFeatures();
var expected = {
testOption1: true,
testOption2: false
};
expect(_features.push).toEqual(expected);
done();
});
it('get features that does not exist', (done) => {
var _features = features.getFeatures();
expect(_features.test).toBeUndefined();
done();
});
it('requires the master key to get all schemas', done => {
it('requires the master key to get features', done => {
request.get({
url: 'http://localhost:8378/1/serverInfo',
json: true,
+9 -2
View File
@@ -51,8 +51,15 @@ var server = app.listen(port);
// Prevent reinitializing the server from clobbering Cloud Code
delete defaultConfiguration.cloud;
var currentConfiguration;
// Allows testing specific configurations of Parse Server
var setServerConfiguration = configuration => {
// the configuration hasn't changed
if (configuration === currentConfiguration) {
return;
}
DatabaseAdapter.clearDatabaseSettings();
currentConfiguration = configuration;
server.close();
cache.clearCache();
app = express();
@@ -72,17 +79,17 @@ Parse.serverURL = 'http://localhost:' + port + '/1';
Parse.Promise.disableAPlusCompliant();
beforeEach(function(done) {
restoreServerConfiguration();
Parse.initialize('test', 'test', 'test');
Parse.serverURL = 'http://localhost:' + port + '/1';
Parse.User.enableUnsafeCurrentUser();
done();
});
afterEach(function(done) {
restoreServerConfiguration();
Parse.User.logOut().then(() => {
return clearData();
}).then(() => {
DatabaseAdapter.clearDatabaseSettings();
done();
}, (error) => {
console.log('error in clearData', error);
+83 -5
View File
@@ -1,6 +1,8 @@
var request = require('request');
var parseServerPackage = require('../package.json');
var MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions');
var ParseServer = require("../src/index");
var express = require('express');
describe('server', () => {
it('requires a master key and app id', done => {
@@ -54,6 +56,7 @@ describe('server', () => {
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
@@ -78,6 +81,7 @@ describe('server', () => {
emailAdapter: {
class: MockEmailAdapterWithOptions,
options: {
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}
@@ -101,8 +105,9 @@ describe('server', () => {
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
module: './Email/SimpleMailgunAdapter',
module: 'parse-server-simple-mailgun-adapter',
options: {
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}
@@ -125,9 +130,9 @@ describe('server', () => {
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: './Email/SimpleMailgunAdapter',
emailAdapter: 'parse-server-simple-mailgun-adapter',
publicServerURL: 'http://localhost:8378/1'
})).toThrow('SimpleMailgunAdapter requires an API Key and domain.');
})).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
done();
});
@@ -145,13 +150,13 @@ describe('server', () => {
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
module: './Email/SimpleMailgunAdapter',
module: 'parse-server-simple-mailgun-adapter',
options: {
domain: 'd',
}
},
publicServerURL: 'http://localhost:8378/1'
})).toThrow('SimpleMailgunAdapter requires an API Key and domain.');
})).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
done();
});
@@ -168,4 +173,77 @@ describe('server', () => {
done();
})
});
it('can create a parse-server', done => {
var parseServer = new ParseServer.default({
appId: "aTestApp",
masterKey: "aTestMasterKey",
serverURL: "http://localhost:12666/parse",
databaseURI: 'mongodb://localhost:27017/aTestApp'
});
expect(Parse.applicationId).toEqual("aTestApp");
var app = express();
app.use('/parse', parseServer.app);
var server = app.listen(12666);
var obj = new Parse.Object("AnObject");
var objId;
obj.save().then((obj) => {
objId = obj.id;
var q = new Parse.Query("AnObject");
return q.first();
}).then((obj) => {
expect(obj.id).toEqual(objId);
server.close();
done();
}).fail((err) => {
server.close();
done();
})
});
it('can create a parse-server', done => {
var parseServer = ParseServer.ParseServer({
appId: "anOtherTestApp",
masterKey: "anOtherTestMasterKey",
serverURL: "http://localhost:12667/parse",
databaseURI: 'mongodb://localhost:27017/anotherTstApp'
});
expect(Parse.applicationId).toEqual("anOtherTestApp");
var app = express();
app.use('/parse', parseServer);
var server = app.listen(12667);
var obj = new Parse.Object("AnObject");
var objId;
obj.save().then((obj) => {
objId = obj.id;
var q = new Parse.Query("AnObject");
return q.first();
}).then((obj) => {
expect(obj.id).toEqual(objId);
server.close();
done();
}).fail((err) => {
server.close();
done();
})
});
it('has createLiveQueryServer', done => {
// original implementation through the factory
expect(typeof ParseServer.ParseServer.createLiveQueryServer).toEqual('function');
// For import calls
expect(typeof ParseServer.default.createLiveQueryServer).toEqual('function');
done();
});
it('exposes all the "core" adapters', done => {
expect(ParseServer.S3Adapter).toThrow();
expect(ParseServer.GCSAdapter).toThrow('GCSAdapter requires an projectId');
expect(ParseServer.FileSystemAdapter).toThrow();
done();
});
});
-227
View File
@@ -1,227 +0,0 @@
"use strict";
const Parse = require('parse/node').Parse;
// TODO: apn does not support the new HTTP/2 protocal. It is fine to use it in V1,
// but probably we will replace it in the future.
const apn = require('apn');
/**
* Create a new connection to the APN service.
* @constructor
* @param {Object|Array} args An argument or a list of arguments to config APNS connection
* @param {String} args.cert The filename of the connection certificate to load from disk
* @param {String} args.key The filename of the connection key to load from disk
* @param {String} args.pfx The filename for private key, certificate and CA certs in PFX or PKCS12 format, it will overwrite cert and key
* @param {String} args.passphrase The passphrase for the connection key, if required
* @param {String} args.bundleId The bundleId for cert
* @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox
*/
function APNS(args) {
// Since for ios, there maybe multiple cert/key pairs,
// typePushConfig can be an array.
let apnsArgsList = [];
if (Array.isArray(args)) {
apnsArgsList = apnsArgsList.concat(args);
} else if (typeof args === 'object') {
apnsArgsList.push(args);
} else {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'APNS Configuration is invalid');
}
this.conns = [];
for (let apnsArgs of apnsArgsList) {
let conn = new apn.Connection(apnsArgs);
if (!apnsArgs.bundleId) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'BundleId is mssing for %j', apnsArgs);
}
conn.bundleId = apnsArgs.bundleId;
// Set the priority of the conns, prod cert has higher priority
if (apnsArgs.production) {
conn.priority = 0;
} else {
conn.priority = 1;
}
// Set apns client callbacks
conn.on('connected', () => {
console.log('APNS Connection %d Connected', conn.index);
});
conn.on('transmissionError', (errCode, notification, apnDevice) => {
handleTransmissionError(this.conns, errCode, notification, apnDevice);
});
conn.on('timeout', () => {
console.log('APNS Connection %d Timeout', conn.index);
});
conn.on('disconnected', () => {
console.log('APNS Connection %d Disconnected', conn.index);
});
conn.on('socketError', () => {
console.log('APNS Connection %d Socket Error', conn.index);
});
conn.on('transmitted', function(notification, device) {
if (device.callback) {
device.callback({
notification: notification,
transmitted: true,
device: device
});
}
console.log('APNS Connection %d Notification transmitted to %s', conn.index, device.token.toString('hex'));
});
this.conns.push(conn);
}
// Sort the conn based on priority ascending, high pri first
this.conns.sort((s1, s2) => {
return s1.priority - s2.priority;
});
// Set index of conns
for (let index = 0; index < this.conns.length; index++) {
this.conns[index].index = index;
}
}
/**
* Send apns request.
* @param {Object} data The data we need to send, the format is the same with api request body
* @param {Array} devices A array of devices
* @returns {Object} A promise which is resolved immediately
*/
APNS.prototype.send = function(data, devices) {
let coreData = data.data;
let expirationTime = data['expiration_time'];
let notification = generateNotification(coreData, expirationTime);
let promises = devices.map((device) => {
let qualifiedConnIndexs = chooseConns(this.conns, device);
// We can not find a valid conn, just ignore this device
if (qualifiedConnIndexs.length == 0) {
return Promise.resolve({
transmitted: false,
result: {error: 'No connection available'}
});
}
let conn = this.conns[qualifiedConnIndexs[0]];
let apnDevice = new apn.Device(device.deviceToken);
apnDevice.connIndex = qualifiedConnIndexs[0];
// Add additional appIdentifier info to apn device instance
if (device.appIdentifier) {
apnDevice.appIdentifier = device.appIdentifier;
}
return new Promise((resolve, reject) => {
apnDevice.callback = resolve;
conn.pushNotification(notification, apnDevice);
});
});
return Parse.Promise.when(promises);
}
function handleTransmissionError(conns, errCode, notification, apnDevice) {
// This means the error notification is not in the cache anymore or the recepient is missing,
// we just ignore this case
if (!notification || !apnDevice) {
return
}
// If currentConn can not send the push notification, we try to use the next available conn.
// Since conns is sorted by priority, the next conn means the next low pri conn.
// If there is no conn available, we give up on sending the notification to that device.
let qualifiedConnIndexs = chooseConns(conns, apnDevice);
let currentConnIndex = apnDevice.connIndex;
let newConnIndex = -1;
// Find the next element of currentConnIndex in qualifiedConnIndexs
for (let index = 0; index < qualifiedConnIndexs.length - 1; index++) {
if (qualifiedConnIndexs[index] === currentConnIndex) {
newConnIndex = qualifiedConnIndexs[index + 1];
break;
}
}
// There is no more available conns, we give up in this case
if (newConnIndex < 0 || newConnIndex >= conns.length) {
if (apnDevice.callback) {
apnDevice.callback({
response: {error: `APNS can not find vaild connection for ${apnDevice.token}`, code: errCode},
status: errCode,
transmitted: false
});
}
return;
}
let newConn = conns[newConnIndex];
// Update device conn info
apnDevice.connIndex = newConnIndex;
// Use the new conn to send the notification
newConn.pushNotification(notification, apnDevice);
}
function chooseConns(conns, device) {
// If device does not have appIdentifier, all conns maybe proper connections.
// Otherwise we try to match the appIdentifier with bundleId
let qualifiedConns = [];
for (let index = 0; index < conns.length; index++) {
let conn = conns[index];
// If the device we need to send to does not have
// appIdentifier, any conn could be a qualified connection
if (!device.appIdentifier || device.appIdentifier === '') {
qualifiedConns.push(index);
continue;
}
if (device.appIdentifier === conn.bundleId) {
qualifiedConns.push(index);
}
}
return qualifiedConns;
}
/**
* Generate the apns notification from the data we get from api request.
* @param {Object} coreData The data field under api request body
* @returns {Object} A apns notification
*/
function generateNotification(coreData, expirationTime) {
let notification = new apn.notification();
let payload = {};
for (let key in coreData) {
switch (key) {
case 'alert':
notification.setAlertText(coreData.alert);
break;
case 'badge':
notification.badge = coreData.badge;
break;
case 'sound':
notification.sound = coreData.sound;
break;
case 'content-available':
notification.setNewsstandAvailable(true);
let isAvailable = coreData['content-available'] === 1;
notification.setContentAvailable(isAvailable);
break;
case 'category':
notification.category = coreData.category;
break;
default:
payload[key] = coreData[key];
break;
}
}
notification.payload = payload;
notification.expiry = expirationTime;
return notification;
}
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
APNS.generateNotification = generateNotification;
APNS.chooseConns = chooseConns;
APNS.handleTransmissionError = handleTransmissionError;
}
module.exports = APNS;
+6 -3
View File
@@ -10,8 +10,12 @@ export function loadAdapter(adapter, defaultAdapter, options) {
try {
return adapter(options);
} catch(e) {
var Adapter = adapter;
return new Adapter(options);
if (e.name === 'TypeError') {
var Adapter = adapter;
return new Adapter(options);
} else {
throw e;
}
}
} else if (typeof adapter === "string") {
adapter = require(adapter);
@@ -19,7 +23,6 @@ export function loadAdapter(adapter, defaultAdapter, options) {
if (adapter.default) {
adapter = adapter.default;
}
return loadAdapter(adapter, undefined, options);
} else if (adapter.module) {
return loadAdapter(adapter.module, undefined, adapter.options);
@@ -1,32 +0,0 @@
import Mailgun from 'mailgun-js';
let SimpleMailgunAdapter = mailgunOptions => {
if (!mailgunOptions || !mailgunOptions.apiKey || !mailgunOptions.domain) {
throw 'SimpleMailgunAdapter requires an API Key and domain.';
}
let mailgun = Mailgun(mailgunOptions);
let sendMail = ({to, subject, text}) => {
let data = {
from: mailgunOptions.fromAddress,
to: to,
subject: subject,
text: text,
}
return new Promise((resolve, reject) => {
mailgun.messages().send(data, (err, body) => {
if (typeof err !== 'undefined') {
reject(err);
}
resolve(body);
});
});
}
return Object.freeze({
sendMail: sendMail
});
}
module.exports = SimpleMailgunAdapter
+7 -9
View File
@@ -13,22 +13,20 @@
export class FilesAdapter {
/* this method is responsible to store the file in order to be retrived later by it's file name
*
*
* @param config the current config
*
* @param filename the filename to save
* @param data the buffer of data from the file
* @param contentType the supposed contentType
* @discussion the contentType can be undefined if the controller was not able to determine it
*
* @discussion the contentType can be undefined if the controller was not able to determine it
*
* @return a promise that should fail if the storage didn't succeed
*
*
*/
createFile(config, filename: string, data, contentType: string) { }
createFile(filename: string, data, contentType: string) { }
deleteFile(config, filename) { }
deleteFile(filename) { }
getFileData(config, filename) { }
getFileData(filename) { }
getFileLocation(config, filename) { }
}
-125
View File
@@ -1,125 +0,0 @@
// GCSAdapter
// Store Parse Files in Google Cloud Storage: https://cloud.google.com/storage
import { storage } from 'gcloud';
import { FilesAdapter } from './FilesAdapter';
import requiredParameter from '../../requiredParameter';
function requiredOrFromEnvironment(env, name) {
let environmentVariable = process.env[env];
if (!environmentVariable) {
requiredParameter(`GCSAdapter requires an ${name}`);
}
return environmentVariable;
}
function fromEnvironmentOrDefault(env, defaultValue) {
let environmentVariable = process.env[env];
if (environmentVariable) {
return environmentVariable;
}
return defaultValue;
}
export class GCSAdapter extends FilesAdapter {
// GCS Project ID and the name of a corresponding Keyfile are required.
// Unlike the S3 adapter, you must create a new Cloud Storage bucket, as this is not created automatically.
// See https://googlecloudplatform.github.io/gcloud-node/#/docs/master/guides/authentication
// for more details.
constructor(
projectId = requiredOrFromEnvironment('GCP_PROJECT_ID', 'projectId'),
keyFilename = requiredOrFromEnvironment('GCP_KEYFILE_PATH', 'keyfile path'),
bucket = requiredOrFromEnvironment('GCS_BUCKET', 'bucket name'),
{ bucketPrefix = fromEnvironmentOrDefault('GCS_BUCKET_PREFIX', ''),
directAccess = fromEnvironmentOrDefault('GCS_DIRECT_ACCESS', false) } = {}) {
super();
this._bucket = bucket;
this._bucketPrefix = bucketPrefix;
this._directAccess = directAccess;
let options = {
projectId: projectId,
keyFilename: keyFilename
};
this._gcsClient = new storage(options);
}
// For a given config object, filename, and data, store a file in GCS.
// Resolves the promise or fails with an error.
createFile(config, filename, data, contentType) {
let params = {
contentType: contentType || 'application/octet-stream'
};
return new Promise((resolve, reject) => {
let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename);
// gcloud supports upload(file) not upload(bytes), so we need to stream.
var uploadStream = file.createWriteStream(params);
uploadStream.on('error', (err) => {
return reject(err);
}).on('finish', () => {
// Second call to set public read ACL after object is uploaded.
if (this._directAccess) {
file.makePublic((err, res) => {
if (err !== null) {
return reject(err);
}
resolve();
});
} else {
resolve();
}
});
uploadStream.write(data);
uploadStream.end();
});
}
// Deletes a file with the given file name.
// Returns a promise that succeeds with the delete response, or fails with an error.
deleteFile(config, filename) {
return new Promise((resolve, reject) => {
let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename);
file.delete((err, res) => {
if(err !== null) {
return reject(err);
}
resolve(res);
});
});
}
// Search for and return a file if found by filename.
// Returns a promise that succeeds with the buffer result from GCS, or fails with an error.
getFileData(config, filename) {
return new Promise((resolve, reject) => {
let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename);
// Check for existence, since gcloud-node seemed to be caching the result
file.exists((err, exists) => {
if (exists) {
file.download((err, data) => {
if (err !== null) {
return reject(err);
}
return resolve(data);
});
} else {
reject(err);
}
});
});
}
// Generates and returns the location of a file stored in GCS for the given request and filename.
// The location is the direct GCS link if the option is set,
// otherwise we serve the file through parse-server.
getFileLocation(config, filename) {
if (this._directAccess) {
return `https://${this._bucket}.storage.googleapis.com/${this._bucketPrefix + filename}`;
}
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
}
}
export default GCSAdapter;
+3 -3
View File
@@ -28,7 +28,7 @@ export class GridStoreAdapter extends FilesAdapter {
// For a given config object, filename, and data, store a file
// Returns a promise
createFile(config, filename: string, data, contentType) {
createFile(filename: string, data, contentType) {
return this._connect().then(database => {
let gridStore = new GridStore(database, filename, 'w');
return gridStore.open();
@@ -39,7 +39,7 @@ export class GridStoreAdapter extends FilesAdapter {
});
}
deleteFile(config, filename: string) {
deleteFile(filename: string) {
return this._connect().then(database => {
let gridStore = new GridStore(database, filename, 'w');
return gridStore.open();
@@ -50,7 +50,7 @@ export class GridStoreAdapter extends FilesAdapter {
});
}
getFileData(config, filename: string) {
getFileData(filename: string) {
return this._connect().then(database => {
return GridStore.exist(database, filename)
.then(() => {
-141
View File
@@ -1,141 +0,0 @@
// S3Adapter
//
// Stores Parse files in AWS S3.
import * as AWS from 'aws-sdk';
import { FilesAdapter } from './FilesAdapter';
import requiredParameter from '../../requiredParameter';
const DEFAULT_S3_REGION = "us-east-1";
function requiredOrFromEnvironment(env, name) {
let environmentVariable = process.env[env];
if (!environmentVariable) {
requiredParameter(`S3Adapter requires an ${name}`);
}
return environmentVariable;
}
function fromEnvironmentOrDefault(env, defaultValue) {
let environmentVariable = process.env[env];
if (environmentVariable) {
return environmentVariable;
}
return defaultValue;
}
export class S3Adapter extends FilesAdapter {
// Creates an S3 session.
// Providing AWS access and secret keys is mandatory
// Region and bucket will use sane defaults if omitted
constructor(
accessKey = requiredOrFromEnvironment('S3_ACCESS_KEY', 'accessKey'),
secretKey = requiredOrFromEnvironment('S3_SECRET_KEY', 'secretKey'),
bucket = fromEnvironmentOrDefault('S3_BUCKET', undefined),
{ region = fromEnvironmentOrDefault('S3_REGION', DEFAULT_S3_REGION),
bucketPrefix = fromEnvironmentOrDefault('S3_BUCKET_PREFIX', ''),
directAccess = fromEnvironmentOrDefault('S3_DIRECT_ACCESS', false) } = {}) {
super();
this._region = region;
this._bucket = bucket;
this._bucketPrefix = bucketPrefix;
this._directAccess = directAccess;
let s3Options = {
accessKeyId: accessKey,
secretAccessKey: secretKey,
params: { Bucket: this._bucket }
};
AWS.config._region = this._region;
this._s3Client = new AWS.S3(s3Options);
this._hasBucket = false;
}
createBucket() {
var promise;
if (this._hasBucket) {
promise = Promise.resolve();
} else {
promise = new Promise((resolve, reject) => {
this._s3Client.createBucket(() => {
this._hasBucket = true;
resolve();
});
});
}
return promise;
}
// For a given config object, filename, and data, store a file in S3
// Returns a promise containing the S3 object creation response
createFile(config, filename, data, contentType) {
let params = {
Key: this._bucketPrefix + filename,
Body: data
};
if (this._directAccess) {
params.ACL = "public-read"
}
if (contentType) {
params.ContentType = contentType;
}
return this.createBucket().then(() => {
return new Promise((resolve, reject) => {
this._s3Client.upload(params, (err, data) => {
if (err !== null) {
return reject(err);
}
resolve(data);
});
});
});
}
deleteFile(config, filename) {
return this.createBucket().then(() => {
return new Promise((resolve, reject) => {
let params = {
Key: this._bucketPrefix + filename
};
this._s3Client.deleteObject(params, (err, data) =>{
if(err !== null) {
return reject(err);
}
resolve(data);
});
});
});
}
// Search for and return a file if found by filename
// Returns a promise that succeeds with the buffer result from S3
getFileData(config, filename) {
let params = {Key: this._bucketPrefix + filename};
return this.createBucket().then(() => {
return new Promise((resolve, reject) => {
this._s3Client.getObject(params, (err, data) => {
if (err !== null) {
return reject(err);
}
// Something happend here...
if (data && !data.Body) {
return reject(data);
}
resolve(data.Body);
});
});
});
}
// Generates and returns the location of a file stored in S3 for the given request and filename
// The location is the direct S3 link if the option is set, otherwise we serve the file through parse-server
getFileLocation(config, filename) {
if (this._directAccess) {
return `https://${this._bucket}.s3.amazonaws.com/${this._bucketPrefix + filename}`;
}
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
}
}
export default S3Adapter;
+24 -129
View File
@@ -1,7 +1,7 @@
// Logger
//
// Wrapper around Winston logging library with custom query
//
//
// expected log entry to be in the shape of:
// {"level":"info","message":"Your Message","timestamp":"2016-02-04T05:59:27.412Z"}
//
@@ -9,6 +9,7 @@ import { LoggerAdapter } from './LoggerAdapter';
import winston from 'winston';
import fs from 'fs';
import { Parse } from 'parse/node';
import { logger, configure } from '../../logger';
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
const CACHE_TIME = 1000 * 60;
@@ -57,24 +58,6 @@ let _hasValidCache = (from, until, level) => {
return false;
}
// renews transports to current date
let _renewTransports = ({infoLogger, errorLogger, logsFolder}) => {
if (infoLogger) {
infoLogger.add(winston.transports.File, {
filename: logsFolder + _getFileName() + '.info',
name: 'info-file',
level: 'info'
});
}
if (errorLogger) {
errorLogger.add(winston.transports.File, {
filename: logsFolder + _getFileName() + '.error',
name: 'error-file',
level: 'error'
});
}
};
// check that log entry has valid time stamp based on query
let _isValidLogEntry = (from, until, entry) => {
var _entry = JSON.parse(entry),
@@ -84,139 +67,51 @@ let _isValidLogEntry = (from, until, entry) => {
: false
};
// ensure that file name is up to date
let _verifyTransports = ({infoLogger, errorLogger, logsFolder}) => {
if (_getNearestDay(currentDate) !== _getNearestDay(new Date())) {
currentDate = new Date();
if (infoLogger) {
infoLogger.remove('info-file');
}
if (errorLogger) {
errorLogger.remove('error-file');
}
_renewTransports({infoLogger, errorLogger, logsFolder});
}
}
export class FileLoggerAdapter extends LoggerAdapter {
constructor(options = {}) {
super();
this._logsFolder = options.logsFolder || LOGS_FOLDER;
// check logs folder exists
if (!fs.existsSync(this._logsFolder)) {
fs.mkdirSync(this._logsFolder);
}
this._errorLogger = new (winston.Logger)({
exitOnError: false,
transports: [
new (winston.transports.File)({
filename: this._logsFolder + _getFileName() + '.error',
name: 'error-file',
level: 'error'
})
]
});
this._infoLogger = new (winston.Logger)({
exitOnError: false,
transports: [
new (winston.transports.File)({
filename: this._logsFolder + _getFileName() + '.info',
name: 'info-file',
level: 'info'
})
]
});
}
info() {
_verifyTransports({infoLogger: this._infoLogger, logsFolder: this._logsFolder});
return this._infoLogger.info.apply(undefined, arguments);
return logger.info.apply(undefined, arguments);
}
error() {
_verifyTransports({errorLogger: this._errorLogger, logsFolder: this._logsFolder});
return this._errorLogger.error.apply(undefined, arguments);
return logger.error.apply(undefined, arguments);
}
// custom query as winston is currently limited
query(options, callback) {
query(options, callback = () => {}) {
if (!options) {
options = {};
}
// defaults to 7 days prior
let from = options.from || new Date(Date.now() - (7 * MILLISECONDS_IN_A_DAY));
let until = options.until || new Date();
let size = options.size || 10;
let limit = options.size || 10;
let order = options.order || 'desc';
let level = options.level || 'info';
let roundedUntil = _getNearestDay(until);
let roundedFrom = _getNearestDay(from);
if (_hasValidCache(roundedFrom, roundedUntil, level)) {
let logs = [];
if (order !== simpleCache.order) {
// reverse order of data
simpleCache.data.forEach((entry) => {
logs.unshift(entry);
});
} else {
logs = simpleCache.data;
}
callback(logs.slice(0, size));
return;
}
var options = {
from,
until,
limit,
order
};
let curDate = roundedUntil;
let curSize = 0;
let method = order === 'desc' ? 'push' : 'unshift';
let files = [];
let promises = [];
// current a batch call, all files with valid dates are read
while (curDate >= from) {
files[method](this._logsFolder + curDate.toISOString() + '.' + level);
curDate = _getPrevDay(curDate);
}
// read each file and split based on newline char.
// limitation is message cannot contain newline
// TODO: strip out delimiter from logged message
files.forEach(function(file, i) {
let promise = new Parse.Promise();
fs.readFile(file, 'utf8', function(err, data) {
return new Promise((resolve, reject) => {
logger.query(options, (err, res) => {
if (err) {
promise.resolve([]);
} else {
let results = data.split('\n').filter((value) => {
return value.trim() !== '';
});
promise.resolve(results);
callback(err);
return reject(err);
}
});
promises[method](promise);
});
Parse.Promise.when(promises).then((results) => {
let logs = [];
results.forEach(function(logEntries, i) {
logEntries.forEach(function(entry) {
if (_isValidLogEntry(from, until, entry)) {
logs[method](JSON.parse(entry));
}
});
});
simpleCache = {
timestamp: new Date(),
from: roundedFrom,
until: roundedUntil,
data: logs,
order,
level,
};
callback(logs.slice(0, size));
if (level == 'error') {
callback(res['parse-server-error']);
resolve(res['parse-server-error']);
} else {
callback(res['parse-server']);
resolve(res['parse-server']);
}
})
});
}
}
-208
View File
@@ -1,208 +0,0 @@
"use strict";
// ParsePushAdapter is the default implementation of
// PushAdapter, it uses GCM for android push and APNS
// for ios push.
import { classifyInstallations } from './PushAdapterUtils';
const Parse = require('parse/node').Parse;
var deepcopy = require('deepcopy');
import PushAdapter from './PushAdapter';
export class OneSignalPushAdapter extends PushAdapter {
constructor(pushConfig = {}) {
super(pushConfig);
this.https = require('https');
this.validPushTypes = ['ios', 'android'];
this.senderMap = {};
this.OneSignalConfig = {};
const { oneSignalAppId, oneSignalApiKey } = pushConfig;
if (!oneSignalAppId || !oneSignalApiKey) {
throw "Trying to initialize OneSignalPushAdapter without oneSignalAppId or oneSignalApiKey";
}
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
this.senderMap['ios'] = this.sendToAPNS.bind(this);
this.senderMap['android'] = this.sendToGCM.bind(this);
}
send(data, installations) {
let deviceMap = classifyInstallations(installations, this.validPushTypes);
let sendPromises = [];
for (let pushType in deviceMap) {
let sender = this.senderMap[pushType];
if (!sender) {
console.log('Can not find sender for push type %s, %j', pushType, data);
continue;
}
let devices = deviceMap[pushType];
if(devices.length > 0) {
sendPromises.push(sender(data, devices));
}
}
return Parse.Promise.when(sendPromises);
}
static classifyInstallations(installations, validTypes) {
return classifyInstallations(installations, validTypes)
}
getValidPushTypes() {
return this.validPushTypes;
}
sendToAPNS(data,tokens) {
data= deepcopy(data['data']);
var post = {};
if(data['badge']) {
if(data['badge'] == "Increment") {
post['ios_badgeType'] = 'Increase';
post['ios_badgeCount'] = 1;
} else {
post['ios_badgeType'] = 'SetTo';
post['ios_badgeCount'] = data['badge'];
}
delete data['badge'];
}
if(data['alert']) {
post['contents'] = {en: data['alert']};
delete data['alert'];
}
if(data['sound']) {
post['ios_sound'] = data['sound'];
delete data['sound'];
}
if(data['content-available'] == 1) {
post['content_available'] = true;
delete data['content-available'];
}
post['data'] = data;
let promise = new Parse.Promise();
var chunk = 2000 // OneSignal can process 2000 devices at a time
var tokenlength=tokens.length;
var offset = 0
// handle onesignal response. Start next batch if there's not an error.
let handleResponse = function(wasSuccessful) {
if (!wasSuccessful) {
return promise.reject("OneSignal Error");
}
if(offset >= tokenlength) {
promise.resolve()
} else {
this.sendNext();
}
}.bind(this)
this.sendNext = function() {
post['include_ios_tokens'] = [];
tokens.slice(offset,offset+chunk).forEach(function(i) {
post['include_ios_tokens'].push(i['deviceToken'])
})
offset+=chunk;
this.sendToOneSignal(post, handleResponse);
}.bind(this)
this.sendNext()
return promise;
}
sendToGCM(data,tokens) {
data= deepcopy(data['data']);
var post = {};
if(data['alert']) {
post['contents'] = {en: data['alert']};
delete data['alert'];
}
if(data['title']) {
post['title'] = {en: data['title']};
delete data['title'];
}
if(data['uri']) {
post['url'] = data['uri'];
}
post['data'] = data;
let promise = new Parse.Promise();
var chunk = 2000 // OneSignal can process 2000 devices at a time
var tokenlength=tokens.length;
var offset = 0
// handle onesignal response. Start next batch if there's not an error.
let handleResponse = function(wasSuccessful) {
if (!wasSuccessful) {
return promise.reject("OneSIgnal Error");
}
if(offset >= tokenlength) {
promise.resolve()
} else {
this.sendNext();
}
}.bind(this);
this.sendNext = function() {
post['include_android_reg_ids'] = [];
tokens.slice(offset,offset+chunk).forEach(function(i) {
post['include_android_reg_ids'].push(i['deviceToken'])
})
offset+=chunk;
this.sendToOneSignal(post, handleResponse);
}.bind(this)
this.sendNext();
return promise;
}
sendToOneSignal(data, cb) {
let headers = {
"Content-Type": "application/json",
"Authorization": "Basic "+this.OneSignalConfig['apiKey']
};
let options = {
host: "onesignal.com",
port: 443,
path: "/api/v1/notifications",
method: "POST",
headers: headers
};
data['app_id'] = this.OneSignalConfig['appId'];
let request = this.https.request(options, function(res) {
if(res.statusCode < 299) {
cb(true);
} else {
console.log('OneSignal Error');
res.on('data', function(chunk) {
console.log(chunk.toString())
});
cb(false)
}
});
request.on('error', function(e) {
console.log("Error connecting to OneSignal")
console.log(e);
cb(false);
});
request.write(JSON.stringify(data))
request.end();
}
}
export default OneSignalPushAdapter;
module.exports = OneSignalPushAdapter;
-70
View File
@@ -1,70 +0,0 @@
"use strict";
// ParsePushAdapter is the default implementation of
// PushAdapter, it uses GCM for android push and APNS
// for ios push.
const Parse = require('parse/node').Parse;
const GCM = require('../../GCM');
const APNS = require('../../APNS');
import PushAdapter from './PushAdapter';
import { classifyInstallations } from './PushAdapterUtils';
export class ParsePushAdapter extends PushAdapter {
supportsPushTracking = true;
constructor(pushConfig = {}) {
super(pushConfig);
this.validPushTypes = ['ios', 'android'];
this.senderMap = {};
// used in PushController for Dashboard Features
this.feature = {
immediatePush: true
};
let pushTypes = Object.keys(pushConfig);
for (let pushType of pushTypes) {
if (this.validPushTypes.indexOf(pushType) < 0) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push to ' + pushTypes + ' is not supported');
}
switch (pushType) {
case 'ios':
this.senderMap[pushType] = new APNS(pushConfig[pushType]);
break;
case 'android':
this.senderMap[pushType] = new GCM(pushConfig[pushType]);
break;
}
}
}
getValidPushTypes() {
return this.validPushTypes;
}
static classifyInstallations(installations, validTypes) {
return classifyInstallations(installations, validTypes)
}
send(data, installations) {
let deviceMap = classifyInstallations(installations, this.validPushTypes);
let sendPromises = [];
for (let pushType in deviceMap) {
let sender = this.senderMap[pushType];
if (!sender) {
sendPromises.push(Promise.resolve({
transmitted: false,
response: {'error': `Can not find sender for push type ${pushType}, ${data}`}
}))
} else {
let devices = deviceMap[pushType];
sendPromises.push(sender.send(data, devices));
}
}
return Parse.Promise.when(sendPromises);
}
}
export default ParsePushAdapter;
module.exports = ParsePushAdapter;
-27
View File
@@ -1,27 +0,0 @@
/**g
* Classify the device token of installations based on its device type.
* @param {Object} installations An array of installations
* @param {Array} validPushTypes An array of valid push types(string)
* @returns {Object} A map whose key is device type and value is an array of device
*/
export function classifyInstallations(installations, validPushTypes) {
// Init deviceTokenMap, create a empty array for each valid pushType
let deviceMap = {};
for (let validPushType of validPushTypes) {
deviceMap[validPushType] = [];
}
for (let installation of installations) {
// No deviceToken, ignore
if (!installation.deviceToken) {
continue;
}
let pushType = installation.deviceType;
if (deviceMap[pushType]) {
deviceMap[pushType].push({
deviceToken: installation.deviceToken,
appIdentifier: installation.appIdentifier
});
}
}
return deviceMap;
}
-4
View File
@@ -18,12 +18,8 @@ export class AdaptableController {
this.options = options;
this.appId = appId;
this.adapter = adapter;
this.setFeature();
}
// sets features for Dashboard to consume from features router
setFeature() {}
set adapter(adapter) {
this.validateAdapter(adapter);
this[_adapter] = adapter;
+3 -3
View File
@@ -9,7 +9,7 @@ import mime from 'mime';
export class FilesController extends AdaptableController {
getFileData(config, filename) {
return this.adapter.getFileData(config, filename);
return this.adapter.getFileData(filename);
}
createFile(config, filename, data, contentType) {
@@ -27,7 +27,7 @@ export class FilesController extends AdaptableController {
filename = randomHexString(32) + '_' + filename;
var location = this.adapter.getFileLocation(config, filename);
return this.adapter.createFile(config, filename, data, contentType).then(() => {
return this.adapter.createFile(filename, data, contentType).then(() => {
return Promise.resolve({
url: location,
name: filename
@@ -36,7 +36,7 @@ export class FilesController extends AdaptableController {
}
deleteFile(config, filename) {
return this.adapter.deleteFile(config, filename);
return this.adapter.deleteFile(filename);
}
/**
+7 -15
View File
@@ -3,7 +3,6 @@ import PromiseRouter from '../PromiseRouter';
import AdaptableController from './AdaptableController';
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
const Promise = Parse.Promise;
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
export const LogLevel = {
@@ -17,21 +16,21 @@ export const LogOrder = {
}
export class LoggerController extends AdaptableController {
// check that date input is valid
static validDateTime(date) {
if (!date) {
return null;
return null;
}
date = new Date(date);
if (!isNaN(date.getTime())) {
return date;
}
return null;
}
static parseOptions(options = {}) {
let from = LoggerController.validDateTime(options.from) ||
new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY);
@@ -39,7 +38,7 @@ export class LoggerController extends AdaptableController {
let size = Number(options.size) || 10;
let order = options.order || LogOrder.DESCENDING;
let level = options.level || LogLevel.INFO;
return {
from,
until,
@@ -61,17 +60,10 @@ export class LoggerController extends AdaptableController {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Logger adapter is not availabe');
}
let promise = new Parse.Promise();
options = LoggerController.parseOptions(options);
this.adapter.query(options, (result) => {
promise.resolve(result);
});
return promise;
return this.adapter.query(options);
}
expectedAdapterType() {
return LoggerAdapter;
}
+15 -25
View File
@@ -1,22 +1,17 @@
import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import rest from '../rest';
import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import rest from '../rest';
import AdaptableController from './AdaptableController';
import { PushAdapter } from '../Adapters/Push/PushAdapter';
import deepcopy from 'deepcopy';
import features from '../features';
import RestQuery from '../RestQuery';
import pushStatusHandler from '../pushStatusHandler';
import { PushAdapter } from '../Adapters/Push/PushAdapter';
import deepcopy from 'deepcopy';
import RestQuery from '../RestQuery';
import pushStatusHandler from '../pushStatusHandler';
const FEATURE_NAME = 'push';
const UNSUPPORTED_BADGE_KEY = "unsupported";
export class PushController extends AdaptableController {
setFeature() {
features.setFeature(FEATURE_NAME, this.adapter.feature || {});
}
/**
* Check whether the deviceType parameter in qury condition is valid or not.
* @param {Object} where A query condition
@@ -39,9 +34,13 @@ export class PushController extends AdaptableController {
}
}
get pushIsAvailable() {
return !!this.adapter;
}
sendPush(body = {}, where = {}, config, auth, wait) {
var pushAdapter = this.adapter;
if (!pushAdapter) {
if (!this.pushIsAvailable) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push adapter is not available');
}
@@ -57,7 +56,7 @@ export class PushController extends AdaptableController {
if (body.data && body.data.badge) {
let badge = body.data.badge;
let op = {};
if (badge == "Increment") {
if (typeof badge == 'string' && badge.toLowerCase() === 'increment') {
op = { $inc: { badge: 1 } }
} else if (Number(badge)) {
op = { $set: { badge: badge } }
@@ -98,7 +97,7 @@ export class PushController extends AdaptableController {
}
sendToAdapter(body, installations, pushStatus, config) {
if (body.data && body.data.badge && body.data.badge == "Increment") {
if (body.data && body.data.badge && typeof body.data.badge == 'string' && body.data.badge.toLowerCase() == "increment") {
// Collect the badges to reduce the # of calls
let badgeInstallationsMap = installations.reduce((map, installation) => {
let badge = installation.badge;
@@ -120,16 +119,7 @@ export class PushController extends AdaptableController {
}
return this.adapter.send(payload, badgeInstallationsMap[badge]);
});
// Flatten the promises results
return Promise.all(promises).then((results) => {
if (Array.isArray(results)) {
return Promise.resolve(results.reduce((memo, result) => {
return memo.concat(result);
},[]));
} else {
return Promise.resolve(results);
}
})
return Promise.all(promises);
}
return this.adapter.send(body, installations);
}
+21 -15
View File
@@ -1,7 +1,8 @@
import { randomString } from '../cryptoUtils';
import { inflate } from '../triggers';
import { randomString } from '../cryptoUtils';
import { inflate } from '../triggers';
import AdaptableController from './AdaptableController';
import MailAdapter from '../Adapters/Email/MailAdapter';
import MailAdapter from '../Adapters/Email/MailAdapter';
import rest from '../rest';
var DatabaseAdapter = require('../DatabaseAdapter');
var RestWrite = require('../RestWrite');
@@ -99,14 +100,13 @@ export class UserController extends AdaptableController {
})
}
sendVerificationEmail(user) {
if (!this.shouldVerifyEmails) {
return;
}
const token = encodeURIComponent(user._email_verify_token);
// We may need to fetch the user in case of update email
this.getUserIfNeeded(user).then((user) => {
const token = encodeURIComponent(user._email_verify_token);
const username = encodeURIComponent(user.username);
let link = `${this.config.verifyEmailURL}?token=${token}&username=${username}`;
let options = {
@@ -165,14 +165,22 @@ export class UserController extends AdaptableController {
}
updatePassword(username, token, password, config) {
return this.checkResetTokenValidity(username, token).then(() => {
return updateUserPassword(username, token, password, this.config);
});
return this.checkResetTokenValidity(username, token).then((user) => {
return updateUserPassword(user._id, password, this.config);
}).then(() => {
// clear reset password token
return this.config.database.adaptiveCollection('_User').then(function (collection) {
// Need direct database access because verification token is not a parse field
return collection.findOneAndUpdate({ username: username },// query
{ $unset: { _perishable_token: null } } // update
);
});
});
}
defaultVerificationEmail({link, user, appName, }) {
let text = "Hi,\n\n" +
"You are being asked to confirm the e-mail address " + user.email + " with " + appName + "\n\n" +
"You are being asked to confirm the e-mail address " + user.get("email") + " with " + appName + "\n\n" +
"" +
"Click here to confirm it:\n" + link;
let to = user.get("email");
@@ -192,12 +200,10 @@ export class UserController extends AdaptableController {
}
// Mark this private
function updateUserPassword(username, token, password, config) {
var write = new RestWrite(config, Auth.master(config), '_User', {
username: username,
_perishable_token: token
}, {password: password, _perishable_token: null }, undefined);
return write.execute();
function updateUserPassword(userId, password, config) {
return rest.update(config, Auth.master(config), '_User', userId, {
password: password
});
}
export default UserController;
-151
View File
@@ -1,151 +0,0 @@
"use strict";
const Parse = require('parse/node').Parse;
const gcm = require('node-gcm');
const cryptoUtils = require('./cryptoUtils');
const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
const GCMRegistrationTokensMax = 1000;
function GCM(args) {
if (typeof args !== 'object' || !args.apiKey) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'GCM Configuration is invalid');
}
this.sender = new gcm.Sender(args.apiKey);
}
/**
* Send gcm request.
* @param {Object} data The data we need to send, the format is the same with api request body
* @param {Array} devices A array of devices
* @returns {Object} A promise which is resolved after we get results from gcm
*/
GCM.prototype.send = function(data, devices) {
// Make a new array
devices = new Array(...devices);
let timestamp = Date.now();
// For android, we can only have 1000 recepients per send, so we need to slice devices to
// chunk if necessary
let slices = sliceDevices(devices, GCMRegistrationTokensMax);
if (slices.length > 1) {
// Make 1 send per slice
let promises = slices.reduce((memo, slice) => {
let promise = this.send(data, slice, timestamp);
memo.push(promise);
return memo;
}, [])
return Parse.Promise.when(promises).then((results) => {
let allResults = results.reduce((memo, result) => {
return memo.concat(result);
}, []);
return Parse.Promise.as(allResults);
});
}
// get the devices back...
devices = slices[0];
let expirationTime;
// We handle the expiration_time convertion in push.js, so expiration_time is a valid date
// in Unix epoch time in milliseconds here
if (data['expiration_time']) {
expirationTime = data['expiration_time'];
}
// Generate gcm payload
let gcmPayload = generateGCMPayload(data.data, timestamp, expirationTime);
// Make and send gcm request
let message = new gcm.Message(gcmPayload);
// Build a device map
let devicesMap = devices.reduce((memo, device) => {
memo[device.deviceToken] = device;
return memo;
}, {});
let deviceTokens = Object.keys(devicesMap);
let promises = deviceTokens.map(() => new Parse.Promise());
let registrationTokens = deviceTokens;
this.sender.send(message, { registrationTokens: registrationTokens }, 5, (error, response) => {
// example response:
/*
{ "multicast_id":7680139367771848000,
"success":0,
"failure":4,
"canonical_ids":0,
"results":[ {"error":"InvalidRegistration"},
{"error":"InvalidRegistration"},
{"error":"InvalidRegistration"},
{"error":"InvalidRegistration"}] }
*/
let { results, multicast_id } = response || {};
registrationTokens.forEach((token, index) => {
let promise = promises[index];
let result = results ? results[index] : undefined;
let device = devicesMap[token];
let resolution = {
device,
multicast_id,
response: error || result,
};
if (!result || result.error) {
resolution.transmitted = false;
} else {
resolution.transmitted = true;
}
promise.resolve(resolution);
});
});
return Parse.Promise.when(promises);
}
/**
* Generate the gcm payload from the data we get from api request.
* @param {Object} coreData The data field under api request body
* @param {String} pushId A random string
* @param {Number} timeStamp A number whose format is the Unix Epoch
* @param {Number|undefined} expirationTime A number whose format is the Unix Epoch or undefined
* @returns {Object} A promise which is resolved after we get results from gcm
*/
function generateGCMPayload(coreData, timeStamp, expirationTime) {
let payloadData = {
'time': new Date(timeStamp).toISOString(),
'data': JSON.stringify(coreData)
}
let payload = {
priority: 'normal',
data: payloadData
};
if (expirationTime) {
// The timeStamp and expiration is in milliseconds but gcm requires second
let timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
if (timeToLive < 0) {
timeToLive = 0;
}
if (timeToLive >= GCMTimeToLiveMax) {
timeToLive = GCMTimeToLiveMax;
}
payload.timeToLive = timeToLive;
}
return payload;
}
/**
* Slice a list of devices to several list of devices with fixed chunk size.
* @param {Array} devices An array of devices
* @param {Number} chunkSize The size of the a chunk
* @returns {Array} An array which contaisn several arries of devices with fixed chunk size
*/
function sliceDevices(devices, chunkSize) {
let chunkDevices = [];
while (devices.length > 0) {
chunkDevices.push(devices.splice(0, chunkSize));
}
return chunkDevices;
}
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
GCM.generateGCMPayload = generateGCMPayload;
GCM.sliceDevices = sliceDevices;
}
module.exports = GCM;
+2 -38
View File
@@ -1,41 +1,5 @@
let LogLevel = {
'VERBOSE': 0,
'DEBUG': 1,
'INFO': 2,
'ERROR': 3,
'NONE': 4
}
import { addGroup } from '../logger';
function getCurrentLogLevel() {
if (PLog.logLevel && PLog.logLevel in LogLevel) {
return LogLevel[PLog.logLevel];
}
return LogLevel['ERROR'];
}
function verbose(): void {
if (getCurrentLogLevel() <= LogLevel['VERBOSE']) {
console.log.apply(console, arguments)
}
}
function log(): void {
if (getCurrentLogLevel() <= LogLevel['INFO']) {
console.log.apply(console, arguments)
}
}
function error(): void {
if (getCurrentLogLevel() <= LogLevel['ERROR']) {
console.error.apply(console, arguments)
}
}
let PLog = {
log: log,
error: error,
verbose: verbose,
logLevel: 'INFO'
};
let PLog = addGroup('parse-live-query-server');
module.exports = PLog;
+1 -2
View File
@@ -26,8 +26,7 @@ class ParseLiveQueryServer {
config = config || {};
// Set LogLevel
PLog.logLevel = config.logLevel || 'INFO';
PLog.level = config.logLevel || 'INFO';
// Store keys, convert obj to map
let keyPairs = config.keyPairs || {};
this.keyPairs = new Map();
+284
View File
@@ -0,0 +1,284 @@
// ParseServer - open-source compatible API Server for Parse apps
import 'babel-polyfill';
var batch = require('./batch'),
bodyParser = require('body-parser'),
DatabaseAdapter = require('./DatabaseAdapter'),
express = require('express'),
middlewares = require('./middlewares'),
multer = require('multer'),
Parse = require('parse/node').Parse,
authDataManager = require('./authDataManager');
import { logger,
configureLogger } from './logger';
import cache from './cache';
import Config from './Config';
import parseServerPackage from '../package.json';
import PromiseRouter from './PromiseRouter';
import requiredParameter from './requiredParameter';
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
import { ClassesRouter } from './Routers/ClassesRouter';
import { FeaturesRouter } from './Routers/FeaturesRouter';
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { FilesController } from './Controllers/FilesController';
import { FilesRouter } from './Routers/FilesRouter';
import { FunctionsRouter } from './Routers/FunctionsRouter';
import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
import { HooksController } from './Controllers/HooksController';
import { HooksRouter } from './Routers/HooksRouter';
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
import { InstallationsRouter } from './Routers/InstallationsRouter';
import { loadAdapter } from './Adapters/AdapterLoader';
import { LiveQueryController } from './Controllers/LiveQueryController';
import { LoggerController } from './Controllers/LoggerController';
import { LogsRouter } from './Routers/LogsRouter';
import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer';
import { PublicAPIRouter } from './Routers/PublicAPIRouter';
import { PushController } from './Controllers/PushController';
import { PushRouter } from './Routers/PushRouter';
import { randomString } from './cryptoUtils';
import { RolesRouter } from './Routers/RolesRouter';
import { SchemasRouter } from './Routers/SchemasRouter';
import { SessionsRouter } from './Routers/SessionsRouter';
import { UserController } from './Controllers/UserController';
import { UsersRouter } from './Routers/UsersRouter';
import ParsePushAdapter from 'parse-server-push-adapter';
// Mutate the Parse object to add the Cloud Code handlers
addParseCloud();
// ParseServer works like a constructor of an express app.
// The args that we understand are:
// "databaseAdapter": a class like DatabaseController providing create, find,
// update, and delete
// "filesAdapter": a class like GridStoreAdapter providing create, get,
// and delete
// "loggerAdapter": a class like FileLoggerAdapter providing info, error,
// and query
// "databaseURI": a uri like mongodb://localhost:27017/dbname to tell us
// what database this Parse API connects to.
// "cloud": relative location to cloud code to require, or a function
// that is given an instance of Parse as a parameter. Use this instance of Parse
// to register your cloud code hooks and functions.
// "appId": the application id to host
// "masterKey": the master key for requests to this app
// "facebookAppIds": an array of valid Facebook Application IDs, required
// if using Facebook login
// "collectionPrefix": optional prefix for database collection names
// "fileKey": optional key from Parse dashboard for supporting older files
// hosted by Parse
// "clientKey": optional key from Parse dashboard
// "dotNetKey": optional key from Parse dashboard
// "restAPIKey": optional key from Parse dashboard
// "javascriptKey": optional key from Parse dashboard
// "push": optional key from configure push
class ParseServer {
constructor({
appId = requiredParameter('You must provide an appId!'),
masterKey = requiredParameter('You must provide a masterKey!'),
appName,
databaseAdapter,
filesAdapter,
push,
loggerAdapter,
logsFolder,
databaseURI = DatabaseAdapter.defaultDatabaseURI,
databaseOptions,
cloud,
collectionPrefix = '',
clientKey,
javascriptKey,
dotNetKey,
restAPIKey,
fileKey = 'invalid-file-key',
facebookAppIds = [],
enableAnonymousUsers = true,
allowClientClassCreation = true,
oauth = {},
serverURL = requiredParameter('You must provide a serverURL!'),
maxUploadSize = '20mb',
verifyUserEmails = false,
emailAdapter,
publicServerURL,
customPages = {
invalidLink: undefined,
verifyEmailSuccess: undefined,
choosePassword: undefined,
passwordResetSuccess: undefined
},
liveQuery = {}
}) {
// Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
Parse.serverURL = serverURL;
if (logsFolder) {
configureLogger({
logsFolder
})
}
if (databaseAdapter) {
DatabaseAdapter.setAdapter(databaseAdapter);
}
if (databaseOptions) {
DatabaseAdapter.setAppDatabaseOptions(appId, databaseOptions);
}
if (databaseURI) {
DatabaseAdapter.setAppDatabaseURI(appId, databaseURI);
}
if (cloud) {
addParseCloud();
if (typeof cloud === 'function') {
cloud(Parse)
} else if (typeof cloud === 'string') {
require(cloud);
} else {
throw "argument 'cloud' must either be a string or a function";
}
}
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
return new GridStoreAdapter(databaseURI);
});
// Pass the push options too as it works with the default
const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push);
const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter);
const emailControllerAdapter = loadAdapter(emailAdapter);
// We pass the options and the base class for the adatper,
// Note that passing an instance would work too
const filesController = new FilesController(filesControllerAdapter, appId);
const pushController = new PushController(pushControllerAdapter, appId);
const loggerController = new LoggerController(loggerControllerAdapter, appId);
const hooksController = new HooksController(appId, collectionPrefix);
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
const liveQueryController = new LiveQueryController(liveQuery);
cache.apps.set(appId, {
masterKey: masterKey,
serverURL: serverURL,
collectionPrefix: collectionPrefix,
clientKey: clientKey,
javascriptKey: javascriptKey,
dotNetKey: dotNetKey,
restAPIKey: restAPIKey,
fileKey: fileKey,
facebookAppIds: facebookAppIds,
filesController: filesController,
pushController: pushController,
loggerController: loggerController,
hooksController: hooksController,
userController: userController,
verifyUserEmails: verifyUserEmails,
allowClientClassCreation: allowClientClassCreation,
authDataManager: authDataManager(oauth, enableAnonymousUsers),
appName: appName,
publicServerURL: publicServerURL,
customPages: customPages,
maxUploadSize: maxUploadSize,
liveQueryController: liveQueryController
});
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
if (process.env.FACEBOOK_APP_ID) {
cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
}
Config.validate(cache.apps.get(appId));
this.config = cache.apps.get(appId);
hooksController.load();
}
get app() {
return ParseServer.app(this.config);
}
static app({maxUploadSize = '20mb'}) {
// This app serves the Parse API directly.
// It's the equivalent of https://api.parse.com/1 in the hosted Parse API.
var api = express();
//api.use("/apps", express.static(__dirname + "/public"));
// File handling needs to be before default middlewares are applied
api.use('/', middlewares.allowCrossDomain, new FilesRouter().getExpressRouter({
maxUploadSize: maxUploadSize
}));
api.use('/', bodyParser.urlencoded({extended: false}), new PublicAPIRouter().expressApp());
// TODO: separate this from the regular ParseServer object
if (process.env.TESTING == 1) {
api.use('/', require('./testing-routes').router);
}
api.use(bodyParser.json({ 'type': '*/*' , limit: maxUploadSize }));
api.use(middlewares.allowCrossDomain);
api.use(middlewares.allowMethodOverride);
api.use(middlewares.handleParseHeaders);
let routers = [
new ClassesRouter(),
new UsersRouter(),
new SessionsRouter(),
new RolesRouter(),
new AnalyticsRouter(),
new InstallationsRouter(),
new FunctionsRouter(),
new SchemasRouter(),
new PushRouter(),
new LogsRouter(),
new IAPValidationRouter(),
new FeaturesRouter(),
new GlobalConfigRouter(),
];
if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) {
routers.push(new HooksRouter());
}
let routes = routers.reduce((memo, router) => {
return memo.concat(router.routes);
}, []);
let appRouter = new PromiseRouter(routes);
batch.mountOnto(appRouter);
api.use(appRouter.expressApp());
api.use(middlewares.handleParseErrors);
//This causes tests to spew some useless warnings, so disable in test
if (!process.env.TESTING) {
process.on('uncaughtException', (err) => {
if ( err.code === "EADDRINUSE" ) { // user-friendly message for this common error
console.error(`Unable to listen on port ${err.port}. The port is already in use.`);
process.exit(0);
} else {
throw err;
}
});
}
return api;
}
static createLiveQueryServer(httpServer, config) {
return new ParseLiveQueryServer(httpServer, config);
}
}
function addParseCloud() {
const ParseCloud = require("./cloud-code/Parse.Cloud");
Object.assign(Parse.Cloud, ParseCloud);
global.Parse = Parse;
}
export default ParseServer;
+7 -17
View File
@@ -6,6 +6,7 @@
// components that external developers may be modifying.
import express from 'express';
import log from './logger';
export default class PromiseRouter {
// Each entry should be an object with:
@@ -146,9 +147,6 @@ export default class PromiseRouter {
}
}
// Global flag. Set this to true to log every request and response.
PromiseRouter.verbose = process.env.VERBOSE || false;
// A helper function to make an express handler out of a a promise
// handler.
// Express handlers should never throw; if a promise handler throws we
@@ -156,18 +154,14 @@ PromiseRouter.verbose = process.env.VERBOSE || false;
function makeExpressHandler(promiseHandler) {
return function(req, res, next) {
try {
if (PromiseRouter.verbose) {
console.log(req.method, req.originalUrl, req.headers,
JSON.stringify(req.body, null, 2));
}
log.verbose(req.method, req.originalUrl, req.headers,
JSON.stringify(req.body, null, 2));
promiseHandler(req).then((result) => {
if (!result.response && !result.location && !result.text) {
console.log('BUG: the handler did not include a "response" or a "location" field');
log.error('the handler did not include a "response" or a "location" field');
throw 'control should not get here';
}
if (PromiseRouter.verbose) {
console.log('response:', JSON.stringify(result, null, 2));
}
log.verbose(JSON.stringify(result, null, 2));
var status = result.status || 200;
res.status(status);
@@ -186,15 +180,11 @@ function makeExpressHandler(promiseHandler) {
}
res.json(result.response);
}, (e) => {
if (PromiseRouter.verbose) {
console.log('error:', e);
}
log.verbose('error:', e);
next(e);
});
} catch (e) {
if (PromiseRouter.verbose) {
console.log('error:', e);
}
log.verbose('exception:', e);
next(e);
}
}
+39 -21
View File
@@ -83,6 +83,8 @@ RestWrite.prototype.execute = function() {
return this.handleFollowup();
}).then(() => {
return this.runAfterTrigger();
}).then(() => {
return this.cleanUserAuthData();
}).then(() => {
return this.response;
});
@@ -281,26 +283,26 @@ RestWrite.prototype.handleAuthData = function(authData) {
this.storage['authProvider'] = Object.keys(authData).join(',');
if (results.length == 0) {
this.data.username = cryptoUtils.newToken();
} else if (!this.query) {
// Login with auth data
// Short circuit
delete results[0].password;
// need to set the objectId first otherwise location has trailing undefined
this.data.objectId = results[0].objectId;
this.response = {
response: results[0],
location: this.location()
};
} else if (this.query && this.query.objectId) {
// Trying to update auth data but users
// are different
if (results[0].objectId !== this.query.objectId) {
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
'this auth is already used');
if (results.length > 0) {
if (!this.query) {
// Login with auth data
// Short circuit
delete results[0].password;
// need to set the objectId first otherwise location has trailing undefined
this.data.objectId = results[0].objectId;
this.response = {
response: results[0],
location: this.location()
};
} else if (this.query && this.query.objectId) {
// Trying to update auth data but users
// are different
if (results[0].objectId !== this.query.objectId) {
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
'this auth is already used');
}
}
}
}
return Promise.resolve();
});
}
@@ -327,11 +329,11 @@ RestWrite.prototype.transformUser = function() {
objectId: this.objectId()
},
createdWith: {
'action': 'login',
'action': 'signup',
'authProvider': this.storage['authProvider'] || 'password'
},
restricted: false,
installationId: this.data.installationId,
installationId: this.auth.installationId,
expiresAt: Parse._encode(expiresAt)
};
if (this.response && this.response.response) {
@@ -824,5 +826,21 @@ RestWrite.prototype.sanitizedData = function() {
return Parse._decode(undefined, data);
}
RestWrite.prototype.cleanUserAuthData = function() {
if (this.response && this.response.response && this.className === '_User') {
let user = this.response.response;
if (user.authData) {
Object.keys(user.authData).forEach((provider) => {
if (user.authData[provider] === null) {
delete user.authData[provider];
}
});
if (Object.keys(user.authData).length == 0) {
delete user.authData;
}
}
}
};
export default RestWrite;
module.exports = RestWrite;
+39 -3
View File
@@ -1,13 +1,49 @@
import { version } from '../../package.json';
import PromiseRouter from '../PromiseRouter';
import * as middleware from "../middlewares";
import { getFeatures } from '../features';
export class FeaturesRouter extends PromiseRouter {
mountRoutes() {
this.route('GET','/serverInfo', middleware.promiseEnforceMasterKeyAccess, () => {
this.route('GET','/serverInfo', middleware.promiseEnforceMasterKeyAccess, req => {
const features = {
globalConfig: {
create: true,
read: true,
update: true,
delete: true,
},
hooks: {
create: false,
read: false,
update: false,
delete: false,
},
logs: {
level: false,
size: false,
order: false,
until: false,
from: false,
},
push: {
immediatePush: req.config.pushController.pushIsAvailable,
scheduledPush: false,
storedPushData: false,
pushAudiences: false,
},
schemas: {
addField: true,
removeField: true,
addClass: true,
removeClass: true,
clearAllDataFromClass: false,
exportClass: false,
editClassLevelPermissions: true,
},
};
return { response: {
features: getFeatures(),
features: features,
parseServerVersion: version,
} };
});
+12 -1
View File
@@ -18,8 +18,19 @@ export class GlobalConfigRouter extends PromiseRouter {
}
updateGlobalConfig(req) {
const params = req.body.params;
const update = Object.keys(params).reduce((acc, key) => {
if(params[key] && params[key].__op && params[key].__op === "Delete") {
if (!acc.$unset) acc.$unset = {};
acc.$unset[`params.${key}`] = "";
} else {
if (!acc.$set) acc.$set = {};
acc.$set[`params.${key}`] = params[key];
}
return acc;
}, {});
return req.config.database.adaptiveCollection('_GlobalConfig')
.then(coll => coll.upsertOne({ _id: 1 }, { $set: req.body }))
.then(coll => coll.upsertOne({ _id: 1 }, update))
.then(() => ({ response: { result: true } }));
}
+6 -16
View File
@@ -27,17 +27,7 @@ export class UsersRouter extends ClassesRouter {
req.body = data;
req.params.className = '_User';
//req.config.userController.setEmailVerifyToken(req.body);
return super.handleCreate(req);
// if (req.config.verifyUserEmails) {
// // Send email as fire-and-forget once the user makes it into the DB.
// p.then(() => {
// req.config.userController.sendVerificationEmail(req.body);
// });
// }
// return p;
}
handleUpdate(req) {
@@ -102,9 +92,9 @@ export class UsersRouter extends ClassesRouter {
let token = 'r:' + cryptoUtils.newToken();
user.sessionToken = token;
delete user.password;
// Sometimes the authData still has null on that keys
// https://github.com/ParsePlatform/parse-server/issues/935
// https://github.com/ParsePlatform/parse-server/issues/935
if (user.authData) {
Object.keys(user.authData).forEach((provider) => {
if (user.authData[provider] === null) {
@@ -115,7 +105,7 @@ export class UsersRouter extends ClassesRouter {
delete user.authData;
}
}
req.config.filesController.expandFilesInObject(req.config, user);
let expiresAt = new Date();
@@ -165,14 +155,14 @@ export class UsersRouter extends ClassesRouter {
}
return Promise.resolve(success);
}
handleResetRequest(req) {
let { email } = req.body;
if (!email) {
throw new Parse.Error(Parse.Error.EMAIL_MISSING, "you must provide an email");
}
let userController = req.config.userController;
return userController.sendPasswordResetEmail(email).then((token) => {
return Promise.resolve({
response: {}
@@ -181,7 +171,7 @@ export class UsersRouter extends ClassesRouter {
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `no user found with email ${email}`);
});
}
mountRoutes() {
this.route('GET', '/users', req => { return this.handleFind(req); });
+19 -17
View File
@@ -17,7 +17,7 @@
var Parse = require('parse/node').Parse;
var transform = require('./transform');
var defaultColumns = {
const defaultColumns = Object.freeze({
// Contain the default columns for every parse object type (except _Join collection)
_Default: {
"objectId": {type:'String'},
@@ -84,14 +84,14 @@ var defaultColumns = {
"sentPerType": {type:'Object'},
"failedPerType":{type:'Object'},
}
};
});
var requiredColumns = {
const requiredColumns = Object.freeze({
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"],
_Role: ["name", "ACL"]
}
});
const systemClasses = ['_User', '_Installation', '_Role', '_Session', '_Product'];
const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product']);
// 10 alpha numberic chars + uppercase
const userIdRegex = /^[a-zA-Z0-9]{10}$/;
@@ -100,7 +100,7 @@ const roleRegex = /^role:.*/;
// * permission
const publicRegex = /^\*$/
const permissionKeyRegex = [userIdRegex, roleRegex, publicRegex];
const permissionKeyRegex = Object.freeze([userIdRegex, roleRegex, publicRegex]);
function verifyPermissionKey(key) {
let result = permissionKeyRegex.reduce((isGood, regEx) => {
@@ -112,13 +112,15 @@ function verifyPermissionKey(key) {
}
}
let CLPValidKeys = ['find', 'get', 'create', 'update', 'delete', 'addField'];
let DefaultClassLevelPermissions = CLPValidKeys.reduce((perms, key) => {
const CLPValidKeys = Object.freeze(['find', 'get', 'create', 'update', 'delete', 'addField']);
let DefaultClassLevelPermissions = () => {
return CLPValidKeys.reduce((perms, key) => {
perms[key] = {
'*': true
};
return perms;
}, {});
}
function validateCLP(perms) {
if (!perms) {
@@ -899,21 +901,21 @@ function mongoSchemaToSchemaAPIResponse(schema) {
fields: mongoSchemaAPIResponseFields(schema),
};
let classLevelPermissions = DefaultClassLevelPermissions;
let classLevelPermissions = DefaultClassLevelPermissions();
if (schema._metadata && schema._metadata.class_permissions) {
classLevelPermissions = Object.assign(classLevelPermissions, schema._metadata.class_permissions);
classLevelPermissions = Object.assign({}, classLevelPermissions, schema._metadata.class_permissions);
}
result.classLevelPermissions = classLevelPermissions;
return result;
}
module.exports = {
load: load,
classNameIsValid: classNameIsValid,
invalidClassNameMessage: invalidClassNameMessage,
schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType,
buildMergedSchemaObject: buildMergedSchemaObject,
mongoFieldTypeToSchemaAPIType: mongoFieldTypeToSchemaAPIType,
export {
load,
classNameIsValid,
invalidClassNameMessage,
schemaAPITypeToMongoFieldType,
buildMergedSchemaObject,
mongoFieldTypeToSchemaAPIType,
mongoSchemaToSchemaAPIResponse,
systemClasses,
};
+5 -4
View File
@@ -2,6 +2,7 @@ import request from 'request';
import Parse from 'parse/node';
import HTTPResponse from './HTTPResponse';
import querystring from 'querystring';
import log from '../logger';
var encodeBody = function({body, headers = {}}) {
if (typeof body !== 'object') {
@@ -14,13 +15,13 @@ var encodeBody = function({body, headers = {}}) {
if (contentTypeKeys.length == 0) {
// no content type
// As per https://parse.com/docs/cloudcode/guide#cloud-code-advanced-sending-a-post-request the default encoding is supposedly x-www-form-urlencoded
body = querystring.stringify(body);
headers['Content-Type'] = 'application/x-www-form-urlencoded';
} else {
/* istanbul ignore next */
if (contentTypeKeys.length > 1) {
console.error('multiple content-type headers are set.');
log.error('Parse.Cloud.httpRequest', 'multiple content-type headers are set.');
}
// There maybe many, we'll just take the 1st one
var contentType = contentTypeKeys[0];
@@ -62,8 +63,8 @@ module.exports = function(options) {
return promise.reject(error);
}
let httpResponse = new HTTPResponse(response);
// Consider <200 && >= 400 as errors
// Consider <200 && >= 400 as errors
if (httpResponse.status < 200 || httpResponse.status >= 400) {
if (callbacks.error) {
callbacks.error(httpResponse);
-91
View File
@@ -1,91 +0,0 @@
/**
* features.js
* Feature config file that holds information on the features that are currently
* available on Parse Server. This is primarily created to work with an UI interface
* like the web dashboard. The list of features will change depending on the your
* app, choice of adapter as well as Parse Server version. This approach will enable
* the dashboard to be built independently and still support these use cases.
*
*
* Default features and feature options are listed in the features object.
*
* featureSwitch is a convenient way to turn on/off features without changing the config
*
* Features that use Adapters should specify the feature options through
* the setFeature method in your controller and feature
* Reference PushController and ParsePushAdapter as an example.
*
* NOTE: When adding new endpoints be sure to update this list both (features, featureSwitch)
* if you are planning to have a UI consume it.
*/
// default features
let features = {
globalConfig: {
create: false,
read: false,
update: false,
delete: false,
},
hooks: {
create: false,
read: false,
update: false,
delete: false,
},
logs: {
level: false,
size: false,
order: false,
until: false,
from: false,
},
push: {
immediatePush: false,
scheduledPush: false,
storedPushData: false,
pushAudiences: false,
},
schemas: {
addField: true,
removeField: true,
addClass: true,
removeClass: true,
clearAllDataFromClass: false,
exportClass: false,
},
};
// master switch for features
let featuresSwitch = {
globalConfig: true,
hooks: true,
logs: true,
push: true,
schemas: true,
};
/**
* set feature config options
*/
function setFeature(key, value) {
features[key] = value;
}
/**
* get feature config options
*/
function getFeatures() {
let result = {};
Object.keys(features).forEach((key) => {
if (featuresSwitch[key] && features[key]) {
result[key] = features[key];
}
});
return result;
}
module.exports = {
getFeatures,
setFeature,
};
+17 -271
View File
@@ -1,278 +1,24 @@
// ParseServer - open-source compatible API Server for Parse apps
import winston from 'winston';
import ParseServer from './ParseServer';
import GCSAdapter from 'parse-server-gcs-adapter';
import S3Adapter from 'parse-server-s3-adapter';
import FileSystemAdapter from 'parse-server-fs-adapter';
import 'babel-polyfill';
var batch = require('./batch'),
bodyParser = require('body-parser'),
DatabaseAdapter = require('./DatabaseAdapter'),
express = require('express'),
middlewares = require('./middlewares'),
multer = require('multer'),
Parse = require('parse/node').Parse,
authDataManager = require('./authDataManager');
//import passwordReset from './passwordReset';
import cache from './cache';
import Config from './Config';
import parseServerPackage from '../package.json';
import ParsePushAdapter from './Adapters/Push/ParsePushAdapter';
import PromiseRouter from './PromiseRouter';
import requiredParameter from './requiredParameter';
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
import { ClassesRouter } from './Routers/ClassesRouter';
import { FeaturesRouter } from './Routers/FeaturesRouter';
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { FilesController } from './Controllers/FilesController';
import { FilesRouter } from './Routers/FilesRouter';
import { FunctionsRouter } from './Routers/FunctionsRouter';
import { GCSAdapter } from './Adapters/Files/GCSAdapter';
import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
import { HooksController } from './Controllers/HooksController';
import { HooksRouter } from './Routers/HooksRouter';
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
import { InstallationsRouter } from './Routers/InstallationsRouter';
import { loadAdapter } from './Adapters/AdapterLoader';
import { LiveQueryController } from './Controllers/LiveQueryController';
import { LoggerController } from './Controllers/LoggerController';
import { LogsRouter } from './Routers/LogsRouter';
import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer';
import { PublicAPIRouter } from './Routers/PublicAPIRouter';
import { PushController } from './Controllers/PushController';
import { PushRouter } from './Routers/PushRouter';
import { randomString } from './cryptoUtils';
import { RolesRouter } from './Routers/RolesRouter';
import { S3Adapter } from './Adapters/Files/S3Adapter';
import { SchemasRouter } from './Routers/SchemasRouter';
import { SessionsRouter } from './Routers/SessionsRouter';
import { setFeature } from './features';
import { UserController } from './Controllers/UserController';
import { UsersRouter } from './Routers/UsersRouter';
// Mutate the Parse object to add the Cloud Code handlers
addParseCloud();
// ParseServer works like a constructor of an express app.
// The args that we understand are:
// "databaseAdapter": a class like DatabaseController providing create, find,
// update, and delete
// "filesAdapter": a class like GridStoreAdapter providing create, get,
// and delete
// "loggerAdapter": a class like FileLoggerAdapter providing info, error,
// and query
// "databaseURI": a uri like mongodb://localhost:27017/dbname to tell us
// what database this Parse API connects to.
// "cloud": relative location to cloud code to require, or a function
// that is given an instance of Parse as a parameter. Use this instance of Parse
// to register your cloud code hooks and functions.
// "appId": the application id to host
// "masterKey": the master key for requests to this app
// "facebookAppIds": an array of valid Facebook Application IDs, required
// if using Facebook login
// "collectionPrefix": optional prefix for database collection names
// "fileKey": optional key from Parse dashboard for supporting older files
// hosted by Parse
// "clientKey": optional key from Parse dashboard
// "dotNetKey": optional key from Parse dashboard
// "restAPIKey": optional key from Parse dashboard
// "javascriptKey": optional key from Parse dashboard
// "push": optional key from configure push
function ParseServer({
appId = requiredParameter('You must provide an appId!'),
masterKey = requiredParameter('You must provide a masterKey!'),
appName,
databaseAdapter,
filesAdapter,
push,
loggerAdapter,
databaseURI = DatabaseAdapter.defaultDatabaseURI,
databaseOptions,
cloud,
collectionPrefix = '',
clientKey,
javascriptKey,
dotNetKey,
restAPIKey,
fileKey = 'invalid-file-key',
facebookAppIds = [],
enableAnonymousUsers = true,
allowClientClassCreation = true,
oauth = {},
serverURL = requiredParameter('You must provide a serverURL!'),
maxUploadSize = '20mb',
verifyUserEmails = false,
emailAdapter,
publicServerURL,
customPages = {
invalidLink: undefined,
verifyEmailSuccess: undefined,
choosePassword: undefined,
passwordResetSuccess: undefined
},
liveQuery = {}
}) {
setFeature('serverVersion', parseServerPackage.version);
// Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
Parse.serverURL = serverURL;
if (databaseAdapter) {
DatabaseAdapter.setAdapter(databaseAdapter);
}
if (databaseURI) {
DatabaseAdapter.setAppDatabaseURI(appId, databaseURI);
}
if (databaseOptions) {
DatabaseAdapter.setAppDatabaseOptions(appId, databaseOptions);
}
if (cloud) {
addParseCloud();
if (typeof cloud === 'function') {
cloud(Parse)
} else if (typeof cloud === 'string') {
require(cloud);
} else {
throw "argument 'cloud' must either be a string or a function";
}
}
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
return new GridStoreAdapter(databaseURI);
});
// Pass the push options too as it works with the default
const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push);
const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter);
const emailControllerAdapter = loadAdapter(emailAdapter);
// We pass the options and the base class for the adatper,
// Note that passing an instance would work too
const filesController = new FilesController(filesControllerAdapter, appId);
const pushController = new PushController(pushControllerAdapter, appId);
const loggerController = new LoggerController(loggerControllerAdapter, appId);
const hooksController = new HooksController(appId, collectionPrefix);
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
const liveQueryController = new LiveQueryController(liveQuery);
cache.apps.set(appId, {
masterKey: masterKey,
serverURL: serverURL,
collectionPrefix: collectionPrefix,
clientKey: clientKey,
javascriptKey: javascriptKey,
dotNetKey: dotNetKey,
restAPIKey: restAPIKey,
fileKey: fileKey,
facebookAppIds: facebookAppIds,
filesController: filesController,
pushController: pushController,
loggerController: loggerController,
hooksController: hooksController,
userController: userController,
verifyUserEmails: verifyUserEmails,
allowClientClassCreation: allowClientClassCreation,
authDataManager: authDataManager(oauth, enableAnonymousUsers),
appName: appName,
publicServerURL: publicServerURL,
customPages: customPages,
liveQueryController: liveQueryController
});
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
if (process.env.FACEBOOK_APP_ID) {
cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
}
Config.validate(cache.apps.get(appId));
// This app serves the Parse API directly.
// It's the equivalent of https://api.parse.com/1 in the hosted Parse API.
var api = express();
//api.use("/apps", express.static(__dirname + "/public"));
// File handling needs to be before default middlewares are applied
api.use('/', middlewares.allowCrossDomain, new FilesRouter().getExpressRouter({
maxUploadSize: maxUploadSize
}));
api.use('/', bodyParser.urlencoded({extended: false}), new PublicAPIRouter().expressApp());
// TODO: separate this from the regular ParseServer object
if (process.env.TESTING == 1) {
api.use('/', require('./testing-routes').router);
}
api.use(bodyParser.json({ 'type': '*/*' , limit: maxUploadSize }));
api.use(middlewares.allowCrossDomain);
api.use(middlewares.allowMethodOverride);
api.use(middlewares.handleParseHeaders);
let routers = [
new ClassesRouter(),
new UsersRouter(),
new SessionsRouter(),
new RolesRouter(),
new AnalyticsRouter(),
new InstallationsRouter(),
new FunctionsRouter(),
new SchemasRouter(),
new PushRouter(),
new LogsRouter(),
new IAPValidationRouter(),
new FeaturesRouter(),
];
if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) {
routers.push(new GlobalConfigRouter());
}
if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) {
routers.push(new HooksRouter());
}
let routes = routers.reduce((memo, router) => {
return memo.concat(router.routes);
}, []);
let appRouter = new PromiseRouter(routes);
batch.mountOnto(appRouter);
api.use(appRouter.expressApp());
api.use(middlewares.handleParseErrors);
//This causes tests to spew some useless warnings, so disable in test
if (!process.env.TESTING) {
process.on('uncaughtException', (err) => {
if( err.code === "EADDRINUSE" ) { // user-friendly message for this common error
console.log(`Unable to listen on port ${err.port}. The port is already in use.`);
process.exit(0);
}
else {
throw err;
}
});
}
hooksController.load();
return api;
if (process.env.VERBOSE || process.env.VERBOSE_PARSE_SERVER) {
winston.level = 'silly';
}
function addParseCloud() {
const ParseCloud = require("./cloud-code/Parse.Cloud");
Object.assign(Parse.Cloud, ParseCloud);
global.Parse = Parse;
if (process.env.DEBUG || process.env.DEBUG_PARSE_SERVER) {
winston.level = 'debug';
}
ParseServer.createLiveQueryServer = function(httpServer, config) {
return new ParseLiveQueryServer(httpServer, config);
// Factory function
let _ParseServer = function(options) {
let server = new ParseServer(options);
return server.app;
}
// Mount the create liveQueryServer
_ParseServer.createLiveQueryServer = ParseServer.createLiveQueryServer;
module.exports = {
ParseServer: ParseServer,
S3Adapter: S3Adapter,
GCSAdapter: GCSAdapter
};
export default ParseServer;
export { S3Adapter, GCSAdapter, FileSystemAdapter, _ParseServer as ParseServer };
+86
View File
@@ -0,0 +1,86 @@
import winston from 'winston';
import fs from 'fs';
import path from 'path';
import DailyRotateFile from 'winston-daily-rotate-file';
let LOGS_FOLDER = './logs/';
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
LOGS_FOLDER = './test_logs/'
}
let currentLogsFolder = LOGS_FOLDER;
var currentTransports;
const logger = new winston.Logger();
export function configureLogger({logsFolder}) {
logsFolder = logsFolder || currentLogsFolder;
if (!path.isAbsolute(logsFolder)) {
logsFolder = path.resolve(process.cwd(), logsFolder);
}
if (!fs.existsSync(logsFolder)) {
fs.mkdirSync(logsFolder);
}
currentLogsFolder = logsFolder;
currentTransports = [
new (winston.transports.Console)({
colorize: true,
level: process.env.VERBOSE ? 'verbose': 'info'
}),
new (DailyRotateFile)({
filename: 'parse-server.info',
dirname: currentLogsFolder,
name: 'parse-server',
level: process.env.VERBOSE ? 'verbose': 'info'
}),
new (DailyRotateFile)({
filename: 'parse-server.err',
dirname: currentLogsFolder,
name: 'parse-server-error',
level: 'error'
})
]
logger.configure({
transports: currentTransports
})
}
configureLogger({logsFolder: LOGS_FOLDER});
export function addGroup(groupName) {
let level = process.env.VERBOSE ? 'verbose': 'info';
winston.loggers.add(groupName, {
transports: [
new (winston.transports.Console)({
colorize: true,
level: level
}),
new (DailyRotateFile)({
filename: groupName,
dirname: currentLogsFolder,
name: groupName,
level: level
}),
new (DailyRotateFile)({
filename: 'parse-server.info',
name: 'parse-server',
dirname: currentLogsFolder,
level: level
}),
new (DailyRotateFile)({
filename: 'parse-server.err',
dirname: currentLogsFolder,
name: 'parse-server-error',
level: 'error'
})
]
});
return winston.loggers.get(groupName);
}
export { logger };
export default winston;
+5 -4
View File
@@ -1,4 +1,5 @@
import cache from './cache';
import log from './logger';
var Parse = require('parse/node').Parse;
@@ -97,7 +98,7 @@ function handleParseHeaders(req, res, next) {
// Client keys are not required in parse-server, but if any have been configured in the server, validate them
// to preserve original behavior.
let keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"];
// We do it with mismatching keys to support no-keys config
var keyMismatch = keys.reduce(function(mismatch, key){
@@ -107,7 +108,7 @@ function handleParseHeaders(req, res, next) {
}
return mismatch;
}, 0);
// All keys mismatch
if (keyMismatch == keys.length) {
return invalidRequest(req, res);
@@ -128,7 +129,7 @@ function handleParseHeaders(req, res, next) {
})
.catch((error) => {
// TODO: Determine the correct error scenario.
console.log(error);
log.error('error getting auth for sessionToken', error);
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
});
}
@@ -178,7 +179,7 @@ var handleParseErrors = function(err, req, res, next) {
res.status(err.status);
res.json({error: err.message});
} else {
console.log('Uncaught internal server error.', err, err.stack);
log.error('Uncaught internal server error.', err, err.stack);
res.status(500);
res.json({code: Parse.Error.INTERNAL_SERVER_ERROR,
message: 'Internal server error.'});
+12
View File
@@ -1,5 +1,16 @@
import { md5Hash, newObjectId } from './cryptoUtils';
export function flatten(array) {
return array.reduce((memo, element) => {
if (Array.isArray(element)) {
memo = memo.concat(flatten(element));
} else {
memo = memo.concat(element);
}
return memo;
}, []);
}
export default function pushStatusHandler(config) {
let initialPromise;
@@ -53,6 +64,7 @@ export default function pushStatusHandler(config) {
numFailed: 0,
};
if (Array.isArray(results)) {
results = flatten(results);
results.reduce((memo, result) => {
// Cannot handle that
if (!result.device || !result.device.deviceType) {
+7 -3
View File
@@ -1,3 +1,4 @@
import log from './logger';
var mongodb = require('mongodb');
var Parse = require('parse/node').Parse;
@@ -398,6 +399,9 @@ function transformAtom(atom, force, options) {
if (FileCoder.isValidJSON(atom)) {
return (inArray || inObject ? atom : FileCoder.JSONToDatabase(atom));
}
if (inArray || inObject) {
return atom;
}
if (force) {
throw new Parse.Error(Parse.Error.INVALID_JSON,
@@ -449,7 +453,7 @@ function transformConstraint(constraint, inArray) {
'bad ' + key + ' value');
}
answer[key] = arr.map((v) => {
return transformAtom(v, true);
return transformAtom(v, true, { inArray: inArray });
});
break;
@@ -688,13 +692,13 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals
expected = schema.getExpectedType(className, newKey);
}
if (!expected) {
console.log(
log.info('transform.js',
'Found a pointer column not in the schema, dropping it.',
className, newKey);
break;
}
if (expected && expected[0] != '*') {
console.log('Found a pointer in a non-pointer column, dropping it.', className, key);
log.info('transform.js', 'Found a pointer in a non-pointer column, dropping it.', className, key);
break;
}
if (mongoObject[key] === null) {
+13
View File
@@ -66,6 +66,7 @@ const slashedProtocol = {
};
const querystring = require('querystring');
/* istanbul ignore next: improve coverage */
function urlParse(url, parseQueryString, slashesDenoteHost) {
if (url instanceof Url) return url;
@@ -74,6 +75,7 @@ function urlParse(url, parseQueryString, slashesDenoteHost) {
return u;
}
/* istanbul ignore next: improve coverage */
Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
if (typeof url !== 'string') {
throw new TypeError('Parameter "url" must be a string, not ' + typeof url);
@@ -394,6 +396,7 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
return this;
};
/* istanbul ignore next: improve coverage */
function validateHostname(self, rest, hostname) {
for (var i = 0, lastPos; i <= hostname.length; ++i) {
var code;
@@ -429,6 +432,7 @@ function validateHostname(self, rest, hostname) {
}
}
/* istanbul ignore next: improve coverage */
function autoEscapeStr(rest) {
var newRest = '';
var lastPos = 0;
@@ -531,6 +535,7 @@ function autoEscapeStr(rest) {
}
// format a parsed object into a url string
/* istanbul ignore next: improve coverage */
function urlFormat(obj) {
// ensure it's an object, and not a string url.
// If it's an obj, this is a no-op.
@@ -547,6 +552,7 @@ function urlFormat(obj) {
return obj.format();
}
/* istanbul ignore next: improve coverage */
Url.prototype.format = function() {
var auth = this.auth || '';
if (auth) {
@@ -623,19 +629,23 @@ Url.prototype.format = function() {
return protocol + host + pathname + search + hash;
};
/* istanbul ignore next: improve coverage */
function urlResolve(source, relative) {
return urlParse(source, false, true).resolve(relative);
}
/* istanbul ignore next: improve coverage */
Url.prototype.resolve = function(relative) {
return this.resolveObject(urlParse(relative, false, true)).format();
};
/* istanbul ignore next: improve coverage */
function urlResolveObject(source, relative) {
if (!source) return relative;
return urlParse(source, false, true).resolveObject(relative);
}
/* istanbul ignore next: improve coverage */
Url.prototype.resolveObject = function(relative) {
if (typeof relative === 'string') {
var rel = new Url();
@@ -908,6 +918,7 @@ Url.prototype.resolveObject = function(relative) {
return result;
};
/* istanbul ignore next: improve coverage */
Url.prototype.parseHost = function() {
var host = this.host;
var port = portPattern.exec(host);
@@ -922,6 +933,7 @@ Url.prototype.parseHost = function() {
};
// About 1.5x faster than the two-arg version of Array#splice().
/* istanbul ignore next: improve coverage */
function spliceOne(list, index) {
for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1)
list[i] = list[k];
@@ -931,6 +943,7 @@ function spliceOne(list, index) {
var hexTable = new Array(256);
for (var i = 0; i < 256; ++i)
hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
/* istanbul ignore next: improve coverage */
function encodeAuth(str) {
// faster encodeURIComponent alternative for encoding auth uri components
var out = '';