mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c2f1d583b | ||
|
|
3b40d2227d | ||
|
|
c5e8470e36 | ||
|
|
9bff44b446 | ||
|
|
f4422c491e | ||
|
|
82fb06f9c9 | ||
|
|
32d8c36ff5 | ||
|
|
fd862950e2 | ||
|
|
57da2def1d | ||
|
|
4dcbc4245b | ||
|
|
642e2ffe12 | ||
|
|
d2b04ddd92 | ||
|
|
33173c4d9b | ||
|
|
ff0c04ffa3 | ||
|
|
df274dcca9 | ||
|
|
a619119684 | ||
|
|
77ed10f57b | ||
|
|
a9ebc2b9af | ||
|
|
8fe3265c74 | ||
|
|
95550f4c77 | ||
|
|
fc47f9b84f | ||
|
|
c0f86ae1d1 | ||
|
|
ad244d6654 | ||
|
|
bfd0c4bf2f | ||
|
|
d36c7c7f90 | ||
|
|
10232673d9 | ||
|
|
c0e3672e32 | ||
|
|
2c357df33e | ||
|
|
3999de7e0b | ||
|
|
a3e5b20536 | ||
|
|
bb1641419f | ||
|
|
0b69700f18 | ||
|
|
ac2695e196 | ||
|
|
098d085621 | ||
|
|
a1f5f37af7 | ||
|
|
d48b30c734 | ||
|
|
3b5dff195f | ||
|
|
fad7b46c65 | ||
|
|
04588bccb9 | ||
|
|
b1017ac17c | ||
|
|
ebf65bfc5d | ||
|
|
22801d2d8f | ||
|
|
8380107e6b | ||
|
|
a77b0a238f | ||
|
|
6c81635188 | ||
|
|
fed40128a5 | ||
|
|
ee151de56a | ||
|
|
b0bcbd8731 | ||
|
|
06be78802e | ||
|
|
0fd8493929 | ||
|
|
710848938f | ||
|
|
1a5c05b6a8 | ||
|
|
306826fe0f | ||
|
|
a3230011eb | ||
|
|
6abf29dd97 | ||
|
|
c36ef99368 |
@@ -6,7 +6,7 @@
|
||||
"presets": [
|
||||
["env", {
|
||||
"targets": {
|
||||
"node": "6.11.4"
|
||||
"node": "8"
|
||||
}
|
||||
}]
|
||||
]
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: Report an issue
|
||||
about: Report an issue on parse-server
|
||||
|
||||
---
|
||||
|
||||
We use GitHub Issues for bugs.
|
||||
|
||||
If you have a non-bug question, ask on Stack Overflow or Server Fault:
|
||||
- https://stackoverflow.com/questions/tagged/parse.com
|
||||
- https://serverfault.com/tags/parse
|
||||
|
||||
If you have a vulnerability disclosure, please follow our policy available here https://github.com/parse-community/parse-server/blob/master/SECURITY.md
|
||||
|
||||
You may also search through existing issues before opening a new one: https://github.com/parse-community/parse-server/issues?utf8=%E2%9C%93&q=is%3Aissue
|
||||
|
||||
--- Please use this template. If you don't use this template, your issue may be closed without comment. ---
|
||||
|
||||
### Issue Description
|
||||
|
||||
Describe your issue in as much detail as possible.
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
Please include a detailed list of steps that reproduce the issue. Include curl commands when applicable.
|
||||
|
||||
#### Expected Results
|
||||
|
||||
What you expected to happen.
|
||||
|
||||
#### Actual Outcome
|
||||
|
||||
What is happening instead.
|
||||
|
||||
### Environment Setup
|
||||
|
||||
- **Server**
|
||||
- parse-server version (Be specific! Don't say 'latest'.) : [FILL THIS OUT]
|
||||
- Operating System: [FILL THIS OUT]
|
||||
- Hardware: [FILL THIS OUT]
|
||||
- Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): [FILL THIS OUT]
|
||||
|
||||
- **Database**
|
||||
- MongoDB version: [FILL THIS OUT]
|
||||
- Storage engine: [FILL THIS OUT]
|
||||
- Hardware: [FILL THIS OUT]
|
||||
- Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): [FILL THIS OUT]
|
||||
|
||||
### Logs/Trace
|
||||
|
||||
Include all relevant logs. You can turn on additional logging by configuring VERBOSE=1 in your environment.
|
||||
+13
-6
@@ -7,8 +7,9 @@ services:
|
||||
- docker
|
||||
addons:
|
||||
postgresql: '9.5'
|
||||
apt_packages:
|
||||
- postgresql-9.5-postgis-2.3
|
||||
apt:
|
||||
packages:
|
||||
- postgresql-9.5-postgis-2.3
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
@@ -25,32 +26,38 @@ stage: test
|
||||
env:
|
||||
global:
|
||||
- COVERAGE_OPTION='./node_modules/.bin/nyc'
|
||||
- NODE_VERSION=6.11.4
|
||||
- NODE_VERSION=8.11.2
|
||||
matrix:
|
||||
- MONGODB_VERSION=3.2.13
|
||||
- MONGODB_VERSION=3.4.4
|
||||
- PARSE_SERVER_TEST_DB=postgres
|
||||
- PARSE_SERVER_TEST_CACHE=redis
|
||||
- NODE_VERSION=8.7
|
||||
- NODE_VERSION=stable
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: NODE_VERSION=stable
|
||||
before_install:
|
||||
- nvm install $NODE_VERSION
|
||||
- nvm use $NODE_VERSION
|
||||
- npm install -g greenkeeper-lockfile@1
|
||||
before_script:
|
||||
- node -e 'require("./lib/index.js")'
|
||||
- psql -c 'create database parse_server_postgres_adapter_test_database;' -U postgres
|
||||
- psql -c 'CREATE EXTENSION postgis;' -U postgres -d parse_server_postgres_adapter_test_database
|
||||
- psql -c 'CREATE EXTENSION postgis_topology;' -U postgres -d parse_server_postgres_adapter_test_database
|
||||
- silent=1 mongodb-runner --start
|
||||
- greenkeeper-lockfile-update
|
||||
script:
|
||||
- npm run coverage
|
||||
after_script:
|
||||
- greenkeeper-lockfile-upload
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
jobs:
|
||||
include:
|
||||
# release on github latest branch
|
||||
- stage: release
|
||||
node_js: '6.11.4'
|
||||
node_js: '8.10'
|
||||
env:
|
||||
before_script: skip
|
||||
after_script: skip
|
||||
@@ -58,7 +65,7 @@ jobs:
|
||||
deploy:
|
||||
- provider: script
|
||||
skip_cleanup: true
|
||||
script: ./resources/npm-git.sh
|
||||
script: ./resources/npm-git.sh && npm install -g https://github.com/parse-community/parse-server#latest
|
||||
on:
|
||||
branch:
|
||||
- master
|
||||
|
||||
+72
-2
@@ -1,7 +1,77 @@
|
||||
## Parse Server Changelog
|
||||
|
||||
### master
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/2.7.4...master)
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/2.8.2...master)
|
||||
|
||||
### 2.8.2
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/2.8.2...2.8.1)
|
||||
|
||||
##### Bug Fixes:
|
||||
* Ensure legacy users without ACL's are not locked out, thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
|
||||
#### Improvements:
|
||||
* Use common HTTP agent to increase webhooks performance, thanks to [Tyler Brock](https://github.com/TylerBrock)
|
||||
* Adds withinPolygon support for Polygon objects, thanks to [Mads Bjerre](https://github.com/madsb)
|
||||
|
||||
#### Dependency Updates:
|
||||
* [ws@5.2.0](https://www.npmjs.com/package/ws)
|
||||
* [commander@2.15.1](https://www.npmjs.com/package/commander)
|
||||
* [nodemon@1.17.5](https://www.npmjs.com/package/nodemon)
|
||||
|
||||
##### Devevelopment Dependencies Updates:
|
||||
* [flow-bin@0.73.0](https://www.npmjs.com/package/flow-bin)
|
||||
* [cross-env@5.1.6](https://www.npmjs.com/package/cross-env)
|
||||
* [gaze@1.1.3](https://www.npmjs.com/package/gaze)
|
||||
* [deepcopy@1.0.0](https://www.npmjs.com/package/deepcopy)
|
||||
* [deep-diff@1.0.1](https://www.npmjs.com/package/deep-diff)
|
||||
|
||||
|
||||
### 2.8.1
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/2.8.1...2.8.0)
|
||||
|
||||
Ensure all the files are properly exported to the final package.
|
||||
|
||||
### 2.8.0
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/2.8.0...2.7.4)
|
||||
|
||||
#### New Features
|
||||
* Adding Mongodb element to add `arrayMatches` the #4762 (#4766), thanks to [Jérémy Piednoel](https://github.com/jeremypiednoel)
|
||||
* Adds ability to Lockout users (#4749), thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
|
||||
#### Bug fixes:
|
||||
* Fixes issue when using afterFind with relations (#4752), thanks to [Florent Vilmart](https://github.com/flovilmart)
|
||||
* New query condition support to match all strings that starts with some other given strings (#3864), thanks to [Eduard Bosch Bertran](https://github.com/eduardbosch)
|
||||
* Allow creation of indices on default fields (#4738), thanks to [Claire Neveu](https://github.com/ClaireNeveu)
|
||||
* Purging empty class (#4676), thanks to [Diamond Lewis](https://github.com/dplewis)
|
||||
* Postgres: Fixes issues comparing to zero or false (#4667), thanks to [Diamond Lewis](https://github.com/dplewis)
|
||||
* Fix Aggregate Match Pointer (#4643), thanks to [Diamond Lewis](https://github.com/dplewis)
|
||||
|
||||
#### Improvements:
|
||||
* Allow Parse.Error when returning from Cloud Code (#4695), thanks to [Saulo Tauil](https://github.com/saulogt)
|
||||
* Fix typo: "requrest" -> "request" (#4761), thanks to [Joseph Frazier](https://github.com/josephfrazier)
|
||||
* Send version for Vkontakte API (#4725), thanks to [oleg](https://github.com/alekoleg)
|
||||
* Ensure we respond with invalid password even if email is unverified (#4708), thanks to [dblythy](https://github.com/dblythy)
|
||||
* Add _password_history to default sensitive data (#4699), thanks to [Jong Eun Lee](https://github.com/yomybaby)
|
||||
* Check for node version in postinstall script (#4657), thanks to [Diamond Lewis](https://github.com/dplewis)
|
||||
* Remove FB Graph API version from URL to use the oldest non deprecated version, thanks to [SebC](https://github.com/SebC99)
|
||||
|
||||
#### Dependency Updates:
|
||||
* [@parse/push-adapter@2.0.3](https://www.npmjs.com/package/@parse/push-adapter)
|
||||
* [@parse/simple-mailgun-adapter@1.0.2](https://www.npmjs.com/package/@parse/simple-mailgun-adapter)
|
||||
* [uws@10.148.0](https://www.npmjs.com/package/uws)
|
||||
* [body-parser@1.18.3](https://www.npmjs.com/package/body-parser)
|
||||
* [mime@2.3.1](https://www.npmjs.com/package/mime)
|
||||
* [request@2.85.0](https://www.npmjs.com/package/request)
|
||||
* [mongodb@3.0.7](https://www.npmjs.com/package/mongodb)
|
||||
* [bcrypt@2.0.1](https://www.npmjs.com/package/bcrypt)
|
||||
* [ws@5.1.1](https://www.npmjs.com/package/ws)
|
||||
|
||||
##### Devevelopment Dependencies Updates:
|
||||
* [cross-env@5.1.5](https://www.npmjs.com/package/cross-env)
|
||||
* [flow-bin@0.71.0](https://www.npmjs.com/package/flow-bin)
|
||||
* [deep-diff@1.0.0](https://www.npmjs.com/package/deep-diff)
|
||||
* [nodemon@1.17.3](https://www.npmjs.com/package/nodemon)
|
||||
|
||||
|
||||
### 2.7.4
|
||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/2.7.4...2.7.3)
|
||||
@@ -1079,7 +1149,7 @@ Other fixes by [Mathias Rangel Wulff](https://github.com/mathiasrw)
|
||||
* Fix: Improve compatability of cloud code beforeSave hook for newly created object
|
||||
* Fix: ACL creation for master key only objects
|
||||
* Fix: Allow uploading files without Content-Type
|
||||
* Fix: Add features to http requrest to match Parse.com
|
||||
* Fix: Add features to http request to match Parse.com
|
||||
* Fix: Bugs in development script when running from locations other than project root
|
||||
* Fix: Can pass query constraints in URL
|
||||
* Fix: Objects with legacy "_tombstone" key now don't cause issues.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:boron
|
||||
FROM node:carbon
|
||||
|
||||
RUN mkdir -p /parse-server
|
||||
COPY ./ /parse-server/
|
||||
|
||||
Generated
+12921
File diff suppressed because it is too large
Load Diff
+23
-21
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "2.7.4",
|
||||
"version": "2.8.2",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ParsePlatform/parse-server"
|
||||
"url": "https://github.com/parse-community/parse-server"
|
||||
},
|
||||
"files": [
|
||||
"bin/",
|
||||
@@ -14,35 +14,36 @@
|
||||
"views/",
|
||||
"LICENSE",
|
||||
"PATENTS",
|
||||
"postinstall.js",
|
||||
"README.md"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@parse/fs-files-adapter": "1.0.1",
|
||||
"@parse/push-adapter": "2.0.2",
|
||||
"@parse/push-adapter": "3.0.0-alpha2",
|
||||
"@parse/s3-files-adapter": "1.2.1",
|
||||
"@parse/simple-mailgun-adapter": "1.0.1",
|
||||
"@parse/simple-mailgun-adapter": "1.0.2",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.18.2",
|
||||
"commander": "2.15.0",
|
||||
"deepcopy": "0.6.3",
|
||||
"body-parser": "1.18.3",
|
||||
"commander": "2.15.1",
|
||||
"deepcopy": "1.0.0",
|
||||
"express": "4.16.2",
|
||||
"intersect": "1.0.1",
|
||||
"lodash": "4.17.5",
|
||||
"lru-cache": "4.1.2",
|
||||
"mime": "2.2.0",
|
||||
"mongodb": "3.0.4",
|
||||
"mime": "2.3.1",
|
||||
"mongodb": "3.0.7",
|
||||
"multer": "1.3.0",
|
||||
"parse": "1.11.1",
|
||||
"pg-promise": "8.2.1",
|
||||
"pg-promise": "8.4.0",
|
||||
"redis": "2.8.0",
|
||||
"request": "2.83.0",
|
||||
"request": "2.85.0",
|
||||
"semver": "5.5.0",
|
||||
"tv4": "1.3.0",
|
||||
"uuid": "^3.1.0",
|
||||
"winston": "2.4.1",
|
||||
"winston-daily-rotate-file": "1.7.2",
|
||||
"ws": "5.0.0"
|
||||
"ws": "5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "6.26.0",
|
||||
@@ -52,16 +53,16 @@
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-preset-env": "1.6.1",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"cross-env": "5.1.4",
|
||||
"deep-diff": "0.3.8",
|
||||
"cross-env": "5.1.6",
|
||||
"deep-diff": "1.0.1",
|
||||
"eslint": "^4.9.0",
|
||||
"eslint-plugin-flowtype": "^2.39.1",
|
||||
"flow-bin": "^0.67.1",
|
||||
"gaze": "1.1.2",
|
||||
"flow-bin": "^0.73.0",
|
||||
"gaze": "1.1.3",
|
||||
"jasmine": "3.1.0",
|
||||
"jasmine-spec-reporter": "^4.1.0",
|
||||
"mongodb-runner": "3.6.1",
|
||||
"nodemon": "1.17.1",
|
||||
"nodemon": "1.17.5",
|
||||
"nyc": "^11.0.2",
|
||||
"request-promise": "4.2.2"
|
||||
},
|
||||
@@ -73,17 +74,18 @@
|
||||
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 TESTING=1 jasmine",
|
||||
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 TESTING=1 nyc jasmine",
|
||||
"start": "node ./bin/parse-server",
|
||||
"prepublish": "npm run build"
|
||||
"prepare": "npm run build",
|
||||
"postinstall": "node -p 'require(\"./postinstall.js\")()'"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.11.4"
|
||||
"node": ">= 8"
|
||||
},
|
||||
"bin": {
|
||||
"parse-server": "./bin/parse-server"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bcrypt": "1.0.3",
|
||||
"uws": "^9.14.0"
|
||||
"bcrypt": "2.0.1",
|
||||
"uws": "^10.148.0"
|
||||
},
|
||||
"collective": {
|
||||
"type": "opencollective",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
const pkg = require('./package.json');
|
||||
|
||||
const version = parseFloat(process.version.substr(1));
|
||||
const minimum = parseFloat(pkg.engines.node.match(/\d+/g).join('.'));
|
||||
|
||||
module.exports = function () {
|
||||
const openCollective = `
|
||||
1111111111
|
||||
1111111111111111
|
||||
1111111111111111111111
|
||||
11111111111111111111111111
|
||||
111111111111111 11111111
|
||||
1111111111111 111111
|
||||
1111111111111 111111111 111111
|
||||
111111111111 11111111111 111111
|
||||
1111111111111 11111111111 111111
|
||||
1111111111111 1111111111 111111
|
||||
1111111111111111111111111 1111111
|
||||
11111111 11111111
|
||||
111111 1111111111111111111
|
||||
11111 11111 111111111111111111
|
||||
11111 11111111111111111
|
||||
111111 111111111111111111
|
||||
11111111111111111111111111
|
||||
1111111111111111111111
|
||||
111111111111111111
|
||||
11111111111
|
||||
|
||||
|
||||
Thanks for installing parse 🙏
|
||||
Please consider donating to our open collective
|
||||
to help us maintain this package.
|
||||
|
||||
👉 https://opencollective.com/parse-server
|
||||
|
||||
`;
|
||||
process.stdout.write(openCollective);
|
||||
if (version >= minimum) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const errorMessage = `
|
||||
⚠️ parse-server requires at least node@${minimum}!
|
||||
You have node@${version}
|
||||
|
||||
`;
|
||||
|
||||
process.stdout.write(errorMessage);
|
||||
process.exit(1);
|
||||
};
|
||||
@@ -25,6 +25,7 @@ cp README.md $BUILD_DIR/
|
||||
cp LICENSE $BUILD_DIR/
|
||||
cp PATENTS $BUILD_DIR/
|
||||
cp CHANGELOG.md $BUILD_DIR/
|
||||
cp postinstall.js $BUILD_DIR/
|
||||
cp -R lib $BUILD_DIR
|
||||
cp -R bin $BUILD_DIR
|
||||
cp -R public_html $BUILD_DIR
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('InMemoryCache', function() {
|
||||
let value = cache.get(KEY);
|
||||
expect(value).toEqual(VALUE);
|
||||
|
||||
wait(BASE_TTL.ttl * 2).then(() => {
|
||||
wait(BASE_TTL.ttl * 10).then(() => {
|
||||
value = cache.get(KEY)
|
||||
expect(value).toEqual(null);
|
||||
done();
|
||||
|
||||
@@ -353,6 +353,41 @@ describe('parseObjectToMongoObjectForCreate', () => {
|
||||
expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z');
|
||||
done();
|
||||
});
|
||||
|
||||
it('$regex in $all list', (done) => {
|
||||
const input = {
|
||||
arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$regex: '^\\Qtwo\\E'}, {$regex: '^\\Qthree\\E'}]},
|
||||
};
|
||||
const outputValue = {
|
||||
arrayField: {'$all': [/^\Qone\E/, /^\Qtwo\E/, /^\Qthree\E/]},
|
||||
};
|
||||
|
||||
const output = transform.transformWhere(null, input);
|
||||
jequal(outputValue.arrayField, output.arrayField);
|
||||
done();
|
||||
});
|
||||
|
||||
it('$regex in $all list must be { $regex: "string" }', (done) => {
|
||||
const input = {
|
||||
arrayField: {'$all': [{$regex: 1}]},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
transform.transformWhere(null, input)
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
|
||||
it('all values in $all must be $regex (start with string) or non $regex (start with string)', (done) => {
|
||||
const input = {
|
||||
arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$unknown: '^\\Qtwo\\E'}]},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
transform.transformWhere(null, input)
|
||||
}).toThrow();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformUpdate', () => {
|
||||
|
||||
@@ -1493,6 +1493,11 @@ describe('miscellaneous', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('purge empty class', (done) => {
|
||||
const testSchema = new Parse.Schema('UnknownClass');
|
||||
testSchema.purge().then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it('should not update schema beforeSave #2672', (done) => {
|
||||
Parse.Cloud.beforeSave('MyObject', (request, response) => {
|
||||
if (request.object.get('secret')) {
|
||||
|
||||
@@ -496,6 +496,116 @@ describe('Parse.GeoPoint testing', () => {
|
||||
}, done.fail);
|
||||
});
|
||||
|
||||
it('supports withinPolygon Polygon object', (done) => {
|
||||
const inbound = new Parse.GeoPoint(1.5, 1.5);
|
||||
const onbound = new Parse.GeoPoint(10, 10);
|
||||
const outbound = new Parse.GeoPoint(20, 20);
|
||||
const obj1 = new Parse.Object('Polygon', {location: inbound});
|
||||
const obj2 = new Parse.Object('Polygon', {location: onbound});
|
||||
const obj3 = new Parse.Object('Polygon', {location: outbound});
|
||||
const polygon = {
|
||||
__type: 'Polygon',
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[10, 0],
|
||||
[10, 10],
|
||||
[0, 10],
|
||||
[0, 0]
|
||||
]
|
||||
}
|
||||
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
|
||||
const where = {
|
||||
location: {
|
||||
$geoWithin: {
|
||||
$polygon: polygon
|
||||
}
|
||||
}
|
||||
};
|
||||
return rp.post({
|
||||
url: Parse.serverURL + '/classes/Polygon',
|
||||
json: { where, '_method': 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Javascript-Key': Parse.javaScriptKey
|
||||
}
|
||||
});
|
||||
}).then((resp) => {
|
||||
expect(resp.results.length).toBe(2);
|
||||
done();
|
||||
}, done.fail);
|
||||
});
|
||||
|
||||
it('invalid Polygon object withinPolygon', (done) => {
|
||||
const point = new Parse.GeoPoint(1.5, 1.5);
|
||||
const obj = new Parse.Object('Polygon', {location: point});
|
||||
const polygon = {
|
||||
__type: 'Polygon',
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[10, 0],
|
||||
]
|
||||
}
|
||||
obj.save().then(() => {
|
||||
const where = {
|
||||
location: {
|
||||
$geoWithin: {
|
||||
$polygon: polygon
|
||||
}
|
||||
}
|
||||
};
|
||||
return rp.post({
|
||||
url: Parse.serverURL + '/classes/Polygon',
|
||||
json: { where, '_method': 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Javascript-Key': Parse.javaScriptKey
|
||||
}
|
||||
});
|
||||
}).then((resp) => {
|
||||
fail(`no request should succeed: ${JSON.stringify(resp)}`);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
expect(err.error.code).toEqual(Parse.Error.INVALID_JSON);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('out of bounds Polygon object withinPolygon', (done) => {
|
||||
const point = new Parse.GeoPoint(1.5, 1.5);
|
||||
const obj = new Parse.Object('Polygon', {location: point});
|
||||
const polygon = {
|
||||
__type: 'Polygon',
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[181, 0],
|
||||
[0, 10]
|
||||
]
|
||||
}
|
||||
obj.save().then(() => {
|
||||
const where = {
|
||||
location: {
|
||||
$geoWithin: {
|
||||
$polygon: polygon
|
||||
}
|
||||
}
|
||||
};
|
||||
return rp.post({
|
||||
url: Parse.serverURL + '/classes/Polygon',
|
||||
json: { where, '_method': 'GET' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Javascript-Key': Parse.javaScriptKey
|
||||
}
|
||||
});
|
||||
}).then((resp) => {
|
||||
fail(`no request should succeed: ${JSON.stringify(resp)}`);
|
||||
done();
|
||||
}).catch((err) => {
|
||||
expect(err.error.code).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('invalid input withinPolygon', (done) => {
|
||||
const point = new Parse.GeoPoint(1.5, 1.5);
|
||||
const obj = new Parse.Object('Polygon', {location: point});
|
||||
|
||||
@@ -102,46 +102,47 @@ describe('ParseLiveQueryServer', function() {
|
||||
parseLiveQueryServer.server.close(done);
|
||||
});
|
||||
|
||||
it('can be initialized through ParseServer without liveQueryServerOptions', function(done) {
|
||||
const parseServer = ParseServer.start({
|
||||
appId: 'hello',
|
||||
masterKey: 'world',
|
||||
port: 22345,
|
||||
mountPath: '/1',
|
||||
serverURL: 'http://localhost:12345/1',
|
||||
liveQuery: {
|
||||
classNames: ['Yolo']
|
||||
},
|
||||
startLiveQueryServer: true
|
||||
describe_only_db('mongo')('initialization', () => {
|
||||
it('can be initialized through ParseServer without liveQueryServerOptions', function(done) {
|
||||
const parseServer = ParseServer.start({
|
||||
appId: 'hello',
|
||||
masterKey: 'world',
|
||||
port: 22345,
|
||||
mountPath: '/1',
|
||||
serverURL: 'http://localhost:12345/1',
|
||||
liveQuery: {
|
||||
classNames: ['Yolo']
|
||||
},
|
||||
startLiveQueryServer: true
|
||||
});
|
||||
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).toBe(parseServer.server);
|
||||
parseServer.server.close(() => done());
|
||||
});
|
||||
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).toBe(parseServer.server);
|
||||
parseServer.server.close(() => done());
|
||||
});
|
||||
it('can be initialized through ParseServer with liveQueryServerOptions', function(done) {
|
||||
const parseServer = ParseServer.start({
|
||||
appId: 'hello',
|
||||
masterKey: 'world',
|
||||
port: 22346,
|
||||
mountPath: '/1',
|
||||
serverURL: 'http://localhost:12345/1',
|
||||
liveQuery: {
|
||||
classNames: ['Yolo']
|
||||
},
|
||||
liveQueryServerOptions: {
|
||||
port: 22347,
|
||||
}
|
||||
});
|
||||
|
||||
it('can be initialized through ParseServer with liveQueryServerOptions', function(done) {
|
||||
const parseServer = ParseServer.start({
|
||||
appId: 'hello',
|
||||
masterKey: 'world',
|
||||
port: 22346,
|
||||
mountPath: '/1',
|
||||
serverURL: 'http://localhost:12345/1',
|
||||
liveQuery: {
|
||||
classNames: ['Yolo']
|
||||
},
|
||||
liveQueryServerOptions: {
|
||||
port: 22347,
|
||||
}
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
|
||||
parseServer.liveQueryServer.server.close();
|
||||
parseServer.server.close(() => done());
|
||||
});
|
||||
|
||||
expect(parseServer.liveQueryServer).not.toBeUndefined();
|
||||
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
|
||||
parseServer.liveQueryServer.server.close();
|
||||
parseServer.server.close(() => done());
|
||||
});
|
||||
|
||||
|
||||
it('can handle connect command', function() {
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
||||
const parseWebSocket = {
|
||||
|
||||
@@ -13,6 +13,10 @@ const masterKeyOptions = {
|
||||
json: true
|
||||
}
|
||||
|
||||
const PointerObject = Parse.Object.extend({
|
||||
className: "PointerObject"
|
||||
});
|
||||
|
||||
const loadTestData = () => {
|
||||
const data1 = {score: 10, name: 'foo', sender: {group: 'A'}, views: 900, size: ['S', 'M']};
|
||||
const data2 = {score: 10, name: 'foo', sender: {group: 'A'}, views: 800, size: ['M', 'L']};
|
||||
@@ -448,8 +452,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
|
||||
it('match pointer query', (done) => {
|
||||
const pointer1 = new TestObject();
|
||||
const pointer2 = new TestObject();
|
||||
const pointer1 = new PointerObject();
|
||||
const pointer2 = new PointerObject();
|
||||
const obj1 = new TestObject({ pointer: pointer1 });
|
||||
const obj2 = new TestObject({ pointer: pointer2 });
|
||||
const obj3 = new TestObject({ pointer: pointer1 });
|
||||
@@ -618,8 +622,8 @@ describe('Parse.Query Aggregate testing', () => {
|
||||
});
|
||||
|
||||
it('distinct pointer', (done) => {
|
||||
const pointer1 = new TestObject();
|
||||
const pointer2 = new TestObject();
|
||||
const pointer1 = new PointerObject();
|
||||
const pointer2 = new PointerObject();
|
||||
const obj1 = new TestObject({ pointer: pointer1 });
|
||||
const obj2 = new TestObject({ pointer: pointer2 });
|
||||
const obj3 = new TestObject({ pointer: pointer1 });
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
'use strict';
|
||||
|
||||
const Parse = require('parse/node');
|
||||
const rp = require('request-promise');
|
||||
|
||||
const masterKeyHeaders = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'test',
|
||||
'X-Parse-Master-Key': 'test'
|
||||
}
|
||||
|
||||
const masterKeyOptions = {
|
||||
headers: masterKeyHeaders,
|
||||
json: true
|
||||
}
|
||||
|
||||
describe('Parse.Query testing', () => {
|
||||
it("basic query", function(done) {
|
||||
@@ -497,6 +509,354 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('containsAllStartingWith should match all strings that starts with string', (done) => {
|
||||
|
||||
const object = new Parse.Object('Object');
|
||||
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
|
||||
const object2 = new Parse.Object('Object');
|
||||
object2.set('strings', ['the', 'brown', 'fox', 'jumps']);
|
||||
const object3 = new Parse.Object('Object');
|
||||
object3.set('strings', ['over', 'the', 'lazy', 'dog']);
|
||||
|
||||
const objectList = [object, object2, object3];
|
||||
|
||||
Parse.Object.saveAll(objectList).then((results) => {
|
||||
equal(objectList.length, results.length);
|
||||
|
||||
return require('request-promise').get({
|
||||
url: Parse.serverURL + "/classes/Object",
|
||||
json: {
|
||||
where: {
|
||||
strings: {
|
||||
$all: [
|
||||
{$regex: '\^\\Qthe\\E'},
|
||||
{$regex: '\^\\Qfox\\E'},
|
||||
{$regex: '\^\\Qlazy\\E'}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Javascript-Key': Parse.javaScriptKey
|
||||
}
|
||||
})
|
||||
.then(function (results) {
|
||||
equal(results.results.length, 1);
|
||||
arrayContains(results.results, object);
|
||||
|
||||
return require('request-promise').get({
|
||||
url: Parse.serverURL + "/classes/Object",
|
||||
json: {
|
||||
where: {
|
||||
strings: {
|
||||
$all: [
|
||||
{$regex: '\^\\Qthe\\E'},
|
||||
{$regex: '\^\\Qlazy\\E'}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Javascript-Key': Parse.javaScriptKey
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(function (results) {
|
||||
equal(results.results.length, 2);
|
||||
arrayContains(results.results, object);
|
||||
arrayContains(results.results, object3);
|
||||
|
||||
return require('request-promise').get({
|
||||
url: Parse.serverURL + "/classes/Object",
|
||||
json: {
|
||||
where: {
|
||||
strings: {
|
||||
$all: [
|
||||
{$regex: '\^\\Qhe\\E'},
|
||||
{$regex: '\^\\Qlazy\\E'}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Javascript-Key': Parse.javaScriptKey
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(function (results) {
|
||||
equal(results.results.length, 0);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('containsAllStartingWith values must be all of type starting with regex', (done) => {
|
||||
|
||||
const object = new Parse.Object('Object');
|
||||
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
|
||||
|
||||
object.save().then(() => {
|
||||
equal(object.isNew(), false);
|
||||
|
||||
return require('request-promise').get({
|
||||
url: Parse.serverURL + "/classes/Object",
|
||||
json: {
|
||||
where: {
|
||||
strings: {
|
||||
$all: [
|
||||
{$regex: '\^\\Qthe\\E'},
|
||||
{$regex: '\^\\Qlazy\\E'},
|
||||
{$regex: '\^\\Qfox\\E'},
|
||||
{$unknown: /unknown/}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Javascript-Key': Parse.javaScriptKey
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
}, function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('containsAllStartingWith empty array values should return empty results', (done) => {
|
||||
|
||||
const object = new Parse.Object('Object');
|
||||
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
|
||||
|
||||
object.save().then(() => {
|
||||
equal(object.isNew(), false);
|
||||
|
||||
return require('request-promise').get({
|
||||
url: Parse.serverURL + "/classes/Object",
|
||||
json: {
|
||||
where: {
|
||||
strings: {
|
||||
$all: []
|
||||
}
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Javascript-Key': Parse.javaScriptKey
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(function (results) {
|
||||
equal(results.results.length, 0);
|
||||
done();
|
||||
}, function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('containsAllStartingWith single empty value returns empty results', (done) => {
|
||||
|
||||
const object = new Parse.Object('Object');
|
||||
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
|
||||
|
||||
object.save().then(() => {
|
||||
equal(object.isNew(), false);
|
||||
|
||||
return require('request-promise').get({
|
||||
url: Parse.serverURL + "/classes/Object",
|
||||
json: {
|
||||
where: {
|
||||
strings: {
|
||||
$all: [ {} ]
|
||||
}
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Javascript-Key': Parse.javaScriptKey
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(function (results) {
|
||||
equal(results.results.length, 0);
|
||||
done();
|
||||
}, function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('containsAllStartingWith single regex value should return corresponding matching results', (done) => {
|
||||
|
||||
const object = new Parse.Object('Object');
|
||||
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
|
||||
const object2 = new Parse.Object('Object');
|
||||
object2.set('strings', ['the', 'brown', 'fox', 'jumps']);
|
||||
const object3 = new Parse.Object('Object');
|
||||
object3.set('strings', ['over', 'the', 'lazy', 'dog']);
|
||||
|
||||
const objectList = [object, object2, object3];
|
||||
|
||||
Parse.Object.saveAll(objectList).then((results) => {
|
||||
equal(objectList.length, results.length);
|
||||
|
||||
return require('request-promise').get({
|
||||
url: Parse.serverURL + "/classes/Object",
|
||||
json: {
|
||||
where: {
|
||||
strings: {
|
||||
$all: [ {$regex: '\^\\Qlazy\\E'} ]
|
||||
}
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Javascript-Key': Parse.javaScriptKey
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(function (results) {
|
||||
equal(results.results.length, 2);
|
||||
done();
|
||||
}, function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('containsAllStartingWith single invalid regex returns empty results', (done) => {
|
||||
|
||||
const object = new Parse.Object('Object');
|
||||
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
|
||||
|
||||
object.save().then(() => {
|
||||
equal(object.isNew(), false);
|
||||
|
||||
return require('request-promise').get({
|
||||
url: Parse.serverURL + "/classes/Object",
|
||||
json: {
|
||||
where: {
|
||||
strings: {
|
||||
$all: [ {$unknown: '\^\\Qlazy\\E'} ]
|
||||
}
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Javascript-Key': Parse.javaScriptKey
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(function (results) {
|
||||
equal(results.results.length, 0);
|
||||
done();
|
||||
}, function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('containedBy pointer array', (done) => {
|
||||
const objects = Array.from(Array(10).keys()).map((idx) => {
|
||||
const obj = new Parse.Object('Object');
|
||||
obj.set('key', idx);
|
||||
return obj;
|
||||
});
|
||||
|
||||
const parent = new Parse.Object('Parent');
|
||||
const parent2 = new Parse.Object('Parent');
|
||||
const parent3 = new Parse.Object('Parent');
|
||||
|
||||
Parse.Object.saveAll(objects).then(() => {
|
||||
// [0, 1, 2]
|
||||
parent.set('objects', objects.slice(0, 3));
|
||||
|
||||
const shift = objects.shift();
|
||||
// [2, 0]
|
||||
parent2.set('objects', [objects[1], shift]);
|
||||
|
||||
// [1, 2, 3, 4]
|
||||
parent3.set('objects', objects.slice(1, 4));
|
||||
|
||||
return Parse.Object.saveAll([parent, parent2, parent3]);
|
||||
}).then(() => {
|
||||
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
const pointers = objects.map(object => object.toPointer());
|
||||
|
||||
// Return all Parent where all parent.objects are contained in objects
|
||||
return rp.get({
|
||||
url: Parse.serverURL + "/classes/Parent",
|
||||
json: {
|
||||
where: {
|
||||
objects: {
|
||||
$containedBy: pointers
|
||||
}
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Javascript-Key': Parse.javaScriptKey
|
||||
}
|
||||
});
|
||||
}).then((results) => {
|
||||
expect(results.results[0].objectId).not.toBeUndefined();
|
||||
expect(results.results[0].objectId).toBe(parent3.id);
|
||||
expect(results.results.length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('containedBy number array', (done) => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
where: { numbers: { $containedBy: [1, 2, 3, 4, 5, 6, 7, 8, 9] } },
|
||||
}
|
||||
});
|
||||
const obj1 = new TestObject({ numbers: [0, 1, 2] });
|
||||
const obj2 = new TestObject({ numbers: [2, 0] });
|
||||
const obj3 = new TestObject({ numbers: [1, 2, 3, 4] });
|
||||
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
|
||||
return rp.get(Parse.serverURL + "/classes/TestObject", options);
|
||||
}).then((results) => {
|
||||
expect(results.results[0].objectId).not.toBeUndefined();
|
||||
expect(results.results[0].objectId).toBe(obj3.id);
|
||||
expect(results.results.length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('containedBy empty array', (done) => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
where: { numbers: { $containedBy: [] } },
|
||||
}
|
||||
});
|
||||
const obj1 = new TestObject({ numbers: [0, 1, 2] });
|
||||
const obj2 = new TestObject({ numbers: [2, 0] });
|
||||
const obj3 = new TestObject({ numbers: [1, 2, 3, 4] });
|
||||
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
|
||||
return rp.get(Parse.serverURL + "/classes/TestObject", options);
|
||||
}).then((results) => {
|
||||
expect(results.results.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('containedBy invalid query', (done) => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
where: { objects: { $containedBy: 1234 } },
|
||||
}
|
||||
});
|
||||
const obj = new TestObject();
|
||||
obj.save().then(() => {
|
||||
return rp.get(Parse.serverURL + "/classes/TestObject", options);
|
||||
}).then(done.fail).catch((error) => {
|
||||
equal(error.error.code, Parse.Error.INVALID_JSON);
|
||||
equal(error.error.error, 'bad $containedBy: should be an array');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
const BoxedNumber = Parse.Object.extend({
|
||||
className: "BoxedNumber"
|
||||
});
|
||||
@@ -570,6 +930,38 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lessThan zero queries", (done) => {
|
||||
const makeBoxedNumber = (i) => {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
const numbers = [-3, -2, -1, 0, 1];
|
||||
const boxedNumbers = numbers.map(makeBoxedNumber);
|
||||
Parse.Object.saveAll(boxedNumbers).then(() => {
|
||||
const query = new Parse.Query(BoxedNumber);
|
||||
query.lessThan('number', 0);
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
equal(results.length, 3);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("lessThanOrEqualTo zero queries", (done) => {
|
||||
const makeBoxedNumber = (i) => {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
const numbers = [-3, -2, -1, 0, 1];
|
||||
const boxedNumbers = numbers.map(makeBoxedNumber);
|
||||
Parse.Object.saveAll(boxedNumbers).then(() => {
|
||||
const query = new Parse.Query(BoxedNumber);
|
||||
query.lessThanOrEqualTo('number', 0);
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
equal(results.length, 4);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("greaterThan queries", function(done) {
|
||||
const makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
@@ -606,6 +998,38 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("greaterThan zero queries", (done) => {
|
||||
const makeBoxedNumber = (i) => {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
const numbers = [-3, -2, -1, 0, 1];
|
||||
const boxedNumbers = numbers.map(makeBoxedNumber);
|
||||
Parse.Object.saveAll(boxedNumbers).then(() => {
|
||||
const query = new Parse.Query(BoxedNumber);
|
||||
query.greaterThan('number', 0);
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
equal(results.length, 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("greaterThanOrEqualTo zero queries", (done) => {
|
||||
const makeBoxedNumber = (i) => {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
const numbers = [-3, -2, -1, 0, 1];
|
||||
const boxedNumbers = numbers.map(makeBoxedNumber);
|
||||
Parse.Object.saveAll(boxedNumbers).then(() => {
|
||||
const query = new Parse.Query(BoxedNumber);
|
||||
query.greaterThanOrEqualTo('number', 0);
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
equal(results.length, 2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("lessThanOrEqualTo greaterThanOrEqualTo queries", function(done) {
|
||||
const makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
@@ -662,6 +1086,101 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("notEqualTo zero queries", (done) => {
|
||||
const makeBoxedNumber = (i) => {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
const numbers = [-3, -2, -1, 0, 1];
|
||||
const boxedNumbers = numbers.map(makeBoxedNumber);
|
||||
Parse.Object.saveAll(boxedNumbers).then(() => {
|
||||
const query = new Parse.Query(BoxedNumber);
|
||||
query.notEqualTo('number', 0);
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
equal(results.length, 4);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("equalTo zero queries", (done) => {
|
||||
const makeBoxedNumber = (i) => {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
const numbers = [-3, -2, -1, 0, 1];
|
||||
const boxedNumbers = numbers.map(makeBoxedNumber);
|
||||
Parse.Object.saveAll(boxedNumbers).then(() => {
|
||||
const query = new Parse.Query(BoxedNumber);
|
||||
query.equalTo('number', 0);
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
equal(results.length, 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("number equalTo boolean queries", (done) => {
|
||||
const makeBoxedNumber = (i) => {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
const numbers = [-3, -2, -1, 0, 1];
|
||||
const boxedNumbers = numbers.map(makeBoxedNumber);
|
||||
Parse.Object.saveAll(boxedNumbers).then(() => {
|
||||
const query = new Parse.Query(BoxedNumber);
|
||||
query.equalTo('number', false);
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
equal(results.length, 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("equalTo false queries", (done) => {
|
||||
const obj1 = new TestObject({ field: false });
|
||||
const obj2 = new TestObject({ field: true });
|
||||
Parse.Object.saveAll([obj1, obj2]).then(() => {
|
||||
const query = new Parse.Query(TestObject);
|
||||
query.equalTo('field', false);
|
||||
return query.find();
|
||||
}).then((results) => {
|
||||
equal(results.length, 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("where $eq false queries (rest)", (done) => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
where: { field: { $eq: false } },
|
||||
}
|
||||
});
|
||||
const obj1 = new TestObject({ field: false });
|
||||
const obj2 = new TestObject({ field: true });
|
||||
Parse.Object.saveAll([obj1, obj2]).then(() => {
|
||||
rp.get(Parse.serverURL + '/classes/TestObject', options)
|
||||
.then((resp) => {
|
||||
equal(resp.results.length, 1);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it("where $eq null queries (rest)", (done) => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
where: { field: { $eq: null } },
|
||||
}
|
||||
});
|
||||
const obj1 = new TestObject({ field: false });
|
||||
const obj2 = new TestObject({ field: null });
|
||||
Parse.Object.saveAll([obj1, obj2]).then(() => {
|
||||
rp.get(Parse.serverURL + '/classes/TestObject', options)
|
||||
.then((resp) => {
|
||||
equal(resp.results.length, 1);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it("containedIn queries", function(done) {
|
||||
const makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
@@ -680,6 +1199,40 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("containedIn false queries", (done) => {
|
||||
const makeBoxedNumber = (i) => {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
const numbers = [-3, -2, -1, 0, 1];
|
||||
const boxedNumbers = numbers.map(makeBoxedNumber);
|
||||
Parse.Object.saveAll(boxedNumbers).then(() => {
|
||||
const query = new Parse.Query(BoxedNumber);
|
||||
query.containedIn('number', false);
|
||||
return query.find();
|
||||
}).then(done.fail).catch((error) => {
|
||||
equal(error.code, Parse.Error.INVALID_JSON);
|
||||
equal(error.message, 'bad $in value');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("notContainedIn false queries", (done) => {
|
||||
const makeBoxedNumber = (i) => {
|
||||
return new BoxedNumber({ number: i });
|
||||
};
|
||||
const numbers = [-3, -2, -1, 0, 1];
|
||||
const boxedNumbers = numbers.map(makeBoxedNumber);
|
||||
Parse.Object.saveAll(boxedNumbers).then(() => {
|
||||
const query = new Parse.Query(BoxedNumber);
|
||||
query.notContainedIn('number', false);
|
||||
return query.find();
|
||||
}).then(done.fail).catch((error) => {
|
||||
equal(error.code, Parse.Error.INVALID_JSON);
|
||||
equal(error.message, 'bad $nin value');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("notContainedIn queries", function(done) {
|
||||
const makeBoxedNumber = function(i) {
|
||||
return new BoxedNumber({ number: i });
|
||||
@@ -2014,6 +2567,63 @@ describe('Parse.Query testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('$nor valid query', (done) => {
|
||||
const objects = Array.from(Array(10).keys()).map((rating) => {
|
||||
return new TestObject({ 'rating': rating });
|
||||
});
|
||||
|
||||
const highValue = 5;
|
||||
const lowValue = 3;
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
where: {
|
||||
$nor: [
|
||||
{ rating : { $gt : highValue } },
|
||||
{ rating : { $lte : lowValue } },
|
||||
]
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
Parse.Object.saveAll(objects).then(() => {
|
||||
return rp.get(Parse.serverURL + "/classes/TestObject", options);
|
||||
}).then((results) => {
|
||||
expect(results.results.length).toBe(highValue - lowValue);
|
||||
expect(results.results.every(res => res.rating > lowValue && res.rating <= highValue)).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('$nor invalid query - empty array', (done) => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
where: { $nor: [] },
|
||||
}
|
||||
});
|
||||
const obj = new TestObject();
|
||||
obj.save().then(() => {
|
||||
return rp.get(Parse.serverURL + "/classes/TestObject", options);
|
||||
}).then(done.fail).catch((error) => {
|
||||
equal(error.error.code, Parse.Error.INVALID_QUERY);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('$nor invalid query - wrong type', (done) => {
|
||||
const options = Object.assign({}, masterKeyOptions, {
|
||||
body: {
|
||||
where: { $nor: 1337 },
|
||||
}
|
||||
});
|
||||
const obj = new TestObject();
|
||||
obj.save().then(() => {
|
||||
return rp.get(Parse.serverURL + "/classes/TestObject", options);
|
||||
}).then(done.fail).catch((error) => {
|
||||
equal(error.error.code, Parse.Error.INVALID_QUERY);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("dontSelect query", function(done) {
|
||||
const RestaurantObject = Parse.Object.extend("Restaurant");
|
||||
const PersonObject = Parse.Object.extend("Person");
|
||||
|
||||
@@ -801,4 +801,37 @@ describe('Parse.Relation testing', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures beforeFind on relation doesnt side effect', (done) => {
|
||||
const parent = new Parse.Object('Parent');
|
||||
const child = new Parse.Object('Child');
|
||||
child.save().then(() => {
|
||||
parent.relation('children').add(child);
|
||||
return parent.save();
|
||||
}).then(() => {
|
||||
// We need to use a new reference otherwise the JS SDK remembers the className for a relation
|
||||
// After saves or finds
|
||||
const otherParent = new Parse.Object('Parent');
|
||||
otherParent.id = parent.id;
|
||||
return otherParent.relation('children').query().find();
|
||||
}).then((children) => {
|
||||
// Without an after find all is good, all results have been redirected with proper className
|
||||
children.forEach((child) => expect(child.className).toBe('Child'));
|
||||
// Setup the afterFind
|
||||
Parse.Cloud.afterFind('Child', (req) => {
|
||||
return Promise.resolve(req.objects.map((child) => {
|
||||
child.set('afterFound', true);
|
||||
return child;
|
||||
}));
|
||||
});
|
||||
const otherParent = new Parse.Object('Parent');
|
||||
otherParent.id = parent.id;
|
||||
return otherParent.relation('children').query().find();
|
||||
}).then((children) => {
|
||||
children.forEach((child) => {
|
||||
expect(child.className).toBe('Child');
|
||||
expect(child.get('afterFound')).toBe(true);
|
||||
});
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
+112
-7
@@ -7,6 +7,7 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
import MongoStorageAdapter from '../src/Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||
const request = require('request');
|
||||
const passwordCrypto = require('../src/password');
|
||||
const Config = require('../src/Config');
|
||||
@@ -213,6 +214,106 @@ describe('Parse.User testing', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('should let masterKey lockout user', (done) => {
|
||||
const user = new Parse.User();
|
||||
const ACL = new Parse.ACL();
|
||||
ACL.setPublicReadAccess(false);
|
||||
ACL.setPublicWriteAccess(false);
|
||||
user.setUsername('asdf');
|
||||
user.setPassword('zxcv');
|
||||
user.setACL(ACL);
|
||||
user.signUp().then(() => {
|
||||
return Parse.User.logIn("asdf", "zxcv");
|
||||
}).then((user) => {
|
||||
equal(user.get("username"), "asdf");
|
||||
// Lock the user down
|
||||
const ACL = new Parse.ACL();
|
||||
user.setACL(ACL);
|
||||
return user.save(null, { useMasterKey: true });
|
||||
}).then(() => {
|
||||
expect(user.getACL().getPublicReadAccess()).toBe(false);
|
||||
return Parse.User.logIn("asdf", "zxcv");
|
||||
}).then(done.fail).catch((err) => {
|
||||
expect(err.message).toBe('Invalid username/password.');
|
||||
expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it_only_db('mongo')('should let legacy users without ACL login', async() => {
|
||||
const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||
const adapter = new MongoStorageAdapter({ collectionPrefix: 'test_', uri: databaseURI });
|
||||
await adapter.connect();
|
||||
await adapter.database.dropDatabase();
|
||||
delete adapter.connectionPromise;
|
||||
|
||||
const user = new Parse.User();
|
||||
await user.signUp({
|
||||
username: 'newUser',
|
||||
password: 'password',
|
||||
});
|
||||
|
||||
const collection = await adapter._adaptiveCollection('_User');
|
||||
await collection.insertOne({
|
||||
// the hashed password is 'password' hashed
|
||||
"_hashed_password": "$2b$10$mJ2ca2UbCM9hlojYHZxkQe8pyEXe5YMg0nMdvP4AJBeqlTEZJ6/Uu",
|
||||
"_session_token": "xxx",
|
||||
"email": "xxx@a.b",
|
||||
"username": "oldUser",
|
||||
"emailVerified": true,
|
||||
"_email_verify_token": "yyy",
|
||||
});
|
||||
|
||||
// get the 2 users
|
||||
const users = await collection.find();
|
||||
expect(users.length).toBe(2);
|
||||
|
||||
const aUser = await Parse.User.logIn('oldUser', 'password');
|
||||
expect(aUser).not.toBeUndefined();
|
||||
|
||||
const newUser = await Parse.User.logIn('newUser', 'password');
|
||||
expect(newUser).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should be let masterKey lock user out with authData', (done) => {
|
||||
let objectId;
|
||||
let sessionToken;
|
||||
|
||||
rp.post({
|
||||
url: 'http://localhost:8378/1/classes/_User',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: { key: "value", authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
|
||||
}).then((body) => {
|
||||
objectId = body.objectId;
|
||||
sessionToken = body.sessionToken;
|
||||
expect(sessionToken).toBeDefined();
|
||||
expect(objectId).toBeDefined();
|
||||
const user = new Parse.User();
|
||||
user.id = objectId;
|
||||
const ACL = new Parse.ACL();
|
||||
user.setACL(ACL);
|
||||
return user.save(null, { useMasterKey: true });
|
||||
}).then(() => {
|
||||
// update the user
|
||||
const options = {
|
||||
url: `http://localhost:8378/1/classes/_User/`,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
json: { key: "otherValue", authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
|
||||
}
|
||||
return rp.post(options);
|
||||
}).then((res) => {
|
||||
// Because the user is locked out, this should behave as creating a new user
|
||||
expect(res.objectId).not.toEqual(objectId);
|
||||
}).then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it("user login with files", (done) => {
|
||||
const file = new Parse.File("yolo.txt", [1,2,3], "text/plain");
|
||||
file.save().then((file) => {
|
||||
@@ -3515,14 +3616,18 @@ describe('Parse.User testing', () => {
|
||||
email: 'yo@lo.com'
|
||||
}).then(() => {
|
||||
const token = user.getSessionToken();
|
||||
const promises = [];
|
||||
while(promises.length != 5) {
|
||||
promises.push(Parse.User.logIn('yolo', 'yolo').then((res) => {
|
||||
// ensure a new session token is generated at each login
|
||||
expect(res.getSessionToken()).not.toBe(token);
|
||||
}));
|
||||
let promise = Promise.resolve();
|
||||
let count = 0;
|
||||
while(count < 5) {
|
||||
promise = promise.then(() => {
|
||||
return Parse.User.logIn('yolo', 'yolo').then((res) => {
|
||||
// ensure a new session token is generated at each login
|
||||
expect(res.getSessionToken()).not.toBe(token);
|
||||
});
|
||||
});
|
||||
count++;
|
||||
}
|
||||
return Promise.all(promises);
|
||||
return promise;
|
||||
}).then(() => {
|
||||
// wait because session destruction is not synchronous
|
||||
return new Promise((resolve) => {
|
||||
|
||||
@@ -6,22 +6,14 @@ const express = require('express');
|
||||
//public schema
|
||||
const databaseOptions1 = {
|
||||
initOptions: {
|
||||
connect: function (client, dc, isFresh) {
|
||||
if (isFresh) {
|
||||
client.query('SET search_path = public');
|
||||
}
|
||||
}
|
||||
schema: 'public'
|
||||
}
|
||||
};
|
||||
|
||||
//not exists schema
|
||||
const databaseOptions2 = {
|
||||
initOptions: {
|
||||
connect: function (client, dc, isFresh) {
|
||||
if (isFresh) {
|
||||
client.query('SET search_path = not_exists_schema');
|
||||
}
|
||||
}
|
||||
schema: 'not_exists_schema'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+8
-1
@@ -217,7 +217,14 @@ afterEach(function(done) {
|
||||
});
|
||||
})
|
||||
.then(() => Parse.User.logOut())
|
||||
.then(afterLogOut, afterLogOut)
|
||||
.then(() => {}, () => {}) // swallow errors
|
||||
.then(() => {
|
||||
// Connection close events are not immediate on node 10+... wait a bit
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
})
|
||||
.then(afterLogOut)
|
||||
});
|
||||
|
||||
const TestObject = Parse.Object.extend({
|
||||
|
||||
@@ -1852,6 +1852,29 @@ describe('schemas', () => {
|
||||
})
|
||||
});
|
||||
|
||||
it('can create index on default field', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
body: {},
|
||||
}, () => {
|
||||
request.put({
|
||||
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
body: {
|
||||
indexes: {
|
||||
name1: { createdAt: 1},
|
||||
}
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(body.indexes.name1).toEqual({ createdAt: 1});
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('cannot create compound index if field does not exist', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||
|
||||
@@ -37,7 +37,7 @@ function validateAppId(appIds, authData) {
|
||||
// A promisey wrapper for FB graph requests.
|
||||
function graphRequest(path) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
https.get('https://graph.facebook.com/v2.5/' + path, function(res) {
|
||||
https.get('https://graph.facebook.com/' + path, function(res) {
|
||||
var data = '';
|
||||
res.on('data', function(chunk) {
|
||||
data += chunk;
|
||||
|
||||
@@ -10,7 +10,7 @@ var logger = require('../../logger').default;
|
||||
function validateAuthData(authData, params) {
|
||||
return vkOAuth2Request(params).then(function (response) {
|
||||
if (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) {
|
||||
return request("api.vk.com", "method/secure.checkToken?token=" + authData.access_token + "&client_secret=" + params.appSecret + "&access_token=" + response.access_token + "&v=5.59").then(function (response) {
|
||||
if (response && response.response && response.response.user_id == authData.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
handleError<T>(error: ?Error): Promise<T> {
|
||||
handleError<T>(error: ?(Error | Parse.Error)): Promise<T> {
|
||||
if (error && error.code === 13) { // Unauthorized error
|
||||
delete this.client;
|
||||
delete this.database;
|
||||
@@ -567,7 +567,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
if (stage.$match) {
|
||||
for (const field in stage.$match) {
|
||||
if (schema.fields[field] && schema.fields[field].type === 'Pointer') {
|
||||
const transformMatch = { [`_p_${field}`] : `${className}$${stage.$match[field]}` };
|
||||
const transformMatch = { [`_p_${field}`] : `${schema.fields[field].targetClass}$${stage.$match[field]}` };
|
||||
stage.$match = transformMatch;
|
||||
}
|
||||
if (field === 'objectId') {
|
||||
|
||||
@@ -123,6 +123,44 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
|
||||
return {key, value};
|
||||
}
|
||||
|
||||
const isRegex = value => {
|
||||
return value && (value instanceof RegExp)
|
||||
}
|
||||
|
||||
const isStartsWithRegex = value => {
|
||||
if (!isRegex(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matches = value.toString().match(/\/\^\\Q.*\\E\//);
|
||||
return !!matches;
|
||||
}
|
||||
|
||||
const isAllValuesRegexOrNone = values => {
|
||||
if (!values || !Array.isArray(values) || values.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const firstValuesIsRegex = isStartsWithRegex(values[0]);
|
||||
if (values.length === 1) {
|
||||
return firstValuesIsRegex;
|
||||
}
|
||||
|
||||
for (let i = 1, length = values.length; i < length; ++i) {
|
||||
if (firstValuesIsRegex !== isStartsWithRegex(values[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const isAnyValueRegex = values => {
|
||||
return values.some(function (value) {
|
||||
return isRegex(value);
|
||||
});
|
||||
}
|
||||
|
||||
const transformInteriorValue = restValue => {
|
||||
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");
|
||||
@@ -209,9 +247,9 @@ function transformQueryKeyValue(className, key, value, schema) {
|
||||
case '_perishable_token':
|
||||
case '_email_verify_token': return {key, value}
|
||||
case '$or':
|
||||
return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, schema))};
|
||||
case '$and':
|
||||
return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, schema))};
|
||||
case '$nor':
|
||||
return {key: key, value: value.map(subQuery => transformWhere(className, subQuery, schema))};
|
||||
case 'lastUsed':
|
||||
if (valueAsDate(value)) {
|
||||
return {key: '_last_used', value: valueAsDate(value)}
|
||||
@@ -252,6 +290,9 @@ function transformQueryKeyValue(className, key, value, schema) {
|
||||
if (transformedConstraint.$text) {
|
||||
return {key: '$text', value: transformedConstraint.$text};
|
||||
}
|
||||
if (transformedConstraint.$elemMatch) {
|
||||
return { key: '$nor', value: [{ [key]: transformedConstraint }] };
|
||||
}
|
||||
return {key, value: transformedConstraint};
|
||||
}
|
||||
|
||||
@@ -469,6 +510,8 @@ const transformInteriorAtom = (atom) => {
|
||||
return DateCoder.JSONToDatabase(atom);
|
||||
} else if (BytesCoder.isValidJSON(atom)) {
|
||||
return BytesCoder.JSONToDatabase(atom);
|
||||
} else if (typeof atom === 'object' && atom && atom.$regex !== undefined) {
|
||||
return new RegExp(atom.$regex);
|
||||
} else {
|
||||
return atom;
|
||||
}
|
||||
@@ -740,6 +783,13 @@ function transformConstraint(constraint, field) {
|
||||
'bad ' + key + ' value');
|
||||
}
|
||||
answer[key] = arr.map(transformInteriorAtom);
|
||||
|
||||
const values = answer[key];
|
||||
if (isAnyValueRegex(values) && !isAllValuesRegexOrNone(values)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'All $all values must be of regex type or none: '
|
||||
+ values);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case '$regex':
|
||||
@@ -750,6 +800,19 @@ function transformConstraint(constraint, field) {
|
||||
answer[key] = s;
|
||||
break;
|
||||
|
||||
case '$containedBy': {
|
||||
const arr = constraint[key];
|
||||
if (!(arr instanceof Array)) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
`bad $containedBy: should be an array`
|
||||
);
|
||||
}
|
||||
answer.$elemMatch = {
|
||||
$nin: arr.map(transformer)
|
||||
};
|
||||
break;
|
||||
}
|
||||
case '$options':
|
||||
answer[key] = constraint[key];
|
||||
break;
|
||||
@@ -842,19 +905,34 @@ function transformConstraint(constraint, field) {
|
||||
|
||||
case '$geoWithin': {
|
||||
const polygon = constraint[key]['$polygon'];
|
||||
if (!(polygon instanceof Array)) {
|
||||
let points;
|
||||
if (typeof polygon === 'object' && polygon.__type === 'Polygon') {
|
||||
if (!polygon.coordinates || polygon.coordinates.length < 3) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'bad $geoWithin value; Polygon.coordinates should contain at least 3 lon/lat pairs'
|
||||
);
|
||||
}
|
||||
points = polygon.coordinates;
|
||||
} else if (polygon instanceof Array) {
|
||||
if (polygon.length < 3) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'bad $geoWithin value; $polygon should contain at least 3 GeoPoints'
|
||||
);
|
||||
}
|
||||
points = polygon;
|
||||
} else {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'bad $geoWithin value; $polygon should contain at least 3 GeoPoints'
|
||||
'bad $geoWithin value; $polygon should be Polygon object or Array of Parse.GeoPoint\'s'
|
||||
);
|
||||
}
|
||||
if (polygon.length < 3) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'bad $geoWithin value; $polygon should contain at least 3 GeoPoints'
|
||||
);
|
||||
}
|
||||
const points = polygon.map((point) => {
|
||||
points = points.map((point) => {
|
||||
if (point instanceof Array && point.length === 2) {
|
||||
Parse.GeoPoint._validate(point[1], point[0]);
|
||||
return point;
|
||||
}
|
||||
if (!GeoPointCoder.isValidJSON(point)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value');
|
||||
} else {
|
||||
|
||||
@@ -233,7 +233,13 @@ const joinTablesForSchema = (schema) => {
|
||||
return list;
|
||||
}
|
||||
|
||||
const buildWhereClause = ({ schema, query, index }) => {
|
||||
interface WhereClause {
|
||||
pattern: string;
|
||||
values: Array<any>;
|
||||
sorts: Array<any>;
|
||||
}
|
||||
|
||||
const buildWhereClause = ({ schema, query, index }): WhereClause => {
|
||||
const patterns = [];
|
||||
let values = [];
|
||||
const sorts = [];
|
||||
@@ -287,13 +293,20 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
index += 2;
|
||||
} else if (typeof fieldValue === 'boolean') {
|
||||
patterns.push(`$${index}:name = $${index + 1}`);
|
||||
values.push(fieldName, fieldValue);
|
||||
// Can't cast boolean to double precision
|
||||
if (schema.fields[fieldName] && schema.fields[fieldName].type === 'Number') {
|
||||
// Should always return zero results
|
||||
const MAX_INT_PLUS_ONE = 9223372036854775808;
|
||||
values.push(fieldName, MAX_INT_PLUS_ONE);
|
||||
} else {
|
||||
values.push(fieldName, fieldValue);
|
||||
}
|
||||
index += 2;
|
||||
} else if (typeof fieldValue === 'number') {
|
||||
patterns.push(`$${index}:name = $${index + 1}`);
|
||||
values.push(fieldName, fieldValue);
|
||||
index += 2;
|
||||
} else if (fieldName === '$or' || fieldName === '$and') {
|
||||
} else if (['$or', '$nor', '$and'].includes(fieldName)) {
|
||||
const clauses = [];
|
||||
const clauseValues = [];
|
||||
fieldValue.forEach((subQuery) => {
|
||||
@@ -304,8 +317,11 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
index += clause.values.length;
|
||||
}
|
||||
});
|
||||
const orOrAnd = fieldName === '$or' ? ' OR ' : ' AND ';
|
||||
patterns.push(`(${clauses.join(orOrAnd)})`);
|
||||
|
||||
const orOrAnd = fieldName === '$and' ? ' AND ' : ' OR ';
|
||||
const not = fieldName === '$nor' ? ' NOT ' : '';
|
||||
|
||||
patterns.push(`${not}(${clauses.join(orOrAnd)})`);
|
||||
values.push(...clauseValues);
|
||||
}
|
||||
|
||||
@@ -329,11 +345,16 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
values.push(fieldName, fieldValue.$ne);
|
||||
index += 2;
|
||||
}
|
||||
|
||||
if (fieldValue.$eq) {
|
||||
patterns.push(`$${index}:name = $${index + 1}`);
|
||||
values.push(fieldName, fieldValue.$eq);
|
||||
index += 2;
|
||||
if (fieldValue.$eq !== undefined) {
|
||||
if (fieldValue.$eq === null) {
|
||||
patterns.push(`$${index}:name IS NULL`);
|
||||
values.push(fieldName);
|
||||
index += 1;
|
||||
} else {
|
||||
patterns.push(`$${index}:name = $${index + 1}`);
|
||||
values.push(fieldName, fieldValue.$eq);
|
||||
index += 2;
|
||||
}
|
||||
}
|
||||
const isInOrNin = Array.isArray(fieldValue.$in) || Array.isArray(fieldValue.$nin);
|
||||
if (Array.isArray(fieldValue.$in) &&
|
||||
@@ -393,10 +414,27 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
if (fieldValue.$nin) {
|
||||
createConstraint(_.flatMap(fieldValue.$nin, elt => elt), true);
|
||||
}
|
||||
} else if(typeof fieldValue.$in !== 'undefined') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $in value');
|
||||
} else if (typeof fieldValue.$nin !== 'undefined') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $nin value');
|
||||
}
|
||||
|
||||
if (Array.isArray(fieldValue.$all) && isArrayField) {
|
||||
patterns.push(`array_contains_all($${index}:name, $${index + 1}::jsonb)`);
|
||||
if (isAnyValueRegexStartsWith(fieldValue.$all)) {
|
||||
if (!isAllValuesRegexOrNone(fieldValue.$all)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'All $all values must be of regex type or none: '
|
||||
+ fieldValue.$all);
|
||||
}
|
||||
|
||||
for (let i = 0; i < fieldValue.$all.length; i += 1) {
|
||||
const value = processRegexPattern(fieldValue.$all[i].$regex);
|
||||
fieldValue.$all[i] = value.substring(1) + '%';
|
||||
}
|
||||
patterns.push(`array_contains_all_regex($${index}:name, $${index + 1}::jsonb)`);
|
||||
} else {
|
||||
patterns.push(`array_contains_all($${index}:name, $${index + 1}::jsonb)`);
|
||||
}
|
||||
values.push(fieldName, JSON.stringify(fieldValue.$all));
|
||||
index += 2;
|
||||
}
|
||||
@@ -411,6 +449,20 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if (fieldValue.$containedBy) {
|
||||
const arr = fieldValue.$containedBy;
|
||||
if (!(arr instanceof Array)) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
`bad $containedBy: should be an array`
|
||||
);
|
||||
}
|
||||
|
||||
patterns.push(`$${index}:name <@ $${index + 1}::jsonb`);
|
||||
values.push(fieldName, JSON.stringify(arr));
|
||||
index += 2;
|
||||
}
|
||||
|
||||
if (fieldValue.$text) {
|
||||
const search = fieldValue.$text.$search;
|
||||
let language = 'english';
|
||||
@@ -485,19 +537,34 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
|
||||
if (fieldValue.$geoWithin && fieldValue.$geoWithin.$polygon) {
|
||||
const polygon = fieldValue.$geoWithin.$polygon;
|
||||
if (!(polygon instanceof Array)) {
|
||||
let points;
|
||||
if (typeof polygon === 'object' && polygon.__type === 'Polygon') {
|
||||
if (!polygon.coordinates || polygon.coordinates.length < 3) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'bad $geoWithin value; Polygon.coordinates should contain at least 3 lon/lat pairs'
|
||||
);
|
||||
}
|
||||
points = polygon.coordinates;
|
||||
} else if ((polygon instanceof Array)) {
|
||||
if (polygon.length < 3) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'bad $geoWithin value; $polygon should contain at least 3 GeoPoints'
|
||||
);
|
||||
}
|
||||
points = polygon;
|
||||
} else {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'bad $geoWithin value; $polygon should contain at least 3 GeoPoints'
|
||||
'bad $geoWithin value; $polygon should be Polygon object or Array of Parse.GeoPoint\'s'
|
||||
);
|
||||
}
|
||||
if (polygon.length < 3) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'bad $geoWithin value; $polygon should contain at least 3 GeoPoints'
|
||||
);
|
||||
}
|
||||
const points = polygon.map((point) => {
|
||||
points = points.map((point) => {
|
||||
if (point instanceof Array && point.length === 2) {
|
||||
Parse.GeoPoint._validate(point[1], point[0]);
|
||||
return `(${point[0]}, ${point[1]})`;
|
||||
}
|
||||
if (typeof point !== 'object' || point.__type !== 'GeoPoint') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value');
|
||||
} else {
|
||||
@@ -578,7 +645,7 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
}
|
||||
|
||||
Object.keys(ParseToPosgresComparator).forEach(cmp => {
|
||||
if (fieldValue[cmp]) {
|
||||
if (fieldValue[cmp] || fieldValue[cmp] === 0) {
|
||||
const pgComparator = ParseToPosgresComparator[cmp];
|
||||
patterns.push(`$${index}:name ${pgComparator} $${index + 1}`);
|
||||
values.push(fieldName, toPostgresValue(fieldValue[cmp]));
|
||||
@@ -595,6 +662,9 @@ const buildWhereClause = ({ schema, query, index }) => {
|
||||
}
|
||||
|
||||
export class PostgresStorageAdapter implements StorageAdapter {
|
||||
|
||||
canSortOnJoinTables: boolean;
|
||||
|
||||
// Private
|
||||
_collectionPrefix: string;
|
||||
_client: any;
|
||||
@@ -609,6 +679,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
const { client, pgp } = createClient(uri, databaseOptions);
|
||||
this._client = client;
|
||||
this._pgp = pgp;
|
||||
this.canSortOnJoinTables = false;
|
||||
}
|
||||
|
||||
handleShutdown() {
|
||||
@@ -846,7 +917,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
return this._client.task('delete-all-classes', function * (t) {
|
||||
try {
|
||||
const results = yield t.any('SELECT * FROM "_SCHEMA"');
|
||||
const joins = results.reduce((list, schema) => {
|
||||
const joins = results.reduce((list: Array<string>, schema: any) => {
|
||||
return list.concat(joinTablesForSchema(schema.schema));
|
||||
}, []);
|
||||
const classes = ['_SCHEMA', '_PushStatus', '_JobStatus', '_JobSchedule', '_Hooks', '_GlobalConfig', '_Audience', ...results.map(result => result.className), ...joins];
|
||||
@@ -879,7 +950,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
// Returns a Promise.
|
||||
deleteFields(className: string, schema: SchemaType, fieldNames: string[]): Promise<void> {
|
||||
debug('deleteFields', className, fieldNames);
|
||||
fieldNames = fieldNames.reduce((list, fieldName) => {
|
||||
fieldNames = fieldNames.reduce((list: Array<string>, fieldName: string) => {
|
||||
const field = schema.fields[fieldName]
|
||||
if (field.type !== 'Relation') {
|
||||
list.push(fieldName);
|
||||
@@ -1133,14 +1204,14 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
} else if (fieldName == 'authData') {
|
||||
// This recursively sets the json_object
|
||||
// Only 1 level deep
|
||||
const generate = (jsonb, key, value) => {
|
||||
const generate = (jsonb: string, key: string, value: any) => {
|
||||
return `json_object_set_key(COALESCE(${jsonb}, '{}'::jsonb), ${key}, ${value})::jsonb`;
|
||||
}
|
||||
const lastKey = `$${index}:name`;
|
||||
const fieldNameIndex = index;
|
||||
index += 1;
|
||||
values.push(fieldName);
|
||||
const update = Object.keys(fieldValue).reduce((lastKey, key) => {
|
||||
const update = Object.keys(fieldValue).reduce((lastKey: string, key: string) => {
|
||||
const str = generate(lastKey, `$${index}::text`, `$${index + 1}::jsonb`)
|
||||
index += 2;
|
||||
let value = fieldValue[key];
|
||||
@@ -1243,13 +1314,13 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
const keysToDelete = Object.keys(originalUpdate).filter(k => {
|
||||
const keysToDelete: Array<string> = Object.keys(originalUpdate).filter(k => {
|
||||
// choose top level fields that have a delete operation set.
|
||||
const value = originalUpdate[k];
|
||||
return value && value.__op === 'Delete' && k.split('.').length === 2 && k.split(".")[0] === fieldName;
|
||||
}).map(k => k.split('.')[1]);
|
||||
|
||||
const deletePatterns = keysToDelete.reduce((p, c, i) => {
|
||||
const deletePatterns = keysToDelete.reduce((p: string, c: string, i: number) => {
|
||||
return p + ` - '$${index + 1 + i}:value'`;
|
||||
}, '');
|
||||
|
||||
@@ -1540,7 +1611,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
aggregate(className: string, schema: any, pipeline: any) {
|
||||
debug('aggregate', className, pipeline);
|
||||
const values = [className];
|
||||
let index = 2;
|
||||
let index: number = 2;
|
||||
let columns: string[] = [];
|
||||
let countField = null;
|
||||
let groupValues = null;
|
||||
@@ -1732,6 +1803,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
||||
t.none(sql.array.addUnique),
|
||||
t.none(sql.array.remove),
|
||||
t.none(sql.array.containsAll),
|
||||
t.none(sql.array.containsAllRegex),
|
||||
t.none(sql.array.contains)
|
||||
]);
|
||||
});
|
||||
@@ -1836,6 +1908,40 @@ function processRegexPattern(s) {
|
||||
return literalizeRegexPart(s);
|
||||
}
|
||||
|
||||
function isStartsWithRegex(value) {
|
||||
if (!value || typeof value !== 'string' || !value.startsWith('^')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matches = value.match(/\^\\Q.*\\E/);
|
||||
return !!matches;
|
||||
}
|
||||
|
||||
function isAllValuesRegexOrNone(values) {
|
||||
if (!values || !Array.isArray(values) || values.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const firstValuesIsRegex = isStartsWithRegex(values[0].$regex);
|
||||
if (values.length === 1) {
|
||||
return firstValuesIsRegex;
|
||||
}
|
||||
|
||||
for (let i = 1, length = values.length; i < length; ++i) {
|
||||
if (firstValuesIsRegex !== isStartsWithRegex(values[i].$regex)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isAnyValueRegexStartsWith(values) {
|
||||
return values.some(function (value) {
|
||||
return isStartsWithRegex(value.$regex);
|
||||
});
|
||||
}
|
||||
|
||||
function createLiteralRegex(remaining) {
|
||||
return remaining.split('').map(c => {
|
||||
if (c.match(/[0-9a-zA-Z]/) !== null) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
CREATE OR REPLACE FUNCTION array_contains_all_regex(
|
||||
"array" jsonb,
|
||||
"values" jsonb
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT CASE
|
||||
WHEN 0 = jsonb_array_length("values") THEN true = false
|
||||
ELSE (SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements_text("array") as elt WHERE elt LIKE ANY (SELECT jsonb_array_elements_text("values"))) as RES)
|
||||
END;
|
||||
$function$;
|
||||
@@ -7,5 +7,8 @@ CREATE OR REPLACE FUNCTION array_contains_all(
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $function$
|
||||
SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES;
|
||||
SELECT CASE
|
||||
WHEN 0 = jsonb_array_length("values") THEN true = false
|
||||
ELSE (SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements_text("array") as elt WHERE elt IN (SELECT jsonb_array_elements_text("values"))) as RES)
|
||||
END;
|
||||
$function$;
|
||||
|
||||
@@ -9,6 +9,7 @@ module.exports = {
|
||||
addUnique: sql('array/add-unique.sql'),
|
||||
contains: sql('array/contains.sql'),
|
||||
containsAll: sql('array/contains-all.sql'),
|
||||
containsAllRegex: sql('array/contains-all-regex.sql'),
|
||||
remove: sql('array/remove.sql')
|
||||
},
|
||||
misc: {
|
||||
|
||||
@@ -24,6 +24,8 @@ export type UpdateQueryOptions = {
|
||||
export type FullQueryOptions = QueryOptions & UpdateQueryOptions;
|
||||
|
||||
export interface StorageAdapter {
|
||||
canSortOnJoinTables: boolean;
|
||||
|
||||
classExists(className: string): Promise<boolean>;
|
||||
setClassLevelPermissions(className: string, clps: any): Promise<void>;
|
||||
createClass(className: string, schema: SchemaType): Promise<void>;
|
||||
|
||||
@@ -50,7 +50,7 @@ const transformObjectACL = ({ ACL, ...result }) => {
|
||||
return 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 specialQuerykeys = ['$and', '$or', '$nor', '_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;
|
||||
@@ -111,6 +111,14 @@ const validateQuery = (query: any): void => {
|
||||
}
|
||||
}
|
||||
|
||||
if (query.$nor) {
|
||||
if (query.$nor instanceof Array && query.$nor.length > 0) {
|
||||
query.$nor.forEach(validateQuery);
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $nor format - use an array of at least 1 value.');
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(query).forEach(key => {
|
||||
if (query && query[key] && query[key].$regex) {
|
||||
if (typeof query[key].$options === 'string') {
|
||||
@@ -147,6 +155,7 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => {
|
||||
delete object._failed_login_count;
|
||||
delete object._account_lockout_expires_at;
|
||||
delete object._password_changed_at;
|
||||
delete object._password_history;
|
||||
|
||||
if ((aclGroup.indexOf(object.objectId) > -1)) {
|
||||
return object;
|
||||
|
||||
@@ -6,8 +6,14 @@ import * as Parse from "parse/node";
|
||||
// @flow-disable-next
|
||||
import * as request from "request";
|
||||
import { logger } from '../logger';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
|
||||
const DefaultHooksCollectionName = "_Hooks";
|
||||
const HTTPAgents = {
|
||||
http: new http.Agent({ keepAlive: true }),
|
||||
https: new https.Agent({ keepAlive: true }),
|
||||
}
|
||||
|
||||
export class HooksController {
|
||||
_applicationId:string;
|
||||
@@ -177,9 +183,12 @@ function wrapToHTTPRequest(hook, key) {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(jsonBody)
|
||||
body: JSON.stringify(jsonBody),
|
||||
};
|
||||
|
||||
const agent = hook.url.startsWith('https') ? HTTPAgents['https'] : HTTPAgents['http'];
|
||||
jsonRequest.agent = agent;
|
||||
|
||||
if (key) {
|
||||
jsonRequest.headers['X-Parse-Webhook-Key'] = key;
|
||||
} else {
|
||||
|
||||
@@ -530,6 +530,8 @@ export default class SchemaController {
|
||||
delete existingFields._rperm;
|
||||
delete existingFields._wperm;
|
||||
const newSchema = buildMergedSchemaObject(existingFields, submittedFields);
|
||||
const defaultFields = defaultColumns[className] || defaultColumns._Default;
|
||||
const fullNewSchema = Object.assign({}, newSchema, defaultFields);
|
||||
const validationError = this.validateSchemaData(className, newSchema, classLevelPermissions, Object.keys(existingFields));
|
||||
if (validationError) {
|
||||
throw new Parse.Error(validationError.code, validationError.error);
|
||||
@@ -561,7 +563,7 @@ export default class SchemaController {
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then(() => this.setPermissions(className, classLevelPermissions, newSchema))
|
||||
.then(() => this._dbAdapter.setIndexesWithSchemaFormat(className, indexes, schema.indexes, newSchema))
|
||||
.then(() => this._dbAdapter.setIndexesWithSchemaFormat(className, indexes, schema.indexes, fullNewSchema))
|
||||
.then(() => this.reloadData({ clearCache: true }))
|
||||
//TODO: Move this logic into the database adapter
|
||||
.then(() => {
|
||||
|
||||
@@ -149,7 +149,7 @@ export function getDatabaseController(options: ParseServerOptions, cacheControll
|
||||
export function getHooksController(options: ParseServerOptions, databaseController: DatabaseController): HooksController {
|
||||
const {
|
||||
appId,
|
||||
webhookKey
|
||||
webhookKey,
|
||||
} = options;
|
||||
return new HooksController(appId, databaseController, webhookKey);
|
||||
}
|
||||
@@ -228,4 +228,3 @@ export function getDatabaseAdapter(databaseURI, collectionPrefix, databaseOption
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -590,7 +590,18 @@ RestQuery.prototype.runAfterFindTrigger = function() {
|
||||
}
|
||||
// Run afterFind trigger and set the new results
|
||||
return triggers.maybeRunAfterFindTrigger(triggers.Types.afterFind, this.auth, this.className,this.response.results, this.config).then((results) => {
|
||||
this.response.results = results;
|
||||
// Ensure we properly set the className back
|
||||
if (this.redirectClassName) {
|
||||
this.response.results = results.map((object) => {
|
||||
if (object instanceof Parse.Object) {
|
||||
object = object.toJSON();
|
||||
}
|
||||
object.className = this.redirectClassName;
|
||||
return object;
|
||||
});
|
||||
} else {
|
||||
this.response.results = results;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+4
-2
@@ -282,7 +282,9 @@ RestWrite.prototype.findUsersWithAuthData = function(authData) {
|
||||
RestWrite.prototype.handleAuthData = function(authData) {
|
||||
let results;
|
||||
return this.findUsersWithAuthData(authData).then((r) => {
|
||||
results = r;
|
||||
results = r.filter((user) => {
|
||||
return !this.auth.isMaster && user.ACL && Object.keys(user.ACL).length > 0;
|
||||
});
|
||||
if (results.length > 1) {
|
||||
// More than 1 user with the passed id's
|
||||
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
|
||||
@@ -980,7 +982,7 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
||||
if (this.query) {
|
||||
// Force the user to not lockout
|
||||
// Matched with parse.com
|
||||
if (this.className === '_User' && this.data.ACL) {
|
||||
if (this.className === '_User' && this.data.ACL && this.auth.isMaster !== true) {
|
||||
this.data.ACL[this.query.objectId] = { read: true, write: true };
|
||||
}
|
||||
// update password timestamp if user password is being changed
|
||||
|
||||
@@ -89,6 +89,9 @@ export class FunctionsRouter extends PromiseRouter {
|
||||
},
|
||||
error: function(code, message) {
|
||||
if (!message) {
|
||||
if (code instanceof Parse.Error) {
|
||||
return reject(code)
|
||||
}
|
||||
message = code;
|
||||
code = Parse.Error.SCRIPT_FAILED;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from '../middlewares';
|
||||
import Parse from 'parse/node';
|
||||
|
||||
export class PurgeRouter extends PromiseRouter {
|
||||
|
||||
@@ -16,6 +17,11 @@ export class PurgeRouter extends PromiseRouter {
|
||||
cacheAdapter.role.clear();
|
||||
}
|
||||
return {response: {}};
|
||||
}).catch((error) => {
|
||||
if (!error || (error && error.code === Parse.Error.OBJECT_NOT_FOUND)) {
|
||||
return {response: {}};
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -103,9 +103,6 @@ export class UsersRouter extends ClassesRouter {
|
||||
user = results[0];
|
||||
}
|
||||
|
||||
if (req.config.verifyUserEmails && req.config.preventLoginWithUnverifiedEmail && !user.emailVerified) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.');
|
||||
}
|
||||
return passwordCrypto.compare(password, user.password);
|
||||
})
|
||||
.then((correct) => {
|
||||
@@ -117,7 +114,16 @@ export class UsersRouter extends ClassesRouter {
|
||||
if (!isValidPassword) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
|
||||
}
|
||||
|
||||
// Ensure the user isn't locked out
|
||||
// A locked out user won't be able to login
|
||||
// To lock a user out, just set the ACL to `masterKey` only ({}).
|
||||
// Empty ACL is OK
|
||||
if (!req.auth.isMaster && user.ACL && Object.keys(user.ACL).length == 0) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
|
||||
}
|
||||
if (req.config.verifyUserEmails && req.config.preventLoginWithUnverifiedEmail && !user.emailVerified) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.');
|
||||
}
|
||||
// handle password expiry policy
|
||||
if (req.config.passwordPolicy && req.config.passwordPolicy.maxPasswordAge) {
|
||||
let changedAt = user._password_changed_at;
|
||||
|
||||
@@ -224,6 +224,9 @@ export function getResponseObject(request, resolve, reject) {
|
||||
},
|
||||
error: function(code, message) {
|
||||
if (!message) {
|
||||
if (code instanceof Parse.Error) {
|
||||
return reject(code)
|
||||
}
|
||||
message = code;
|
||||
code = Parse.Error.SCRIPT_FAILED;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user