Compare commits

..
56 Commits
Author SHA1 Message Date
Florent Vilmart 0c2f1d583b ⚡ Release 2.8.2 (#4802) 2018-06-04 13:07:52 -04:00
greenkeeper[bot] 3b40d2227d Update flow-bin to the latest version 🚀 (#4786)
* chore(package): update flow-bin to version 0.73.0

* chore(package): update lockfile

https://npm.im/greenkeeper-lockfile
2018-06-01 09:32:33 -04:00
Tyler Brock c5e8470e36 Always use http-agent for hooks and triggers (#4800) 2018-06-01 09:31:10 -04:00
Florent Vilmart 9bff44b446 Ensure users with undefined ACL are treated as readable (#4795)
* Adds test to reproduce issue #4790

* Attempt to allow failure on node STABLE

* Use new format for apt packages
2018-05-30 12:55:15 -04:00
Tyler Brock f4422c491e Update defs for new hookKeepAlive config option (#4794)
All tests pass on re-run. :-)
2018-05-29 17:30:26 -07:00
Tyler Brock 82fb06f9c9 Use http agents for hook requests (#4791) 2018-05-29 17:48:06 -04:00
greenkeeper[bot] 32d8c36ff5 Update cross-env to the latest version 🚀 (#4780)
* chore(package): update cross-env to version 5.1.6

* chore(package): update lockfile

https://npm.im/greenkeeper-lockfile
2018-05-24 16:28:37 -04:00
greenkeeper[bot] fd862950e2 Update nodemon to the latest version 🚀 (#4781)
* chore(package): update nodemon to version 1.17.5

* chore(package): update lockfile

https://npm.im/greenkeeper-lockfile
2018-05-24 16:28:17 -04:00
Mads Bjerre 57da2def1d Adds withinPolygon support for Polygon object (#4067)
* Whitespace

* Add Polygon type to $polygon query

* Add tests

Polygon object in $polygon query
$geoIntersects queries

* Refactor

* Postgres support

* More tests

* Remove duplicate test

* Missing semicolon

* fix tests
2018-05-22 12:06:43 -04:00
Florent Vilmart 4dcbc4245b fix(package): update ws to version 5.2.0 (#4779)
* fix(package): update ws to version 5.2.0

* chore(package): update lockfile

https://npm.im/greenkeeper-lockfile
2018-05-21 21:43:50 -04:00
Florent Vilmart 642e2ffe12 Adds greenkeeper package-lock management (#4778) 2018-05-21 17:38:54 -04:00
greenkeeper[bot] d2b04ddd92 chore(package): update gaze to version 1.1.3 (#4776) 2018-05-21 15:45:10 -04:00
greenkeeper[bot] 33173c4d9b fix(package): update deepcopy to version 1.0.0 (#4775) 2018-05-20 15:13:17 -04:00
Florent Vilmart ff0c04ffa3 chore(package): update deep-diff to version 1.0.1 (#4774)
Closes #4733
2018-05-19 11:24:58 -04:00
greenkeeper[bot] df274dcca9 fix(package): update commander to version 2.15.1 (#4663) 2018-05-18 15:49:40 -04:00
Florent Vilmart a619119684 Bumps minimum node engine to 8+ (#4474)
* Use node 8 as development environment

* fixup! Use node 8 as development environment

* bump node to 8.10

* Targets node 8 for everything

* Run npm install so lock file is up to date

* Use push adapter v3

* Deflake a test on ParseUser

* Adds slight delay after logout

* Ensure we wait even if call fails

* Use node carbon
2018-05-18 15:49:00 -04:00
Jérémy Piednoel 77ed10f57b Adding $nor operator support (#4768)
* adding nor to specialQuerykeys

* adding nor suport

* adding test

* CRLF

* adding postgres NOR

* adding nor validation

* adding NOR test

* adding test amd fixing NOR cases

* revert the nor logic
2018-05-18 14:26:33 -05:00
Florent Vilmart a9ebc2b9af ⚡ Release 2.8.1 (#4771)
* unfuck release 2.8.0

* ⚡ Release 2.8.1

* Ensure we can install also on latest
2018-05-18 13:06:12 -04:00
Florent Vilmart 8fe3265c74 Release 2.8.0 (#4767)
* Ensure issue template has proper line endings

* Bumps to 2.8.0

* Adds Changelog
2018-05-18 10:55:20 -04:00
Saulo Tauil 95550f4c77 Making response.error accept a Parse.Error object as parameter (#4695) 2018-05-18 10:15:25 -04:00
Florent Vilmart fc47f9b84f issue(afterFind): Fixes issue when using afterFind with relations (#4752)
* Adds failing test for the issue

* Adds fix for the issue
2018-05-18 09:36:56 -04:00
Jérémy Piednoel c0f86ae1d1 Adding Mongodb element to add arrayMatches the #4762 (#4766)
* Adding elemMatch and nor

* lint

* adding test

* adding edge test

* postgres support

* clean up

* empty test
2018-05-18 08:35:50 -05:00
Florent Vilmart ad244d6654 feature: User Lockout (#4749)
* Allows masterKey to lock _User object and prevent login with email / password

* Ensure the authData based auth can be locked out as well when accounts is masterKey only
2018-05-16 15:40:02 -04:00
greenkeeper[bot] bfd0c4bf2f chore(package): update cross-env to version 5.1.5 (#4757) 2018-05-16 15:39:40 -04:00
greenkeeper[bot] d36c7c7f90 chore(package): update uws to version 10.148.0 (#4758) 2018-05-16 15:39:29 -04:00
greenkeeper[bot] 10232673d9 fix(package): update body-parser to version 1.18.3 (#4763) 2018-05-16 15:39:10 -04:00
Eduard Bosch Bertran c0e3672e32 New query condition support to match all strings that starts with some other given strings (#3864)
* feat: Convert $regex value to RegExp object

* feat: Add lib folder

* Revert "feat: Add lib folder"

This reverts commit c9dfbcbf69.

* feat: Add $regex test in $all array

* test: Test regex with $all only in MongoDB

* Revert "test: Test regex with $all only in MongoDB"

This reverts commit d7194c7869.

* feat: Add tests for containsAllStartingWith

* feat: Add postgres support

Thanks to @dplewis

* feat: Check that all values in $all must be regex or none

* test: Check that $all vaules must be regex or none

* feat: Update tests to use only REST API

* refactor: Move $all regex check to adapter

* feat: Check for valid $all values in progres

* refactor: Update function name

* fix:  Postgres $all values regex checking

* fix: Check starts with as string

* fix: Define contains all regex sql function

* fix: Wrong value check

* fix: Check valid data

* fix: Check regex when there is only one value

* fix: Constains all starting with string returns empty with bad params

* fix: Pass correct regex value

* feat: Add missing tests

* feat: Add missing tests

* feat: Add more tests

* fix: Unify MongoDB and PostgreSQL functionality

* fix: Lint checks

* fix: Test broken

$regex in $all list must be { $regex: "string" }

* test for empty $all
2018-05-15 20:42:32 -05:00
Joseph Frazier 2c357df33e Fix typo: "requrest" -> "request" (#4761) 2018-05-13 13:49:07 -04:00
Florent Vilmart 3999de7e0b Update issue templates 2018-05-08 16:40:07 -04:00
Florent Vilmart a3e5b20536 Bumps @parse/simple-mailgun-adapter (#4753) 2018-05-08 13:48:51 -04:00
Florent Vilmart bb1641419f fix(package): update mime to version 2.3.1 (#4721)
Closes #4716
2018-05-01 14:02:56 -04:00
Florent Vilmart 0b69700f18 chore(package): update flow-bin to version 0.71.0 (#4746)
Closes #4730
2018-05-01 13:32:12 -04:00
greenkeeper[bot] ac2695e196 chore(package): update deep-diff to version 1.0.0 (#4733) 2018-05-01 13:31:47 -04:00
Florent Vilmart 098d085621 fix(package): update request to version 2.85.0 (#4745)
Closes #4634
2018-05-01 13:31:02 -04:00
Vitaly Tomilov a1f5f37af7 upgrading pg-promise to 8.4.0 (#4736) 2018-05-01 13:27:29 -04:00
greenkeeper[bot] d48b30c734 fix(package): update mongodb to version 3.0.7 (#4732) 2018-05-01 13:26:04 -04:00
greenkeeper[bot] 3b5dff195f chore(package): update bcrypt to version 2.0.1 (#4735) 2018-05-01 13:25:37 -04:00
Florent Vilmart fad7b46c65 chore: Fixes issue related to flow types (#4724)
* Fixes issue related to flow types

* Improves type inference on where clause, index

* run parse-server start on liveQueryServer only on mongo, state leaks on postgres
2018-05-01 07:37:38 -04:00
Claire Neveu 04588bccb9 Allow creation of indices on default fields (#4738)
* Allow creation of indexes on default fields

* Update test

* Try to address flaky cache test
2018-04-25 16:06:10 -07:00
oleg b1017ac17c Send version for Vkontakte API (#4725) 2018-04-15 15:09:28 -04:00
Florent Vilmart ebf65bfc5d Updates @parse/push-adapter (#4723) 2018-04-15 09:52:18 -04:00
dblythy 22801d2d8f Ensure we respond with invalid password even if email is unverified (#4708) 2018-04-11 15:39:32 -04:00
Florent Vilmart 8380107e6b chore(package): update nodemon to version 1.17.3 (#4712)
Closes #4637
2018-04-10 13:57:11 -04:00
Florent Vilmart a77b0a238f chore(package): update flow-bin to version 0.69.0 (#4711)
Closes #4651
2018-04-10 13:56:46 -04:00
Florent Vilmart 6c81635188 fix(package): update mongodb to version 3.0.6 (#4710)
Closes #4674
2018-04-10 13:56:05 -04:00
Florent Vilmart fed40128a5 fix(package): update mime to version 2.2.2 (#4709)
Closes #4687
2018-04-10 13:55:21 -04:00
greenkeeper[bot] ee151de56a fix(package): update ws to version 5.1.1 (#4691) 2018-04-10 13:55:02 -04:00
Jong Eun Lee b0bcbd8731 add _password_history to default sensitive data (#4699) 2018-04-10 13:54:35 -04:00
greenkeeper[bot] 06be78802e chore(package): update bcrypt to version 2.0.0 (#4703) 2018-04-10 13:53:49 -04:00
Diamond Lewis 0fd8493929 Fix purging empty class (#4676)
* Fix purging empty class

* remove fit
2018-03-25 08:41:25 -05:00
Diamond Lewis 710848938f PG: Fixes issues comparing to zero or false (#4667)
* initial test

* more test

* clean up

* added correct errors

* test fix

* $eq null test

* type check
2018-03-24 17:45:34 -05:00
Samarth Verma 1a5c05b6a8 Fix organization url (#4639) 2018-03-21 14:54:08 -04:00
greenkeeper[bot] 306826fe0f fix(package): update ws to version 5.1.0 (#4662) 2018-03-21 14:52:50 -04:00
Diamond Lewis a3230011eb Check for node version in postinstall script (#4657)
Added open collective as a bonus.

I notice a lot of user still fork parse-server-example. They then update to the latest version but their engines.node in package.json stays the same.

This will output directions to support latest version.
2018-03-16 16:09:00 -05:00
SebC 6abf29dd97 Remove FB Graph API version from URL to use the oldest non deprecated version (2.5 for now, which will be deprecated soon) (#4650) 2018-03-15 14:05:41 -05:00
Diamond Lewis c36ef99368 Fix Aggregate Match Pointer (#4643) 2018-03-14 21:57:30 -05:00
41 changed files with 14426 additions and 144 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"presets": [
["env", {
"targets": {
"node": "6.11.4"
"node": "8"
}
}]
]
+51
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
8.10
+13 -6
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
FROM node:boron
FROM node:carbon
RUN mkdir -p /parse-server
COPY ./ /parse-server/
+12921
View File
File diff suppressed because it is too large Load Diff
+23 -21
View File
@@ -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",
+50
View File
@@ -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);
};
+1
View File
@@ -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
+1 -1
View File
@@ -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();
+35
View File
@@ -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', () => {
+5
View File
@@ -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')) {
+110
View File
@@ -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});
+35 -34
View File
@@ -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 = {
+8 -4
View File
@@ -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 });
+610
View File
@@ -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");
+33
View File
@@ -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
View File
@@ -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) => {
+2 -10
View File
@@ -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
View File
@@ -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({
+23
View File
@@ -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',
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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') {
+89 -11
View File
@@ -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: {
+2
View File
@@ -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>;
+10 -1
View File
@@ -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;
+10 -1
View File
@@ -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 {
+3 -1
View File
@@ -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(() => {
+1 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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;
}
+6
View File
@@ -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;
});
}
+10 -4
View File
@@ -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;
+3
View File
@@ -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;
}