Compare commits

...
16 Commits
Author SHA1 Message Date
Florent Vilmart e97579ea85 Adds changelog for 2.2.20 (#2724)
* Adds changelog for 2.2.20

* nit
2016-09-18 07:55:37 -04:00
Jose Luis e5fa3b5e27 I only add a line in the readme for a new SMTP adapter for send emails (#2726) 2016-09-17 23:42:47 -07:00
Greenkeeper 56f3e63112 chore(package): update request to version 2.75.0 (#2725)
https://greenkeeper.io/
2016-09-17 19:06:42 -04:00
Florent Vilmart 263ca5e052 Adds CloudCode handler for beforeFind (#2715)
* Adds CloudCode handler for beforeFind

- Allows cloud code to modify a query before it is run
- Works with promises for a safer environment
- Supports modifiying the current query
- Supports issuing new queries

* Adds test for cornercase empty queries from rest

* Makes sure restOptions is always definied
2016-09-17 13:52:35 -07:00
Florent Vilmart ddb0fb8a27 Adds redis cache for distributed environments (#2691)
* Makes schemaCache clearning promise-based

* Adds redis cache adapter for distributed systems

* Adds redis service to travis

* allow pg to fail
2016-09-17 13:52:02 -07:00
Florent Vilmart f9dca6072a Travis: Makes sure the global success is reported on build leader (#2723)
* Adds exporting vars after success

* Echo var

* remove echo
2016-09-17 16:46:56 -04:00
Florent Vilmart ba0e529f81 Build merges on master to lastest branch (#2722)
* Build merges on master to lastest branch

* Only on master
2016-09-17 15:57:51 -04:00
Florent Vilmart 109bc9a0ce handling matching api.parse.com when calling upgradeToRevocableSession without a sessionToken (#2721) 2016-09-17 12:52:52 -07:00
Florent Vilmart 90e9994195 Removes runtime dependency babel-polyfill (#2692)
* Removes runtime dependency babel-polyfill

* removes references to polyfilled array includes
2016-09-17 12:50:04 -07:00
Florent Vilmart 97c124bbbe Supports boolean values in cluster options (#2689) 2016-09-17 12:45:12 -07:00
Lama Chandrasena 22c1a87d80 Saved filename fixed so that clients display it correctly #2415 (#2705)
* increased filename length to 36 to make sure clients display correct filename

* increased filename length to 36 to make sure clients display correct filename

* added uuid prefix to saved filenames
2016-09-17 14:27:31 -04:00
Greenkeeper 4c927dd898 chore(package): update winston-daily-rotate-file to version 1.3.1 (#2706)
https://greenkeeper.io/
2016-09-17 13:31:33 -04:00
Greenkeeper 408764a7b7 chore(package): update mongodb to version 2.2.10 (#2714)
https://greenkeeper.io/
2016-09-17 13:08:35 -04:00
Greenkeeper 86a0f1ad02 chore(package): update jasmine to version 2.5.2 (#2719)
https://greenkeeper.io/
2016-09-17 12:14:36 -04:00
Steven Shipton 0773523bc2 Ensure _acl is updated when _rperm and _wperm updated (#2701)
* Ensure _acl is updated when _rperm and _wperm updated

* alternative solution

* Only try to apply $set for permission updates
2016-09-17 11:05:33 -04:00
Mathias Rangel Wulff f6312a1b98 Correct check for undefined variable (#2693)
The code was comparing the result of `typeof` with a variable named `undefined`.

As typeof returns a string it should compare to `'undefined'`
2016-09-10 17:32:10 -04:00
24 changed files with 580 additions and 62 deletions
+3
View File
@@ -23,6 +23,9 @@ coverage
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# build folder for automated releases
latest
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
+27 -11
View File
@@ -4,6 +4,7 @@ node_js:
- '6.1'
services:
- postgresql
- redis-server
addons:
postgresql: '9.4'
before_script:
@@ -15,11 +16,14 @@ env:
global:
- COVERAGE_OPTION='./node_modules/.bin/istanbul cover'
matrix:
- PARSE_SERVER_TEST_DB=postgres
- MONGODB_VERSION=3.0.8
- MONGODB_VERSION=3.2.6
- PARSE_SERVER_TEST_DB=postgres
- PARSE_SERVER_TEST_CACHE=redis
matrix:
fast_finish: true
allow_failures:
- env: PARSE_SERVER_TEST_DB=postgres
branches:
only:
- master
@@ -30,14 +34,26 @@ cache:
- "$HOME/.mongodb/versions"
after_script:
- bash <(curl -s https://codecov.io/bash)
after_failure:
- (cd resources; python travis_after_all.py)
after_success:
- (cd resources; python travis_after_all.py)
- export $(cat resources/.to_export_back)
deploy:
provider: npm
email:
secure: T3LwSJFcu632SDfCoavcKL39NktWLEOOFzonAjeHAglmMyDp9hvU8xHwQ4JJy2CRA03c+ezAe2gH3Va+jfxvh1JtFUan+p88vaCHHKuNwPSp4WZBPt1ZTDmG+ACG6j+ZWHK+eP7hLvtlCi/M9/v4/bVojpm7v01LLiM+MRhnE9E7eLemQC4zb6RVtf2oGQ3mX51kMeya218kEm5bsQPpRJElI9jwletFa8qYK5AFgaCHpll059RRHQTTE9MTNcN58P/Kf6Qb3KGpKNoaHTXoOD4U6LcXLWvdHONVB6AzGefxg2b/dvtaO4nd5lDooMBR3u9kWpopXkyAuf+fO/8je9JTxO6CDUtDjHRcR2mCEXWX6rculIAbNXOv1ccRYCTQ8aFtYnFlMSb1+QIAdyT6HHmeT5ktk3+XJRuEv0PJJvqHTo+j7tPngjiv5sPNutgGFlOFO90omTbzEkIT/D/LsgXbWm2QKYWejbLiuSINg+FiFiZN90doCf4aCpm2y1Q/9GzvK+eXcpqzPCGxOykE7EuPZK3+S/ZU2oaWL2uSpbKPtC0qxebrneR307aVEa7C85HCkTMMBzE4tjUr8h5HhLqtWvqmDcnBo3PfQhf9cEO6jQLd3qhEVJmcwKz/yOCfgPXlCbRqiKk7j94perpNCIalXtMI9ySYjJNjHsB4600=
api_key:
secure: WLjhHVAdUkDy6UdNWveTpZqphw9olN0alCpyPpU0cJjlN/hk7YxPP+YHOSVnMZaRZoHM0LL11jPKLf970nymBfvJHDWxKk71c/5xyTX8pBtSxmGmGy23a5g2VrHVMd2JxsI7NEAH500tlFz+01t4E3Steo0NpAkHR3Q51kj01gQy8IumsksfhRc9uvjZ7zjm99Yk4L+cxyei79v4esfpx7Bgm0HTfAAAG/19F+r0hvvFiV517SREDU/YtcX/rIjVepEy1uiLKEohnmYtVIRSA3Hjh6VlHVhdj2WDd/dYrxi/Ioysq2zOM9pZVLamO9asB1e6JrTEipWE9jSZXxsbnfFzuNLxcOjKv1wd3OzQKF/7pGKGiWRTv5Xm19D/FrXoE/ULs6bHcC/Ke8Gs2RxPlOCdvAFehCkyf5P4HOPCQdR7o8Yiuvt+5JWKBflElNbSd4nWgwUOo3Yv8vC4Vj53fwmM+Uqfu3IgYFWktYxCm4RWIKMfB/gtmjcF6QYFfjvEMwAvRfBV81kuynCsnubXWzQeBE/b3JOhBfpGciKCjKfy+tS6bZfFjCtQV98hMMiCPre8Y7PahHDYc65wU9Ake+ZE+dDaSbeV3DZ5JeifLJHzKW2J2dWeRANkOaiSwO9VBC9/rht5ulK5qQ1pB2+sKDToAaiKy6RMlB/HgIoYqsU=
on:
tags: true
all_branches: true
condition: "$MONGODB_VERSION = '3.0.8'"
repo: ParsePlatform/parse-server
- provider: script
skip_cleanup: true
script: ./resources/npm-git.sh
on:
branch: master
condition: "$BUILD_LEADER$BUILD_AGGREGATE_STATUS = YESothers_succeeded"
- provider: npm
email:
secure: T3LwSJFcu632SDfCoavcKL39NktWLEOOFzonAjeHAglmMyDp9hvU8xHwQ4JJy2CRA03c+ezAe2gH3Va+jfxvh1JtFUan+p88vaCHHKuNwPSp4WZBPt1ZTDmG+ACG6j+ZWHK+eP7hLvtlCi/M9/v4/bVojpm7v01LLiM+MRhnE9E7eLemQC4zb6RVtf2oGQ3mX51kMeya218kEm5bsQPpRJElI9jwletFa8qYK5AFgaCHpll059RRHQTTE9MTNcN58P/Kf6Qb3KGpKNoaHTXoOD4U6LcXLWvdHONVB6AzGefxg2b/dvtaO4nd5lDooMBR3u9kWpopXkyAuf+fO/8je9JTxO6CDUtDjHRcR2mCEXWX6rculIAbNXOv1ccRYCTQ8aFtYnFlMSb1+QIAdyT6HHmeT5ktk3+XJRuEv0PJJvqHTo+j7tPngjiv5sPNutgGFlOFO90omTbzEkIT/D/LsgXbWm2QKYWejbLiuSINg+FiFiZN90doCf4aCpm2y1Q/9GzvK+eXcpqzPCGxOykE7EuPZK3+S/ZU2oaWL2uSpbKPtC0qxebrneR307aVEa7C85HCkTMMBzE4tjUr8h5HhLqtWvqmDcnBo3PfQhf9cEO6jQLd3qhEVJmcwKz/yOCfgPXlCbRqiKk7j94perpNCIalXtMI9ySYjJNjHsB4600=
api_key:
secure: WLjhHVAdUkDy6UdNWveTpZqphw9olN0alCpyPpU0cJjlN/hk7YxPP+YHOSVnMZaRZoHM0LL11jPKLf970nymBfvJHDWxKk71c/5xyTX8pBtSxmGmGy23a5g2VrHVMd2JxsI7NEAH500tlFz+01t4E3Steo0NpAkHR3Q51kj01gQy8IumsksfhRc9uvjZ7zjm99Yk4L+cxyei79v4esfpx7Bgm0HTfAAAG/19F+r0hvvFiV517SREDU/YtcX/rIjVepEy1uiLKEohnmYtVIRSA3Hjh6VlHVhdj2WDd/dYrxi/Ioysq2zOM9pZVLamO9asB1e6JrTEipWE9jSZXxsbnfFzuNLxcOjKv1wd3OzQKF/7pGKGiWRTv5Xm19D/FrXoE/ULs6bHcC/Ke8Gs2RxPlOCdvAFehCkyf5P4HOPCQdR7o8Yiuvt+5JWKBflElNbSd4nWgwUOo3Yv8vC4Vj53fwmM+Uqfu3IgYFWktYxCm4RWIKMfB/gtmjcF6QYFfjvEMwAvRfBV81kuynCsnubXWzQeBE/b3JOhBfpGciKCjKfy+tS6bZfFjCtQV98hMMiCPre8Y7PahHDYc65wU9Ake+ZE+dDaSbeV3DZ5JeifLJHzKW2J2dWeRANkOaiSwO9VBC9/rht5ulK5qQ1pB2+sKDToAaiKy6RMlB/HgIoYqsU=
on:
tags: true
all_branches: true
condition: "$MONGODB_VERSION = '3.0.8'"
repo: ParsePlatform/parse-server
+13
View File
@@ -1,5 +1,18 @@
## Parse Server Changelog
### 2.2.20
* New: Adds CloudCode handler for `beforeFind`, thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: RedisCacheAdapter for syncing schema, role and user caches across servers, thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: Latest master build available at `ParsePlatform/parse-server#latest`, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Better support for upgradeToRevocableSession with missing session token, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Removes babel-polyfill runtime dependency, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Cluster option now support a boolean value for automatically choosing the right number of processes, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Filenames now appear correctly, thanks to [Lama Chandrasena](https://github.com/lama-buddy)
* Fix: `_acl` is properly updated, thanks to [Steven Shipton](https://github.com/steven-supersolid)
Other fixes by [Mathias Rangel Wulff](https://github.com/mathiasrw)
### 2.2.19
* New: support for upgrading to revocable sessions, thanks to [Florent Vilmart](https://github.com/flovilmart)
+14 -1
View File
@@ -266,7 +266,7 @@ var server = ParseServer({
// if the account lockout policy is set and there are more than `threshold` number of failed login attempts then the `login` api call returns error code `Parse.Error.OBJECT_NOT_FOUND` with error message `Your account is locked due to multiple failed login attempts. Please try again after <duration> minute(s)`. After `duration` minutes of no login attempts, the application will allow the user to try login again.
accountLockout: {
duration: 5, // duration policy setting determines the number of minutes that a locked-out account remains locked out before automatically becoming unlocked. Set it to a value greater than 0 and less than 100000.
threshold: 3, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000.
threshold: 3, // threshold policy setting determines the number of failed sign-in attempts that will cause a user account to be locked. Set it to an integer value greater than 0 and less than 1000.
},
});
```
@@ -278,6 +278,7 @@ You can also use other email adapters contributed by the community such as:
- [parse-server-simple-ses-adapter](https://www.npmjs.com/package/parse-server-simple-ses-adapter)
- [parse-server-mailgun-adapter-template](https://www.npmjs.com/package/parse-server-mailgun-adapter-template)
- [parse-server-mailjet-adapter](https://www.npmjs.com/package/parse-server-mailjet-adapter)
- [simple-parse-smtp-adapter](https://www.npmjs.com/package/simple-parse-smtp-adapter)
### Using environment variables to configure Parse Server
@@ -325,6 +326,18 @@ If you believe you've found an issue with Parse Server, make sure these boxes ar
- [ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
# Want to ride the bleeding edge?
The `latest` branch in this repository is automatically maintained to be the last
commit to `master` to pass all tests, in the same form found on npm. It is
recommend to use builds deployed npm for many reasons, but if you want to use
the latest not-yet-released version of parse-server, you can do so by depending
directly on this branch:
```
npm install parseplatform/parse-server.git#latest
```
# Contributing
We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md).
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "2.2.19",
"version": "2.2.20",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -18,7 +18,6 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"babel-polyfill": "6.13.0",
"bcryptjs": "2.3.0",
"body-parser": "1.15.2",
"commander": "2.9.0",
@@ -28,8 +27,9 @@
"lodash": "4.15.0",
"lru-cache": "4.0.1",
"mime": "1.3.4",
"mongodb": "2.2.9",
"mongodb": "2.2.10",
"multer": "1.2.0",
"node-uuid": "^1.4.7",
"parse": "1.9.1",
"parse-server-fs-adapter": "1.0.1",
"parse-server-push-adapter": "1.1.0",
@@ -37,11 +37,11 @@
"parse-server-simple-mailgun-adapter": "1.0.0",
"pg-promise": "5.3.3",
"redis": "2.6.2",
"request": "2.74.0",
"request": "2.75.0",
"semver": "5.2.0",
"tv4": "1.2.7",
"winston": "2.2.0",
"winston-daily-rotate-file": "1.3.0",
"winston-daily-rotate-file": "1.3.1",
"ws": "1.1.1"
},
"devDependencies": {
@@ -57,7 +57,7 @@
"deep-diff": "0.3.4",
"gaze": "1.1.1",
"istanbul": "1.0.0-alpha.1",
"jasmine": "2.4.1",
"jasmine": "2.5.2",
"mongodb-runner": "3.4.0",
"nodemon": "1.10.0",
"request-promise": "4.1.1"
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh -e
# This script maintains a git branch which mirrors master but in a form that
# what will eventually be deployed to npm, allowing npm dependencies to use:
#
# "parse-server": "parseplatform/parse-server#latest"
#
# From: https://github.com/graphql/graphql-js/blob/master/resources/npm-git.sh
BUILD_DIR=latest
npm run build
mkdir -p $BUILD_DIR
cp package.json $BUILD_DIR/
cp README.md $BUILD_DIR/
cp LICENSE $BUILD_DIR/
cp PATENTS $BUILD_DIR/
cp CHANGELOG.md $BUILD_DIR/
cp -R lib $BUILD_DIR
cp -R bin $BUILD_DIR
cp -R public_html $BUILD_DIR
cp -R views $BUILD_DIR
cd $BUILD_DIR
git init
git config user.name "Travis CI"
git config user.email "github@fb.com"
git add .
git commit -m "Deploy master to LATEST branch"
git push --force --quiet "https://${GH_TOKEN}@github.com/parseplatform/parse-server.git" master:latest
+102
View File
@@ -0,0 +1,102 @@
"""
https://github.com/dmakhno/travis_after_all/blob/master/travis_after_all.py
https://github.com/graphql/graphql-js/blob/master/resources/travis_after_all.py
"""
import os
import json
import time
import logging
try:
import urllib.request as urllib2
except ImportError:
import urllib2
log = logging.getLogger("travis.leader")
log.addHandler(logging.StreamHandler())
log.setLevel(logging.INFO)
TRAVIS_JOB_NUMBER = 'TRAVIS_JOB_NUMBER'
TRAVIS_BUILD_ID = 'TRAVIS_BUILD_ID'
POLLING_INTERVAL = 'LEADER_POLLING_INTERVAL'
build_id = os.getenv(TRAVIS_BUILD_ID)
polling_interval = int(os.getenv(POLLING_INTERVAL, '5'))
#assume, first job is the leader
is_leader = lambda job_number: job_number.endswith('.1')
if not os.getenv(TRAVIS_JOB_NUMBER):
# seems even for builds with only one job, this won't get here
log.fatal("Don't use defining leader for build without matrix")
exit(1)
elif is_leader(os.getenv(TRAVIS_JOB_NUMBER)):
log.info("This is a leader")
else:
#since python is subprocess, env variables are exported back via file
with open(".to_export_back", "w") as export_var:
export_var.write("BUILD_MINION=YES")
log.info("This is a minion")
exit(0)
class MatrixElement(object):
def __init__(self, json_raw):
self.is_finished = json_raw['finished_at'] is not None
self.is_succeeded = json_raw['result'] == 0
self.number = json_raw['number']
self.is_leader = is_leader(self.number)
def matrix_snapshot():
"""
:return: Matrix List
"""
response = urllib2.build_opener().open("https://api.travis-ci.org/builds/{0}".format(build_id)).read()
raw_json = json.loads(response)
matrix_without_leader = [MatrixElement(element) for element in raw_json["matrix"]]
return matrix_without_leader
def wait_others_to_finish():
def others_finished():
"""
Dumps others to finish
Leader cannot finish, it is working now
:return: tuple(True or False, List of not finished jobs)
"""
snapshot = matrix_snapshot()
finished = [el.is_finished for el in snapshot if not el.is_leader]
return reduce(lambda a, b: a and b, finished), [el.number for el in snapshot if
not el.is_leader and not el.is_finished]
while True:
finished, waiting_list = others_finished()
if finished: break
log.info("Leader waits for minions {0}...".format(waiting_list)) # just in case do not get "silence timeout"
time.sleep(polling_interval)
try:
wait_others_to_finish()
final_snapshot = matrix_snapshot()
log.info("Final Results: {0}".format([(e.number, e.is_succeeded) for e in final_snapshot]))
BUILD_AGGREGATE_STATUS = 'BUILD_AGGREGATE_STATUS'
others_snapshot = [el for el in final_snapshot if not el.is_leader]
if reduce(lambda a, b: a and b, [e.is_succeeded for e in others_snapshot]):
os.environ[BUILD_AGGREGATE_STATUS] = "others_succeeded"
elif reduce(lambda a, b: a and b, [not e.is_succeeded for e in others_snapshot]):
log.error("Others Failed")
os.environ[BUILD_AGGREGATE_STATUS] = "others_failed"
else:
log.warn("Others Unknown")
os.environ[BUILD_AGGREGATE_STATUS] = "unknown"
#since python is subprocess, env variables are exported back via file
with open(".to_export_back", "w") as export_var:
export_var.write("BUILD_LEADER=YES {0}={1}".format(BUILD_AGGREGATE_STATUS, os.environ[BUILD_AGGREGATE_STATUS]))
except Exception as e:
log.fatal(e)
+107
View File
@@ -1174,3 +1174,110 @@ it('beforeSave should not affect fetched pointers', done => {
}
});
});
describe('beforeFind hooks', () => {
it('should add beforeFind trigger', (done) => {
Parse.Cloud.beforeFind('MyObject', (req, res) => {
let q = req.query;
expect(q instanceof Parse.Query).toBe(true);
let jsonQuery = q.toJSON();
expect(jsonQuery.where.key).toEqual('value');
expect(jsonQuery.where.some).toEqual({'$gt': 10});
expect(jsonQuery.include).toEqual('otherKey,otherValue');
expect(jsonQuery.limit).toEqual(100);
expect(jsonQuery.skip).toBe(undefined);
});
let query = new Parse.Query('MyObject');
query.equalTo('key', 'value');
query.greaterThan('some', 10);
query.include('otherKey');
query.include('otherValue');
query.find().then(() => {
done();
});
});
it('should use modify', (done) => {
Parse.Cloud.beforeFind('MyObject', (req) => {
let q = req.query;
q.equalTo('forced', true);
});
let obj0 = new Parse.Object('MyObject');
obj0.set('forced', false);
let obj1 = new Parse.Object('MyObject');
obj1.set('forced', true);
Parse.Object.saveAll([obj0, obj1]).then(() => {
let query = new Parse.Query('MyObject');
query.equalTo('forced', false);
query.find().then((results) => {
expect(results.length).toBe(1);
let firstResult = results[0];
expect(firstResult.get('forced')).toBe(true);
done();
});
});
});
it('should use the modified the query', (done) => {
Parse.Cloud.beforeFind('MyObject', (req) => {
let q = req.query;
let otherQuery = new Parse.Query('MyObject');
otherQuery.equalTo('forced', true);
return Parse.Query.or(q, otherQuery);
});
let obj0 = new Parse.Object('MyObject');
obj0.set('forced', false);
let obj1 = new Parse.Object('MyObject');
obj1.set('forced', true);
Parse.Object.saveAll([obj0, obj1]).then(() => {
let query = new Parse.Query('MyObject');
query.equalTo('forced', false);
query.find().then((results) => {
expect(results.length).toBe(2);
done();
});
});
});
it('should reject queries', (done) => {
Parse.Cloud.beforeFind('MyObject', (req) => {
return Promise.reject('Do not run that query');
});
let query = new Parse.Query('MyObject');
query.find().then(() => {
fail('should not succeed');
done();
}, (err) => {
expect(err.code).toBe(1);
expect(err.message).toEqual('Do not run that query');
done();
});
});
it('should handle empty where', (done) => {
Parse.Cloud.beforeFind('MyObject', (req) => {
let otherQuery = new Parse.Query('MyObject');
otherQuery.equalTo('some', true);
return Parse.Query.or(req.query, otherQuery);
});
rp.get({
url: 'http://localhost:8378/1/classes/MyObject',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
}).then((result) => {
done();
}, (err) =>  {
fail(err);
done();
});
});
})
+23 -4
View File
@@ -221,7 +221,7 @@ describe('parseObjectToMongoObjectForCreate', () => {
done();
});
it('writes the old ACL format in addition to rperm and wperm', (done) => {
it('writes the old ACL format in addition to rperm and wperm on create', (done) => {
var input = {
_rperm: ['*'],
_wperm: ['Kevin'],
@@ -229,10 +229,29 @@ describe('parseObjectToMongoObjectForCreate', () => {
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
expect(typeof output._acl).toEqual('object');
expect(output._acl["Kevin"].w).toBeTruthy();
expect(output._acl["Kevin"].r).toBeUndefined();
expect(output._acl['Kevin'].w).toBeTruthy();
expect(output._acl['Kevin'].r).toBeUndefined();
expect(output._rperm).toEqual(input._rperm);
expect(output._wperm).toEqual(input._wperm);
done();
})
});
it('writes the old ACL format in addition to rperm and wperm on update', (done) => {
var input = {
_rperm: ['*'],
_wperm: ['Kevin']
};
var output = transform.transformUpdate(null, input, { fields: {} });
var set = output.$set;
expect(typeof set).toEqual('object');
expect(typeof set._acl).toEqual('object');
expect(set._acl['Kevin'].w).toBeTruthy();
expect(set._acl['Kevin'].r).toBeUndefined();
expect(set._rperm).toEqual(input._rperm);
expect(set._wperm).toEqual(input._wperm);
done();
});
it('untransforms from _rperm and _wperm to ACL', (done) => {
var input = {
+20
View File
@@ -89,4 +89,24 @@ describe_only_db('mongo')('revocable sessions', () => {
done();
});
});
it('should not crash without session token #2720', done => {
rp.post({
url: Parse.serverURL+'/upgradeToRevocableSession',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Rest-API-Key': 'rest'
},
json: true
}).then((res) => {
fail('should not be able to upgrade a bad token');
}, (response) => {
expect(response.statusCode).toBe(404);
expect(response.error).not.toBeUndefined();
expect(response.error.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
expect(response.error.error).toEqual('invalid session');
}).then(() => {
done();
});
});
})
+5
View File
@@ -24,6 +24,7 @@ var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAda
const GridStoreAdapter = require('../src/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
const FSAdapter = require('parse-server-fs-adapter');
const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter');
const RedisCacheAdapter = require('../src/Adapters/Cache/RedisCacheAdapter').default;
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
@@ -101,6 +102,10 @@ var defaultConfiguration = {
}
};
if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
defaultConfiguration.cacheAdapter = new RedisCacheAdapter();
}
let openConnections = {};
// Set up a default API server for testing with default configuration.
+69
View File
@@ -0,0 +1,69 @@
import redis from 'redis';
import logger from '../../logger';
function debug() {
logger.debug.apply(logger, ['RedisCacheAdapter', ...arguments]);
}
export class RedisCacheAdapter {
constructor(ctx) {
this.client = redis.createClient(ctx);
this.p = Promise.resolve();
}
get(key) {
debug('get', key);
this.p = this.p.then(() => {
return new Promise((resolve, _) => {
this.client.get(key, function(err, res) {
debug('-> get', key, res);
if(!res) {
return resolve(null);
}
resolve(JSON.parse(res));
});
});
});
return this.p;
}
put(key, value, ttl) {
value = JSON.stringify(value);
debug('put', key, value, ttl);
this.p = this.p.then(() => {
return new Promise((resolve, _) => {
this.client.set(key, value, function(err, res) {
resolve();
});
});
});
return this.p;
}
del(key) {
debug('del', key);
this.p = this.p.then(() => {
return new Promise((resolve, _) => {
this.client.del(key, function(err, res) {
resolve();
});
});
});
return this.p;
}
clear() {
debug('clear');
this.p = this.p.then(() => {
return new Promise((resolve, _) => {
this.client.flushall(function(err, res) {
resolve();
});
});
});
return this.p;
}
}
export default RedisCacheAdapter;
+4 -4
View File
@@ -99,7 +99,7 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
}
const transformInteriorValue = restValue => {
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) {
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.indexOf('$') >= 0 || key.indexOf('.') >= 0)) {
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
}
// Handle atomic values
@@ -293,7 +293,7 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) =>
}
// Handle normal objects by recursing
if (Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) {
if (Object.keys(restValue).some(key => key.indexOf('$') >= 0 || key.indexOf('.') >= 0)) {
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
}
value = _.mapValues(restValue, transformInteriorValue);
@@ -330,8 +330,8 @@ const parseObjectToMongoObjectForCreate = (className, restCreate, schema) => {
// Main exposed method to help update old objects.
const transformUpdate = (className, restUpdate, parseFormatSchema) => {
let mongoUpdate = {};
let acl = addLegacyACL(restUpdate)._acl;
if (acl) {
let acl = addLegacyACL(restUpdate);
if (acl._rperm || acl._wperm || acl._acl) {
mongoUpdate.$set = {};
if (acl._rperm) {
mongoUpdate.$set._rperm = acl._rperm;
@@ -415,7 +415,7 @@ export class PostgresStorageAdapter {
return toParseSchema(schema)
})
.catch((err) => {
if (err.code === PostgresUniqueIndexViolationError && err.detail.includes(className)) {
if (err.code === PostgresUniqueIndexViolationError && err.detail.indexOf(className) >= 0) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`)
}
throw err;
@@ -445,7 +445,7 @@ export class PostgresStorageAdapter {
relations.push(fieldName)
return;
}
if (['_rperm', '_wperm'].includes(fieldName)) {
if (['_rperm', '_wperm'].indexOf(fieldName) >=0 ) {
parseType.contents = { type: 'String' };
}
valuesArray.push(fieldName);
@@ -678,7 +678,7 @@ export class PostgresStorageAdapter {
valuesArray.push(object[fieldName].objectId);
break;
case 'Array':
if (['_rperm', '_wperm'].includes(fieldName)) {
if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) {
valuesArray.push(object[fieldName]);
} else {
valuesArray.push(JSON.stringify(object[fieldName]));
@@ -707,7 +707,7 @@ export class PostgresStorageAdapter {
let initialValues = valuesArray.map((val, index) => {
let termination = '';
let fieldName = columnsArray[index];
if (['_rperm','_wperm'].includes(fieldName)) {
if (['_rperm','_wperm'].indexOf(fieldName) >= 0) {
termination = '::text[]';
} else if (schema.fields[fieldName] && schema.fields[fieldName].type === 'Array') {
termination = '::jsonb';
@@ -1031,9 +1031,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.includes(constraintName)) {
if (error.code === PostgresDuplicateRelationError && error.message.indexOf(constraintName) >= 0) {
// Index already exists. Ignore error.
} else if (error.code === PostgresUniqueIndexViolationError && error.message.includes(constraintName)) {
} else if (error.code === PostgresUniqueIndexViolationError && error.message.indexOf(constraintName) >= 0) {
// 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 {
+2 -1
View File
@@ -5,6 +5,7 @@ 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}-.*");
@@ -26,7 +27,7 @@ export class FilesController extends AdaptableController {
contentType = mime.lookup(filename);
}
filename = randomHexString(32) + '_' + filename;
filename = uuid.v4() + '_' + filename;
var location = this.adapter.getFileLocation(config, filename);
return this.adapter.createFile(filename, data, contentType).then(() => {
+32 -19
View File
@@ -323,15 +323,20 @@ export default class SchemaController {
}
reloadData(options = {clearCache: false}) {
let promise = Promise.resolve();
if (options.clearCache) {
this._cache.clear();
promise = promise.then(() => {
return this._cache.clear();
});
}
if (this.reloadDataPromise && !options.clearCache) {
return this.reloadDataPromise;
}
this.data = {};
this.perms = {};
this.reloadDataPromise = this.getAllClasses(options)
this.reloadDataPromise = promise.then(() => {
return this.getAllClasses(options);
})
.then(allSchemas => {
allSchemas.forEach(schema => {
this.data[schema.className] = injectDefaultSchema(schema).fields;
@@ -355,10 +360,13 @@ export default class SchemaController {
}
getAllClasses(options = {clearCache: false}) {
let promise = Promise.resolve();
if (options.clearCache) {
this._cache.clear();
promise = this._cache.clear();
}
return this._cache.getAllClasses().then((allClasses) => {
return promise.then(() => {
return this._cache.getAllClasses()
}).then((allClasses) => {
if (allClasses && allClasses.length && !options.clearCache) {
return Promise.resolve(allClasses);
}
@@ -373,22 +381,25 @@ export default class SchemaController {
}
getOneSchema(className, allowVolatileClasses = false, options = {clearCache: false}) {
let promise = Promise.resolve();
if (options.clearCache) {
this._cache.clear();
promise = this._cache.clear();
}
if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) {
return Promise.resolve(this.data[className]);
}
return this._cache.getOneSchema(className).then((cached) => {
if (cached && !options.clearCache) {
return Promise.resolve(cached);
return promise.then(() => {
if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) {
return Promise.resolve(this.data[className]);
}
return this._dbAdapter.getClass(className)
.then(injectDefaultSchema)
.then((result) => {
return this._cache.setOneSchema(className, result).then(() => {
return result;
})
return this._cache.getOneSchema(className).then((cached) => {
if (cached && !options.clearCache) {
return Promise.resolve(cached);
}
return this._dbAdapter.getClass(className)
.then(injectDefaultSchema)
.then((result) => {
return this._cache.setOneSchema(className, result).then(() => {
return result;
})
});
});
});
}
@@ -409,8 +420,9 @@ export default class SchemaController {
return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields, classLevelPermissions, className }))
.then(convertAdapterSchemaToParseSchema)
.then((res) => {
this._cache.clear();
return res;
return this._cache.clear().then(() => {
return Promise.resolve(res);
});
})
.catch(error => {
if (error && error.code === Parse.Error.DUPLICATE_VALUE) {
@@ -508,6 +520,7 @@ 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');
});
-4
View File
@@ -9,10 +9,6 @@ var batch = require('./batch'),
path = require('path'),
authDataManager = require('./authDataManager');
if (!global._babelPolyfill) {
require('babel-polyfill');
}
import defaults from './defaults';
import * as logging from './logger';
import AppCache from './cache';
+1 -1
View File
@@ -262,7 +262,7 @@ RestWrite.prototype.findUsersWithAuthData = function(authData) {
memo.push(query);
return memo;
}, []).filter((q) => {
return typeof q !== undefined;
return typeof q !== 'undefined';
});
let findPromise = Promise.resolve([]);
+5
View File
@@ -54,6 +54,11 @@ export class SessionsRouter extends ClassesRouter {
const config = req.config;
const masterAuth = Auth.master(config)
const user = req.auth.user;
// Issue #2720
// Calling without a session token would result in a not found user
if (!user) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'invalid session');
}
const expiresAt = config.generateSessionExpiresAt();
const sessionData = {
sessionToken: 'r:' + newToken(),
+10 -1
View File
@@ -8,6 +8,15 @@ function numberParser(key) {
}
}
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;
@@ -229,6 +238,6 @@ export default {
},
"cluster": {
help: "Run with cluster, optionally set the number of processes default to os.cpus().length",
action: numberParser("cluster"),
action: numberOrBoolParser("cluster")
}
};
+5
View File
@@ -45,6 +45,11 @@ ParseCloud.afterDelete = function(parseClass, handler) {
triggers.addTrigger(triggers.Types.afterDelete, className, handler, Parse.applicationId);
};
ParseCloud.beforeFind = function(parseClass, handler) {
var className = getClassName(parseClass);
triggers.addTrigger(triggers.Types.beforeFind, className, handler, Parse.applicationId);
};
ParseCloud._removeAllHooks = () => {
triggers._unregisterAll();
}
+2 -1
View File
@@ -3,6 +3,7 @@ import S3Adapter from 'parse-server-s3-adapter'
import FileSystemAdapter from 'parse-server-fs-adapter'
import InMemoryCacheAdapter from './Adapters/Cache/InMemoryCacheAdapter'
import NullCacheAdapter from './Adapters/Cache/NullCacheAdapter'
import RedisCacheAdapter from './Adapters/Cache/RedisCacheAdapter'
import TestUtils from './TestUtils';
import { useExternal } from './deprecated';
import { getLogger } from './logger';
@@ -22,4 +23,4 @@ Object.defineProperty(module.exports, 'logger', {
});
export default ParseServer;
export { S3Adapter, GCSAdapter, FileSystemAdapter, InMemoryCacheAdapter, NullCacheAdapter, TestUtils, _ParseServer as ParseServer };
export { S3Adapter, GCSAdapter, FileSystemAdapter, InMemoryCacheAdapter, NullCacheAdapter, RedisCacheAdapter, TestUtils, _ParseServer as ParseServer };
+6 -2
View File
@@ -17,8 +17,12 @@ var triggers = require('./triggers');
// Returns a promise for an object with optional keys 'results' and 'count'.
function find(config, auth, className, restWhere, restOptions, clientSDK) {
enforceRoleSecurity('find', className, auth);
let query = new RestQuery(config, auth, className, restWhere, restOptions, clientSDK);
return query.execute();
return triggers.maybeRunQueryTrigger(triggers.Types.beforeFind, className, restWhere, restOptions, config, auth).then((result) => {
restWhere = result.restWhere || restWhere;
restOptions = result.restOptions || restOptions;
let query = new RestQuery(config, auth, className, restWhere, restOptions, clientSDK);
return query.execute();
});
}
// get is just like find but only queries an objectId.
+85 -1
View File
@@ -7,7 +7,8 @@ export const Types = {
beforeSave: 'beforeSave',
afterSave: 'afterSave',
beforeDelete: 'beforeDelete',
afterDelete: 'afterDelete'
afterDelete: 'afterDelete',
beforeFind: 'beforeFind'
};
const baseStore = function() {
@@ -154,6 +155,29 @@ export function getRequestObject(triggerType, auth, parseObject, originalParseOb
return request;
}
export function getRequestQueryObject(triggerType, auth, query, config) {
var request = {
triggerName: triggerType,
query: query,
master: false,
log: config.loggerController
};
if (!auth) {
return request;
}
if (auth.isMaster) {
request['master'] = true;
}
if (auth.user) {
request['user'] = auth.user;
}
if (auth.installationId) {
request['installationId'] = auth.installationId;
}
return request;
}
// Creates the response object, and uses the request object to pass data
// The API will call this with REST API formatted objects, this will
// transform them to Parse.Object instances expected by Cloud Code.
@@ -216,6 +240,66 @@ function logTriggerErrorBeforeHook(triggerType, className, input, auth, error) {
});
}
export function maybeRunQueryTrigger(triggerType, className, restWhere, restOptions, config, auth) {
let trigger = getTrigger(className, triggerType, config.applicationId);
if (!trigger) {
return Promise.resolve({
restWhere,
restOptions
});
}
let parseQuery = new Parse.Query(className);
if (restWhere) {
parseQuery._where = restWhere;
}
if (restOptions) {
if (restOptions.include && restOptions.include.length > 0) {
parseQuery._include = restOptions.include.split(',');
}
if (restOptions.skip) {
parseQuery._skip = restOptions.skip;
}
if (restOptions.limit) {
parseQuery._limit = restOptions.limit;
}
}
let requestObject = getRequestQueryObject(triggerType, auth, parseQuery, config);
return Promise.resolve().then(() => {
return trigger(requestObject);
}).then((result) => {
let queryResult = parseQuery;
if (result && result instanceof Parse.Query) {
queryResult = result;
}
let jsonQuery = queryResult.toJSON();
if (jsonQuery.where) {
restWhere = jsonQuery.where;
}
if (jsonQuery.limit) {
restOptions = restOptions || {};
restOptions.limit = jsonQuery.limit;
}
if (jsonQuery.skip) {
restOptions = restOptions || {};
restOptions.skip = jsonQuery.skip;
}
if (jsonQuery.include) {
restOptions = restOptions || {};
restOptions.include = jsonQuery.include;
}
return {
restWhere,
restOptions
};
}, (err) => {
if (typeof err === 'string') {
throw new Parse.Error(1, err);
} else {
throw err;
}
});
}
// To be used as part of the promise chain when saving/deleting an object
// Will resolve successfully if no trigger is configured