mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0faaec3224 | ||
|
|
021fe4d336 | ||
|
|
5dc24334a5 | ||
|
|
86f35014f4 | ||
|
|
758975cf03 | ||
|
|
a6a6f7ff60 | ||
|
|
305b037176 | ||
|
|
b88b0c578f | ||
|
|
d8ba9e8b7d | ||
|
|
60d506615d | ||
|
|
de36d9640b | ||
|
|
7af320932a | ||
|
|
c253daa9c9 | ||
|
|
09f871b786 | ||
|
|
c0c516271c | ||
|
|
2cea0030f3 | ||
|
|
6979bb430b | ||
|
|
d8f0f9e158 | ||
|
|
27b135b1c2 | ||
|
|
954a8a4159 | ||
|
|
a60935dbfb | ||
|
|
509f90b937 | ||
|
|
300150e50f | ||
|
|
b5af4d017c | ||
|
|
9260e8b05f | ||
|
|
2ec2bb5660 | ||
|
|
3ba6e613b3 | ||
|
|
4dcefd6386 | ||
|
|
9c522be00d | ||
|
|
4974dbea37 | ||
|
|
d4593931db | ||
|
|
2183b84565 | ||
|
|
a41cbcbc7f | ||
|
|
ad707457be | ||
|
|
5feceaa6d6 | ||
|
|
e30989c7d3 | ||
|
|
7e037ffa72 | ||
|
|
0ec1e8ca7f |
+1
-1
@@ -1,6 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '4.3'
|
||||
- '4.5'
|
||||
- '6.1'
|
||||
services:
|
||||
- postgresql
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
## Parse Server Changelog
|
||||
|
||||
### 2.2.23
|
||||
|
||||
* Run liveQuery server from CLI with a different port, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Support for Postgres databaseURI, thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
|
||||
* Support for Postgres options, thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
|
||||
* Improved support for google login (id_token and access_token), thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Improvements with VKontakte login, thanks to [Eugene Antropov](https://github.com/antigp)
|
||||
* Improved support for `select` and `include`, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Fix error when updating installation with useMasterKey (#2888), thanks to [Jeremy Louie](https://github.com/JeremyPlease)
|
||||
* Fix bug affecting usage of multiple `notEqualTo`, thanks to [Jeremy Louie](https://github.com/JeremyPlease)
|
||||
* Improved support for null values in arrays, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
|
||||
### 2.2.22
|
||||
|
||||
* Minimum nodejs engine is now 4.5
|
||||
|
||||
#### New Features
|
||||
* New: CLI for parse-live-query-server, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* New: Start parse-live-query-server for parse-server CLI, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
|
||||
#### Bug fixes
|
||||
* Fix: Include with pointers are not conflicting with get CLP anymore, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Fix: Removes dependency on babel-polyfill, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Fix: Support nested select calls, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Fix: Use native column selection instead of runtime, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Fix: installationId header is properly used when updating `_Installation` objects, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Fix: don't crash parse-server on improperly formatted live-query messages, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* Fix: Passwords are properly stripped out of logs, thanks to [Arthur Cinader](https://github.com/acinader)
|
||||
* Fix: Lookup for email in username if email is not set, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
|
||||
### 2.2.21
|
||||
|
||||
* Fix: Reverts removal of babel-polyfill
|
||||
|
||||
### 2.2.20
|
||||
|
||||
* New: Adds CloudCode handler for `beforeFind`, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
|
||||
@@ -110,10 +110,11 @@ We have provided a basic [Node.js application](https://github.com/ParsePlatform/
|
||||
|
||||
* [Heroku and mLab](https://devcenter.heroku.com/articles/deploying-a-parse-server-to-heroku)
|
||||
* [AWS and Elastic Beanstalk](http://mobile.awsblog.com/post/TxCD57GZLM2JR/How-to-set-up-Parse-Server-on-AWS-using-AWS-Elastic-Beanstalk)
|
||||
* [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/)
|
||||
* [SashiDo](https://blog.sashido.io/tag/migration/)
|
||||
* [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)
|
||||
* [Pivotal Web Services](https://github.com/cf-platform-eng/pws-parse-server)
|
||||
* [Back4app](http://blog.back4app.com/2016/03/01/quick-wizard-migration/)
|
||||
* [HyperDev](https://hyperdev.com/blog/use-parse-server-apps-backend-hyperdev/)
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require("../lib/cli/parse-live-query-server");
|
||||
+60
-14
@@ -4,7 +4,7 @@ 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'
|
||||
DEFAULT_MONGODB_URI='mongodb://127.0.0.1:27017/parse'
|
||||
|
||||
confirm() {
|
||||
DEFAULT=$1;
|
||||
@@ -53,15 +53,60 @@ check_npm() {
|
||||
|
||||
|
||||
echo ''
|
||||
echo 'This will setup parse-server in the current directory'
|
||||
echo '
|
||||
`.-://////:-..`
|
||||
`:/oooooooooooooooo+:.`
|
||||
`:+oooooooooooooooooooooo+/`
|
||||
:+ooooooooooooooooooooooooooo/.
|
||||
.+oooooooooooooo/:.....-:+ooooooo-
|
||||
.+ooooooooooooo/` .:///:-` -+oooooo:
|
||||
`+ooooooooooooo: `/ooooooo+- `ooooooo-
|
||||
:oooooooooooooo :ooooooooo+` /oooooo+
|
||||
+ooooooooooooo/ +ooooooooo+ /ooooooo.
|
||||
oooooooooooooo+ ooooooooo` .oooooooo.
|
||||
+ooooooooooo+/: `ooooooo` .:ooooooooo.
|
||||
:ooooooo+.````````````` /+oooooooooo+
|
||||
`+oooooo- `ooo+ /oooooooooooooooooooo-
|
||||
.+ooooo/ :/:` -ooooooooooooooooooo:
|
||||
.+ooooo+:-..-/ooooooooooooooooooo-
|
||||
:+ooooooooooooooooooooooooooo/.
|
||||
`:+oooooooooooooooooooooo+/`
|
||||
`:/oooooooooooooooo+:.`
|
||||
`.-://////:-..`
|
||||
|
||||
parse-server
|
||||
|
||||
'
|
||||
|
||||
|
||||
INSTALL_DIR=""
|
||||
printf "Enter an installation directory\n"
|
||||
printf "(%s): " "${PWD}"
|
||||
read -r INSTALL_DIR
|
||||
|
||||
if [ "$INSTALL_DIR" = "" ]; then
|
||||
INSTALL_DIR="${PWD}"
|
||||
fi
|
||||
|
||||
echo ''
|
||||
printf "This will setup parse-server in %s\n" "${INSTALL_DIR}"
|
||||
confirm 'Y' 'Do you want to continue? (Y/n): '
|
||||
|
||||
check_node
|
||||
check_npm
|
||||
|
||||
echo "Setting up parse-server in $PWD"
|
||||
printf "Setting up parse-server in %s" "${INSTALL_DIR}\n"
|
||||
|
||||
if [ -f './package.json' ]; then
|
||||
if [ -d "${INSTALL_DIR}" ]; then
|
||||
echo "{CHECK} ${INSTALL_DIR} exists"
|
||||
else
|
||||
mkdir -p "${INSTALL_DIR}"
|
||||
echo "${CHECK} Created ${INSTALL_DIR}"
|
||||
fi
|
||||
|
||||
cd "${INSTALL_DIR}"
|
||||
|
||||
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
|
||||
@@ -77,33 +122,33 @@ i=0
|
||||
while [ "$APP_NAME" = "" ]
|
||||
do
|
||||
[[ $i != 0 ]] && printf "${RED}An application name is required!${NC}\n"
|
||||
printf 'Enter your Application Name: '
|
||||
printf "Enter your ${BOLD}Application Name${NC}: "
|
||||
read -r APP_NAME
|
||||
i=$(($i+1))
|
||||
done
|
||||
|
||||
printf 'Enter your appId (leave empty to generate): '
|
||||
printf "Enter your ${BOLD}Application Id${NC} (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): '
|
||||
printf "Enter your ${BOLD}Master Key${NC} (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
|
||||
printf "Enter your ${BOLD}mongodbURI${NC} (%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",
|
||||
"appId": "${APP_ID}",
|
||||
"masterKey": "${MASTER_KEY}",
|
||||
"appName": "${APP_NAME}",
|
||||
"cloud": "./cloud/main",
|
||||
"databaseURI": "$MONGODB_URI"
|
||||
"databaseURI": "${MONGODB_URI}"
|
||||
}
|
||||
EOF
|
||||
echo "${CHECK} Created config.json"
|
||||
@@ -113,8 +158,9 @@ NPM_APP_NAME=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
|
||||
cat > ./package.json << EOF
|
||||
{
|
||||
"name": "$NPM_APP_NAME",
|
||||
"description": "parse-server for $APP_NAME",
|
||||
"scripts": {
|
||||
"start": "parse-server ./config.json"
|
||||
"start": "parse-server config.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"parse-server": "^2.0.0"
|
||||
@@ -149,7 +195,7 @@ fi
|
||||
|
||||
echo "\n${CHECK} running npm install\n"
|
||||
|
||||
npm install
|
||||
npm install -s
|
||||
|
||||
CURL_CMD=$(cat << EOF
|
||||
curl -X POST -H 'X-Parse-Application-Id: ${APP_ID}' \\
|
||||
|
||||
+9
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "2.2.20",
|
||||
"version": "2.2.23",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
@@ -24,13 +24,12 @@
|
||||
"deepcopy": "0.6.3",
|
||||
"express": "4.14.0",
|
||||
"intersect": "1.0.1",
|
||||
"lodash": "4.15.0",
|
||||
"lodash": "4.16.4",
|
||||
"lru-cache": "4.0.1",
|
||||
"mime": "1.3.4",
|
||||
"mongodb": "2.2.10",
|
||||
"multer": "1.2.0",
|
||||
"node-uuid": "^1.4.7",
|
||||
"parse": "1.9.1",
|
||||
"parse": "1.9.2",
|
||||
"parse-server-fs-adapter": "1.0.1",
|
||||
"parse-server-push-adapter": "1.1.0",
|
||||
"parse-server-s3-adapter": "1.0.5",
|
||||
@@ -53,27 +52,27 @@
|
||||
"babel-preset-stage-0": "6.5.0",
|
||||
"babel-register": "6.14.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"cross-env": "2.0.1",
|
||||
"cross-env": "3.1.3",
|
||||
"deep-diff": "0.3.4",
|
||||
"gaze": "1.1.1",
|
||||
"istanbul": "1.0.0-alpha.1",
|
||||
"jasmine": "2.5.2",
|
||||
"mongodb-runner": "3.4.0",
|
||||
"nodemon": "1.10.0",
|
||||
"nodemon": "1.11.0",
|
||||
"request-promise": "4.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm run build && node bin/dev",
|
||||
"build": "babel src/ -d lib/",
|
||||
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js",
|
||||
"test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 babel-node ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
|
||||
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js",
|
||||
"test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 node ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
|
||||
"coverage": "cross-env COVERAGE_OPTION='./node_modules/.bin/istanbul cover' npm test",
|
||||
"coverage:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 babel-node ./node_modules/babel-istanbul/lib/cli.js cover ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
|
||||
"coverage:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 node ./node_modules/babel-istanbul/lib/cli.js cover ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
|
||||
"start": "node ./bin/parse-server",
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.3"
|
||||
"node": ">=4.5"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "./bin/parse-server"
|
||||
|
||||
@@ -712,6 +712,32 @@ describe('Cloud Code', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('beforeSave change propagates through the afterSave #1931', (done) => {
|
||||
Parse.Cloud.beforeSave('ChangingObject', function(request, response) {
|
||||
request.object.unset('file');
|
||||
request.object.unset('date');
|
||||
response.success();
|
||||
});
|
||||
|
||||
Parse.Cloud.afterSave('ChangingObject', function(request, response) {
|
||||
let json = request.object.toJSON();
|
||||
expect(request.object.has("file")).toBe(false);
|
||||
expect(request.object.has("date")).toBe(false);
|
||||
expect(request.object.get('file')).toBeUndefined();
|
||||
return Promise.resolve();
|
||||
});
|
||||
let file = new Parse.File("yolo.txt", [1,2,3], "text/plain");
|
||||
file.save().then(() => {
|
||||
let obj = new Parse.Object('ChangingObject');
|
||||
return obj.save({ file, date: new Date() })
|
||||
}).then(() => {
|
||||
done();
|
||||
}, () => {
|
||||
fail();
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('test cloud function parameter validation success', (done) => {
|
||||
// Register a function with validation
|
||||
|
||||
@@ -55,6 +55,24 @@ describe("Cloud Code Logger", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('trigger should obfuscate password', done => {
|
||||
const logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
Parse.Cloud.beforeSave(Parse.User, (req, res) => {
|
||||
res.success(req.object);
|
||||
});
|
||||
|
||||
Parse.User.signUp('tester123', 'abc')
|
||||
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
|
||||
.then((res) => {
|
||||
const entry = res[0];
|
||||
expect(entry.message).not.toMatch(/password":"abc/);
|
||||
expect(entry.message).toMatch(/\*\*\*\*\*\*\*\*/);
|
||||
done();
|
||||
})
|
||||
.then(null, e => done.fail(e));
|
||||
});
|
||||
|
||||
it("should expose log to trigger", (done) => {
|
||||
var logController = new LoggerController(new WinstonLoggerAdapter());
|
||||
|
||||
|
||||
@@ -2,9 +2,20 @@ module.exports = options => {
|
||||
if (!options) {
|
||||
throw "Options were not provided"
|
||||
}
|
||||
return {
|
||||
let adapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => Promise.resolve()
|
||||
};
|
||||
if (options.sendMail) {
|
||||
adapter.sendMail = options.sendMail
|
||||
}
|
||||
if (options.sendPasswordResetEmail) {
|
||||
adapter.sendPasswordResetEmail = options.sendPasswordResetEmail
|
||||
}
|
||||
if (options.sendVerificationEmail) {
|
||||
adapter.sendVerificationEmail = options.sendVerificationEmail;
|
||||
}
|
||||
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@@ -571,4 +571,28 @@ describe('Parse.File testing', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('return with publicServerURL when provided', done => {
|
||||
reconfigureServer({
|
||||
publicServerURL: 'https://mydomain/parse'
|
||||
}).then(() => {
|
||||
var file = {
|
||||
__type: 'File',
|
||||
name: '123.txt'
|
||||
};
|
||||
var obj = new Parse.Object('FileTest');
|
||||
obj.set('file', file);
|
||||
return obj.save()
|
||||
}).then(() => {
|
||||
var query = new Parse.Query('FileTest');
|
||||
return query.first();
|
||||
}).then(result => {
|
||||
let fileAgain = result.get('file');
|
||||
expect(fileAgain.url().indexOf('https://mydomain/parse')).toBe(0);
|
||||
done();
|
||||
}).catch((e) => {
|
||||
jfail(e);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('Installations', () => {
|
||||
expect(obj.installationId).toEqual(installId);
|
||||
expect(obj.deviceType).toEqual(device);
|
||||
done();
|
||||
}).catch((error) => { console.log(error); });
|
||||
}).catch((error) => { console.log(error); jfail(error); done(); });
|
||||
});
|
||||
|
||||
it('creates an ios installation with ids', (done) => {
|
||||
@@ -55,7 +55,7 @@ describe('Installations', () => {
|
||||
expect(obj.deviceToken).toEqual(t);
|
||||
expect(obj.deviceType).toEqual(device);
|
||||
done();
|
||||
}).catch((error) => { console.log(error); });
|
||||
}).catch((error) => { console.log(error); jfail(error); done(); });
|
||||
});
|
||||
|
||||
it('creates an embedded installation with ids', (done) => {
|
||||
@@ -73,7 +73,7 @@ describe('Installations', () => {
|
||||
expect(obj.installationId).toEqual(installId);
|
||||
expect(obj.deviceType).toEqual(device);
|
||||
done();
|
||||
}).catch((error) => { console.log(error); });
|
||||
}).catch((error) => { console.log(error); jfail(error); done(); });
|
||||
});
|
||||
|
||||
it('creates an android installation with all fields', (done) => {
|
||||
@@ -96,7 +96,7 @@ describe('Installations', () => {
|
||||
expect(obj.channels[0]).toEqual('foo');
|
||||
expect(obj.channels[1]).toEqual('bar');
|
||||
done();
|
||||
}).catch((error) => { console.log(error); });
|
||||
}).catch((error) => { console.log(error); jfail(error); done(); });
|
||||
});
|
||||
|
||||
it('creates an ios installation with all fields', (done) => {
|
||||
@@ -119,7 +119,7 @@ describe('Installations', () => {
|
||||
expect(obj.channels[0]).toEqual('foo');
|
||||
expect(obj.channels[1]).toEqual('bar');
|
||||
done();
|
||||
}).catch((error) => { console.log(error); });
|
||||
}).catch((error) => { console.log(error); jfail(error); done(); });
|
||||
});
|
||||
|
||||
it('should properly fail queying installations', (done) => {
|
||||
@@ -872,6 +872,112 @@ describe('Installations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows you to update installation from header (#2090)', done => {
|
||||
let installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
let device = 'android';
|
||||
let input = {
|
||||
'installationId': installId,
|
||||
'deviceType': device
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(createResult => {
|
||||
let headers = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Installation-Id': installId
|
||||
};
|
||||
request.post({
|
||||
headers: headers,
|
||||
url: 'http://localhost:8378/1/classes/_Installation',
|
||||
json: true,
|
||||
body: {
|
||||
date: new Date()
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(body.updatedAt).not.toBeUndefined();
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
fail('failed');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allows you to update installation with masterKey', done => {
|
||||
let installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
let device = 'android';
|
||||
let input = {
|
||||
'installationId': installId,
|
||||
'deviceType': device
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||
.then(createResult => {
|
||||
let installationObj = Parse.Installation.createWithoutData(createResult.response.objectId);
|
||||
installationObj.set('customField', 'custom value');
|
||||
return installationObj.save(null, {useMasterKey: true});
|
||||
}).then(updateResult => {
|
||||
expect(updateResult).not.toBeUndefined();
|
||||
expect(updateResult.get('customField')).toEqual('custom value');
|
||||
done();
|
||||
}).catch(error => {
|
||||
console.log(error);
|
||||
fail('failed');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly handle installation save #2780', done => {
|
||||
let installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
let device = 'android';
|
||||
let input = {
|
||||
'installationId': installId,
|
||||
'deviceType': device
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input).then(() => {
|
||||
let query = new Parse.Query(Parse.Installation);
|
||||
query.equalTo('installationId', installId);
|
||||
query.first({useMasterKey: true}).then((installation) => {
|
||||
return installation.save({
|
||||
key: 'value'
|
||||
}, {useMasterKey: true});
|
||||
}).then(() => {
|
||||
done();
|
||||
}, (err) => {
|
||||
jfail(err)
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly reject updating installationId', done => {
|
||||
let installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||
let device = 'android';
|
||||
let input = {
|
||||
'installationId': installId,
|
||||
'deviceType': device
|
||||
};
|
||||
rest.create(config, auth.nobody(config), '_Installation', input).then(() => {
|
||||
let query = new Parse.Query(Parse.Installation);
|
||||
query.equalTo('installationId', installId);
|
||||
query.first({useMasterKey: true}).then((installation) => {
|
||||
return installation.save({
|
||||
key: 'value',
|
||||
installationId: '22222222-abcd-abcd-abcd-123456789abc'
|
||||
}, {useMasterKey: true});
|
||||
}).then(() => {
|
||||
fail('should not succeed');
|
||||
done();
|
||||
}, (err) => {
|
||||
expect(err.code).toBe(136);
|
||||
expect(err.message).toBe('installationId may not be changed in this operation');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Look at additional tests from installation_collection_test.go:882
|
||||
// TODO: Do we need to support _tombstone disabling of installations?
|
||||
// TODO: Test deletion, badge increments
|
||||
|
||||
@@ -1922,4 +1922,67 @@ describe('Parse.Object testing', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should handle includes on null arrays #2752', (done) => {
|
||||
let obj1 = new Parse.Object("AnObject");
|
||||
let obj2 = new Parse.Object("AnotherObject");
|
||||
let obj3 = new Parse.Object("NestedObject");
|
||||
obj3.set({
|
||||
"foo": "bar"
|
||||
})
|
||||
obj2.set({
|
||||
"key": obj3
|
||||
})
|
||||
|
||||
Parse.Object.saveAll([obj1, obj2]).then(() => {
|
||||
obj1.set("objects", [null, null, obj2]);
|
||||
return obj1.save();
|
||||
}).then(() => {
|
||||
let query = new Parse.Query("AnObject");
|
||||
query.include("objects.key");
|
||||
return query.find();
|
||||
}).then((res) => {
|
||||
let obj = res[0];
|
||||
expect(obj.get("objects")).not.toBe(undefined);
|
||||
let array = obj.get("objects");
|
||||
expect(Array.isArray(array)).toBe(true);
|
||||
expect(array[0]).toBe(null);
|
||||
expect(array[1]).toBe(null);
|
||||
expect(array[2].get("key").get("foo")).toEqual("bar");
|
||||
done();
|
||||
}).catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('should handle select and include #2786', (done) => {
|
||||
let score = new Parse.Object("GameScore");
|
||||
let player = new Parse.Object("Player");
|
||||
score.set({
|
||||
"score": 1234
|
||||
});
|
||||
|
||||
score.save().then(() => {
|
||||
player.set("gameScore", score);
|
||||
player.set("other", "value");
|
||||
return player.save();
|
||||
}).then(() => {
|
||||
let query = new Parse.Query("Player");
|
||||
query.include("gameScore");
|
||||
query.select("gameScore");
|
||||
return query.find();
|
||||
}).then((res) => {
|
||||
let obj = res[0];
|
||||
let gameScore = obj.get("gameScore");
|
||||
let other = obj.get("other");
|
||||
expect(other).toBeUndefined();
|
||||
expect(gameScore).not.toBeUndefined();
|
||||
expect(gameScore.get("score")).toBe(1234);
|
||||
done();
|
||||
}).catch(err => {
|
||||
jfail(err);
|
||||
done();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,6 +127,22 @@ describe('Parse.Query testing', () => {
|
||||
return query.find().then(function(results){
|
||||
equal(results.length, 0);
|
||||
});
|
||||
}).then(function(){
|
||||
var query = new Parse.Query(Cake);
|
||||
query.notEqualTo("hater", user2);
|
||||
query.notEqualTo("liker", user2);
|
||||
// user2 doesn't like any cake so this should be 0
|
||||
return query.find().then(function(results){
|
||||
equal(results.length, 0);
|
||||
});
|
||||
}).then(function(){
|
||||
var query = new Parse.Query(Cake);
|
||||
query.equalTo("hater", user);
|
||||
query.equalTo("liker", user);
|
||||
// user doesn't hate any cake so this should be 0
|
||||
return query.find().then(function(results){
|
||||
equal(results.length, 0);
|
||||
});
|
||||
}).then(function(){
|
||||
done();
|
||||
}).catch((err) => {
|
||||
@@ -2567,6 +2583,76 @@ describe('Parse.Query testing', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('select nested keys (issue #1567)', function(done) {
|
||||
var Foobar = new Parse.Object('Foobar');
|
||||
var BarBaz = new Parse.Object('Barbaz');
|
||||
BarBaz.set('key', 'value');
|
||||
BarBaz.set('otherKey', 'value');
|
||||
BarBaz.save().then(() => {
|
||||
Foobar.set('foo', 'bar');
|
||||
Foobar.set('fizz', 'buzz');
|
||||
Foobar.set('barBaz', BarBaz);
|
||||
return Foobar.save();
|
||||
}).then(function(savedFoobar){
|
||||
var foobarQuery = new Parse.Query('Foobar');
|
||||
foobarQuery.include('barBaz');
|
||||
foobarQuery.select(['fizz', 'barBaz.key']);
|
||||
foobarQuery.get(savedFoobar.id,{
|
||||
success: function(foobarObj){
|
||||
equal(foobarObj.get('fizz'), 'buzz');
|
||||
equal(foobarObj.get('foo'), undefined);
|
||||
if (foobarObj.has('barBaz')) {
|
||||
equal(foobarObj.get('barBaz').get('key'), 'value');
|
||||
equal(foobarObj.get('barBaz').get('otherKey'), undefined);
|
||||
} else {
|
||||
fail('barBaz should be set');
|
||||
}
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('select nested keys 2 level (issue #1567)', function(done) {
|
||||
var Foobar = new Parse.Object('Foobar');
|
||||
var BarBaz = new Parse.Object('Barbaz');
|
||||
var Bazoo = new Parse.Object('Bazoo');
|
||||
|
||||
Bazoo.set('some', 'thing');
|
||||
Bazoo.set('otherSome', 'value');
|
||||
Bazoo.save().then(() => {
|
||||
BarBaz.set('key', 'value');
|
||||
BarBaz.set('otherKey', 'value');
|
||||
BarBaz.set('bazoo', Bazoo);
|
||||
return BarBaz.save();
|
||||
}).then(() => {
|
||||
Foobar.set('foo', 'bar');
|
||||
Foobar.set('fizz', 'buzz');
|
||||
Foobar.set('barBaz', BarBaz);
|
||||
return Foobar.save();
|
||||
}).then(function(savedFoobar){
|
||||
var foobarQuery = new Parse.Query('Foobar');
|
||||
foobarQuery.include('barBaz');
|
||||
foobarQuery.include('barBaz.bazoo');
|
||||
foobarQuery.select(['fizz', 'barBaz.key', 'barBaz.bazoo.some']);
|
||||
foobarQuery.get(savedFoobar.id,{
|
||||
success: function(foobarObj){
|
||||
equal(foobarObj.get('fizz'), 'buzz');
|
||||
equal(foobarObj.get('foo'), undefined);
|
||||
if (foobarObj.has('barBaz')) {
|
||||
equal(foobarObj.get('barBaz').get('key'), 'value');
|
||||
equal(foobarObj.get('barBaz').get('otherKey'), undefined);
|
||||
equal(foobarObj.get('barBaz').get('bazoo').get('some'), 'thing');
|
||||
equal(foobarObj.get('barBaz').get('bazoo').get('otherSome'), undefined);
|
||||
} else {
|
||||
fail('barBaz should be set');
|
||||
}
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('properly handles nested ors', function(done) {
|
||||
var objects = [];
|
||||
while(objects.length != 4) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
const parser = require('../src/Adapters/Storage/Postgres/PostgresConfigParser');
|
||||
|
||||
const queryParamTests = {
|
||||
'a=1&b=2': { a: '1', b: '2' },
|
||||
'a=abcd%20efgh&b=abcd%3Defgh': { a: 'abcd efgh', b: 'abcd=efgh' },
|
||||
'a=1&b&c=true': { a: '1', b: '', c: 'true' }
|
||||
}
|
||||
|
||||
describe('PostgresConfigParser.parseQueryParams', () => {
|
||||
it('creates a map from a query string', () => {
|
||||
|
||||
for (const key in queryParamTests) {
|
||||
const result = parser.parseQueryParams(key);
|
||||
|
||||
const testObj = queryParamTests[key];
|
||||
|
||||
expect(Object.keys(result).length)
|
||||
.toEqual(Object.keys(testObj).length);
|
||||
|
||||
for (const k in result) {
|
||||
expect(result[k]).toEqual(testObj[k]);
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
const baseURI = 'postgres://username:password@localhost:5432/db-name'
|
||||
|
||||
const dbOptionsTest = {};
|
||||
dbOptionsTest[`${baseURI}?ssl=true&binary=true&application_name=app_name&fallback_application_name=f_app_name&poolSize=10`] = {
|
||||
ssl: true,
|
||||
binary: true,
|
||||
application_name: 'app_name',
|
||||
fallback_application_name: 'f_app_name',
|
||||
poolSize: 10
|
||||
};
|
||||
dbOptionsTest[`${baseURI}?ssl=&binary=aa`] = {
|
||||
ssl: false,
|
||||
binary: false
|
||||
}
|
||||
|
||||
describe('PostgresConfigParser.getDatabaseOptionsFromURI', () => {
|
||||
it('creates a db options map from a query string', () => {
|
||||
|
||||
for (const key in dbOptionsTest) {
|
||||
const result = parser.getDatabaseOptionsFromURI(key);
|
||||
|
||||
const testObj = dbOptionsTest[key];
|
||||
|
||||
for (const k in testObj) {
|
||||
expect(result[k]).toEqual(testObj[k]);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
it('sets the poolSize to 10 if the it is not a number', () => {
|
||||
|
||||
const result = parser.getDatabaseOptionsFromURI(`${baseURI}?poolSize=sdf`);
|
||||
|
||||
expect(result.poolSize).toEqual(10);
|
||||
|
||||
});
|
||||
});
|
||||
@@ -479,6 +479,45 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds sending a password reset username if appName, publicServerURL, and email adapter are prodvided', done => {
|
||||
let adapter = MockEmailAdapterWithOptions({
|
||||
fromAddress: 'parse@example.com',
|
||||
apiKey: 'k',
|
||||
domain: 'd',
|
||||
sendMail: function(options) {
|
||||
expect(options.to).toEqual('testValidConfig@parse.com');
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
// delete that handler to force using the default
|
||||
delete adapter.sendPasswordResetEmail;
|
||||
|
||||
spyOn(adapter, 'sendMail').and.callThrough();
|
||||
reconfigureServer({
|
||||
appName: 'coolapp',
|
||||
publicServerURL: 'http://localhost:1337/1',
|
||||
emailAdapter: adapter
|
||||
})
|
||||
.then(() => {
|
||||
let user = new Parse.User();
|
||||
user.setPassword("asdf");
|
||||
user.setUsername("testValidConfig@parse.com");
|
||||
user.signUp(null)
|
||||
.then(user => Parse.User.requestPasswordReset("testValidConfig@parse.com"))
|
||||
.then(result => {
|
||||
expect(adapter.sendMail).toHaveBeenCalled();
|
||||
done();
|
||||
}, error => {
|
||||
done(error);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
fail(JSON.stringify(error));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send verification email if email verification is disabled', done => {
|
||||
var emailAdapter = {
|
||||
sendVerificationEmail: () => Promise.resolve(),
|
||||
|
||||
@@ -97,20 +97,4 @@ describe('verbose logs', () => {
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it("should not mask information in non _User class", (done) => {
|
||||
let obj = new Parse.Object('users');
|
||||
obj.set('password', 'pw');
|
||||
obj.save().then(() => {
|
||||
let winstonLoggerAdapter = new WinstonLoggerAdapter();
|
||||
return winstonLoggerAdapter.query({
|
||||
from: new Date(Date.now() - 500),
|
||||
size: 100,
|
||||
level: 'verbose'
|
||||
});
|
||||
}).then((results) => {
|
||||
expect(results[1].body.password).toEqual("pw");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+8
-3
@@ -14,6 +14,11 @@ global.on_db = (db, callback, elseCallback) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (global._babelPolyfill) {
|
||||
console.error('We should not use polyfilled tests');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var cache = require('../src/cache').default;
|
||||
var express = require('express');
|
||||
var facebook = require('../src/authDataManager/facebook');
|
||||
@@ -214,7 +219,7 @@ afterEach(function(done) {
|
||||
} else {
|
||||
// Other system classes will break Parse.com, so make sure that we don't save anything to _SCHEMA that will
|
||||
// break it.
|
||||
return ['_User', '_Installation', '_Role', '_Session', '_Product'].includes(className);
|
||||
return ['_User', '_Installation', '_Role', '_Session', '_Product'].indexOf(className) >= 0;
|
||||
}
|
||||
}});
|
||||
});
|
||||
@@ -387,7 +392,7 @@ global.jfail = function(err) {
|
||||
}
|
||||
|
||||
global.it_exclude_dbs = excluded => {
|
||||
if (excluded.includes(process.env.PARSE_SERVER_TEST_DB)) {
|
||||
if (excluded.indexOf(process.env.PARSE_SERVER_TEST_DB) >= 0) {
|
||||
return xit;
|
||||
} else {
|
||||
return it;
|
||||
@@ -395,7 +400,7 @@ global.it_exclude_dbs = excluded => {
|
||||
}
|
||||
|
||||
global.fit_exclude_dbs = excluded => {
|
||||
if (excluded.includes(process.env.PARSE_SERVER_TEST_DB)) {
|
||||
if (excluded.indexOf(process.env.PARSE_SERVER_TEST_DB) >= 0) {
|
||||
return xit;
|
||||
} else {
|
||||
return fit;
|
||||
|
||||
@@ -1548,6 +1548,34 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can query with include and CLP (issue #2005)', (done) => {
|
||||
setPermissionsOnClass('AnotherObject', {
|
||||
get: {"*": true},
|
||||
find: {},
|
||||
create: {'*': true},
|
||||
update: {'*': true},
|
||||
delete: {'*': true},
|
||||
addField:{'*': true}
|
||||
}).then(() => {
|
||||
let obj = new Parse.Object('AnObject');
|
||||
let anotherObject = new Parse.Object('AnotherObject');
|
||||
return obj.save({
|
||||
anotherObject
|
||||
})
|
||||
}).then(() => {
|
||||
let query = new Parse.Query('AnObject');
|
||||
query.include('anotherObject');
|
||||
return query.find();
|
||||
}).then((res) => {
|
||||
expect(res.length).toBe(1);
|
||||
expect(res[0].get('anotherObject')).not.toBeUndefined();
|
||||
done();
|
||||
}).catch((err) => {
|
||||
jfail(err);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
it('can add field as master (issue #1257)', (done) => {
|
||||
setPermissionsOnClass('AClass', {
|
||||
'addField': {}
|
||||
|
||||
@@ -95,5 +95,5 @@ export function removeTransport(transport) {
|
||||
});
|
||||
}
|
||||
|
||||
export { logger, addTransport, configureLogger, removeTransport };
|
||||
export { logger };
|
||||
export default logger;
|
||||
|
||||
@@ -13,8 +13,8 @@ export default class MongoCollection {
|
||||
// none, then build the geoindex.
|
||||
// This could be improved a lot but it's not clear if that's a good
|
||||
// idea. Or even if this behavior is a good idea.
|
||||
find(query, { skip, limit, sort } = {}) {
|
||||
return this._rawFind(query, { skip, limit, sort })
|
||||
find(query, { skip, limit, sort, keys } = {}) {
|
||||
return this._rawFind(query, { skip, limit, sort, keys })
|
||||
.catch(error => {
|
||||
// Check for "no geoindex" error
|
||||
if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) {
|
||||
@@ -30,14 +30,18 @@ export default class MongoCollection {
|
||||
index[key] = '2d';
|
||||
return this._mongoCollection.createIndex(index)
|
||||
// Retry, but just once.
|
||||
.then(() => this._rawFind(query, { skip, limit, sort }));
|
||||
.then(() => this._rawFind(query, { skip, limit, sort, keys }));
|
||||
});
|
||||
}
|
||||
|
||||
_rawFind(query, { skip, limit, sort } = {}) {
|
||||
return this._mongoCollection
|
||||
_rawFind(query, { skip, limit, sort, keys } = {}) {
|
||||
let findOperation = this._mongoCollection
|
||||
.find(query, { skip, limit, sort })
|
||||
.toArray();
|
||||
|
||||
if (keys) {
|
||||
findOperation = findOperation.project(keys);
|
||||
}
|
||||
return findOperation.toArray();
|
||||
}
|
||||
|
||||
count(query, { skip, limit, sort } = {}) {
|
||||
|
||||
@@ -320,12 +320,16 @@ export class MongoStorageAdapter {
|
||||
}
|
||||
|
||||
// Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }.
|
||||
find(className, schema, query, { skip, limit, sort }) {
|
||||
find(className, schema, query, { skip, limit, sort, keys }) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
let mongoWhere = transformWhere(className, query, schema);
|
||||
let mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema));
|
||||
let mongoKeys = _.reduce(keys, (memo, key) => {
|
||||
memo[transformKey(className, key, schema)] = 1;
|
||||
return memo;
|
||||
}, {});
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.find(mongoWhere, { skip, limit, sort: mongoSort }))
|
||||
.then(collection => collection.find(mongoWhere, { skip, limit, sort: mongoSort, keys: mongoKeys }))
|
||||
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@ const transformKey = (className, fieldName, schema) => {
|
||||
case 'updatedAt': return '_updated_at';
|
||||
case 'sessionToken': return '_session_token';
|
||||
}
|
||||
|
||||
|
||||
if (schema.fields[fieldName] && schema.fields[fieldName].__type == 'Pointer') {
|
||||
fieldName = '_p_' + fieldName;
|
||||
} else if (schema.fields[fieldName] && schema.fields[fieldName].type == 'Pointer') {
|
||||
fieldName = '_p_' + fieldName;
|
||||
}
|
||||
|
||||
return fieldName;
|
||||
@@ -99,7 +101,7 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
|
||||
}
|
||||
|
||||
const transformInteriorValue = restValue => {
|
||||
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.indexOf('$') >= 0 || key.indexOf('.') >= 0)) {
|
||||
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$')|| key.includes('.'))) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
|
||||
}
|
||||
// Handle atomic values
|
||||
@@ -293,7 +295,7 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) =>
|
||||
}
|
||||
|
||||
// Handle normal objects by recursing
|
||||
if (Object.keys(restValue).some(key => key.indexOf('$') >= 0 || key.indexOf('.') >= 0)) {
|
||||
if (Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
|
||||
}
|
||||
value = _.mapValues(restValue, transformInteriorValue);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
const pgp = require('pg-promise')();
|
||||
const parser = require('./PostgresConfigParser');
|
||||
|
||||
export function createClient(uri, databaseOptions) {
|
||||
let client;
|
||||
let dbOptions = {};
|
||||
databaseOptions = databaseOptions || {};
|
||||
|
||||
if (uri) {
|
||||
dbOptions = parser.getDatabaseOptionsFromURI(uri);
|
||||
}
|
||||
|
||||
for (const key in databaseOptions) {
|
||||
dbOptions[key] = databaseOptions[key];
|
||||
}
|
||||
|
||||
client = pgp(dbOptions);
|
||||
|
||||
if (dbOptions.pgOptions) {
|
||||
for (const key in dbOptions.pgOptions) {
|
||||
client.pg.defaults[key] = dbOptions.pgOptions[key];
|
||||
}
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
const url = require('url');
|
||||
|
||||
function getDatabaseOptionsFromURI(uri) {
|
||||
const databaseOptions = {};
|
||||
|
||||
const parsedURI = url.parse(uri);
|
||||
const queryParams = parseQueryParams(parsedURI.query);
|
||||
const authParts = parsedURI.auth ? parsedURI.auth.split(':') : [];
|
||||
|
||||
databaseOptions.host = parsedURI.hostname || 'localhost';
|
||||
databaseOptions.port = parsedURI.port ? parseInt(parsedURI.port) : 5432;
|
||||
databaseOptions.database = parsedURI.pathname
|
||||
? parsedURI.pathname.substr(1)
|
||||
: undefined;
|
||||
|
||||
databaseOptions.user = authParts.length > 0 ? authParts[0] : '';
|
||||
databaseOptions.password = authParts.length > 1 ? authParts[1] : '';
|
||||
|
||||
databaseOptions.ssl =
|
||||
queryParams.ssl && queryParams.ssl.toLowerCase() === 'true' ? true : false;
|
||||
databaseOptions.binary =
|
||||
queryParams.binary && queryParams.binary.toLowerCase() === 'true' ? true : false;
|
||||
|
||||
databaseOptions.client_encoding = queryParams.client_encoding;
|
||||
databaseOptions.application_name = queryParams.application_name;
|
||||
databaseOptions.fallback_application_name = queryParams.fallback_application_name;
|
||||
|
||||
if (queryParams.poolSize) {
|
||||
databaseOptions.poolSize = parseInt(queryParams.poolSize) || 10;
|
||||
}
|
||||
|
||||
return databaseOptions;
|
||||
}
|
||||
|
||||
function parseQueryParams(queryString) {
|
||||
queryString = queryString || '';
|
||||
|
||||
return queryString
|
||||
.split('&')
|
||||
.reduce((p, c) => {
|
||||
const parts = c.split('=');
|
||||
p[decodeURIComponent(parts[0])] =
|
||||
parts.length > 1
|
||||
? decodeURIComponent(parts.slice(1).join('='))
|
||||
: '';
|
||||
return p;
|
||||
}, {});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseQueryParams: parseQueryParams,
|
||||
getDatabaseOptionsFromURI: getDatabaseOptionsFromURI
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
const pgp = require('pg-promise')();
|
||||
import { createClient } from './PostgresClient';
|
||||
|
||||
const PostgresRelationDoesNotExistError = '42P01';
|
||||
const PostgresDuplicateRelationError = '42P07';
|
||||
@@ -379,9 +379,10 @@ export class PostgresStorageAdapter {
|
||||
constructor({
|
||||
uri,
|
||||
collectionPrefix = '',
|
||||
databaseOptions
|
||||
}) {
|
||||
this._collectionPrefix = collectionPrefix;
|
||||
this._client = pgp(uri);
|
||||
this._client = createClient(uri, databaseOptions);
|
||||
}
|
||||
|
||||
_ensureSchemaCollectionExists() {
|
||||
@@ -415,7 +416,7 @@ export class PostgresStorageAdapter {
|
||||
return toParseSchema(schema)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.code === PostgresUniqueIndexViolationError && err.detail.indexOf(className) >= 0) {
|
||||
if (err.code === PostgresUniqueIndexViolationError && err.detail.includes(className)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`)
|
||||
}
|
||||
throw err;
|
||||
@@ -445,7 +446,7 @@ export class PostgresStorageAdapter {
|
||||
relations.push(fieldName)
|
||||
return;
|
||||
}
|
||||
if (['_rperm', '_wperm'].indexOf(fieldName) >=0 ) {
|
||||
if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) {
|
||||
parseType.contents = { type: 'String' };
|
||||
}
|
||||
valuesArray.push(fieldName);
|
||||
@@ -921,8 +922,8 @@ export class PostgresStorageAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
find(className, schema, query, { skip, limit, sort }) {
|
||||
debug('find', className, query, {skip, limit, sort});
|
||||
find(className, schema, query, { skip, limit, sort, keys }) {
|
||||
debug('find', className, query, {skip, limit, sort, keys });
|
||||
const hasLimit = limit !== undefined;
|
||||
const hasSkip = skip !== undefined;
|
||||
let values = [className];
|
||||
@@ -954,7 +955,19 @@ export class PostgresStorageAdapter {
|
||||
sortPattern = `ORDER BY ${where.sorts.join(',')}`;
|
||||
}
|
||||
|
||||
const qs = `SELECT * FROM $1:name ${wherePattern} ${sortPattern} ${limitPattern} ${skipPattern}`;
|
||||
let columns = '*';
|
||||
if (keys) {
|
||||
// Exclude empty keys
|
||||
keys = keys.filter((key) => {
|
||||
return key.length > 0;
|
||||
});
|
||||
columns = keys.map((key, index) => {
|
||||
return `$${index+values.length+1}:name`;
|
||||
}).join(',');
|
||||
values = values.concat(keys);
|
||||
}
|
||||
|
||||
const qs = `SELECT ${columns} FROM $1:name ${wherePattern} ${sortPattern} ${limitPattern} ${skipPattern}`;
|
||||
debug(qs, values);
|
||||
return this._client.any(qs, values)
|
||||
.catch((err) => {
|
||||
@@ -1031,9 +1044,9 @@ export class PostgresStorageAdapter {
|
||||
const qs = `ALTER TABLE $1:name ADD CONSTRAINT $2:name UNIQUE (${constraintPatterns.join(',')})`;
|
||||
return this._client.none(qs,[className, constraintName, ...fieldNames])
|
||||
.catch(error => {
|
||||
if (error.code === PostgresDuplicateRelationError && error.message.indexOf(constraintName) >= 0) {
|
||||
if (error.code === PostgresDuplicateRelationError && error.message.includes(constraintName)) {
|
||||
// Index already exists. Ignore error.
|
||||
} else if (error.code === PostgresUniqueIndexViolationError && error.message.indexOf(constraintName) >= 0) {
|
||||
} else if (error.code === PostgresUniqueIndexViolationError && error.message.includes(constraintName)) {
|
||||
// Cast the error into the proper parse error
|
||||
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
|
||||
} else {
|
||||
|
||||
@@ -44,6 +44,11 @@ const transformObjectACL = ({ ACL, ...result }) => {
|
||||
}
|
||||
|
||||
const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count'];
|
||||
|
||||
const isSpecialQueryKey = key => {
|
||||
return specialQuerykeys.indexOf(key) >= 0;
|
||||
}
|
||||
|
||||
const validateQuery = query => {
|
||||
if (query.ACL) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
|
||||
@@ -73,7 +78,7 @@ const validateQuery = query => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!specialQuerykeys.includes(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
|
||||
if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`);
|
||||
}
|
||||
});
|
||||
@@ -185,6 +190,11 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => {
|
||||
// one of the provided strings must provide the caller with
|
||||
// write permissions.
|
||||
const specialKeysForUpdate = ['_hashed_password', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count'];
|
||||
|
||||
const isSpecialUpdateKey = key => {
|
||||
return specialKeysForUpdate.indexOf(key) >= 0;
|
||||
}
|
||||
|
||||
DatabaseController.prototype.update = function(className, query, update, {
|
||||
acl,
|
||||
many,
|
||||
@@ -227,7 +237,7 @@ DatabaseController.prototype.update = function(className, query, update, {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`);
|
||||
}
|
||||
fieldName = fieldName.split('.')[0];
|
||||
if (!SchemaController.fieldNameIsValid(fieldName) && !specialKeysForUpdate.includes(fieldName)) {
|
||||
if (!SchemaController.fieldNameIsValid(fieldName) && !isSpecialUpdateKey(fieldName)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`);
|
||||
}
|
||||
});
|
||||
@@ -666,17 +676,12 @@ DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) {
|
||||
return query;
|
||||
}
|
||||
|
||||
DatabaseController.prototype.addNotInObjectIdsIds = function(ids = null, query) {
|
||||
let idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : null;
|
||||
let allIds = [idsFromNin, ids].filter(list => list !== null);
|
||||
let totalLength = allIds.reduce((memo, list) => memo + list.length, 0);
|
||||
DatabaseController.prototype.addNotInObjectIdsIds = function(ids = [], query) {
|
||||
let idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : [];
|
||||
let allIds = [...idsFromNin,...ids].filter(list => list !== null);
|
||||
|
||||
let idsIntersection = [];
|
||||
if (totalLength > 125) {
|
||||
idsIntersection = intersect.big(allIds);
|
||||
} else {
|
||||
idsIntersection = intersect(allIds);
|
||||
}
|
||||
// make a set and spread to remove duplicates
|
||||
allIds = [...new Set(allIds)];
|
||||
|
||||
// Need to make sure we don't clobber existing shorthand $eq constraints on objectId.
|
||||
if (!('objectId' in query)) {
|
||||
@@ -686,8 +691,8 @@ DatabaseController.prototype.addNotInObjectIdsIds = function(ids = null, query)
|
||||
$eq: query.objectId
|
||||
};
|
||||
}
|
||||
query.objectId['$nin'] = idsIntersection;
|
||||
|
||||
query.objectId['$nin'] = allIds;
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -711,10 +716,12 @@ DatabaseController.prototype.find = function(className, query, {
|
||||
acl,
|
||||
sort = {},
|
||||
count,
|
||||
keys,
|
||||
op
|
||||
} = {}) {
|
||||
let isMaster = acl === undefined;
|
||||
let aclGroup = acl || [];
|
||||
let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find';
|
||||
op = op || (typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find');
|
||||
let classExists = true;
|
||||
return this.loadSchema()
|
||||
.then(schemaController => {
|
||||
@@ -779,7 +786,7 @@ DatabaseController.prototype.find = function(className, query, {
|
||||
if (!classExists) {
|
||||
return [];
|
||||
} else {
|
||||
return this.adapter.find(className, schema, query, { skip, limit, sort })
|
||||
return this.adapter.find(className, schema, query, { skip, limit, sort, keys })
|
||||
.then(objects => objects.map(object => {
|
||||
object = untransformObjectACL(object);
|
||||
return filterSensitiveData(isMaster, aclGroup, className, object)
|
||||
|
||||
@@ -5,7 +5,6 @@ import AdaptableController from './AdaptableController';
|
||||
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
|
||||
import path from 'path';
|
||||
import mime from 'mime';
|
||||
import uuid from 'node-uuid';
|
||||
|
||||
const legacyFilesRegex = new RegExp("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}-.*");
|
||||
|
||||
@@ -27,7 +26,7 @@ export class FilesController extends AdaptableController {
|
||||
contentType = mime.lookup(filename);
|
||||
}
|
||||
|
||||
filename = uuid.v4() + '_' + filename;
|
||||
filename = randomHexString(32) + '_' + filename;
|
||||
|
||||
var location = this.adapter.getFileLocation(config, filename);
|
||||
return this.adapter.createFile(filename, data, contentType).then(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Parse } from 'parse/node';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import AdaptableController from './AdaptableController';
|
||||
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
|
||||
import url from 'url';
|
||||
|
||||
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
|
||||
const LOG_STRING_TRUNCATE_LENGTH = 1000;
|
||||
@@ -19,8 +20,48 @@ export const LogOrder = {
|
||||
|
||||
export class LoggerController extends AdaptableController {
|
||||
|
||||
maskSensitiveUrl(urlString) {
|
||||
const password = url.parse(urlString, true).query.password;
|
||||
|
||||
if (password) {
|
||||
urlString = urlString.replace('password=' + password, 'password=********');
|
||||
}
|
||||
return urlString;
|
||||
}
|
||||
|
||||
maskSensitive(argArray) {
|
||||
return argArray.map(e => {
|
||||
if (!e) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if (typeof e === 'string') {
|
||||
return e.replace(/(password".?:.?")[^"]*"/g, '$1********"');
|
||||
}
|
||||
// else it is an object...
|
||||
|
||||
// check the url
|
||||
if (e.url) {
|
||||
e.url = this.maskSensitiveUrl(e.url);
|
||||
}
|
||||
|
||||
if (e.body) {
|
||||
for (let key of Object.keys(e.body)) {
|
||||
if (key === 'password') {
|
||||
e.body[key] = '********';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
log(level, args) {
|
||||
args = [].concat(level, [...args]);
|
||||
// make the passed in arguments object an array with the spread operator
|
||||
args = this.maskSensitive([...args]);
|
||||
args = [].concat(level, args);
|
||||
this.adapter.log.apply(this.adapter, args);
|
||||
}
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ const validNonRelationOrPointerTypes = [
|
||||
];
|
||||
// Returns an error suitable for throwing if the type is invalid
|
||||
const fieldTypeIsInvalid = ({ type, targetClass }) => {
|
||||
if (['Pointer', 'Relation'].includes(type)) {
|
||||
if (['Pointer', 'Relation'].indexOf(type) >= 0) {
|
||||
if (!targetClass) {
|
||||
return new Parse.Error(135, `type ${type} needs a class name`);
|
||||
} else if (typeof targetClass !== 'string') {
|
||||
@@ -232,7 +232,7 @@ const fieldTypeIsInvalid = ({ type, targetClass }) => {
|
||||
if (typeof type !== 'string') {
|
||||
return invalidJsonError;
|
||||
}
|
||||
if (!validNonRelationOrPointerTypes.includes(type)) {
|
||||
if (validNonRelationOrPointerTypes.indexOf(type) < 0) {
|
||||
return new Parse.Error(Parse.Error.INCORRECT_TYPE, `invalid field type: ${type}`);
|
||||
}
|
||||
return undefined;
|
||||
@@ -520,7 +520,6 @@ export default class SchemaController {
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
// The schema still doesn't validate. Give up
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate');
|
||||
});
|
||||
@@ -541,7 +540,7 @@ export default class SchemaController {
|
||||
|
||||
validateSchemaData(className, fields, classLevelPermissions, existingFieldNames) {
|
||||
for (let fieldName in fields) {
|
||||
if (!existingFieldNames.includes(fieldName)) {
|
||||
if (existingFieldNames.indexOf(fieldName) < 0) {
|
||||
if (!fieldNameIsValid(fieldName)) {
|
||||
return {
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
|
||||
@@ -125,7 +125,7 @@ export class UserController extends AdaptableController {
|
||||
}
|
||||
|
||||
setPasswordResetToken(email) {
|
||||
return this.config.database.update('_User', { email }, { _perishable_token: randomString(25) }, {}, true)
|
||||
return this.config.database.update('_User', { $or: [{email}, {username: email, email: {$exists: false}}] }, { _perishable_token: randomString(25) }, {}, true)
|
||||
}
|
||||
|
||||
sendPasswordResetEmail(email) {
|
||||
@@ -181,7 +181,7 @@ export class UserController extends AdaptableController {
|
||||
"You requested to reset your password for " + appName + ".\n\n" +
|
||||
"" +
|
||||
"Click here to reset it:\n" + link;
|
||||
let to = user.get("email");
|
||||
let to = user.get("email") || user.get('username');
|
||||
let subject = 'Password Reset for ' + appName;
|
||||
return { text, to, subject };
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import RequestSchema from './RequestSchema';
|
||||
import { matchesQuery, queryHash } from './QueryTools';
|
||||
import { ParsePubSub } from './ParsePubSub';
|
||||
import { SessionTokenCache } from './SessionTokenCache';
|
||||
import _ from 'lodash';
|
||||
|
||||
class ParseLiveQueryServer {
|
||||
clientId: number;
|
||||
@@ -62,7 +63,13 @@ class ParseLiveQueryServer {
|
||||
// to the subscribers and the handler will be called.
|
||||
this.subscriber.on('message', (channel, messageStr) => {
|
||||
logger.verbose('Subscribe messsage %j', messageStr);
|
||||
let message = JSON.parse(messageStr);
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(messageStr);
|
||||
} catch(e) {
|
||||
logger.error('unable to parse message', messageStr, e);
|
||||
return;
|
||||
}
|
||||
this._inflateParseObject(message);
|
||||
if (channel === 'afterSave') {
|
||||
this._onAfterSave(message);
|
||||
@@ -116,7 +123,7 @@ class ParseLiveQueryServer {
|
||||
if (!isSubscriptionMatched) {
|
||||
continue;
|
||||
}
|
||||
for (let [clientId, requestIds] of subscription.clientRequestIds.entries()) {
|
||||
for (let [clientId, requestIds] of _.entries(subscription.clientRequestIds)) {
|
||||
let client = this.clients.get(clientId);
|
||||
if (typeof client === 'undefined') {
|
||||
continue;
|
||||
@@ -159,7 +166,7 @@ class ParseLiveQueryServer {
|
||||
for (let subscription of classSubscriptions.values()) {
|
||||
let isOriginalSubscriptionMatched = this._matchesSubscription(originalParseObject, subscription);
|
||||
let isCurrentSubscriptionMatched = this._matchesSubscription(currentParseObject, subscription);
|
||||
for (let [clientId, requestIds] of subscription.clientRequestIds.entries()) {
|
||||
for (let [clientId, requestIds] of _.entries(subscription.clientRequestIds)) {
|
||||
let client = this.clients.get(clientId);
|
||||
if (typeof client === 'undefined') {
|
||||
continue;
|
||||
@@ -229,7 +236,12 @@ class ParseLiveQueryServer {
|
||||
_onConnect(parseWebsocket: any): void {
|
||||
parseWebsocket.on('message', (request) => {
|
||||
if (typeof request === 'string') {
|
||||
request = JSON.parse(request);
|
||||
try {
|
||||
request = JSON.parse(request);
|
||||
} catch(e) {
|
||||
logger.error('unable to parse request', request, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
logger.verbose('Request: %j', request);
|
||||
|
||||
@@ -269,7 +281,7 @@ class ParseLiveQueryServer {
|
||||
this.clients.delete(clientId);
|
||||
|
||||
// Delete client from subscriptions
|
||||
for (let [requestId, subscriptionInfo] of client.subscriptionInfos.entries()) {
|
||||
for (let [requestId, subscriptionInfo] of _.entries(client.subscriptionInfos)) {
|
||||
let subscription = subscriptionInfo.subscription;
|
||||
subscription.deleteClientSubscription(clientId, requestId);
|
||||
|
||||
|
||||
+25
-5
@@ -7,6 +7,7 @@ var batch = require('./batch'),
|
||||
multer = require('multer'),
|
||||
Parse = require('parse/node').Parse,
|
||||
path = require('path'),
|
||||
url = require('url'),
|
||||
authDataManager = require('./authDataManager');
|
||||
|
||||
import defaults from './defaults';
|
||||
@@ -54,6 +55,7 @@ import DatabaseController from './Controllers/DatabaseController';
|
||||
import SchemaCache from './Controllers/SchemaCache';
|
||||
import ParsePushAdapter from 'parse-server-push-adapter';
|
||||
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||
import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageAdapter';
|
||||
|
||||
import { ParseServerRESTController } from './ParseServerRESTController';
|
||||
// Mutate the Parse object to add the Cloud Code handlers
|
||||
@@ -145,11 +147,7 @@ class ParseServer {
|
||||
if ((databaseOptions || (databaseURI && databaseURI != defaults.DefaultMongoURI) || collectionPrefix !== '') && databaseAdapter) {
|
||||
throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/collectionPrefix.';
|
||||
} else if (!databaseAdapter) {
|
||||
databaseAdapter = new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
collectionPrefix,
|
||||
mongoOptions: databaseOptions,
|
||||
});
|
||||
databaseAdapter = this.getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions)
|
||||
} else {
|
||||
databaseAdapter = loadAdapter(databaseAdapter)
|
||||
}
|
||||
@@ -252,6 +250,28 @@ class ParseServer {
|
||||
}
|
||||
}
|
||||
|
||||
getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions) {
|
||||
let protocol;
|
||||
try{
|
||||
const parsedURI = url.parse(databaseURI);
|
||||
protocol = parsedURI.protocol ? parsedURI.protocol.toLowerCase() : null;
|
||||
}catch(e){}
|
||||
switch (protocol) {
|
||||
case 'postgres:':
|
||||
return new PostgresStorageAdapter({
|
||||
uri: databaseURI,
|
||||
collectionPrefix,
|
||||
databaseOptions
|
||||
});
|
||||
default:
|
||||
return new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
collectionPrefix,
|
||||
mongoOptions: databaseOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get app() {
|
||||
return ParseServer.app(this.config);
|
||||
}
|
||||
|
||||
+2
-22
@@ -144,7 +144,7 @@ function makeExpressHandler(appId, promiseHandler) {
|
||||
return function(req, res, next) {
|
||||
try {
|
||||
let url = maskSensitiveUrl(req);
|
||||
let body = maskSensitiveBody(req);
|
||||
let body = Object.assign({}, req.body);
|
||||
let stringifiedBody = JSON.stringify(body, null, 2);
|
||||
log.verbose(`REQUEST for [${req.method}] ${url}: ${stringifiedBody}`, {
|
||||
method: req.method,
|
||||
@@ -198,33 +198,13 @@ function makeExpressHandler(appId, promiseHandler) {
|
||||
}
|
||||
}
|
||||
|
||||
function maskSensitiveBody(req) {
|
||||
let maskBody = Object.assign({}, req.body);
|
||||
let shouldMaskBody = (req.method === 'POST' && req.originalUrl.endsWith('/users')
|
||||
&& !req.originalUrl.includes('classes')) ||
|
||||
(req.method === 'PUT' && /users\/\w+$/.test(req.originalUrl)
|
||||
&& !req.originalUrl.includes('classes')) ||
|
||||
(req.originalUrl.includes('classes/_User'));
|
||||
if (shouldMaskBody) {
|
||||
for (let key of Object.keys(maskBody)) {
|
||||
if (key == 'password') {
|
||||
maskBody[key] = '********';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return maskBody;
|
||||
}
|
||||
|
||||
function maskSensitiveUrl(req) {
|
||||
let maskUrl = req.originalUrl.toString();
|
||||
let shouldMaskUrl = req.method === 'GET' && req.originalUrl.includes('/login')
|
||||
&& !req.originalUrl.includes('classes');
|
||||
if (shouldMaskUrl) {
|
||||
let password = url.parse(req.originalUrl, true).query.password;
|
||||
if (password) {
|
||||
maskUrl = maskUrl.replace('password=' + password, 'password=********')
|
||||
}
|
||||
maskUrl = log.maskSensitiveUrl(maskUrl);
|
||||
}
|
||||
return maskUrl;
|
||||
}
|
||||
|
||||
+44
-27
@@ -20,6 +20,7 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
|
||||
this.auth = auth;
|
||||
this.className = className;
|
||||
this.restWhere = restWhere;
|
||||
this.restOptions = restOptions;
|
||||
this.clientSDK = clientSDK;
|
||||
this.response = null;
|
||||
this.findOptions = {};
|
||||
@@ -56,6 +57,7 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
|
||||
switch(option) {
|
||||
case 'keys':
|
||||
this.keys = new Set(restOptions.keys.split(','));
|
||||
// Add the default
|
||||
this.keys.add('objectId');
|
||||
this.keys.add('createdAt');
|
||||
this.keys.add('updatedAt');
|
||||
@@ -111,11 +113,11 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
|
||||
// Returns a promise for the response - an object with optional keys
|
||||
// 'results' and 'count'.
|
||||
// TODO: consolidate the replaceX functions
|
||||
RestQuery.prototype.execute = function() {
|
||||
RestQuery.prototype.execute = function(executeOptions) {
|
||||
return Promise.resolve().then(() => {
|
||||
return this.buildRestWhere();
|
||||
}).then(() => {
|
||||
return this.runFind();
|
||||
return this.runFind(executeOptions);
|
||||
}).then(() => {
|
||||
return this.runCount();
|
||||
}).then(() => {
|
||||
@@ -385,13 +387,22 @@ RestQuery.prototype.replaceDontSelect = function() {
|
||||
|
||||
// Returns a promise for whether it was successful.
|
||||
// Populates this.response with an object that only has 'results'.
|
||||
RestQuery.prototype.runFind = function() {
|
||||
RestQuery.prototype.runFind = function(options = {}) {
|
||||
if (this.findOptions.limit === 0) {
|
||||
this.response = {results: []};
|
||||
return Promise.resolve();
|
||||
}
|
||||
let findOptions = Object.assign({}, this.findOptions);
|
||||
if (this.keys) {
|
||||
findOptions.keys = Array.from(this.keys).map((key) => {
|
||||
return key.split('.')[0];
|
||||
});
|
||||
}
|
||||
if (options.op) {
|
||||
findOptions.op = options.op;
|
||||
}
|
||||
return this.config.database.find(
|
||||
this.className, this.restWhere, this.findOptions).then((results) => {
|
||||
this.className, this.restWhere, findOptions).then((results) => {
|
||||
if (this.className === '_User') {
|
||||
for (var result of results) {
|
||||
delete result.password;
|
||||
@@ -411,19 +422,6 @@ RestQuery.prototype.runFind = function() {
|
||||
|
||||
this.config.filesController.expandFilesInObject(this.config, results);
|
||||
|
||||
if (this.keys) {
|
||||
var keySet = this.keys;
|
||||
results = results.map((object) => {
|
||||
var newObject = {};
|
||||
for (var key in object) {
|
||||
if (keySet.has(key)) {
|
||||
newObject[key] = object[key];
|
||||
}
|
||||
}
|
||||
return newObject;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.redirectClassName) {
|
||||
for (var r of results) {
|
||||
r.className = this.redirectClassName;
|
||||
@@ -455,7 +453,7 @@ RestQuery.prototype.handleInclude = function() {
|
||||
}
|
||||
|
||||
var pathResponse = includePath(this.config, this.auth,
|
||||
this.response, this.include[0]);
|
||||
this.response, this.include[0], this.restOptions);
|
||||
if (pathResponse.then) {
|
||||
return pathResponse.then((newResponse) => {
|
||||
this.response = newResponse;
|
||||
@@ -473,13 +471,12 @@ RestQuery.prototype.handleInclude = function() {
|
||||
// Adds included values to the response.
|
||||
// Path is a list of field names.
|
||||
// Returns a promise for an augmented response.
|
||||
function includePath(config, auth, response, path) {
|
||||
function includePath(config, auth, response, path, restOptions = {}) {
|
||||
var pointers = findPointers(response.results, path);
|
||||
if (pointers.length == 0) {
|
||||
return response;
|
||||
}
|
||||
let pointersHash = {};
|
||||
var objectIds = {};
|
||||
for (var pointer of pointers) {
|
||||
if (!pointer) {
|
||||
continue;
|
||||
@@ -487,15 +484,35 @@ function includePath(config, auth, response, path) {
|
||||
let className = pointer.className;
|
||||
// only include the good pointers
|
||||
if (className) {
|
||||
pointersHash[className] = pointersHash[className] || [];
|
||||
pointersHash[className].push(pointer.objectId);
|
||||
pointersHash[className] = pointersHash[className] || new Set();
|
||||
pointersHash[className].add(pointer.objectId);
|
||||
}
|
||||
}
|
||||
let includeRestOptions = {};
|
||||
if (restOptions.keys) {
|
||||
let keys = new Set(restOptions.keys.split(','));
|
||||
let keySet = Array.from(keys).reduce((set, key) => {
|
||||
let keyPath = key.split('.');
|
||||
let i=0;
|
||||
for (i; i<path.length; i++) {
|
||||
if (path[i] != keyPath[i]) {
|
||||
return set;
|
||||
}
|
||||
}
|
||||
if (i < keyPath.length) {
|
||||
set.add(keyPath[i]);
|
||||
}
|
||||
return set;
|
||||
}, new Set());
|
||||
if (keySet.size > 0) {
|
||||
includeRestOptions.keys = Array.from(keySet).join(',');
|
||||
}
|
||||
}
|
||||
|
||||
let queryPromises = Object.keys(pointersHash).map((className) => {
|
||||
var where = {'objectId': {'$in': pointersHash[className]}};
|
||||
var query = new RestQuery(config, auth, className, where);
|
||||
return query.execute().then((results) => {
|
||||
let where = {'objectId': {'$in': Array.from(pointersHash[className])}};
|
||||
var query = new RestQuery(config, auth, className, where, includeRestOptions);
|
||||
return query.execute({op: 'get'}).then((results) => {
|
||||
results.className = className;
|
||||
return Promise.resolve(results);
|
||||
})
|
||||
@@ -541,7 +558,7 @@ function findPointers(object, path) {
|
||||
return answer;
|
||||
}
|
||||
|
||||
if (typeof object !== 'object') {
|
||||
if (typeof object !== 'object' || !object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -571,7 +588,7 @@ function replacePointers(object, path, replace) {
|
||||
.filter((obj) => typeof obj !== 'undefined');
|
||||
}
|
||||
|
||||
if (typeof object !== 'object') {
|
||||
if (typeof object !== 'object' || !object) {
|
||||
return object;
|
||||
}
|
||||
|
||||
|
||||
+23
-15
@@ -561,31 +561,34 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.query && !this.data.deviceToken && !this.data.installationId) {
|
||||
if (!this.query && !this.data.deviceToken && !this.data.installationId && !this.auth.installationId) {
|
||||
throw new Parse.Error(135,
|
||||
'at least one ID field (deviceToken, installationId) ' +
|
||||
'must be specified in this operation');
|
||||
}
|
||||
|
||||
if (!this.query && !this.data.deviceType) {
|
||||
throw new Parse.Error(135,
|
||||
'deviceType must be specified in this operation');
|
||||
}
|
||||
|
||||
// If the device token is 64 characters long, we assume it is for iOS
|
||||
// and lowercase it.
|
||||
if (this.data.deviceToken && this.data.deviceToken.length == 64) {
|
||||
this.data.deviceToken = this.data.deviceToken.toLowerCase();
|
||||
}
|
||||
|
||||
// TODO: We may need installationId from headers, plumb through Auth?
|
||||
// per installation_handler.go
|
||||
|
||||
// We lowercase the installationId if present
|
||||
if (this.data.installationId) {
|
||||
this.data.installationId = this.data.installationId.toLowerCase();
|
||||
}
|
||||
|
||||
let installationId = this.data.installationId;
|
||||
|
||||
// If data.installationId is not set and we're not master, we can lookup in auth
|
||||
if (!installationId && !this.auth.isMaster) {
|
||||
installationId = this.auth.installationId;
|
||||
}
|
||||
|
||||
if (installationId) {
|
||||
installationId = installationId.toLowerCase();
|
||||
}
|
||||
|
||||
var promise = Promise.resolve();
|
||||
|
||||
var idMatch; // Will be a match on either objectId or installationId
|
||||
@@ -600,9 +603,9 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
objectId: this.query.objectId
|
||||
});
|
||||
}
|
||||
if (this.data.installationId) {
|
||||
if (installationId) {
|
||||
orQueries.push({
|
||||
'installationId': this.data.installationId
|
||||
'installationId': installationId
|
||||
});
|
||||
}
|
||||
if (this.data.deviceToken) {
|
||||
@@ -622,7 +625,7 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
if (this.query && this.query.objectId && result.objectId == this.query.objectId) {
|
||||
objectIdMatch = result;
|
||||
}
|
||||
if (result.installationId == this.data.installationId) {
|
||||
if (result.installationId == installationId) {
|
||||
installationIdMatch = result;
|
||||
}
|
||||
if (result.deviceToken == this.data.deviceToken) {
|
||||
@@ -661,16 +664,21 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
idMatch = objectIdMatch;
|
||||
}
|
||||
|
||||
if (this.data.installationId && installationIdMatch) {
|
||||
if (installationId && installationIdMatch) {
|
||||
idMatch = installationIdMatch;
|
||||
}
|
||||
// need to specify deviceType only if it's new
|
||||
if (!this.query && !this.data.deviceType && !idMatch) {
|
||||
throw new Parse.Error(135,
|
||||
'deviceType must be specified in this operation');
|
||||
}
|
||||
|
||||
}).then(() => {
|
||||
if (!idMatch) {
|
||||
if (!deviceTokenMatches.length) {
|
||||
return;
|
||||
} else if (deviceTokenMatches.length == 1 &&
|
||||
(!deviceTokenMatches[0]['installationId'] || !this.data.installationId)
|
||||
(!deviceTokenMatches[0]['installationId'] || !installationId)
|
||||
) {
|
||||
// Single match on device token but none on installationId, and either
|
||||
// the passed object or the match is missing an installationId, so we
|
||||
@@ -689,7 +697,7 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
var delQuery = {
|
||||
'deviceToken': this.data.deviceToken,
|
||||
'installationId': {
|
||||
'$ne': this.data.installationId
|
||||
'$ne': installationId
|
||||
}
|
||||
};
|
||||
if (this.data.appIdentifier) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import rest from '../rest';
|
||||
|
||||
import _ from 'lodash';
|
||||
import url from 'url';
|
||||
|
||||
const ALLOWED_GET_QUERY_KEYS = ['keys', 'include'];
|
||||
@@ -115,7 +115,7 @@ export class ClassesRouter extends PromiseRouter {
|
||||
|
||||
static JSONFromQuery(query) {
|
||||
let json = {};
|
||||
for (let [key, value] of Object.entries(query)) {
|
||||
for (let [key, value] of _.entries(query)) {
|
||||
try {
|
||||
json[key] = JSON.parse(value);
|
||||
} catch (e) {
|
||||
|
||||
@@ -103,7 +103,7 @@ export class FunctionsRouter extends PromiseRouter {
|
||||
const applicationId = req.config.applicationId;
|
||||
const theFunction = triggers.getFunction(functionName, applicationId);
|
||||
const theValidator = triggers.getValidator(req.params.functionName, applicationId);
|
||||
if (theFunction) {
|
||||
if (theFunction) {
|
||||
let params = Object.assign({}, req.body, req.query);
|
||||
params = parseParams(params);
|
||||
var request = {
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
var https = require('https');
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return request("tokeninfo?id_token="+authData.access_token)
|
||||
function validateIdToken(id, token) {
|
||||
return request("tokeninfo?id_token="+token)
|
||||
.then((response) => {
|
||||
if (response && response.sub == authData.id) {
|
||||
if (response && response.sub == id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
@@ -15,7 +14,34 @@ function validateAuthData(authData) {
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
function validateAuthToken(id, token) {
|
||||
return request("tokeninfo?access_token="+token)
|
||||
.then((response) => {
|
||||
if (response && response.user_id == id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Google auth is invalid for this user.');
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills if this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
if (authData.id_token) {
|
||||
return validateIdToken(authData.id, authData.id_token);
|
||||
} else {
|
||||
return validateAuthToken(authData.id, authData.access_token).then(() => {
|
||||
// Validation with auth token worked
|
||||
return;
|
||||
}, () => {
|
||||
// Try with the id_token param
|
||||
return validateIdToken(authData.id, authData.access_token);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills if this app id is valid.
|
||||
function validateAppId() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -1,28 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
// Helper functions for accessing the vkontakte API.
|
||||
|
||||
var https = require('https');
|
||||
var Parse = require('parse/node').Parse;
|
||||
var logger = require('../logger').default;
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return request("users.get?v=V&access_token=" + authData.access_token).then(function (response) {
|
||||
if (response && response.response && response.response[0].uid == authData.id) {
|
||||
return;
|
||||
function validateAuthData(authData, params) {
|
||||
return vkOAuth2Request(params).then(function (response) {
|
||||
if (response && response && response.access_token) {
|
||||
return request("api.vk.com", "method/secure.checkToken?token=" + authData.access_token + "&client_secret=" + params.appSecret + "&access_token=" + response.access_token).then(function (response) {
|
||||
if (response && response.response && response.response.user_id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
|
||||
});
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
|
||||
logger.error('Vk Auth', 'Vk appIds or appSecret is incorrect.');
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk appIds or appSecret is incorrect.');
|
||||
});
|
||||
}
|
||||
|
||||
function vkOAuth2Request(params) {
|
||||
var promise = new Parse.Promise();
|
||||
return promise.then(function(){
|
||||
if (!params || !params.appIds || !params.appIds.length || !params.appSecret || !params.appSecret.length ) {
|
||||
logger.error('Vk Auth', 'Vk auth is not configured. Missing appIds or appSecret.');
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is not configured. Missing appIds or appSecret.');
|
||||
}
|
||||
return request("oauth.vk.com", "access_token?client_id=" + params.appIds + "&client_secret=" + params.appSecret + "&v=5.59&grant_type=client_credentials")
|
||||
})
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
function validateAppId() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// A promisey wrapper for api requests
|
||||
function request(path) {
|
||||
function request(host, path) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
https.get("https://api.vk.com/method/" + path, function (res) {
|
||||
https.get("https://" + host + "/" + path, function (res) {
|
||||
var data = '';
|
||||
res.on('data', function (chunk) {
|
||||
data += chunk;
|
||||
@@ -40,4 +59,4 @@ function request(path) {
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
numberParser,
|
||||
numberOrBoolParser,
|
||||
objectParser,
|
||||
arrayParser,
|
||||
moduleOrObjectParser,
|
||||
booleanParser,
|
||||
nullParser
|
||||
} from '../utils/parsers';
|
||||
|
||||
|
||||
export default {
|
||||
"appId": {
|
||||
required: true,
|
||||
help: "Required. This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId."
|
||||
},
|
||||
"masterKey": {
|
||||
required: true,
|
||||
help: "Required. This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey."
|
||||
},
|
||||
"serverURL": {
|
||||
required: true,
|
||||
help: "Required. This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL."
|
||||
},
|
||||
"redisURL": {
|
||||
help: "Optional. This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey."
|
||||
},
|
||||
"keyPairs": {
|
||||
help: "Optional. A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details."
|
||||
},
|
||||
"websocketTimeout": {
|
||||
help: "Optional. Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients. Defaults to 10 * 1000 ms (10 s).",
|
||||
action: numberParser("websocketTimeout")
|
||||
},
|
||||
"cacheTimeout": {
|
||||
help: "Optional. Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details. Defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).",
|
||||
action: numberParser("cacheTimeout")
|
||||
},
|
||||
"logLevel": {
|
||||
help: "Optional. This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE. Defaults to INFO.",
|
||||
},
|
||||
"port": {
|
||||
env: "PORT",
|
||||
help: "The port to run the ParseServer. defaults to 1337.",
|
||||
default: 1337,
|
||||
action: numberParser("port")
|
||||
},
|
||||
};
|
||||
@@ -1,52 +1,13 @@
|
||||
function numberParser(key) {
|
||||
return function(opt) {
|
||||
opt = parseInt(opt);
|
||||
if (!Number.isInteger(opt)) {
|
||||
throw new Error(`The ${key} is invalid`);
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
}
|
||||
import {
|
||||
numberParser,
|
||||
numberOrBoolParser,
|
||||
objectParser,
|
||||
arrayParser,
|
||||
moduleOrObjectParser,
|
||||
booleanParser,
|
||||
nullParser
|
||||
} from '../utils/parsers';
|
||||
|
||||
function numberOrBoolParser(key) {
|
||||
return function(opt) {
|
||||
if (typeof opt === 'boolean') {
|
||||
return opt;
|
||||
}
|
||||
return numberParser(key)(opt);
|
||||
}
|
||||
}
|
||||
|
||||
function objectParser(opt) {
|
||||
if (typeof opt == 'object') {
|
||||
return opt;
|
||||
}
|
||||
return JSON.parse(opt)
|
||||
}
|
||||
|
||||
function moduleOrObjectParser(opt) {
|
||||
if (typeof opt == 'object') {
|
||||
return opt;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(opt);
|
||||
} catch(e) {}
|
||||
return opt;
|
||||
}
|
||||
|
||||
function booleanParser(opt) {
|
||||
if (opt == true || opt == "true" || opt == "1") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function nullParser(opt) {
|
||||
if (opt == 'null') {
|
||||
return null;
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
|
||||
export default {
|
||||
"appId": {
|
||||
@@ -128,9 +89,7 @@ export default {
|
||||
env: "PARSE_SERVER_FACEBOOK_APP_IDS",
|
||||
help: "Comma separated list for your facebook app Ids",
|
||||
type: "list",
|
||||
action: function(opt) {
|
||||
return opt.split(",")
|
||||
}
|
||||
action: arrayParser
|
||||
},
|
||||
"enableAnonymousUsers": {
|
||||
env: "PARSE_SERVER_ENABLE_ANON_USERS",
|
||||
@@ -239,5 +198,28 @@ export default {
|
||||
"cluster": {
|
||||
help: "Run with cluster, optionally set the number of processes default to os.cpus().length",
|
||||
action: numberOrBoolParser("cluster")
|
||||
}
|
||||
},
|
||||
"liveQuery": {
|
||||
help: "parse-server's LiveQuery configuration object",
|
||||
action: objectParser
|
||||
},
|
||||
"liveQuery.classNames": {
|
||||
help: "parse-server's LiveQuery classNames",
|
||||
action: arrayParser
|
||||
},
|
||||
"liveQuery.redisURL": {
|
||||
help: "parse-server's LiveQuery redisURL",
|
||||
},
|
||||
"startLiveQueryServer": {
|
||||
help: "Starts the liveQuery server",
|
||||
action: booleanParser
|
||||
},
|
||||
"liveQueryPort": {
|
||||
help: 'Specific port to start the live query server',
|
||||
action: numberParser("liveQueryPort")
|
||||
},
|
||||
"liveQueryServerOptions": {
|
||||
help: "Live query server configuration options (will start the liveQuery server)",
|
||||
action: objectParser
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import definitions from './definitions/parse-live-query-server';
|
||||
import runner from './utils/runner';
|
||||
import { ParseServer } from '../index';
|
||||
import express from 'express';
|
||||
|
||||
runner({
|
||||
definitions,
|
||||
start: function(program, options, logOptions) {
|
||||
logOptions();
|
||||
var app = express();
|
||||
var httpServer = require('http').createServer(app);
|
||||
httpServer.listen(options.port);
|
||||
ParseServer.createLiveQueryServer(httpServer, options);
|
||||
}
|
||||
})
|
||||
+66
-58
@@ -1,18 +1,12 @@
|
||||
import path from 'path';
|
||||
import express from 'express';
|
||||
import { ParseServer } from '../index';
|
||||
import definitions from './cli-definitions';
|
||||
import program from './utils/commander';
|
||||
import { mergeWithOptions } from './utils/commander';
|
||||
import definitions from './definitions/parse-server';
|
||||
import cluster from 'cluster';
|
||||
import os from 'os';
|
||||
import runner from './utils/runner';
|
||||
|
||||
program.loadDefinitions(definitions);
|
||||
|
||||
program
|
||||
.usage('[options] <path/to/configuration.json>');
|
||||
|
||||
program.on('--help', function(){
|
||||
const help = function(){
|
||||
console.log(' Get Started guide:');
|
||||
console.log('');
|
||||
console.log(' Please have a look at the get started guide!')
|
||||
@@ -32,33 +26,7 @@ program.on('--help', function(){
|
||||
console.log(' $ parse-server -- --appId APP_ID --masterKey MASTER_KEY --serverURL serverURL');
|
||||
console.log(' $ parse-server -- --appId APP_ID --masterKey MASTER_KEY --serverURL serverURL');
|
||||
console.log('');
|
||||
});
|
||||
|
||||
program.parse(process.argv, process.env);
|
||||
|
||||
let options = program.getOptions();
|
||||
|
||||
if (!options.serverURL) {
|
||||
options.serverURL = `http://localhost:${options.port}${options.mountPath}`;
|
||||
}
|
||||
|
||||
if (!options.appId || !options.masterKey || !options.serverURL) {
|
||||
program.outputHelp();
|
||||
console.error("");
|
||||
console.error('\u001b[31mERROR: appId and masterKey are required\u001b[0m');
|
||||
console.error("");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function logStartupOptions(options) {
|
||||
for (let key in options) {
|
||||
let value = options[key];
|
||||
if (key == "masterKey") {
|
||||
value = "***REDACTED***";
|
||||
}
|
||||
console.log(`${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function startServer(options, callback) {
|
||||
const app = express();
|
||||
@@ -66,7 +34,15 @@ function startServer(options, callback) {
|
||||
app.use(options.mountPath, api);
|
||||
|
||||
var server = app.listen(options.port, callback);
|
||||
|
||||
if (options.startLiveQueryServer || options.liveQueryServerOptions) {
|
||||
let liveQueryServer = server;
|
||||
if (options.liveQueryPort) {
|
||||
liveQueryServer = express().listen(options.liveQueryPort, () => {
|
||||
console.log('ParseLiveQuery listening on ' + options.liveQueryPort);
|
||||
});
|
||||
}
|
||||
ParseServer.createLiveQueryServer(liveQueryServer, options.liveQueryServerOptions);
|
||||
}
|
||||
var handleShutdown = function() {
|
||||
console.log('Termination signal received. Shutting down.');
|
||||
server.close(function () {
|
||||
@@ -77,27 +53,59 @@ function startServer(options, callback) {
|
||||
process.on('SIGINT', handleShutdown);
|
||||
}
|
||||
|
||||
if (options.cluster) {
|
||||
const numCPUs = typeof options.cluster === 'number' ? options.cluster : os.cpus().length;
|
||||
if (cluster.isMaster) {
|
||||
logStartupOptions(options);
|
||||
for(var i = 0; i < numCPUs; i++) {
|
||||
cluster.fork();
|
||||
|
||||
runner({
|
||||
definitions,
|
||||
help,
|
||||
usage: '[options] <path/to/configuration.json>',
|
||||
start: function(program, options, logOptions) {
|
||||
if (!options.serverURL) {
|
||||
options.serverURL = `http://localhost:${options.port}${options.mountPath}`;
|
||||
}
|
||||
cluster.on('exit', (worker, code, signal) => {
|
||||
console.log(`worker ${worker.process.pid} died... Restarting`);
|
||||
cluster.fork();
|
||||
});
|
||||
} else {
|
||||
startServer(options, () => {
|
||||
console.log('['+process.pid+'] parse-server running on '+options.serverURL);
|
||||
});
|
||||
|
||||
if (!options.appId || !options.masterKey || !options.serverURL) {
|
||||
program.outputHelp();
|
||||
console.error("");
|
||||
console.error('\u001b[31mERROR: appId and masterKey are required\u001b[0m');
|
||||
console.error("");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options["liveQuery.classNames"]) {
|
||||
options.liveQuery = options.liveQuery || {};
|
||||
options.liveQuery.classNames = options["liveQuery.classNames"];
|
||||
delete options["liveQuery.classNames"];
|
||||
}
|
||||
if (options["liveQuery.redisURL"]) {
|
||||
options.liveQuery = options.liveQuery || {};
|
||||
options.liveQuery.redisURL = options["liveQuery.redisURL"];
|
||||
delete options["liveQuery.redisURL"];
|
||||
}
|
||||
|
||||
if (options.cluster) {
|
||||
const numCPUs = typeof options.cluster === 'number' ? options.cluster : os.cpus().length;
|
||||
if (cluster.isMaster) {
|
||||
for(var i = 0; i < numCPUs; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
cluster.on('exit', (worker, code, signal) => {
|
||||
console.log(`worker ${worker.process.pid} died... Restarting`);
|
||||
cluster.fork();
|
||||
});
|
||||
} else {
|
||||
startServer(options, () => {
|
||||
console.log('['+process.pid+'] parse-server running on '+options.serverURL);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
startServer(options, () => {
|
||||
logOptions();
|
||||
console.log('');
|
||||
console.log('['+process.pid+'] parse-server running on '+options.serverURL);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startServer(options, () => {
|
||||
logStartupOptions(options);
|
||||
console.log('');
|
||||
console.log('['+process.pid+'] parse-server running on '+options.serverURL);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
export function numberParser(key) {
|
||||
return function(opt) {
|
||||
opt = parseInt(opt);
|
||||
if (!Number.isInteger(opt)) {
|
||||
throw new Error(`The ${key} is invalid`);
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
}
|
||||
|
||||
export function numberOrBoolParser(key) {
|
||||
return function(opt) {
|
||||
if (typeof opt === 'boolean') {
|
||||
return opt;
|
||||
}
|
||||
return numberParser(key)(opt);
|
||||
}
|
||||
}
|
||||
|
||||
export function objectParser(opt) {
|
||||
if (typeof opt == 'object') {
|
||||
return opt;
|
||||
}
|
||||
return JSON.parse(opt)
|
||||
}
|
||||
|
||||
export function arrayParser(opt) {
|
||||
if (Array.isArray(opt)) {
|
||||
return opt;
|
||||
} else if (typeof opt === 'string') {
|
||||
return opt.split(',');
|
||||
} else {
|
||||
throw new Error(`${opt} should be a comma separated string or an array`);
|
||||
}
|
||||
}
|
||||
|
||||
export function moduleOrObjectParser(opt) {
|
||||
if (typeof opt == 'object') {
|
||||
return opt;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(opt);
|
||||
} catch(e) {}
|
||||
return opt;
|
||||
}
|
||||
|
||||
export function booleanParser(opt) {
|
||||
if (opt == true || opt == "true" || opt == "1") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function nullParser(opt) {
|
||||
if (opt == 'null') {
|
||||
return null;
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
import program from './commander';
|
||||
import { mergeWithOptions } from './commander';
|
||||
|
||||
function logStartupOptions(options) {
|
||||
for (let key in options) {
|
||||
let value = options[key];
|
||||
if (key == "masterKey") {
|
||||
value = "***REDACTED***";
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
console.log(`${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default function({
|
||||
definitions,
|
||||
help,
|
||||
usage,
|
||||
start
|
||||
}) {
|
||||
program.loadDefinitions(definitions);
|
||||
if (usage) {
|
||||
program.usage(usage);
|
||||
}
|
||||
if (help) {
|
||||
program.on('--help', help);
|
||||
}
|
||||
program.parse(process.argv, process.env);
|
||||
|
||||
let options = program.getOptions();
|
||||
start(program, options, function() {
|
||||
logStartupOptions(options);
|
||||
});
|
||||
}
|
||||
+14
-1
@@ -1,5 +1,18 @@
|
||||
'use strict';
|
||||
let logger;
|
||||
import defaults from './defaults';
|
||||
import { WinstonLoggerAdapter } from './Adapters/Logger/WinstonLoggerAdapter';
|
||||
import { LoggerController } from './Controllers/LoggerController';
|
||||
|
||||
function defaultLogger() {
|
||||
let adapter = new WinstonLoggerAdapter({
|
||||
logsFolder: defaults.logsFolder,
|
||||
jsonLogs: defaults.jsonLogs,
|
||||
verbose: defaults.verbose,
|
||||
silent: defaults.silent });
|
||||
return new LoggerController(adapter);
|
||||
}
|
||||
|
||||
let logger = defaultLogger();
|
||||
|
||||
export function setLogger(aLogger) {
|
||||
logger = aLogger;
|
||||
|
||||
Reference in New Issue
Block a user