mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2819df7e5 | ||
|
|
5dc5ef239e | ||
|
|
f25bcc490a | ||
|
|
aa5ebd49f0 | ||
|
|
106ac95622 | ||
|
|
1f4393cd4d | ||
|
|
e361ccf5a5 | ||
|
|
342a3a92fe | ||
|
|
507fa54570 | ||
|
|
1b8e613dfb | ||
|
|
f259a3b17e | ||
|
|
6be067cff5 | ||
|
|
73f8bbc2b3 | ||
|
|
337d3c297a | ||
|
|
cb4b036947 | ||
|
|
ef08dcf76c | ||
|
|
9dd860948a | ||
|
|
6dc7aa87c6 | ||
|
|
592334756b | ||
|
|
247a06f399 | ||
|
|
74b847f1d1 | ||
|
|
75f49b4d1a | ||
|
|
5c1fe3a325 | ||
|
|
cd05e3bd19 | ||
|
|
236c7d15c2 | ||
|
|
6b9fd16273 | ||
|
|
94b10de7b8 | ||
|
|
aeda714b87 | ||
|
|
114f7d1b58 | ||
|
|
d47a756f80 | ||
|
|
6bea950b32 | ||
|
|
1796ae3b7c | ||
|
|
8731337677 | ||
|
|
f2f7c8bad3 | ||
|
|
e3cb473903 | ||
|
|
ebfbe5794c | ||
|
|
7cdf85b367 | ||
|
|
b8b86264f9 | ||
|
|
ec35c39724 | ||
|
|
19e6293638 | ||
|
|
7738d33399 | ||
|
|
4ff52de01c | ||
|
|
27a3198d81 | ||
|
|
b4ee31322e | ||
|
|
3e45e04cf1 | ||
|
|
958d77207a | ||
|
|
259de3f104 | ||
|
|
152c7e88b8 | ||
|
|
f28b719b83 | ||
|
|
2f00a02936 | ||
|
|
f4ad4c1af5 | ||
|
|
83f5207d44 | ||
|
|
82ebba4db8 | ||
|
|
3d3ed53c5f | ||
|
|
1e45ac753a | ||
|
|
603bf97c0b | ||
|
|
120c6feb61 | ||
|
|
b3c5e836d5 | ||
|
|
53e152e975 | ||
|
|
ee8f85baef | ||
|
|
7a73d74869 | ||
|
|
047c8e71ff | ||
|
|
cbd1b878b9 | ||
|
|
24146753b9 | ||
|
|
ea211e658f | ||
|
|
16b6ec4879 | ||
|
|
ddb76a8650 | ||
|
|
f5fdedfbc7 | ||
|
|
3ce634bd82 | ||
|
|
b30805b47b | ||
|
|
0e662190b3 | ||
|
|
7d6f359434 | ||
|
|
0a4cc606c6 | ||
|
|
8124abab34 | ||
|
|
b7a723fa7e | ||
|
|
4e39975239 | ||
|
|
e506784741 | ||
|
|
5d19c4ab45 | ||
|
|
cc6c3f46fe | ||
|
|
ffbca83aa3 | ||
|
|
4bd32c8a4b | ||
|
|
260cc21b67 | ||
|
|
fb57d91b83 | ||
|
|
414af99644 | ||
|
|
134b6de2b1 | ||
|
|
a31baa4ebd | ||
|
|
3603b82eac | ||
|
|
937dde592f | ||
|
|
24f0fffb41 | ||
|
|
e4e7e42c65 | ||
|
|
44add9d6ac | ||
|
|
c39ba3bdc7 | ||
|
|
13a33a9497 | ||
|
|
46c66667c1 |
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,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', () => {
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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
@@ -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();
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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]);
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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
|
||||
@@ -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) { }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
@@ -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']);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
} };
|
||||
});
|
||||
|
||||
@@ -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 } }));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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.'});
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Vendored
+13
@@ -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 = '';
|
||||
|
||||
Reference in New Issue
Block a user