Compare commits

..
107 Commits
Author SHA1 Message Date
Steven Shipton 593a7a6264 Release 2.3.7 2017-03-10 18:05:13 +00:00
steven-supersolid 4addf9dfa9 Update version to 2.3.7 2017-03-10 17:49:55 +00:00
Steven Shipton 2547fd08ae Update CHANGELOG.md
style nits
2017-03-10 17:15:34 +00:00
Steven Shipton 581f919d1a Changelog 2.3.7 2017-03-10 17:12:56 +00:00
Steven Shipton 0b1a61cd85 Merge pull request #3614 from ParsePlatform/greenkeeper-request-2.81.0
Update request to version 2.81.0 🚀
2017-03-10 16:37:00 +00:00
greenkeeperio-bot 943b84b13f chore(package): update request to version 2.81.0
https://greenkeeper.io/
2017-03-09 08:09:13 -08:00
Steven Shipton 70f60267c5 upgrade body-parser and express (#3605) 2017-03-07 08:53:53 -08:00
Greenkeeper f0f04d6351 chore(package): update body-parser to version 1.17.0 (#3589)
https://greenkeeper.io/
2017-03-05 09:11:53 -08:00
Arthur Cinader 1d8edd73c3 default for userSensitiveFields should be an array not a string. (#3599) 2017-03-05 09:06:45 -08:00
Greenkeeper 8d802b9461 chore(package): update request to version 2.80.0 (#3596)
https://greenkeeper.io/
2017-03-04 15:57:37 -08:00
Vitaly Tomilov 271608bc82 Update PostgresStorageAdapter.js (#3578)
* Update PostgresStorageAdapter.js

proper database API, via transaction.

* Update PostgresStorageAdapter.js

fixing bracket.

* Update PostgresStorageAdapter.js

adding the same rejection approach as before, the functionality remains identical.

* Update PostgresStorageAdapter.js

* Update PostgresStorageAdapter.js

lint fixing

* Update PostgresStorageAdapter.js

fixing misspelling.
2017-03-04 15:56:53 -08:00
Greenkeeper 5c5ef0313b chore(package): update ws to version 2.2.0 (#3580)
https://greenkeeper.io/
2017-03-04 17:50:54 -06:00
Xy Ziemba 29fec01a42 Resend Verification Email Endpoint (#3543)
* Endpoint to Handle Verification Email Request

* Adds tests for verificationEmailRequest endpoint

* Better error responses for `/verificationEmailRequest`
2017-03-04 13:30:52 -08:00
Greenkeeper 92b699920e chore(package): update express to version 4.15.0 (#3590)
https://greenkeeper.io/
2017-03-04 13:27:59 -08:00
Natan Rolnik 9bfa0c60c4 Adds index on _Role name property (#3586)
* Adds index on _Role name property

In order to avoid having different _Role objects with the same name, adding an index on the name property of _Role is necessary.

Fixes #3579

* Uses throw instead of Promise.reject when enforcing unique indexes

* Fixes wrong sorting of results in schemas tests
2017-03-04 12:42:19 -08:00
Greenkeeper 0181fb51b3 chore(package): update winston-daily-rotate-file to version 1.4.5 (#3594)
https://greenkeeper.io/
2017-03-03 11:57:25 -08:00
Ryan Foster e6006e800e adding TTL option for redis cache adapter (#3397)
* adding TTL option for redis cache adapter

* adding test for RedisCacheAdapter

* Fixing adapater var name

* changing timeout

* updating default time

* Fix the redis cache spec to construct the cache with the anticipated ttl

make timeout values really really small so our test run fast :).
2017-02-27 12:29:15 -08:00
Florent Vilmart a45d1848db version 2.3.6 (#3574)
* version 2.3.6

* Update CHANGELOG.md
2017-02-27 19:10:06 +02:00
Greenkeeper 6381e15e6d chore(package): update pg-promise to version 5.6.2 (#3573)
https://greenkeeper.io/
2017-02-27 11:45:43 -05:00
Jack Wearden 73195627c0 Alleviate SERVER-13732 on all top level filters (#3564)
In a prior commit, improvements were made to the addition of `_rperm`
in the case of `$or` queries, to avoid MongoDB bug SERVER-13732.

As the vast majority of $or queries previously hit this bug due to the
presence of `_rperm` on most Parse queries), the present solution
avoids the bug and improves query performance in most cases.

However, it's still possible for clients to supply their own queries
which hit that bug, such as those with `_created_at` or `_updated_at`
filters, or their own properties from their data model.

This commit makes the logic currently present for `_rperm` available
to all top level filters that exist alongside an $or query, meaning
SERVER-13732 should be avoided in all cases where keys at the top and
inner levels do not have name clashes.

- https://github.com/ParsePlatform/parse-server/pull/3476
- https://jira.mongodb.org/browse/SERVER-13732
2017-02-26 13:37:33 -05:00
Greenkeeper 053bbefda9 chore(package): update pg-promise to version 5.6.1 (#3572)
https://greenkeeper.io/
2017-02-26 13:34:19 -05:00
Paulo Vítor S Reis 032d5e55ca Logger MaskSentive just for strings and array string items (#3571) 2017-02-26 13:34:03 -05:00
shaukatnamal fe5947ce4c chore(package): update eslint to version 3.16.1 (#3563)
https://greenkeeper.io/
2017-02-24 16:18:57 -08:00
Bhaskar Reddy Yasa 0e900cbefd allow flow through to passwordPolicy in case of empty ('') password (#3560) 2017-02-24 07:21:50 -05:00
Florent Vilmart 41358d2226 Adds ability to pass a middleware to CLI for instrumentation (#3554)
* Adds ability to pass a middleware to CLI for instrumentation

* Adds readme
2017-02-22 23:42:21 +02:00
Greenkeeper 73260897cd chore(package): update pg-promise to version 5.5.8 (#3551)
https://greenkeeper.io/
2017-02-21 14:28:12 -08:00
Florent Vilmart d3e6c0dea3 Adds test for #3544 (#3545) 2017-02-21 15:06:45 -05:00
Florent Vilmart b4e27e1160 Adds testing safeguard on lib (#3537) 2017-02-20 22:45:45 +02:00
Greenkeeper 79b0c790f0 chore(package): update uws to version 0.13.0 (#3542)
https://greenkeeper.io/
2017-02-20 12:26:44 -08:00
Greenkeeper 64f608ec9f chore(package): update ws to version 2.1.0 (#3526)
https://greenkeeper.io/
2017-02-20 12:25:39 -08:00
Tyler Brock 67260b5fe0 Factor out checking for triggers/liveQuery in rest.js (#3539) 2017-02-20 12:25:25 -08:00
Florent Vilmart 411daf4d41 Release 2.3.5 2017-02-19 11:34:42 -05:00
Natan Rolnik 7a3c9cd33a Changelog 2.3.4 (#3533) 2017-02-19 08:44:33 -05:00
Florent Vilmart 193e5a4278 Make sure we don't treat dot notation keys as topLevel atoms (#3531)
Fixing GeoPoints and Files in _GlobalConfig
2017-02-19 12:07:54 +02:00
Jeremy Louie 6ae0675010 Use flushdb instead of flushall in RedisCacheAdapter (#3523) 2017-02-18 19:36:43 -05:00
David Starke a520ea0243 Fix LiveQuery unsafe user (#3525)
* LiveQuery should not use unsafe user setting

* server should issue queries with the master key
2017-02-18 19:36:20 -05:00
Greenkeeper 58bdeeee2d chore(package): update pg-promise to version 5.5.7 (#3528)
https://greenkeeper.io/
2017-02-18 19:24:32 -05:00
Vitaly Tomilov fc7f9f76f5 removing inline sql. (#3529)
* removing inline sql.

* Update index.js

trying to fix indentation.

* Update index.js

commenting out console output for errors.

* Rename json-object-set-key.sql to json-object-set-keys.sql

* Update index.js

* Update add-unique.sql

correcting sql.

* Update add.sql

correcting sql.

* Update contains-all.sql

correcting sql.

* Update contains.sql

correcting sql.

* Update remove.sql

correcting sql.

* Update json-object-set-keys.sql

fixing sql reserved words.
2017-02-18 14:26:25 -05:00
Greenkeeper ef8d534679 chore(package): update babel-core to version 6.23.1 (#3512)
https://greenkeeper.io/
2017-02-17 10:18:12 -08:00
Greenkeeper 1c9852e53f chore(package): update babel-core to version 6.23.0 (#3509)
https://greenkeeper.io/
2017-02-17 10:16:43 -08:00
Greenkeeper 3f0de0c2ed chore(package): update babel-register to version 6.23.0 (#3510)
https://greenkeeper.io/
2017-02-17 10:14:27 -08:00
Greenkeeper eccdeccb59 chore(package): update babel-cli to version 6.23.0 (#3511)
https://greenkeeper.io/
2017-02-17 10:14:10 -08:00
Greenkeeper f16f8423db chore(package): update mongodb to version 2.2.24 (#3513)
https://greenkeeper.io/
2017-02-17 10:13:55 -08:00
Greenkeeper bd60a5c1d1 chore(package): update body-parser to version 1.16.1 (#3499)
https://greenkeeper.io/
2017-02-11 15:03:05 -08:00
Robin Naundorf 40b9f953ca Fix small display errors (#3500)
* Add missing '$' to declare variable
* Move \n to correct position
2017-02-11 12:24:09 -05:00
Héctor Ramos 15eafd4dcc Update ISSUE_TEMPLATE.md 2017-02-10 11:56:58 -08:00
Héctor Ramos 11cde12245 Merge pull request #3497 from acinader/allow-empty-client-key
Allow empty client key
2017-02-09 16:16:49 -08:00
Arthur Cinader 5861996cb0 explicitly check if auth keys are undefined
Simply checking if they are truthy causes a false negative
if the value is ''.
2017-02-09 15:20:10 -08:00
Arthur Cinader ca70ad0141 Add a unit test to fail when clientKey='' 2017-02-09 14:50:28 -08:00
Natan Rolnik 5a22df13a5 Changelog 2.3.3 (#3493) 2017-02-09 17:13:15 +02:00
Greenkeeper 5b7144176c chore(package): update ws to version 2.0.3 (#3488)
https://greenkeeper.io/
2017-02-08 10:43:00 -08:00
Arthur Cinader 4979503f63 Two tests that fail often. Give em a little more time. (#3453) 2017-02-08 10:06:42 -08:00
Greenkeeper e32a37c2ec chore(package): update bcryptjs to version 2.4.3 (#3485)
https://greenkeeper.io/
2017-02-08 09:09:29 -08:00
Florent Vilmart f7f17cdb23 Update package.json (#3480)
* Update package.json

* Update .travis.yml
2017-02-06 10:41:33 -07:00
nodechefMatt ecf422b2d9 Mongo object to Parse object date serialization - avoid re-serialization of iso of type Date (#3389)
* Mongo object to Parse object date serialization - avoid nested ios

* Mongo object to Parse object date serialization

* Remove file from previous commit
2017-02-04 08:32:35 -08:00
Greenkeeper 49dc8df1de chore(package): update babel-cli to version 6.22.2 (#3411)
https://greenkeeper.io/
2017-02-03 21:40:32 -08:00
Greenkeeper c3c5f06efd chore(package): update babel-register to version 6.22.0 (#3409)
https://greenkeeper.io/
2017-02-03 21:40:21 -08:00
Greenkeeper 2a454fec35 chore(package): update babel-preset-es2015 to version 6.22.0 (#3408)
https://greenkeeper.io/
2017-02-03 21:40:07 -08:00
Greenkeeper 01b4c4e0cb chore(package): update babel-plugin-transform-flow-strip-types to version 6.22.0 (#3406)
https://greenkeeper.io/
2017-02-03 21:39:18 -08:00
Greenkeeper 3714d68aaf chore(package): update babel-preset-stage-0 to version 6.22.0 (#3407)
https://greenkeeper.io/
2017-02-03 21:38:49 -08:00
Greenkeeper 77daa3ca18 chore(package): update ws to version 2.0.2 (#3475)
https://greenkeeper.io/
2017-02-03 21:37:24 -08:00
Greenkeeper 0b574f04a9 chore(package): update babel-core to version 6.22.1 (#3412)
https://greenkeeper.io/
2017-02-03 21:37:05 -08:00
Greenkeeper a89a30182e chore(package): update mongodb to version 2.2.22 (#3426)
https://greenkeeper.io/
2017-02-03 21:36:53 -08:00
Jack Wearden 96a9588a6e MongoDB $or Queries avoid SERVER-13732 bug (#3476)
MongoDB has an unfixed bug in all supported versions 2.6-3.4 which
results in suboptimal index usage for `$or` queries when the query has
implicit `$and`s at the query root.

When adding `_rperm` to `$or` queries, Parse accidentally creates
queries which hit this bug.

The fix in this commit applies the suggested workaround of putting the
`_rperm` property inside all `$or` subdocuments, moving it from the top
level and leaving `$or` as the only top-level operator.

MongoDB Bug Link: https://jira.mongodb.org/browse/SERVER-13732
2017-02-03 21:34:19 -08:00
Greenkeeper 66cdfaed4b chore(package): update ws to version 2.0.1 (#3465)
https://greenkeeper.io/
2017-02-02 14:05:05 -08:00
Greenkeeper 9262147a6f chore(package): update pg-promise to version 5.5.6 (#3461)
https://greenkeeper.io/
2017-02-02 12:00:07 -08:00
Greenkeeper 8b4e9ca637 chore(package): update multer to version 1.3.0 (#3434)
https://greenkeeper.io/
2017-02-02 11:52:20 -08:00
Greenkeeper 2160d10990 chore(package): update redis to version 2.6.5 (#3377)
https://greenkeeper.io/
2017-02-02 09:10:21 -08:00
Arthur Cinader e7f2995143 Adjust a test for change to logging. (#3452) 2017-01-30 13:28:52 -05:00
Arthur Cinader d5940b17b6 Add logging on failure to create file (#3424) 2017-01-30 12:36:30 -05:00
Arthur Cinader f864141663 Log Parse Errors so they are intelligible. (#3431)
The problem this pr is trying to solve:

When an error occurs on the server, a message should
be returned to the client, and a message should be logged.

Currently, on the server, the log is just [object, object]

This pr will stop calling the default express error handler
which causes two problems: 1. it writes to console instead of log file
2. the output is completely useless! :)

Instead, we'll log the error ourselves using the ParseServer's logger.

fixes: #661
2017-01-30 12:32:54 -05:00
Elliot Blackburn 711db9ccd2 Remove duplicate word from the README log section (#3450)
In the "Logging" section it originally read: "Parse Server, will, by default, will log:", I've removed the second "will" to make "Parse Server will, by default, log:".
2017-01-30 14:39:18 +02:00
Greenkeeper 6a6829ce80 chore(package): update express to version 4.14.1 (#3445)
https://greenkeeper.io/
2017-01-29 15:08:47 -05:00
Ricardo Matsui d67be492f5 Verify pushes are sent to the expected number of installations (#3418) 2017-01-28 11:10:21 -08:00
Greenkeeper ea45f69af6 chore(package): update winston-daily-rotate-file to version 1.4.4 (#3442)
https://greenkeeper.io/
2017-01-28 09:18:21 -08:00
Greenkeeper 43c3d8e2a2 chore(package): update winston to version 2.3.1 (#3414)
https://greenkeeper.io/
2017-01-23 10:29:40 -07:00
Greenkeeper d8a1a614e8 chore(package): update body-parser to version 1.16.0 (#3391)
https://greenkeeper.io/
2017-01-19 13:42:17 -08:00
Greenkeeper 553db0ada5 chore(package): update pg-promise to version 5.5.2 (#3379)
https://greenkeeper.io/
2017-01-19 11:01:03 -08:00
Greenkeeper 388a59d816 chore(package): update pg-promise to version 5.5.1 (#3362)
https://greenkeeper.io/
2017-01-19 11:00:34 -08:00
Greenkeeper de4c1ee418 Update mongodb to version 2.2.21 🚀 (#3368)
* chore(package): update mongodb to version 2.2.20

https://greenkeeper.io/

* Fix race when fetching schema frequently

* Bumps mongo to 2.2.21
2017-01-16 13:26:14 -08:00
Florent Vilmart cb15c0b616 Disallow failures for PG (#3372) 2017-01-16 13:24:45 -08:00
Greenkeeper 8dd864a033 chore(package): update parse-server-push-adapter to version 1.2.0 (#3386)
https://greenkeeper.io/
2017-01-16 11:27:19 -05:00
Florent Vilmart c26db2248f use uWS as optional dependency for ws server (#3231)
* use uWS as optional dependency for ws server

* Update ParseWebSocketServer.js
2017-01-14 07:43:31 -08:00
Greenkeeper b2a3479dbf chore(package): update winston-daily-rotate-file to version 1.4.3 (#3369)
https://greenkeeper.io/
2017-01-14 07:40:57 -08:00
Florent Vilmart deedf7b370 Push scalability (#3080)
* Update status through increment
* adds support for incrementing nested keys
* fix issue when having spaces in keys for ordering
* Refactors PushController to use worker
* Adds tests for custom push queue config
* Makes PushController adapter independant
* Better logging of _PushStatus in VERBOSE
2017-01-13 19:34:04 -05:00
Greenkeeper 5f849ca662 chore(package): update redis to version 2.6.4 (#3364)
https://greenkeeper.io/
2017-01-12 21:45:14 -05:00
Greenkeeper 0279b5e5f3 chore(package): update jasmine to version 2.5.3 (#3358)
https://greenkeeper.io/
2017-01-11 18:54:23 -08:00
Arthur Cinader 4cb6e7d209 Add lint rule space-infix-ops (#3237)
Disallows: 1+1.  Must be 1 + 1.
2017-01-11 12:31:40 -08:00
Greenkeeper 56bb505df2 chore(package): update jasmine-spec-reporter to version 3.1.0 (#3344)
https://greenkeeper.io/
2017-01-11 12:31:15 -08:00
Arthur Cinader ceb5a1b5f0 chore(package): update jasmine-spec-reporter to version 3.0.0 (#3333)
https://greenkeeper.io/

Fix parse-server to work with jasmine-spec-reporter-3.0.0
2017-01-11 11:53:24 -08:00
Natan Rolnik 0958b15297 Changelog 2.3.2 (#3343)
* Updates package.json: version to 2.3.2

* Updates Changelog for 2.3.2
2017-01-09 15:46:34 +02:00
Lenart Rudel 5d9dbea07b Add parseFrameURL for masking user-facing pages (#3267)
* Add parseFrameURL for masking user-facing pages.

Allow users to specify a different address which is used to mask parse
requests for verifying email and resetting password. This is how Parse.com
used to allow customers to gain control over page content, styling etc.

On the destination page javascript is used to check the link in the request
and embed the parse server page using IFRAME.

* Fix code indentation

* Rename method for building link and pass config to it.

* Add customPages options to README.md.

* Add tests for parseFrameURL email link building, and parseFrameURL option.

* Add parseFrameURL for masking user-facing pages.

Allow users to specify a different address which is used to mask parse
requests for verifying email and resetting password. This is how Parse.com
used to allow customers to gain control over page content, styling etc.

On the destination page javascript is used to check the link in the request
and embed the parse server page using IFRAME.

* Fix code indentation

* Rename method for building link and pass config to it.

* Add customPages options to README.md.

* Don't Object.assign to defaultConfiguration global
2017-01-08 10:56:57 -08:00
Bhaskar Reddy Yasa f331f6644c Add support for regex string for password policy validatorPattern setting (#3331) 2017-01-08 07:12:44 -08:00
David Starke df029b82eb LiveQuery should match subobjects with dot notation (#3322)
* LiveQuery should match subobjects with dot notation

* one additional test case
2017-01-06 17:06:25 -08:00
Arthur Cinader edba550cf6 winston-daily-roate-file-1.4.2 makes a change where timestamp is not on by default anymore. (#3335)
see: https://github.com/winstonjs/winston-daily-rotate-file/commit/aa28f522718778fe8210ced1090353ffc29c6f31

pretty incredible that they would release this as a patch release, but my fix is solid and defensive

chore(package): update winston-daily-rotate-file to version 1.4.2

https://greenkeeper.io/
2017-01-06 16:32:03 -08:00
Greenkeeper c7775eb0b9 chore(package): update cross-env to version 3.1.4 (#3308)
https://greenkeeper.io/
2017-01-06 14:50:09 -08:00
Greenkeeper 0b44820a7d chore(package): update winston-daily-rotate-file to version 1.4.1 (#3317)
https://greenkeeper.io/
2017-01-06 13:20:12 -08:00
Greenkeeper b7322064d8 chore(package): update pg-promise to version 5.5.0 (#3224)
https://greenkeeper.io/
2017-01-04 09:41:21 -05:00
jeacott1 ac3f5cd686 this function was extremely slow. (#3264)
this fix reduces the time to process 165k installations
from 282738.599ms (almost 5 minutes of total server lockup!)
to just 16.283ms!
2017-01-04 09:27:47 -05:00
Greenkeeper 9fd34fd25e chore(package): update bcryptjs to version 2.4.0 (#3225)
https://greenkeeper.io/
2017-01-03 16:42:59 -06:00
Greenkeeper 7ff3a3813d chore(package): update babel-core to version 6.21.0 (#3249)
https://greenkeeper.io/
2017-01-02 15:04:18 -06:00
Greenkeeper 754f5d0505 chore(package): update babel-plugin-transform-flow-strip-types to version 6.21.0 (#3250)
https://greenkeeper.io/
2017-01-02 14:40:30 -06:00
Greenkeeper 0e8c3c2457 chore(package): update multer to version 1.2.1 (#3276)
https://greenkeeper.io/
2017-01-02 14:09:24 -06:00
Greenkeeper 423a50133a chore(package): update lodash to version 4.17.4 (#3303)
https://greenkeeper.io/
2017-01-02 13:25:37 -06:00
Greenkeeper a3d416de4a chore(package): update bcrypt to version 1.0.2 (#3304)
https://greenkeeper.io/
2017-01-01 12:01:59 -06:00
Greenkeeper a2dfcc8da8 chore(package): update babel-core to version 6.20.0 (#3216)
https://greenkeeper.io/
2017-01-01 08:44:38 -06:00
Arthur Cinader f4734a65c0 fix trivial typo (#3238) 2016-12-14 14:28:23 -08:00
101 changed files with 2026 additions and 641 deletions
+2 -1
View File
@@ -20,6 +20,7 @@
"eol-last": 2,
"space-in-parens": ["error", "never"],
"no-multiple-empty-lines": 1,
"prefer-const": "error"
"prefer-const": "error",
"space-infix-ops": "error"
}
}
+9 -33
View File
@@ -1,50 +1,28 @@
Please read the following instructions carefully.
We use GitHub Issues for bugs.
Check out https://github.com/ParsePlatform/parse-server/issues/1271 for an ideal bug report.
The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly!
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
Many members of the community use Stack Overflow and Server Fault to ask questions.
Read through the existing questions or ask your own!
- Stack Overflow: http://stackoverflow.com/questions/tagged/parse.com
- Server Fault: https://serverfault.com/tags/parse
You may also search through existing issues before opening a new one: https://github.com/ParsePlatform/Parse-Server/issues?utf8=%E2%9C%93&q=is%3Aissue
For database migration help, please file a bug report at https://parse.com/help#report
Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server!
- [ ] You've met the prerequisites: https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites.
- [ ] You're running the latest version of Parse Server: https://github.com/ParsePlatform/parse-server/releases
- [ ] You've searched through existing issues: https://github.com/ParsePlatform/Parse-Server/issues?utf8=%E2%9C%93&q=is%3Aissue Chances are that your issue has been reported or resolved before.
- [ ] You have filled out every section below. Issues without sufficient information are more likely to be closed.
--
--- Please use this template. If you don't use this template, your issue may be closed without comment. ---
### Issue Description
[DELETE EVERYTHING ABOVE THIS LINE BEFORE SUBMITTING YOUR ISSUE]
Describe your issue in as much detail as possible.
[FILL THIS OUT]
### Steps to reproduce
Please include a detailed list of steps that reproduce the issue. Include curl commands when applicable.
1. [FILL THIS OUT]
2. [FILL THIS OUT]
3. [FILL THIS OUT]
#### Expected Results
[FILL THIS OUT]
What you expected to happen.
#### Actual Outcome
[FILL THIS OUT]
What is happening instead.
### Environment Setup
@@ -62,6 +40,4 @@ Please include a detailed list of steps that reproduce the issue. Include curl c
### Logs/Trace
You can turn on additional logging by configuring VERBOSE=1 in your environment.
[FILL THIS OUT]
Include all relevant logs. You can turn on additional logging by configuring VERBOSE=1 in your environment.
+2 -4
View File
@@ -1,7 +1,7 @@
language: node_js
dist: trusty
node_js:
- '4.5'
- '4.6'
- '6.1'
services:
- postgresql
@@ -12,7 +12,7 @@ addons:
apt_packages:
- postgresql-9.5-postgis-2.3
before_script:
- ls -al "$HOME/.mongodb/versions"
- 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
@@ -26,8 +26,6 @@ env:
- PARSE_SERVER_TEST_CACHE=redis
matrix:
fast_finish: true
allow_failures:
- env: PARSE_SERVER_TEST_DB=postgres
branches:
only:
- master
+87 -3
View File
@@ -1,5 +1,89 @@
## Parse Server Changelog
### 2.3.7
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.6...2.3.7)
#### New Features
* New endpoint to resend verification email, thanks to [Xy Ziemba](https://github.com/xyziemba)
#### Improvements
* Add TTL option for Redis Cache Adapter, thanks to [Ryan Foster](https://github.com/f0ster)
* Update Postgres Storage Adapter, thanks to [Vitaly Tomilov](https://github.com/vitaly-t)
#### Bug Fixes
* Add index on Role.name, fixes (#3579), thanks to [Natan Rolnik](https://github.com/natanrolnik)
* Fix default value of userSensitiveFields, fixes (#3593), thanks to [Arthur Cinader](https://github.com/acinader)
#### Dependency Updates:
* [body-parser@1.17.1](https://www.npmjs.com/package/body-parser)
* [express@4.15.2](https://www.npmjs.com/package/express)
* [request@2.81.0](https://www.npmjs.com/package/request)
* [winston-daily-rotate-file@1.4.5](https://www.npmjs.com/package/winston-daily-rotate-file)
* [ws@2.2.0](https://www.npmjs.com/package/ws)
### 2.3.6
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.5...2.3.6)
#### Improvements
* Adds support for injecting a middleware for instumentation in the CLI, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Alleviate mongodb bug with $or queries [SERVER-13732](https://jira.mongodb.org/browse/SERVER-13732), thanks to [Jack Wearden](https://github.com/NotBobTheBuilder)
#### Bug Fixes
* Fix issue affecting password policy and empty passwords, thanks to [Bhaskar Reddy Yasa](https://github.com/bhaskaryasa)
* Fix issue when logging url in non string objects, thanks to [Paulo Vítor S Reis](https://github.com/paulovitin)
#### Dependencies updates:
* [ws@2.1.0](https://npmjs.com/package/ws)
* [uws@0.13.0](https://npmjs.com/package/uws)
* [pg-promise@5.6.2](https://npmjs.com/package/pg-promise)
### 2.3.5
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.3...2.3.5)
#### Bug Fixes
* Allow empty client key
(#3497), thanks to [Arthur Cinader](https://github.com/acinader)
* Fix LiveQuery unsafe user
(#3525), thanks to [David Starke](https://github.com/dstarke)
* Use `flushdb` instead of `flushall` in RedisCacheAdapter
(#3523), thanks to [Jeremy Louie](https://github.com/JeremyPlease)
* Fix saving GeoPoints and Files in `_GlobalConfig` (Make sure we don't treat
dot notation keys as topLevel atoms)
(#3531), thanks to [Florent Vilmart](https://github.com/flovilmart)
### 2.3.3
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.2...2.3.3)
#### Breaking Changes
* **Minimum Node engine bumped to 4.6** (#3480), thanks to [Florent Vilmart](https://github.com/flovilmart)
#### Bug Fixes
* Add logging on failure to create file (#3424), thanks to [Arthur Cinader](https://github.com/acinader)
* Log Parse Errors so they are intelligible (#3431), thanks to [Arthur Cinader](https://github.com/acinader)
* MongoDB $or Queries avoid SERVER-13732 bug (#3476), thanks to [Jack Wearden](https://github.com/NotBobTheBuilder)
* Mongo object to Parse object date serialization - avoid re-serialization of iso of type Date (#3389), thanks to [nodechefMatt](https://github.com/nodechefMatt)
#### Improvements
* Ground preparations for push scalability (#3080), thanks to [Florent Vilmart](https://github.com/flovilmart)
* Use uWS as optional dependency for ws server (#3231), thanks to [Florent Vilmart](https://github.com/flovilmart)
### 2.3.2
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.1...2.3.2)
#### New features
* Add parseFrameURL for masking user-facing pages (#3267), thanks to [Lenart Rudel](https://github.com/lenart)
#### Bug fixes
* Fix Parse-Server to work with winston-daily-rotate-1.4.2 (#3335), thanks to [Arthur Cinader](https://github.com/acinader)
#### Improvements
* Add support for regex string for password policy validatorPattern setting (#3331), thanks to [Bhaskar Reddy Yasa](https://github.com/bhaskaryasa)
* LiveQuery should match subobjects with dot notation (#3322), thanks to [David Starke](https://github.com/dstarke)
* Reduce time to process high number of installations for push (#3264), thanks to [jeacott1](https://github.com/jeacott1)
* Fix trivial typo in error message (#3238), thanks to [Arthur Cinader](https://github.com/acinader)
### 2.3.1
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.0...2.3.1)
@@ -17,7 +101,7 @@ This release addresses only that issue.
#### New Features
* Adds ability to restrict access through Class Level Permissions to only authenticated users [see docs](http://parseplatform.github.io/docs/ios/guide/#requires-authentication-permission-requires-parse-server---230)
* Adds ability to strip sensitive data from _User responses, strips emails by default, thanks to [Arthur Cinader](https://github.com/acinader)
* Adds ability to strip sensitive data from `_User` responses, strips emails by default, thanks to [Arthur Cinader](https://github.com/acinader)
* Adds password history support for password policies, thanks to [Bhaskar Reddy Yasa](https://github.com/bhaskaryasa)
#### Improvements
@@ -70,7 +154,7 @@ Postgres support requires v9.5
* Better Postgres support, thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
* Logs the function name when failing (#2963), thanks to [Michael Helvey](https://github.com/michaelhelvey)
* CLI: forces closing the connections with SIGINT/SIGTERM (#2964), thanks to [Kulshekhar Kabra](https://github.com/kulshekhar)
* Reduce the number of calls to the _SCHEMA table (#2912), thanks to [Steven Shipton](https://github.com/steven-supersolid)
* Reduce the number of calls to the `_SCHEMA` table (#2912), thanks to [Steven Shipton](https://github.com/steven-supersolid)
* LiveQuery: Support for Role ACL's, thanks to [Aaron Blondeau](https://github.com/aaron-blondeau-dose)
#### Bug Fixes
@@ -117,7 +201,7 @@ Postgres support requires v9.5
### 2.2.20
* New: Adds CloudCode handler for `beforeFind`, thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: RedisCacheAdapter for syncing schema, role and user caches across servers, thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: RedisCacheAdapter for syncing schema, role and user caches across servers, thanks to [Florent Vilmart](https://github.com/flovilmart)
* New: Latest master build available at `ParsePlatform/parse-server#latest`, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Better support for upgradeToRevocableSession with missing session token, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Removes babel-polyfill runtime dependency, thanks to [Florent Vilmart](https://github.com/flovilmart)
+4 -2
View File
@@ -156,7 +156,7 @@ For a full list of available options, run `parse-server --help`.
## Logging
Parse Server will, by default, will log:
Parse Server will, by default, log:
* to the console
* daily rotating files as new line delimited JSON
@@ -216,6 +216,8 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
* `revokeSessionOnPasswordReset` - When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
* `accountLockout` - Lock account when a malicious user is attempting to determine an account password by trial and error.
* `passwordPolicy` - Optional password policy rules to enforce.
* `customPages` - A hash with urls to override email verification links, password reset links and specify frame url for masking user-facing pages. Available keys: `parseFrameURL`, `invalidLink`, `choosePassword`, `passwordResetSuccess`, `verifyEmailSuccess`.
* `middleware` - (CLI only), a module name, function that is an express middleware. When using the CLI, the express app will load it just **before** mounting parse-server on the mount path. This option is useful for injecting a monitoring middleware.
##### Logging
@@ -281,7 +283,7 @@ var server = ParseServer({
passwordPolicy: {
// Two optional settings to enforce strong passwords. Either one or both can be specified.
// If both are specified, both checks must pass to accept the password
// 1. a RegExp representing the pattern to enforce
// 1. a RegExp object or a regex string representing the pattern to enforce
validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit
// 2. a callback function to be invoked to validate the password
validatorCallback: (password) => { return validatePassword(password) },
+2 -2
View File
@@ -95,10 +95,10 @@ confirm 'Y' 'Do you want to continue? (Y/n): '
check_node
check_npm
printf "Setting up parse-server in %s" "${INSTALL_DIR}\n"
printf "Setting up parse-server in %s\n" "${INSTALL_DIR}"
if [ -d "${INSTALL_DIR}" ]; then
echo "{CHECK} ${INSTALL_DIR} exists"
echo "${CHECK} ${INSTALL_DIR} exists"
else
mkdir -p "${INSTALL_DIR}"
echo "${CHECK} Created ${INSTALL_DIR}"
+29 -27
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "2.3.1",
"version": "2.3.7",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -18,49 +18,50 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"bcryptjs": "2.3.0",
"body-parser": "1.15.2",
"bcryptjs": "2.4.3",
"body-parser": "1.17.1",
"commander": "2.9.0",
"deepcopy": "0.6.3",
"express": "4.14.0",
"express": "4.15.2",
"intersect": "1.0.1",
"lodash": "4.17.2",
"lodash": "4.17.4",
"lru-cache": "4.0.2",
"mime": "1.3.4",
"mongodb": "2.2.10",
"multer": "1.2.0",
"mongodb": "2.2.24",
"multer": "1.3.0",
"parse": "1.9.2",
"parse-server-fs-adapter": "1.0.1",
"parse-server-push-adapter": "1.1.0",
"parse-server-push-adapter": "1.2.0",
"parse-server-s3-adapter": "1.0.6",
"parse-server-simple-mailgun-adapter": "1.0.0",
"pg-promise": "5.4.5",
"redis": "2.6.3",
"request": "2.79.0",
"pg-promise": "5.6.2",
"redis": "2.6.5",
"request": "2.81.0",
"semver": "5.2.0",
"tv4": "1.2.7",
"winston": "2.3.0",
"winston-daily-rotate-file": "1.4.0",
"ws": "1.1.1"
"winston": "2.3.1",
"winston-daily-rotate-file": "1.4.5",
"ws": "2.2.0"
},
"devDependencies": {
"babel-cli": "6.18.0",
"babel-core": "6.18.2",
"babel-cli": "6.23.0",
"babel-core": "6.23.1",
"babel-eslint": "^7.1.1",
"babel-plugin-syntax-flow": "6.13.0",
"babel-plugin-transform-flow-strip-types": "6.18.0",
"babel-preset-es2015": "6.14.0",
"babel-preset-stage-0": "6.5.0",
"babel-register": "6.18.0",
"babel-plugin-transform-flow-strip-types": "6.22.0",
"babel-preset-es2015": "6.22.0",
"babel-preset-stage-0": "6.22.0",
"babel-register": "6.23.0",
"bcrypt-nodejs": "0.0.3",
"cross-env": "3.1.3",
"cross-env": "3.1.4",
"deep-diff": "0.3.4",
"eslint": "^3.10.2",
"eslint": "^3.16.1",
"eslint-plugin-flowtype": "^2.25.0",
"gaze": "1.1.1",
"istanbul": "1.0.0-alpha.1",
"jasmine": "2.5.2",
"jasmine-spec-reporter": "^2.7.0",
"jasmine": "2.5.3",
"jasmine-spec-reporter": "^3.1.0",
"mongodb-runner": "3.4.0",
"nodemon": "1.11.0",
"request-promise": "4.1.1"
@@ -68,7 +69,7 @@
"scripts": {
"dev": "npm run build && node bin/dev",
"lint": "eslint --cache ./",
"build": "babel src/ -d lib/",
"build": "babel src/ -d lib/ --copy-files",
"pretest": "npm run lint",
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 $COVERAGE_OPTION jasmine",
"test:win": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 jasmine",
@@ -78,12 +79,13 @@
"prepublish": "npm run build"
},
"engines": {
"node": ">=4.5"
"node": ">=4.6"
},
"bin": {
"parse-server": "./bin/parse-server"
},
"optionalDependencies": {
"bcrypt": "1.0.1"
"bcrypt": "1.0.2",
"uws": "^0.13.0"
}
}
+25
View File
@@ -3,6 +3,7 @@ var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
var FilesAdapter = require("parse-server-fs-adapter").default;
var S3Adapter = require("parse-server-s3-adapter").default;
var ParsePushAdapter = require("parse-server-push-adapter").default;
const Config = require('../src/Config');
describe("AdapterLoader", ()=>{
@@ -118,6 +119,30 @@ describe("AdapterLoader", ()=>{
done();
});
it("should load custom push adapter from string (#3544)", (done) => {
var adapterPath = require('path').resolve("./spec/MockPushAdapter");
var options = {
ios: {
bundleId: 'bundle.id'
}
}
const pushAdapterOptions = {
adapter: adapterPath,
options
};
expect(() => {
reconfigureServer({
push: pushAdapterOptions,
}).then(() => {
const config = new Config(Parse.applicationId);
const pushAdapter = config.pushWorker.adapter;
expect(pushAdapter.getValidPushTypes()).toEqual(['ios']);
expect(pushAdapter.options).toEqual(pushAdapterOptions);
done();
});
}).not.toThrow();
});
it("should load S3Adapter from direct passing", (done) => {
var s3Adapter = new S3Adapter("key", "secret", "bucket")
expect(() => {
+2 -2
View File
@@ -6,8 +6,8 @@ var path = require('path');
describe('AuthenticationProviers', function() {
["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter", "janrainengage", "janraincapture", "vkontakte"].map(function(providerName){
it("Should validate structure of "+providerName, (done) => {
var provider = require("../src/Adapters/Auth/"+providerName);
it("Should validate structure of " + providerName, (done) => {
var provider = require("../src/Adapters/Auth/" + providerName);
jequal(typeof provider.validateAuthData, "function");
jequal(typeof provider.validateAppId, "function");
jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor);
+1 -1
View File
@@ -1361,7 +1361,7 @@ describe('afterFind hooks', () => {
Parse.Cloud.afterFind('MyObject', (req, res) => {
const filteredResults = [];
for(let i = 0 ; i < req.objects.length ; i++){
if(req.objects[i].get("secretField")==="SSID1") {
if(req.objects[i].get("secretField") === "SSID1") {
filteredResults.push(req.objects[i]);
}
}
+3 -2
View File
@@ -156,8 +156,9 @@ describe("Cloud Code Logger", () => {
.save()
.then(
() => done.fail('this is not supposed to succeed'),
() => logController.getLogs({ from: Date.now() - 500, size: 1000 })
() => new Promise(resolve => setTimeout(resolve, 100))
)
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
.then(logs => {
const log = logs[1]; // 0 is the 'uh oh!' from rejection...
expect(log.level).toEqual('error');
@@ -194,7 +195,7 @@ describe("Cloud Code Logger", () => {
Parse.Cloud.run('aFunction', { foo: 'bar' })
.then(null, () => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
.then(logs => {
const log = logs[1];
const log = logs[2];
expect(log.level).toEqual('error');
expect(log.message).toMatch(
/Failed running cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Error: {"code":141,"message":"it failed!"}/);
+28
View File
@@ -0,0 +1,28 @@
var DatabaseController = require('../src/Controllers/DatabaseController.js');
var validateQuery = DatabaseController._validateQuery;
describe('DatabaseController', function() {
describe('validateQuery', function() {
it('should restructure simple cases of SERVER-13732', (done) => {
var query = {$or: [{a: 1}, {a: 2}], _rperm: {$in: ['a', 'b']}, foo: 3};
validateQuery(query);
expect(query).toEqual({$or: [{a: 1, _rperm: {$in: ['a', 'b']}, foo: 3},
{a: 2, _rperm: {$in: ['a', 'b']}, foo: 3}]});
done();
});
it('should reject invalid queries', (done) => {
expect(() => validateQuery({$or: {'a': 1}})).toThrow();
done();
});
it('should accept valid queries', (done) => {
expect(() => validateQuery({$or: [{'a': 1}, {'b': 2}]})).not.toThrow();
done();
});
});
});
+252
View File
@@ -1,6 +1,7 @@
"use strict";
const request = require('request');
const requestp = require('request-promise');
const Config = require('../src/Config');
describe("Email Verification Token Expiration: ", () => {
@@ -482,6 +483,257 @@ describe("Email Verification Token Expiration: ", () => {
});
});
it('should send a new verification email when a resend is requested and the user is UNVERIFIED', done => {
var user = new Parse.User();
var sendEmailOptions;
var sendVerificationEmailCallCount = 0;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
sendVerificationEmailCallCount++;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1'
})
.then(() => {
user.setUsername('resends_verification_token');
user.setPassword('expiringToken');
user.set('email', 'user@parse.com');
return user.signUp();
})
.then(() => {
expect(sendVerificationEmailCallCount).toBe(1);
return requestp.post({
uri: 'http://localhost:8378/1/verificationEmailRequest',
body: {
email: 'user@parse.com'
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: true,
resolveWithFullResponse: true,
simple: false // this promise is only rejected if the call itself failed
})
.then((response) => {
expect(response.statusCode).toBe(200);
expect(sendVerificationEmailCallCount).toBe(2);
expect(sendEmailOptions).toBeDefined();
done();
});
})
.catch(error => {
jfail(error);
done();
});
});
it('should not send a new verification email when a resend is requested and the user is VERIFIED', done => {
var user = new Parse.User();
var sendEmailOptions;
var sendVerificationEmailCallCount = 0;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
sendVerificationEmailCallCount++;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1'
})
.then(() => {
user.setUsername('no_new_verification_token_once_verified');
user.setPassword('expiringToken');
user.set('email', 'user@parse.com');
return user.signUp();
})
.then(() => {
return requestp.get({
url: sendEmailOptions.link,
followRedirect: false,
resolveWithFullResponse: true,
simple: false
})
.then((response) => {
expect(response.statusCode).toEqual(302);
});
})
.then(() => {
expect(sendVerificationEmailCallCount).toBe(1);
return requestp.post({
uri: 'http://localhost:8378/1/verificationEmailRequest',
body: {
email: 'user@parse.com'
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: true,
resolveWithFullResponse: true,
simple: false // this promise is only rejected if the call itself failed
})
.then((response) => {
expect(response.statusCode).toBe(400);
expect(sendVerificationEmailCallCount).toBe(1);
done();
});
})
.catch(error => {
jfail(error);
done();
});
});
it('should not send a new verification email if this user does not exist', done => {
var sendEmailOptions;
var sendVerificationEmailCallCount = 0;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
sendVerificationEmailCallCount++;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1'
})
.then(() => {
return requestp.post({
uri: 'http://localhost:8378/1/verificationEmailRequest',
body: {
email: 'user@parse.com'
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: true,
resolveWithFullResponse: true,
simple: false
})
.then(response => {
expect(response.statusCode).toBe(400);
expect(sendVerificationEmailCallCount).toBe(0);
expect(sendEmailOptions).not.toBeDefined();
done();
});
})
.catch(error => {
jfail(error);
done();
});
});
it('should fail if no email is supplied', done => {
var sendEmailOptions;
var sendVerificationEmailCallCount = 0;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
sendVerificationEmailCallCount++;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1'
})
.then(() => {
request.post({
uri: 'http://localhost:8378/1/verificationEmailRequest',
body: {},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: true,
resolveWithFullResponse: true,
simple: false
}, (err, response) => {
expect(response.statusCode).toBe(400);
expect(response.body.code).toBe(Parse.Error.EMAIL_MISSING);
expect(response.body.error).toBe('you must provide an email');
expect(sendVerificationEmailCallCount).toBe(0);
expect(sendEmailOptions).not.toBeDefined();
done();
});
})
.catch(error => {
jfail(error);
done();
});
});
it('should fail if email is not a string', done => {
var sendEmailOptions;
var sendVerificationEmailCallCount = 0;
var emailAdapter = {
sendVerificationEmail: options => {
sendEmailOptions = options;
sendVerificationEmailCallCount++;
},
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
reconfigureServer({
appName: 'emailVerifyToken',
verifyUserEmails: true,
emailAdapter: emailAdapter,
emailVerifyTokenValidityDuration: 5, // 5 seconds
publicServerURL: 'http://localhost:8378/1'
})
.then(() => {
request.post({
uri: 'http://localhost:8378/1/verificationEmailRequest',
body: {email: 3},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: true,
resolveWithFullResponse: true,
simple: false
}, (err, response) => {
expect(response.statusCode).toBe(400);
expect(response.body.code).toBe(Parse.Error.INVALID_EMAIL_ADDRESS);
expect(response.body.error).toBe('you must provide a valid email string');
expect(sendVerificationEmailCallCount).toBe(0);
expect(sendEmailOptions).not.toBeDefined();
done();
});
})
.catch(error => {
jfail(error);
done();
});
});
it('client should not see the _email_verify_token_expires_at field', done => {
var user = new Parse.User();
var sendEmailOptions;
+39 -4
View File
@@ -1,7 +1,17 @@
var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
var Config = require("../src/Config");
var FilesController = require('../src/Controllers/FilesController').default;
const LoggerController = require('../src/Controllers/LoggerController').LoggerController;
const WinstonLoggerAdapter = require('../src/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter;
const GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter;
const Config = require("../src/Config");
const FilesController = require('../src/Controllers/FilesController').default;
const mockAdapter = {
createFile: () => {
return Parse.Promise.reject(new Error('it failed'));
},
deleteFile: () => { },
getFileData: () => { },
getFileLocation: () => 'xyz'
}
// Small additional tests to improve overall coverage
describe("FilesController",() =>{
@@ -26,5 +36,30 @@ describe("FilesController",() =>{
expect(anObject.aFile.url).toEqual("http://an.url");
done();
})
});
it('should create a server log on failure', done => {
const logController = new LoggerController(new WinstonLoggerAdapter());
reconfigureServer({ filesAdapter: mockAdapter })
.then(() => new Promise(resolve => setTimeout(resolve, 1000)))
.then(() => new Parse.File("yolo.txt", [1,2,3], "text/plain").save())
.then(
() => done.fail('should not succeed'),
() => setImmediate(() => Parse.Promise.as('done'))
)
.then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 }))
.then((logs) => {
// we get two logs here: 1. the source of the failure to save the file
// and 2 the message that will be sent back to the client.
const log1 = logs.pop();
expect(log1.level).toBe('error');
expect(log1.message).toBe('it failed');
const log2 = logs.pop();
expect(log2.level).toBe('error');
expect(log2.code).toBe(130);
expect(log2.message).toBe('Could not store file.');
done();
});
});
});
+10 -10
View File
@@ -6,7 +6,7 @@ var httpRequest = require("../src/cloud-code/httpRequest"),
express = require("express");
var port = 13371;
var httpRequestServer = "http://localhost:"+port;
var httpRequestServer = "http://localhost:" + port;
var app = express();
app.use(bodyParser.json({ 'type': '*/*' }));
@@ -39,7 +39,7 @@ app.listen(13371);
describe("httpRequest", () => {
it("should do /hello", (done) => {
httpRequest({
url: httpRequestServer+"/hello"
url: httpRequestServer + "/hello"
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}'));
@@ -55,7 +55,7 @@ describe("httpRequest", () => {
it("should do /hello with callback and promises", (done) => {
var calls = 0;
httpRequest({
url: httpRequestServer+"/hello",
url: httpRequestServer + "/hello",
success: function() { calls++; },
error: function() { calls++; }
}).then(function(httpResponse){
@@ -74,7 +74,7 @@ describe("httpRequest", () => {
it("should do not follow redirects by default", (done) => {
httpRequest({
url: httpRequestServer+"/301"
url: httpRequestServer + "/301"
}).then(function(httpResponse){
expect(httpResponse.status).toBe(301);
done();
@@ -87,7 +87,7 @@ describe("httpRequest", () => {
it("should follow redirects when set", (done) => {
httpRequest({
url: httpRequestServer+"/301",
url: httpRequestServer + "/301",
followRedirects: true
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
@@ -104,7 +104,7 @@ describe("httpRequest", () => {
it("should fail on 404", (done) => {
var calls = 0;
httpRequest({
url: httpRequestServer+"/404",
url: httpRequestServer + "/404",
success: function() {
calls++;
fail("should not succeed");
@@ -124,7 +124,7 @@ describe("httpRequest", () => {
it("should fail on 404", (done) => {
httpRequest({
url: httpRequestServer+"/404",
url: httpRequestServer + "/404",
}).then(function(){
fail("should not succeed");
done();
@@ -141,7 +141,7 @@ describe("httpRequest", () => {
var calls = 0;
httpRequest({
method: "POST",
url: httpRequestServer+"/echo",
url: httpRequestServer + "/echo",
body: {
foo: "bar"
},
@@ -218,7 +218,7 @@ describe("httpRequest", () => {
it("should params object to query string", (done) => {
httpRequest({
url: httpRequestServer+"/qs",
url: httpRequestServer + "/qs",
params: {
foo: "bar"
}
@@ -234,7 +234,7 @@ describe("httpRequest", () => {
it("should params string to query string", (done) => {
httpRequest({
url: httpRequestServer+"/qs",
url: httpRequestServer + "/qs",
params: "foo=bar&foo2=bar2"
}).then(function(httpResponse){
expect(httpResponse.status).toBe(200);
+1 -1
View File
@@ -52,7 +52,7 @@ describe('Logger', () => {
logging.logger.info('hi', {key: 'value'});
expect(process.stdout.write).toHaveBeenCalled();
var firstLog = process.stdout.write.calls.first().args[0];
expect(firstLog).toEqual(JSON.stringify({key: 'value', level: 'info', message: 'hi' })+'\n');
expect(firstLog).toEqual(JSON.stringify({key: 'value', level: 'info', message: 'hi' }) + '\n');
return reconfigureServer({
jsonLogs: false
});
+13
View File
@@ -79,6 +79,19 @@ describe('middlewares', () => {
});
});
it('should succeed when client key supplied but empty', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
clientKey: '',
masterKey: 'masterKey',
restAPIKey: 'restAPIKey'
});
fakeReq.headers['x-parse-client-key'] = '';
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeRes.status).not.toHaveBeenCalled();
done();
});
});
it('should succeed when no keys are configured and none supplied', (done) => {
AppCache.put(fakeReq.body._ApplicationId, {
masterKey: 'masterKey'
+9
View File
@@ -0,0 +1,9 @@
module.exports = function(options) {
return {
options: options,
send: function() {},
getValidPushTypes: function() {
return Object.keys(options.options);
}
};
};
+26
View File
@@ -293,6 +293,32 @@ describe('parseObjectToMongoObjectForCreate', () => {
expect(output.double).toBe(Number.MAX_VALUE);
done();
});
it('Date object where iso attribute is of type Date', (done) => {
var input = {
ts : { __type: 'Date', iso: new Date('2017-01-18T00:00:00.000Z') }
}
var output = transform.mongoObjectToParseObject(null, input, {
fields : {
ts : { type : 'Date' }
}
});
expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z');
done();
});
it('Date object where iso attribute is of type String', (done) => {
var input = {
ts : { __type: 'Date', iso: '2017-01-18T00:00:00.000Z' }
}
var output = transform.mongoObjectToParseObject(null, input, {
fields : {
ts : { type : 'Date' }
}
});
expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z');
done();
});
});
describe('transformUpdate', () => {
+1 -1
View File
@@ -64,7 +64,7 @@ describe('OAuth', function() {
var req = oauthClient.buildRequest(method, path, {"query": "param"});
jequal(req.host, options.host);
jequal(req.path, "/"+path+"?query=param");
jequal(req.path, "/" + path + "?query=param");
jequal(req.method, "GET");
jequal(req.headers['Content-Type'], 'application/x-www-form-urlencoded');
jequal(req.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="wNkyEkDE%2F0JZ2idmqyrgHdvC0rs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"')
+34 -9
View File
@@ -2,22 +2,32 @@
const request = require('request');
const delayPromise = (delay) => {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
describe('Parse.Push', () => {
var setup = function() {
var sendToInstallationSpy = jasmine.createSpy();
var pushAdapter = {
send: function(body, installations) {
var badge = body.data.badge;
const promises = installations.map((installation) => {
sendToInstallationSpy(installation);
if (installation.deviceType == "ios") {
expect(installation.badge).toEqual(badge);
expect(installation.originalBadge+1).toEqual(installation.badge);
expect(installation.originalBadge + 1).toEqual(installation.badge);
} else {
expect(installation.badge).toBeUndefined();
}
return Promise.resolve({
err: null,
deviceType: installation.deviceType,
result: true
device: installation,
transmitted: true
})
});
return Promise.all(promises);
@@ -39,21 +49,29 @@ describe('Parse.Push', () => {
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("installationId", "installation_" + installations.length);
installation.set("deviceToken","device_token_" + installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
return Parse.Object.saveAll(installations);
}).catch((err) => {
})
.then(() => {
return {
sendToInstallationSpy,
};
})
.catch((err) => {
console.error(err);
throw err;
})
}
it('should properly send push', (done) => {
return setup().then(() => {
return setup().then(({ sendToInstallationSpy }) => {
return Parse.Push.send({
where: {
deviceType: 'ios'
@@ -63,8 +81,13 @@ describe('Parse.Push', () => {
alert: 'Hello world!'
}
}, {useMasterKey: true})
})
.then(() => {
.then(() => {
return delayPromise(500);
})
.then(() => {
expect(sendToInstallationSpy.calls.count()).toEqual(10);
})
}).then(() => {
done();
}).catch((err) => {
jfail(err);
@@ -83,6 +106,8 @@ describe('Parse.Push', () => {
alert: 'Hello world!'
}
}, {useMasterKey: true})
}).then(() => {
return delayPromise(500);
}).then(() => {
done();
}).catch((err) => {
+2 -2
View File
@@ -825,7 +825,7 @@ describe('miscellaneous', function() {
};
request.put({
headers: headers,
url: 'http://localhost:8378/1/classes/GameScore/'+obj.id,
url: 'http://localhost:8378/1/classes/GameScore/' + obj.id,
body: JSON.stringify({
a: 'b',
c: {"__op":"Increment","amount":2},
@@ -1241,7 +1241,7 @@ describe('miscellaneous', function() {
amount: amount
}
},
url: 'http://localhost:8378/1/classes/AnObject/'+object.id
url: 'http://localhost:8378/1/classes/AnObject/' + object.id
})
return new Promise((resolve, reject) => {
request.put(options, (err, res, body) => {
+43
View File
@@ -56,6 +56,49 @@ describe('a GlobalConfig', () => {
});
});
it('can add and retrive files', (done) => {
request.put({
url : 'http://localhost:8378/1/config',
json : true,
body : { params: { file: { __type: 'File', name: 'name', url: 'http://url' } } },
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.result).toEqual(true);
Parse.Config.get().then((res) => {
const file = res.get('file');
expect(file.name()).toBe('name');
expect(file.url()).toBe('http://url');
done();
});
});
});
it('can add and retrive Geopoints', (done) => {
const geopoint = new Parse.GeoPoint(10,-20);
request.put({
url : 'http://localhost:8378/1/config',
json : true,
body : { params: { point: geopoint.toJSON() } },
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.result).toEqual(true);
Parse.Config.get().then((res) => {
const point = res.get('point');
expect(point.latitude).toBe(10);
expect(point.longitude).toBe(-20);
done();
});
});
});
it('properly handles delete op', (done) => {
request.put({
url : 'http://localhost:8378/1/config',
+32 -31
View File
@@ -7,7 +7,7 @@ var express = require("express");
var bodyParser = require('body-parser');
var port = 12345;
var hookServerURL = "http://localhost:"+port;
var hookServerURL = "http://localhost:" + port;
const AppCache = require('../src/cache').AppCache;
var app = express();
@@ -129,7 +129,7 @@ describe('Hooks', () => {
});
it("should fail to register hooks without Master Key", (done) => {
request.post(Parse.serverURL+"/hooks/functions", {
request.post(Parse.serverURL + "/hooks/functions", {
headers: {
"X-Parse-Application-Id": Parse.applicationId,
"X-Parse-REST-API-Key": Parse.restKey,
@@ -143,7 +143,9 @@ describe('Hooks', () => {
});
it("should fail trying to create two times the same function", (done) => {
Parse.Hooks.createFunction("my_new_function", "http://url.com").then(() => {
Parse.Hooks.createFunction("my_new_function", "http://url.com")
.then(() => new Promise(resolve => setTimeout(resolve, 100)))
.then(() => {
return Parse.Hooks.createFunction("my_new_function", "http://url.com")
}, () => {
fail("should create a new function");
@@ -254,7 +256,7 @@ describe('Hooks', () => {
});
it("should fail trying to create a malformed function (REST)", (done) => {
request.post(Parse.serverURL+"/hooks/functions", {
request.post(Parse.serverURL + "/hooks/functions", {
headers: {
"X-Parse-Application-Id": Parse.applicationId,
"X-Parse-Master-Key": Parse.masterKey,
@@ -272,18 +274,18 @@ describe('Hooks', () => {
it("should create hooks and properly preload them", (done) => {
var promises = [];
for (var i = 0; i<5; i++) {
promises.push(Parse.Hooks.createTrigger("MyClass"+i, "beforeSave", "http://url.com/beforeSave/"+i));
promises.push(Parse.Hooks.createFunction("AFunction"+i, "http://url.com/function"+i));
for (var i = 0; i < 5; i++) {
promises.push(Parse.Hooks.createTrigger("MyClass" + i, "beforeSave", "http://url.com/beforeSave/" + i));
promises.push(Parse.Hooks.createFunction("AFunction" + i, "http://url.com/function" + i));
}
Parse.Promise.when(promises).then(function(){
for (var i=0; i<5; i++) {
for (var i = 0; i < 5; i++) {
// Delete everything from memory, as the server just started
triggers.removeTrigger("beforeSave", "MyClass"+i, Parse.applicationId);
triggers.removeFunction("AFunction"+i, Parse.applicationId);
expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).toBeUndefined();
expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).toBeUndefined();
triggers.removeTrigger("beforeSave", "MyClass" + i, Parse.applicationId);
triggers.removeFunction("AFunction" + i, Parse.applicationId);
expect(triggers.getTrigger("MyClass" + i, "beforeSave", Parse.applicationId)).toBeUndefined();
expect(triggers.getFunction("AFunction" + i, Parse.applicationId)).toBeUndefined();
}
const hooksController = new HooksController(Parse.applicationId, AppCache.get('test').databaseController);
return hooksController.load()
@@ -292,9 +294,9 @@ describe('Hooks', () => {
fail('Should properly create all hooks');
done();
}).then(function() {
for (var i=0; i<5; i++) {
expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).not.toBeUndefined();
expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).not.toBeUndefined();
for (var i = 0; i < 5; i++) {
expect(triggers.getTrigger("MyClass" + i, "beforeSave", Parse.applicationId)).not.toBeUndefined();
expect(triggers.getFunction("AFunction" + i, Parse.applicationId)).not.toBeUndefined();
}
done();
}, (err) => {
@@ -310,7 +312,7 @@ describe('Hooks', () => {
res.json({success:"OK!"});
});
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunction").then(function(){
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL + "/SomeFunction").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
jfail(err);
@@ -332,7 +334,7 @@ describe('Hooks', () => {
res.json({error: {code: 1337, error: "hacking that one!"}});
});
// The function is deleted as the DB is dropped between calls
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunctionError").then(function(){
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL + "/SomeFunctionError").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
jfail(err);
@@ -362,7 +364,7 @@ describe('Hooks', () => {
}
});
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/ExpectingKey").then(function(){
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL + "/ExpectingKey").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
jfail(err);
@@ -389,7 +391,7 @@ describe('Hooks', () => {
}
});
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/ExpectingKeyAlso").then(function(){
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL + "/ExpectingKeyAlso").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
jfail(err);
@@ -421,8 +423,8 @@ describe('Hooks', () => {
// But this should override the key upon return
res.json({success: object});
});
// The function is delete as the DB is dropped between calls
Parse.Hooks.createTrigger("SomeRandomObject", "beforeSave" ,hookServerURL+"/BeforeSaveSome").then(function(){
// The function is deleted as the DB is dropped between calls
Parse.Hooks.createTrigger("SomeRandomObject", "beforeSave", hookServerURL + "/BeforeSaveSome").then(function () {
const obj = new Parse.Object("SomeRandomObject");
return obj.save();
}).then(function(res) {
@@ -444,7 +446,7 @@ describe('Hooks', () => {
object.set('hello', "world");
res.json({success: object});
});
Parse.Hooks.createTrigger("SomeRandomObject2", "beforeSave" ,hookServerURL+"/BeforeSaveSome2").then(function(){
Parse.Hooks.createTrigger("SomeRandomObject2", "beforeSave", hookServerURL + "/BeforeSaveSome2").then(function(){
const obj = new Parse.Object("SomeRandomObject2");
return obj.save();
}).then(function(res) {
@@ -470,20 +472,19 @@ describe('Hooks', () => {
res.json({success: {}});
})
});
// The function is delete as the DB is dropped between calls
Parse.Hooks.createTrigger("SomeRandomObject", "afterSave" ,hookServerURL+"/AfterSaveSome").then(function(){
// The function is deleted as the DB is dropped between calls
Parse.Hooks.createTrigger("SomeRandomObject", "afterSave", hookServerURL + "/AfterSaveSome").then(function(){
const obj = new Parse.Object("SomeRandomObject");
return obj.save();
}).then(function() {
var promise = new Parse.Promise();
// Wait a bit here as it's an after save
setTimeout(function(){
// Wait a bit here as it's an after save
setTimeout(() => {
expect(triggerCount).toBe(1);
var q = new Parse.Query("AnotherObject");
q.get(newObjectId).then(function(r){
promise.resolve(r);
});
}, 300)
new Parse.Query("AnotherObject")
.get(newObjectId)
.then((r) => promise.resolve(r));
}, 500);
return promise;
}).then(function(res){
expect(res.get("foo")).toEqual("bar");
+8 -1
View File
@@ -12,6 +12,10 @@ let config;
let database;
const defaultColumns = require('../src/Controllers/SchemaController').defaultColumns;
const delay = function delay(delay) {
return new Promise(resolve => setTimeout(resolve, delay));
}
const installationSchema = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation) };
describe('Installations', () => {
@@ -542,12 +546,14 @@ describe('Installations', () => {
};
return rest.create(config, auth.nobody(config), '_Installation', input);
})
.then(() => delay(100))
.then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId1}, {}))
.then((results) => {
expect(results.length).toEqual(1);
firstObject = results[0];
return database.adapter.find('_Installation', installationSchema, {installationId: installId2}, {});
})
.then(() => delay(100))
.then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId2}, {}))
.then(results => {
expect(results.length).toEqual(1);
secondObject = results[0];
@@ -558,6 +564,7 @@ describe('Installations', () => {
};
return rest.update(config, auth.nobody(config), '_Installation', secondObject.objectId, input);
})
.then(() => delay(100))
.then(() => database.adapter.find('_Installation', installationSchema, {objectId: firstObject.objectId}, {}))
.then(results => {
// The first object should have been deleted
+6 -6
View File
@@ -1390,7 +1390,7 @@ describe('Parse.Object testing', () => {
}
equal(itemsAgain.length, numItems, "Should get the array back");
itemsAgain.forEach(function(item, i) {
var newValue = i*2;
var newValue = i * 2;
item.set("x", newValue);
});
return Parse.Object.saveAll(itemsAgain);
@@ -1400,7 +1400,7 @@ describe('Parse.Object testing', () => {
equal(fetchedItemsAgain.length, numItems,
"Number of items fetched should not change");
fetchedItemsAgain.forEach(function(item, i) {
equal(item.get("x"), i*2);
equal(item.get("x"), i * 2);
});
done();
});
@@ -1457,7 +1457,7 @@ describe('Parse.Object testing', () => {
}
equal(itemsAgain.length, numItems, "Should get the array back");
itemsAgain.forEach(function(item, i) {
var newValue = i*2;
var newValue = i * 2;
item.set("x", newValue);
});
return Parse.Object.saveAll(itemsAgain);
@@ -1467,7 +1467,7 @@ describe('Parse.Object testing', () => {
equal(fetchedItemsAgain.length, numItems,
"Number of items fetched should not change");
fetchedItemsAgain.forEach(function(item, i) {
equal(item.get("x"), i*2);
equal(item.get("x"), i * 2);
});
done();
},
@@ -1581,7 +1581,7 @@ describe('Parse.Object testing', () => {
return;
}
itemsAgain.forEach(function(item, i) {
item.set("x", i*2);
item.set("x", i * 2);
});
return Parse.Object.saveAll(itemsAgain);
}).then(function() {
@@ -1619,7 +1619,7 @@ describe('Parse.Object testing', () => {
return;
}
itemsAgain.forEach(function(item, i) {
item.set("x", i*2);
item.set("x", i * 2);
});
return Parse.Object.saveAll(itemsAgain);
}).then(function() {
+5 -4
View File
@@ -1631,8 +1631,9 @@ describe('Parse.Query testing', () => {
});
expect(total).toBe(0);
done()
}, () => {
}, (e) => {
fail('should not fail');
fail(JSON.stringify(e));
done();
})
});
@@ -1652,8 +1653,8 @@ describe('Parse.Query testing', () => {
}
Parse.Object.saveAll(objects).then(() => {
const object = new Parse.Object("AContainer");
for (var i=0; i<objects.length; i++) {
if (i%2 == 0) {
for (var i = 0; i < objects.length; i++) {
if (i % 2 == 0) {
objects[i].id = 'randomThing'
} else {
total += objects[i].get('key');
@@ -2401,7 +2402,7 @@ describe('Parse.Query testing', () => {
it('query match on array with multiple objects', (done) => {
var target1 = {__type: 'Pointer', className: 'TestObject', objectId: 'abc'};
var target2 = {__type: 'Pointer', className: 'TestObject', objectId: '123'};
var obj= new Parse.Object('TestObject');
var obj = new Parse.Object('TestObject');
obj.set('someObjs', [target1, target2]);
obj.save().then(() => {
var query = new Parse.Query('TestObject');
+9
View File
@@ -575,6 +575,9 @@ describe('Parse.Relation testing', () => {
expect(result.get('key').get('even')).toBe(false);
});
done();
}, (e) => {
fail(JSON.stringify(e));
done();
})
});
@@ -613,6 +616,9 @@ describe('Parse.Relation testing', () => {
done();
}
}));
}, (e) => {
fail(JSON.stringify(e));
done();
});
});
@@ -653,6 +659,9 @@ describe('Parse.Relation testing', () => {
done();
}
}));
}, (e) => {
fail(JSON.stringify(e));
done();
});
});
+20 -2
View File
@@ -124,7 +124,7 @@ describe('Parse Role testing', () => {
expect(roles.length).toEqual(4);
allRoles.forEach(function(name) {
expect(roles.indexOf("role:"+name)).not.toBe(-1);
expect(roles.indexOf("role:" + name)).not.toBe(-1);
});
// 1 Query for the initial setup
@@ -165,7 +165,7 @@ describe('Parse Role testing', () => {
}).then((roles) => {
expect(roles.length).toEqual(3);
rolesNames.forEach((name) => {
expect(roles.indexOf('role:'+name)).not.toBe(-1);
expect(roles.indexOf('role:' + name)).not.toBe(-1);
});
done();
}, function(){
@@ -192,6 +192,24 @@ describe('Parse Role testing', () => {
});
});
it("Different _Role objects cannot have the same name.", (done) => {
const roleName = "MyRole";
let aUser;
createTestUser().then((user) => {
aUser = user;
return createRole(roleName, null, aUser);
}).then((firstRole) => {
expect(firstRole.getName()).toEqual(roleName);
return createRole(roleName, null, aUser);
}).then(() => {
fail("_Role cannot have the same name as another role");
done();
}, (error) => {
expect(error.code).toEqual(137);
done();
});
});
it("Should properly resolve roles", (done) => {
const admin = new Parse.Role("Admin", new Parse.ACL());
const moderator = new Parse.Role("Moderator", new Parse.ACL());
+2 -2
View File
@@ -1678,7 +1678,7 @@ describe('Parse.User testing', () => {
const userId = model.id;
Parse.User.logOut().then(() => {
request.post({
url:Parse.serverURL+'/classes/_User',
url:Parse.serverURL + '/classes/_User',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest'
@@ -1688,7 +1688,7 @@ describe('Parse.User testing', () => {
// make sure the location header is properly set
expect(userId).not.toBeUndefined();
expect(body.objectId).toEqual(userId);
expect(res.headers.location).toEqual(Parse.serverURL+'/users/'+userId);
expect(res.headers.location).toEqual(Parse.serverURL + '/users/' + userId);
done();
});
});
+4 -1
View File
@@ -14,7 +14,9 @@ describe('ParseWebSocketServer', function() {
it('can handle connect event when ws is open', function(done) {
var onConnectCallback = jasmine.createSpy('onConnectCallback');
var parseWebSocketServer = new ParseWebSocketServer({}, onConnectCallback, 5).server;
var http = require('http');
var server = http.createServer();
var parseWebSocketServer = new ParseWebSocketServer(server, onConnectCallback, 5).server;
var ws = {
readyState: 0,
OPEN: 0,
@@ -27,6 +29,7 @@ describe('ParseWebSocketServer', function() {
// Make sure we ping to the client
setTimeout(function() {
expect(ws.ping).toHaveBeenCalled();
server.close();
done();
}, 10)
});
+85 -7
View File
@@ -147,14 +147,14 @@ describe("Password Policy: ", () => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: "abc" // string is not a valid setting
validatorPattern: 1234 // number is not a valid setting
},
publicServerURL: "http://localhost:8378/1"
}).then(() => {
fail('passwordPolicy.validatorPattern type test failed');
done();
}).catch(err => {
expect(err).toEqual('passwordPolicy.validatorPattern must be a RegExp.');
expect(err).toEqual('passwordPolicy.validatorPattern must be a regex string or RegExp object.');
done();
});
});
@@ -197,6 +197,50 @@ describe("Password Policy: ", () => {
})
});
it('signup should fail if password does not conform to the policy enforced using validatorPattern string', (done) => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: "^.{8,}" // password should contain at least 8 char
},
publicServerURL: "http://localhost:8378/1"
}).then(() => {
user.setUsername("user1");
user.setPassword("less");
user.set('email', 'user1@parse.com');
user.signUp().then(() => {
fail('Should have failed as password does not conform to the policy.');
done();
}).catch((error) => {
expect(error.code).toEqual(142);
done();
});
})
});
it('signup should fail if password is empty', (done) => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: "^.{8,}" // password should contain at least 8 char
},
publicServerURL: "http://localhost:8378/1"
}).then(() => {
user.setUsername("user1");
user.setPassword("");
user.set('email', 'user1@parse.com');
user.signUp().then(() => {
fail('Should have failed as password does not conform to the policy.');
done();
}).catch((error) => {
expect(error.message).toEqual('Cannot sign up user with an empty password.');
done();
});
})
});
it('signup should succeed if password conforms to the policy enforced using validatorPattern', (done) => {
const user = new Parse.User();
reconfigureServer({
@@ -231,6 +275,40 @@ describe("Password Policy: ", () => {
})
});
it('signup should succeed if password conforms to the policy enforced using validatorPattern string', (done) => {
const user = new Parse.User();
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
validatorPattern: "[!@#$]+" // password should contain at least one special char
},
publicServerURL: "http://localhost:8378/1"
}).then(() => {
user.setUsername("user1");
user.setPassword("p@sswrod");
user.set('email', 'user1@parse.com');
user.signUp().then(() => {
Parse.User.logOut().then(() => {
Parse.User.logIn("user1", "p@sswrod").then(function () {
done();
}).catch((err) => {
jfail(err);
fail("Should be able to login");
done();
});
}).catch((error) => {
jfail(error);
fail('logout should have succeeded');
done();
});
}).catch((error) => {
jfail(error);
fail('Signup should have succeeded as password conforms to the policy.');
done();
});
})
});
it('signup should fail if password does not conform to the policy enforced using validatorCallback', (done) => {
const user = new Parse.User();
reconfigureServer({
@@ -409,12 +487,12 @@ describe("Password Policy: ", () => {
fail("should login with new password");
done();
});
}).catch((error)=> {
}).catch((error) => {
jfail(error);
fail("Failed to POST request password reset");
done();
});
}).catch((error)=> {
}).catch((error) => {
jfail(error);
fail("Failed to get the reset link");
done();
@@ -839,7 +917,7 @@ describe("Password Policy: ", () => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
maxPasswordAge: 0.5/(24*60*60) // 0.5 sec
maxPasswordAge: 0.5 / (24 * 60 * 60) // 0.5 sec
},
publicServerURL: "http://localhost:8378/1"
}).then(() => {
@@ -880,7 +958,7 @@ describe("Password Policy: ", () => {
reconfigureServer({
appName: 'passwordPolicy',
passwordPolicy: {
maxPasswordAge: 0.5/(24*60*60) // 0.5 sec
maxPasswordAge: 0.5 / (24 * 60 * 60) // 0.5 sec
},
publicServerURL: "http://localhost:8378/1"
}).then(() => {
@@ -979,7 +1057,7 @@ describe("Password Policy: ", () => {
appName: 'passwordPolicy',
emailAdapter: emailAdapter,
passwordPolicy: {
maxPasswordAge: 0.5/(24*60*60) // 0.5 sec
maxPasswordAge: 0.5 / (24 * 60 * 60) // 0.5 sec
},
publicServerURL: "http://localhost:8378/1"
}).then(() => {
+134 -106
View File
@@ -2,6 +2,7 @@
var PushController = require('../src/Controllers/PushController').PushController;
var StatusHandler = require('../src/StatusHandler');
var Config = require('../src/Config');
var validatePushType = require('../src/Push/utils').validatePushType;
const successfulTransmissions = function(body, installations) {
@@ -35,7 +36,7 @@ describe('PushController', () => {
var validPushTypes = ['ios', 'android'];
expect(function(){
PushController.validatePushType(where, validPushTypes);
validatePushType(where, validPushTypes);
}).not.toThrow();
done();
});
@@ -48,7 +49,7 @@ describe('PushController', () => {
var validPushTypes = ['ios', 'android'];
expect(function(){
PushController.validatePushType(where, validPushTypes);
validatePushType(where, validPushTypes);
}).not.toThrow();
done();
});
@@ -63,7 +64,7 @@ describe('PushController', () => {
var validPushTypes = ['ios', 'android'];
expect(function(){
PushController.validatePushType(where, validPushTypes);
validatePushType(where, validPushTypes);
}).not.toThrow();
done();
});
@@ -76,7 +77,7 @@ describe('PushController', () => {
var validPushTypes = ['ios', 'android'];
expect(function(){
PushController.validatePushType(where, validPushTypes);
validatePushType(where, validPushTypes);
}).toThrow();
done();
});
@@ -89,7 +90,7 @@ describe('PushController', () => {
var validPushTypes = ['ios', 'android'];
expect(function(){
PushController.validatePushType(where, validPushTypes);
validatePushType(where, validPushTypes);
}).toThrow();
done();
});
@@ -131,37 +132,13 @@ describe('PushController', () => {
});
it('properly increment badges', (done) => {
var payload = {data:{
alert: "Hello World!",
badge: "Increment",
}}
var installations = [];
while(installations.length != 10) {
const installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
while(installations.length != 15) {
const installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("deviceType", "android");
installations.push(installation);
}
var pushAdapter = {
send: function(body, installations) {
var badge = body.data.badge;
installations.forEach((installation) => {
if (installation.deviceType == "ios") {
expect(installation.badge).toEqual(badge);
expect(installation.originalBadge+1).toEqual(installation.badge);
expect(installation.originalBadge + 1).toEqual(installation.badge);
} else {
expect(installation.badge).toBeUndefined();
}
@@ -172,14 +149,39 @@ describe('PushController', () => {
return ["ios", "android"];
}
}
var payload = {data:{
alert: "Hello World!",
badge: "Increment",
}}
var installations = [];
while(installations.length != 10) {
const installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_" + installations.length);
installation.set("deviceToken","device_token_" + installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
while(installations.length != 15) {
const installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_" + installations.length);
installation.set("deviceToken","device_token_" + installations.length)
installation.set("deviceType", "android");
installations.push(installation);
}
var config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
Parse.Object.saveAll(installations).then(() => {
var pushController = new PushController();
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
return Parse.Object.saveAll(installations)
}).then(() => {
return pushController.sendPush(payload, {}, config, auth);
}).then(() => {
done();
@@ -187,26 +189,10 @@ describe('PushController', () => {
jfail(err);
done();
});
});
it('properly set badges to 1', (done) => {
var payload = {data: {
alert: "Hello World!",
badge: 1,
}}
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
var pushAdapter = {
send: function(body, installations) {
var badge = body.data.badge;
@@ -221,13 +207,32 @@ describe('PushController', () => {
}
}
var payload = {data: {
alert: "Hello World!",
badge: 1,
}}
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_" + installations.length);
installation.set("deviceToken","device_token_" + installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
var config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
Parse.Object.saveAll(installations).then(() => {
var pushController = new PushController();
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
return Parse.Object.saveAll(installations)
}).then(() => {
return pushController.sendPush(payload, {}, config, auth);
}).then(() => {
done();
@@ -235,7 +240,6 @@ describe('PushController', () => {
fail("should not fail");
done();
});
});
it('properly set badges to 1 with complex query #2903 #3022', (done) => {
@@ -249,8 +253,8 @@ describe('PushController', () => {
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("installationId", "installation_" + installations.length);
installation.set("deviceToken","device_token_" + installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
@@ -276,9 +280,14 @@ describe('PushController', () => {
var auth = {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
Parse.Object.saveAll(installations).then((installations) => {
var pushController = new PushController();
reconfigureServer({
push: {
adapter: pushAdapter
}
}).then(() => {
return Parse.Object.saveAll(installations)
}).then((installations) => {
const objectIds = installations.map(installation => {
return installation.id;
})
@@ -286,6 +295,10 @@ describe('PushController', () => {
objectId: {'$in': objectIds.slice(0, 5)}
}
return pushController.sendPush(payload, where, config, auth);
}).then(() => {
return new Promise((res) => {
setTimeout(res, 300);
});
}).then(() => {
expect(matchedInstallationsCount).toBe(5);
const query = new Parse.Query(Parse.Installation);
@@ -305,8 +318,8 @@ describe('PushController', () => {
var installations = [];
while(installations.length != 10) {
const installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("installationId", "installation_" + installations.length);
installation.set("deviceToken","device_token_" + installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
@@ -315,8 +328,8 @@ describe('PushController', () => {
while(installations.length != 15) {
const installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("installationId", "installation_" + installations.length);
installation.set("deviceToken","device_token_" + installations.length)
installation.set("deviceType", "android");
installations.push(installation);
}
@@ -338,46 +351,50 @@ describe('PushController', () => {
var auth = {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
Parse.Object.saveAll(installations).then(() => {
return pushController.sendPush(payload, {}, config, auth);
var pushController = new PushController();
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
});
}).then(() => {
const query = new Parse.Query('_PushStatus');
return query.find({useMasterKey: true});
}).then((results) => {
expect(results.length).toBe(1);
const result = results[0];
expect(result.createdAt instanceof Date).toBe(true);
expect(result.updatedAt instanceof Date).toBe(true);
expect(result.id.length).toBe(10);
expect(result.get('source')).toEqual('rest');
expect(result.get('query')).toEqual(JSON.stringify({}));
expect(typeof result.get('payload')).toEqual("string");
expect(JSON.parse(result.get('payload'))).toEqual(payload.data);
expect(result.get('status')).toEqual('succeeded');
expect(result.get('numSent')).toEqual(10);
expect(result.get('sentPerType')).toEqual({
'ios': 10 // 10 ios
});
expect(result.get('numFailed')).toEqual(5);
expect(result.get('failedPerType')).toEqual({
'android': 5 // android
});
return Parse.Object.saveAll(installations)
})
.then(() => {
return pushController.sendPush(payload, {}, config, auth);
}).then(() => {
// it is enqueued so it can take time
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
});
}).then(() => {
const query = new Parse.Query('_PushStatus');
return query.find({useMasterKey: true});
}).then((results) => {
expect(results.length).toBe(1);
const result = results[0];
expect(result.createdAt instanceof Date).toBe(true);
expect(result.updatedAt instanceof Date).toBe(true);
expect(result.id.length).toBe(10);
expect(result.get('source')).toEqual('rest');
expect(result.get('query')).toEqual(JSON.stringify({}));
expect(typeof result.get('payload')).toEqual("string");
expect(JSON.parse(result.get('payload'))).toEqual(payload.data);
expect(result.get('status')).toEqual('succeeded');
expect(result.get('numSent')).toEqual(10);
expect(result.get('sentPerType')).toEqual({
'ios': 10 // 10 ios
});
expect(result.get('numFailed')).toEqual(5);
expect(result.get('failedPerType')).toEqual({
'android': 5 // android
});
// Try to get it without masterKey
const query = new Parse.Query('_PushStatus');
return query.find();
}).then((results) => {
expect(results.length).toBe(0);
done();
});
const query = new Parse.Query('_PushStatus');
return query.find();
}).then((results) => {
expect(results.length).toBe(0);
done();
});
});
it('should properly report failures in _PushStatus', (done) => {
@@ -404,8 +421,12 @@ describe('PushController', () => {
var auth = {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
pushController.sendPush(payload, where, config, auth).then(() => {
var pushController = new PushController();
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
return pushController.sendPush(payload, where, config, auth)
}).then(() => {
fail('should not succeed');
done();
}).catch(() => {
@@ -416,7 +437,7 @@ describe('PushController', () => {
expect(pushStatus.get('status')).toBe('failed');
done();
});
})
});
});
it('should support full RESTQuery for increment', (done) => {
@@ -433,7 +454,6 @@ describe('PushController', () => {
return ["ios"];
}
}
var config = new Config(Parse.applicationId);
var auth = {
isMaster: true
@@ -450,8 +470,12 @@ describe('PushController', () => {
}
}
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
pushController.sendPush(payload, where, config, auth).then(() => {
var pushController = new PushController();
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
return pushController.sendPush(payload, where, config, auth);
}).then(() => {
done();
}).catch((err) => {
jfail(err);
@@ -491,8 +515,12 @@ describe('PushController', () => {
}
}
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
pushController.sendPush(payload, where, config, auth).then(() => {
var pushController = new PushController();
reconfigureServer({
push: { adapter: pushAdapter }
}).then(() => {
pushController.sendPush(payload, where, config, auth)
}).then(() => {
done();
}).catch(() => {
fail('should not fail');
+1 -1
View File
@@ -70,7 +70,7 @@ describe('PushRouter', () => {
it('sends a push through REST', (done) => {
request.post({
url: Parse.serverURL+"/push",
url: Parse.serverURL + "/push",
json: true,
body: {
'channels': {
+57
View File
@@ -0,0 +1,57 @@
var PushWorker = require('../src').PushWorker;
var Config = require('../src/Config');
describe('PushWorker', () => {
it('should run with small batch', (done) => {
const batchSize = 3;
var sendCount = 0;
reconfigureServer({
push: {
queueOptions: {
disablePushWorker: true,
batchSize
}
}
}).then(() => {
expect(new Config('test').pushWorker).toBeUndefined();
new PushWorker({
send: (body, installations) => {
expect(installations.length <= batchSize).toBe(true);
sendCount += installations.length;
return Promise.resolve();
},
getValidPushTypes: function() {
return ['ios', 'android']
}
});
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_" + installations.length);
installation.set("deviceToken","device_token_" + installations.length)
installation.set("badge", 1);
installation.set("deviceType", "ios");
installations.push(installation);
}
return Parse.Object.saveAll(installations);
}).then(() => {
return Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
alert: 'Hello world!'
}
}, {useMasterKey: true})
}).then(() => {
return new Promise((resolve) => {
setTimeout(resolve, 500);
});
}).then(() => {
expect(sendCount).toBe(10);
done();
}).catch(err => {
jfail(err);
})
});
});
+62
View File
@@ -412,4 +412,66 @@ describe('matchesQuery', function() {
expect(matchesQuery(caltrainStation, q)).toBe(false);
expect(matchesQuery(santaClara, q)).toBe(false);
});
it('matches on subobjects with dot notation', function() {
var message = {
id: new Id('Message', 'O1'),
text: "content",
status: {x: "read", y: "delivered"}
};
var q = new Parse.Query('Message');
q.equalTo("status.x", "read");
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.equalTo("status.z", "read");
expect(matchesQuery(message, q)).toBe(false);
q = new Parse.Query('Message');
q.equalTo("status.x", "delivered");
expect(matchesQuery(message, q)).toBe(false);
q = new Parse.Query('Message');
q.notEqualTo("status.x", "read");
expect(matchesQuery(message, q)).toBe(false);
q = new Parse.Query('Message');
q.notEqualTo("status.z", "read");
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.notEqualTo("status.x", "delivered");
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.exists("status.x");
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.exists("status.z");
expect(matchesQuery(message, q)).toBe(false);
q = new Parse.Query('Message');
q.exists("nonexistent.x");
expect(matchesQuery(message, q)).toBe(false);
q = new Parse.Query('Message');
q.doesNotExist("status.x");
expect(matchesQuery(message, q)).toBe(false);
q = new Parse.Query('Message');
q.doesNotExist("status.z");
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.doesNotExist("nonexistent.z");
expect(matchesQuery(message, q)).toBe(true);
q = new Parse.Query('Message');
q.equalTo("status.x", "read");
q.doesNotExist("status.y");
expect(matchesQuery(message, q)).toBe(false);
});
});
+50
View File
@@ -0,0 +1,50 @@
var RedisCacheAdapter = require('../src/Adapters/Cache/RedisCacheAdapter').default;
describe('RedisCacheAdapter', function() {
var KEY = 'hello';
var VALUE = 'world';
function wait(sleep) {
return new Promise(function(resolve) {
setTimeout(resolve, sleep);
})
}
it('should get/set/clear', (done) => {
var cache = new RedisCacheAdapter({
ttl: NaN
});
cache.put(KEY, VALUE)
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(VALUE))
.then(() => cache.clear())
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(null))
.then(done);
});
it('should expire after ttl', (done) => {
var cache = new RedisCacheAdapter(null, 1);
cache.put(KEY, VALUE)
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(VALUE))
.then(wait.bind(null, 2))
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(null))
.then(done);
});
it('should find un-expired records', (done) => {
var cache = new RedisCacheAdapter(null, 5);
cache.put(KEY, VALUE)
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(VALUE))
.then(wait.bind(null, 1))
.then(() => cache.get(KEY))
.then((value) => expect(value).not.toEqual(null))
.then(done);
});
});
+2 -2
View File
@@ -71,7 +71,7 @@ describe_only_db('mongo')('revocable sessions', () => {
it('should not upgrade bad legacy session token', done => {
rp.post({
url: Parse.serverURL+'/upgradeToRevocableSession',
url: Parse.serverURL + '/upgradeToRevocableSession',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Rest-API-Key': 'rest',
@@ -92,7 +92,7 @@ describe_only_db('mongo')('revocable sessions', () => {
it('should not crash without session token #2720', done => {
rp.post({
url: Parse.serverURL+'/upgradeToRevocableSession',
url: Parse.serverURL + '/upgradeToRevocableSession',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Rest-API-Key': 'rest'
+2 -1
View File
@@ -494,7 +494,8 @@ describe('SchemaController', () => {
it('creates non-custom classes which include relation field', done => {
config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('_Role', {}))
//as `_Role` is always created by default, we only get it here
.then(schema => schema.getOneSchema('_Role'))
.then(actualSchema => {
const expectedSchema = {
className: '_Role',
+8 -10
View File
@@ -4,13 +4,14 @@ describe('SessionTokenCache', function() {
beforeEach(function(done) {
var Parse = require('parse/node');
// Mock parse
var mockUser = {
become: jasmine.createSpy('become').and.returnValue(Parse.Promise.as({
id: 'userId'
}))
}
jasmine.mockLibrary('parse/node', 'User', mockUser);
spyOn(Parse, "Query").and.returnValue({
first: jasmine.createSpy("first").and.returnValue(Parse.Promise.as(new Parse.Object("_Session", {
user: new Parse.User({id:"userId"})
}))),
equalTo: function(){}
})
done();
});
@@ -46,7 +47,4 @@ describe('SessionTokenCache', function() {
});
});
afterEach(function() {
jasmine.restoreLibrary('parse/node', 'User');
});
});
+59
View File
@@ -0,0 +1,59 @@
var UserController = require('../src/Controllers/UserController').UserController;
var emailAdapter = require('./MockEmailAdapter')
var AppCache = require('../src/cache').AppCache;
describe('UserController', () => {
var user = {
_email_verify_token: 'testToken',
username: 'testUser',
email: 'test@example.com'
}
describe('sendVerificationEmail', () => {
describe('parseFrameURL not provided', () => {
it('uses publicServerURL', (done) => {
AppCache.put(defaultConfiguration.appId, Object.assign({}, defaultConfiguration, {
publicServerURL: 'http://www.example.com',
customPages: {
parseFrameURL: undefined
}
}))
emailAdapter.sendVerificationEmail = (options) => {
expect(options.link).toEqual('http://www.example.com/apps/test/verify_email?token=testToken&username=testUser')
done()
}
var userController = new UserController(emailAdapter, 'test', {
verifyUserEmails: true
})
userController.sendVerificationEmail(user)
})
})
describe('parseFrameURL provided', () => {
it('uses parseFrameURL and includes the destination in the link parameter', (done) => {
AppCache.put(defaultConfiguration.appId, Object.assign({}, defaultConfiguration, {
publicServerURL: 'http://www.example.com',
customPages: {
parseFrameURL: 'http://someother.example.com/handle-parse-iframe'
}
}))
emailAdapter.sendVerificationEmail = (options) => {
expect(options.link).toEqual('http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser')
done()
}
var userController = new UserController(emailAdapter, 'test', {
verifyUserEmails: true
})
userController.sendVerificationEmail(user)
})
})
})
});
+6 -4
View File
@@ -12,7 +12,8 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
invalidLink: "myInvalidLink",
verifyEmailSuccess: "myVerifyEmailSuccess",
choosePassword: "myChoosePassword",
passwordResetSuccess: "myPasswordResetSuccess"
passwordResetSuccess: "myPasswordResetSuccess",
parseFrameURL: "http://example.com/handle-parse-iframe"
},
publicServerURL: "https://my.public.server.com/1"
})
@@ -22,6 +23,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess");
expect(config.choosePasswordURL).toEqual("myChoosePassword");
expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess");
expect(config.parseFrameURL).toEqual("http://example.com/handle-parse-iframe");
expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email");
expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset");
done();
@@ -381,7 +383,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
fail('sending password reset email should not have succeeded');
done();
}, error => {
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.')
done();
});
})
@@ -412,7 +414,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
fail('sending password reset email should not have succeeded');
done();
}, error => {
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.')
done();
});
})
@@ -440,7 +442,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => {
fail('sending password reset email should not have succeeded');
done();
}, error => {
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.')
expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.')
done();
});
})
+2 -2
View File
@@ -1,6 +1,6 @@
"use strict"
// Sets up a Parse API server for testing.
const SpecReporter = require('jasmine-spec-reporter');
const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 5000;
@@ -167,7 +167,7 @@ Parse.serverURL = 'http://localhost:' + port + '/1';
Parse.Promise.disableAPlusCompliant();
// 10 minutes timeout
beforeAll(startDB, 10*60*1000);
beforeAll(startDB, 10 * 60 * 1000);
afterAll(stopDB);
+1 -1
View File
@@ -374,7 +374,7 @@ describe('rest create', () => {
var session = r.results[0];
var actual = new Date(session.expiresAt.iso);
var expected = new Date(now.getTime() + (sessionLength*1000));
var expected = new Date(now.getTime() + (sessionLength * 1000));
expect(actual.getFullYear()).toEqual(expected.getFullYear());
expect(actual.getMonth()).toEqual(expected.getMonth());
+21 -4
View File
@@ -102,6 +102,20 @@ const userSchema = {
"classLevelPermissions": defaultClassLevelPermissions,
}
const roleSchema = {
"className": "_Role",
"fields": {
"objectId": {"type": "String"},
"createdAt": {"type": "Date"},
"updatedAt": {"type": "Date"},
"ACL": {"type": "ACL"},
"name": {"type":"String"},
"users": {"type":"Relation", "targetClass":"_User"},
"roles": {"type":"Relation", "targetClass":"_Role"}
},
"classLevelPermissions": defaultClassLevelPermissions,
}
var noAuthHeaders = {
'X-Parse-Application-Id': 'test',
};
@@ -166,7 +180,10 @@ describe('schemas', () => {
json: true,
headers: masterKeyHeaders,
}, (error, response, body) => {
expect(dd(body.results, [userSchema])).toEqual();
var expected = {
results: [userSchema,roleSchema]
};
expect(dd(body.results.sort((s1, s2) => s1.className > s2.className), expected.results.sort((s1, s2) => s1.className > s2.className))).toEqual(undefined);
done();
});
});
@@ -186,9 +203,9 @@ describe('schemas', () => {
headers: masterKeyHeaders,
}, (error, response, body) => {
var expected = {
results: [userSchema,plainOldDataSchema,pointersAndRelationsSchema]
results: [userSchema,roleSchema,plainOldDataSchema,pointersAndRelationsSchema]
};
expect(dd(body, expected)).toEqual(undefined);
expect(dd(body.results.sort((s1, s2) => s1.className > s2.className), expected.results.sort((s1, s2) => s1.className > s2.className))).toEqual(undefined);
done();
})
});
@@ -1230,7 +1247,7 @@ describe('schemas', () => {
}
return new Promise((resolve, reject) => {
op({
url: 'http://localhost:8378/1/schemas/'+className,
url: 'http://localhost:8378/1/schemas/' + className,
headers: masterKeyHeaders,
json: true,
body: {
+1 -1
View File
@@ -98,7 +98,7 @@ export class AccountLockout {
const now = new Date();
const updateFields = {
_account_lockout_expires_at: Parse._encode(new Date(now.getTime() + this._config.accountLockout.duration*60*1000))
_account_lockout_expires_at: Parse._encode(new Date(now.getTime() + this._config.accountLockout.duration * 60 * 1000))
};
this._config.database.update('_User', query, updateFields)
+6 -6
View File
@@ -36,7 +36,7 @@ OAuth.prototype.send = function(method, path, params, body){
OAuth.prototype.buildRequest = function(method, path, params, body) {
if (path.indexOf("/") != 0) {
path = "/"+path;
path = "/" + path;
}
if (params && Object.keys(params).length > 0) {
path += "?" + OAuth.buildParameterString(params);
@@ -118,7 +118,7 @@ OAuth.nonce = function(){
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for(var i=0; i < 30; i++)
for(var i = 0; i < 30; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
@@ -162,7 +162,7 @@ OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_to
oauth_parameters.oauth_nonce = OAuth.nonce();
}
if (!oauth_parameters.oauth_timestamp) {
oauth_parameters.oauth_timestamp = Math.floor(new Date().getTime()/1000);
oauth_parameters.oauth_timestamp = Math.floor(new Date().getTime() / 1000);
}
if (!oauth_parameters.oauth_signature_method) {
oauth_parameters.oauth_signature_method = OAuth.signatureMethod;
@@ -172,7 +172,7 @@ OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_to
}
if(!auth_token_secret){
auth_token_secret="";
auth_token_secret = "";
}
// Force GET method if unset
if (!request.method) {
@@ -193,7 +193,7 @@ OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_to
var parameterString = OAuth.buildParameterString(signatureParams);
// Build the signature string
var url = "https://"+request.host+""+request.path;
var url = "https://" + request.host + "" + request.path;
var signatureString = OAuth.buildSignatureString(request.method, url, parameterString);
// Hash the signature string
@@ -210,7 +210,7 @@ OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_to
// Set the authorization header
var authHeader = Object.keys(oauth_parameters).sort().map(function(key){
var value = oauth_parameters[key];
return key+'="'+value+'"';
return key + '="' + value + '"';
}).join(", ")
request.headers.Authorization = 'OAuth ' + authHeader;
+1 -1
View File
@@ -27,7 +27,7 @@ function request(path, access_token) {
host: 'api.github.com',
path: '/' + path,
headers: {
'Authorization': 'bearer '+access_token,
'Authorization': 'bearer ' + access_token,
'User-Agent': 'parse-server'
}
}, function(res) {
+2 -2
View File
@@ -3,7 +3,7 @@ var https = require('https');
var Parse = require('parse/node').Parse;
function validateIdToken(id, token) {
return request("tokeninfo?id_token="+token)
return request("tokeninfo?id_token=" + token)
.then((response) => {
if (response && (response.sub == id || response.user_id == id)) {
return;
@@ -15,7 +15,7 @@ function validateIdToken(id, token) {
}
function validateAuthToken(id, token) {
return request("tokeninfo?access_token="+token)
return request("tokeninfo?access_token=" + token)
.then((response) => {
if (response && (response.sub == id || response.user_id == id)) {
return;
+1 -1
View File
@@ -4,7 +4,7 @@ var Parse = require('parse/node').Parse;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return request("users/self/?access_token="+authData.access_token)
return request("users/self/?access_token=" + authData.access_token)
.then((response) => {
if (response && response.data && response.data.id == authData.id) {
return;
+1 -1
View File
@@ -27,7 +27,7 @@ function request(path, access_token) {
host: 'api.meetup.com',
path: '/2/' + path,
headers: {
'Authorization': 'bearer '+access_token
'Authorization': 'bearer ' + access_token
}
}, function(res) {
var data = '';
+4 -4
View File
@@ -26,12 +26,12 @@ function graphRequest(path) {
data += chunk;
});
res.on('end', function () {
var starPos=data.indexOf("(");
var endPos=data.indexOf(")");
if(starPos==-1||endPos==-1){
var starPos = data.indexOf("(");
var endPos = data.indexOf(")");
if(starPos == -1 || endPos == -1){
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.');
}
data=data.substring(starPos+1,endPos-1);
data = data.substring(starPos + 1,endPos - 1);
data = JSON.parse(data);
resolve(data);
});
+1 -1
View File
@@ -41,7 +41,7 @@ function request(path, access_token) {
host: 'api.spotify.com',
path: '/v1/' + path,
headers: {
'Authorization': 'Bearer '+access_token
'Authorization': 'Bearer ' + access_token
}
}, function(res) {
var data = '';
+1 -1
View File
@@ -12,7 +12,7 @@ function validateAuthData(authData, options) {
client.auth_token_secret = authData.auth_token_secret;
return client.get("/1.1/account/verify_credentials.json").then((data) => {
if (data && data.id_str == ''+authData.id) {
if (data && data.id_str == '' + authData.id) {
return;
}
throw new Parse.Error(
+1 -1
View File
@@ -4,7 +4,7 @@ var Parse = require('parse/node').Parse;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData) {
return graphRequest('auth?access_token=' + authData.access_token +'&openid=' +authData.id).then(function (data) {
return graphRequest('auth?access_token=' + authData.access_token + '&openid=' + authData.id).then(function (data) {
if (data.errcode == 0) {
return;
}
+5 -4
View File
@@ -9,9 +9,10 @@ function debug() {
export class RedisCacheAdapter {
constructor(ctx) {
this.client = redis.createClient(ctx);
constructor(redisCtx, ttl = DEFAULT_REDIS_TTL) {
this.client = redis.createClient(redisCtx);
this.p = Promise.resolve();
this.ttl = ttl;
}
get(key) {
@@ -30,7 +31,7 @@ export class RedisCacheAdapter {
return this.p;
}
put(key, value, ttl = DEFAULT_REDIS_TTL) {
put(key, value, ttl = this.ttl) {
value = JSON.stringify(value);
debug('put', key, value, ttl);
if (ttl === 0) {
@@ -71,7 +72,7 @@ export class RedisCacheAdapter {
debug('clear');
this.p = this.p.then(() => {
return new Promise((resolve) => {
this.client.flushall(function() {
this.client.flushdb(function() {
resolve();
});
});
+2 -2
View File
@@ -21,12 +21,12 @@ function updateTransports(options) {
Object.assign({}, {
filename: 'parse-server.info',
name: 'parse-server',
}, options));
}, options, { timestamp: true }));
transports['parse-server-error'] = new (DailyRotateFile)(
Object.assign({}, {
filename: 'parse-server.err',
name: 'parse-server-error',
}, options, { level: 'error'}));
}, options, { level: 'error', timestamp: true }));
}
transports.console = new (winston.transports.Console)(
@@ -0,0 +1,65 @@
import events from 'events';
const emitter = new events.EventEmitter();
const subscriptions = new Map();
function unsubscribe(channel: string) {
if (!subscriptions.has(channel)) {
//console.log('No channel to unsub from');
return;
}
//console.log('unsub ', channel);
emitter.removeListener(channel, subscriptions.get(channel));
subscriptions.delete(channel);
}
class Publisher {
emitter: any;
constructor(emitter: any) {
this.emitter = emitter;
}
publish(channel: string, message: string): void {
this.emitter.emit(channel, message);
}
}
class Consumer extends events.EventEmitter {
emitter: any;
constructor(emitter: any) {
super();
this.emitter = emitter;
}
subscribe(channel: string): void {
unsubscribe(channel);
const handler = (message) => {
this.emit('message', channel, message);
}
subscriptions.set(channel, handler);
this.emitter.on(channel, handler);
}
unsubscribe(channel: string): void {
unsubscribe(channel);
}
}
function createPublisher(): any {
return new Publisher(emitter);
}
function createSubscriber(): any {
return new Consumer(emitter);
}
const EventEmitterMQ = {
createPublisher,
createSubscriber
}
export {
EventEmitterMQ
}
+5 -2
View File
@@ -1,3 +1,4 @@
// @flow
/*eslint no-unused-vars: "off"*/
// Push Adapter
//
@@ -11,13 +12,15 @@
// android push and APNS for ios push.
export class PushAdapter {
send(devices, installations, pushStatus) { }
send(body: any, installations: any[], pushStatus: any): ?Promise<*> {}
/**
* Get an array of valid push types.
* @returns {Array} An array of valid push types
*/
getValidPushTypes() {}
getValidPushTypes(): string[] {
return []
}
}
export default PushAdapter;
+10 -2
View File
@@ -89,6 +89,9 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
if (timeField && (typeof value === 'string')) {
value = new Date(value);
}
if (restKey.indexOf('.') > 0) {
return {key, value: restValue}
}
return {key, value};
}
@@ -98,7 +101,7 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
return {key, value};
}
// Handle update operators
// Handle update operators
if (typeof restValue === 'object' && '__op' in restValue) {
return {key, value: transformUpdateOperator(restValue, false)};
}
@@ -109,7 +112,7 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
}
const transformInteriorValue = restValue => {
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$')|| key.includes('.'))) {
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) {
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
}
// Handle atomic values
@@ -726,6 +729,11 @@ const nestedMongoObjectToNestedParseObject = mongoObject => {
return BytesCoder.databaseToJSON(mongoObject);
}
if (mongoObject.hasOwnProperty('__type') && mongoObject.__type == 'Date' && mongoObject.iso instanceof Date) {
mongoObject.iso = mongoObject.iso.toJSON();
return mongoObject;
}
return _.mapValues(mongoObject, nestedMongoObjectToNestedParseObject);
default:
throw 'unknown js type';
@@ -1,6 +1,7 @@
import { createClient } from './PostgresClient';
import Parse from 'parse/node';
import _ from 'lodash';
import sql from './sql';
const PostgresRelationDoesNotExistError = '42P01';
const PostgresDuplicateRelationError = '42P07';
@@ -11,7 +12,7 @@ const logger = require('../../../logger');
const debug = function(){
let args = [...arguments];
args = ['PG: '+arguments[0]].concat(args.slice(1, args.length));
args = ['PG: ' + arguments[0]].concat(args.slice(1, args.length));
const log = logger.getLogger();
log.debug.apply(log, args);
}
@@ -196,8 +197,8 @@ const buildWhereClause = ({ schema, query, index }) => {
}
return `'${cmpt}'`;
});
let name = components.slice(0, components.length-1).join('->');
name+='->>'+components[components.length-1];
let name = components.slice(0, components.length - 1).join('->');
name += '->>' + components[components.length - 1];
patterns.push(`${name} = '${fieldValue}'`);
} else if (typeof fieldValue === 'string') {
patterns.push(`$${index}:name = $${index + 1}`);
@@ -277,7 +278,7 @@ const buildWhereClause = ({ schema, query, index }) => {
if (baseArray.length > 0) {
const not = notIn ? ' NOT ' : '';
if (isArrayField) {
patterns.push(`${not} array_contains($${index}:name, $${index+1})`);
patterns.push(`${not} array_contains($${index}:name, $${index + 1})`);
values.push(fieldName, JSON.stringify(baseArray));
index += 2;
} else {
@@ -305,9 +306,9 @@ const buildWhereClause = ({ schema, query, index }) => {
}
if (Array.isArray(fieldValue.$all) && isArrayField) {
patterns.push(`array_contains_all($${index}:name, $${index+1}::jsonb)`);
patterns.push(`array_contains_all($${index}:name, $${index + 1}::jsonb)`);
values.push(fieldName, JSON.stringify(fieldValue.$all));
index+=2;
index += 2;
}
if (typeof fieldValue.$exists !== 'undefined') {
@@ -323,9 +324,9 @@ const buildWhereClause = ({ schema, query, index }) => {
if (fieldValue.$nearSphere) {
const point = fieldValue.$nearSphere;
const distance = fieldValue.$maxDistance;
const distanceInKM = distance*6371*1000;
patterns.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index+1}, $${index+2})::geometry) <= $${index+3}`);
sorts.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index+1}, $${index+2})::geometry) ASC`)
const distanceInKM = distance * 6371 * 1000;
patterns.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index + 1}, $${index + 2})::geometry) <= $${index + 3}`);
sorts.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index + 1}, $${index + 2})::geometry) ASC`)
values.push(fieldName, point.longitude, point.latitude, distanceInKM);
index += 4;
}
@@ -337,7 +338,7 @@ const buildWhereClause = ({ schema, query, index }) => {
const right = box[1].longitude;
const top = box[1].latitude;
patterns.push(`$${index}:name::point <@ $${index+1}::box`);
patterns.push(`$${index}:name::point <@ $${index + 1}::box`);
values.push(fieldName, `((${left}, ${bottom}), (${right}, ${top}))`);
index += 2;
}
@@ -357,7 +358,7 @@ const buildWhereClause = ({ schema, query, index }) => {
regex = processRegexPattern(regex);
patterns.push(`$${index}:name ${operator} '$${index+1}:raw'`);
patterns.push(`$${index}:name ${operator} '$${index + 1}:raw'`);
values.push(fieldName, regex);
index += 2;
}
@@ -490,11 +491,11 @@ export class PostgresStorageAdapter {
}
valuesArray.push(fieldName);
valuesArray.push(parseTypeToPostgresType(parseType));
patternsArray.push(`$${index}:name $${index+1}:raw`);
patternsArray.push(`$${index}:name $${index + 1}:raw`);
if (fieldName === 'objectId') {
patternsArray.push(`PRIMARY KEY ($${index}:name)`)
}
index = index+2;
index = index + 2;
});
const qs = `CREATE TABLE IF NOT EXISTS $1:name (${patternsArray.join(',')})`;
const values = [className, ...valuesArray];
@@ -618,7 +619,7 @@ export class PostgresStorageAdapter {
const values = [className, ...fieldNames];
const columns = fieldNames.map((name, idx) => {
return `$${idx+2}:name`;
return `$${idx + 2}:name`;
}).join(',');
const doBatch = (t) => {
@@ -699,8 +700,8 @@ export class PostgresStorageAdapter {
}
}
if (fieldName === '_account_lockout_expires_at'||
fieldName === '_perishable_token_expires_at'||
if (fieldName === '_account_lockout_expires_at' ||
fieldName === '_perishable_token_expires_at' ||
fieldName === '_password_changed_at') {
if (object[fieldName]) {
valuesArray.push(object[fieldName].iso);
@@ -762,7 +763,7 @@ export class PostgresStorageAdapter {
const value = geoPoints[key];
valuesArray.push(value.longitude, value.latitude);
const l = valuesArray.length + columnsArray.length;
return `POINT($${l}, $${l+1})`;
return `POINT($${l}, $${l + 1})`;
});
const columnsPattern = columnsArray.map((col, index) => `$${index + 2}:name`).join(',');
@@ -848,11 +849,11 @@ export class PostgresStorageAdapter {
}
const lastKey = `$${index}:name`;
const fieldNameIndex = index;
index+=1;
index += 1;
values.push(fieldName);
const update = Object.keys(fieldValue).reduce((lastKey, key) => {
const str = generate(lastKey, `$${index}::text`, `$${index+1}::jsonb`)
index+=2;
const str = generate(lastKey, `$${index}::text`, `$${index + 1}::jsonb`)
index += 2;
let value = fieldValue[key];
if (value) {
if (value.__op === 'Delete') {
@@ -926,16 +927,34 @@ export class PostgresStorageAdapter {
} else if (typeof fieldValue === 'object'
&& schema.fields[fieldName]
&& schema.fields[fieldName].type === 'Object') {
// Gather keys to increment
const keysToIncrement = Object.keys(originalUpdate).filter(k => {
// choose top level fields that have a delete operation set
return originalUpdate[k].__op === 'Increment' && k.split('.').length === 2 && k.split(".")[0] === fieldName;
}).map(k => k.split('.')[1]);
let incrementPatterns = '';
if (keysToIncrement.length > 0) {
incrementPatterns = ' || ' + keysToIncrement.map((c) => {
const amount = fieldValue[c].amount;
return `CONCAT('{"${c}":', COALESCE($${index}:name->>'${c}','0')::int + ${amount}, '}')::jsonb`;
}).join(' || ');
// Strip the keys
keysToIncrement.forEach((key) => {
delete fieldValue[key];
});
}
const keysToDelete = Object.keys(originalUpdate).filter(k => {
// choose top level fields that have a delete operation set
return originalUpdate[k].__op === 'Delete' && k.split('.').length === 2
return originalUpdate[k].__op === 'Delete' && k.split('.').length === 2 && k.split(".")[0] === fieldName;
}).map(k => k.split('.')[1]);
const deletePatterns = keysToDelete.reduce((p, c, i) => {
return p + ` - '$${index + 1 + i}:value'`;
}, '');
updatePatterns.push(`$${index}:name = ( COALESCE($${index}:name, '{}'::jsonb) ${deletePatterns} || $${index + 1 + keysToDelete.length}::jsonb )`);
updatePatterns.push(`$${index}:name = ( COALESCE($${index}:name, '{}'::jsonb) ${deletePatterns} ${incrementPatterns} || $${index + 1 + keysToDelete.length}::jsonb )`);
values.push(fieldName, ...keysToDelete, JSON.stringify(fieldValue));
index += 2 + keysToDelete.length;
@@ -997,7 +1016,7 @@ export class PostgresStorageAdapter {
if (hasLimit) {
values.push(limit);
}
const skipPattern = hasSkip ? `OFFSET $${values.length+1}` : '';
const skipPattern = hasSkip ? `OFFSET $${values.length + 1}` : '';
if (hasSkip) {
values.push(skip);
}
@@ -1024,7 +1043,7 @@ export class PostgresStorageAdapter {
return key.length > 0;
});
columns = keys.map((key, index) => {
return `$${index+values.length+1}:name`;
return `$${index + values.length + 1}:name`;
}).join(',');
values = values.concat(keys);
}
@@ -1141,41 +1160,35 @@ export class PostgresStorageAdapter {
}
performInitialization({ VolatileClassesSchemas }) {
const now = new Date().getTime();
debug('performInitialization');
let promises = VolatileClassesSchemas.map((schema) => {
return this.createTable(schema.className, schema).catch((err) =>{
const promises = VolatileClassesSchemas.map((schema) => {
return this.createTable(schema.className, schema).catch((err) => {
if (err.code === PostgresDuplicateRelationError || err.code === Parse.Error.INVALID_CLASS_NAME) {
return Promise.resolve();
}
throw err;
});
});
/* eslint-disable no-console */
promises = promises.concat([
this._client.any(json_object_set_key).catch((err) => {
console.error(err);
}),
this._client.any(array_add).catch((err) => {
console.error(err);
}),
this._client.any(array_add_unique).catch((err) => {
console.error(err);
}),
this._client.any(array_remove).catch((err) => {
console.error(err);
}),
this._client.any(array_contains_all).catch((err) => {
console.error(err);
}),
this._client.any(array_contains).catch((err) => {
console.error(err);
return Promise.all(promises)
.then(() => {
return this._client.tx(t => {
return t.batch([
t.none(sql.misc.jsonObjectSetKeys),
t.none(sql.array.add),
t.none(sql.array.addUnique),
t.none(sql.array.remove),
t.none(sql.array.containsAll),
t.none(sql.array.contains)
]);
});
})
]);
/* eslint-enable no-console */
return Promise.all(promises).then(() => {
debug(`initialzationDone in ${new Date().getTime() - now}`);
}, () => {});
.then(data => {
debug(`initializationDone in ${data.duration}`);
})
.catch(error => {
/* eslint-disable no-console */
console.error(error);
});
}
}
@@ -1252,84 +1265,5 @@ function literalizeRegexPart(s) {
);
}
// Function to set a key on a nested JSON document
const json_object_set_key = 'CREATE OR REPLACE FUNCTION "json_object_set_key"(\
"json" jsonb,\
"key_to_set" TEXT,\
"value_to_set" anyelement\
)\
RETURNS jsonb \
LANGUAGE sql \
IMMUTABLE \
STRICT \
AS $function$\
SELECT concat(\'{\', string_agg(to_json("key") || \':\' || "value", \',\'), \'}\')::jsonb\
FROM (SELECT *\
FROM jsonb_each("json")\
WHERE "key" <> "key_to_set"\
UNION ALL\
SELECT "key_to_set", to_json("value_to_set")::jsonb) AS "fields"\
$function$;'
const array_add = `CREATE OR REPLACE FUNCTION "array_add"(
"array" jsonb,
"values" jsonb
)
RETURNS jsonb
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT array_to_json(ARRAY(SELECT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT jsonb_array_elements("values")))))::jsonb;
$function$;`;
const array_add_unique = `CREATE OR REPLACE FUNCTION "array_add_unique"(
"array" jsonb,
"values" jsonb
)
RETURNS jsonb
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT array_to_json(ARRAY(SELECT DISTINCT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT DISTINCT jsonb_array_elements("values")))))::jsonb;
$function$;`;
const array_remove = `CREATE OR REPLACE FUNCTION "array_remove"(
"array" jsonb,
"values" jsonb
)
RETURNS jsonb
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT array_to_json(ARRAY(SELECT * FROM jsonb_array_elements("array") as elt WHERE elt NOT IN (SELECT * FROM (SELECT jsonb_array_elements("values")) AS sub)))::jsonb;
$function$;`;
const array_contains_all = `CREATE OR REPLACE FUNCTION "array_contains_all"(
"array" jsonb,
"values" jsonb
)
RETURNS boolean
LANGUAGE sql
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 ;
$function$;`;
const array_contains = `CREATE OR REPLACE FUNCTION "array_contains"(
"array" jsonb,
"values" jsonb
)
RETURNS boolean
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT RES.CNT >= 1 FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES ;
$function$;`;
export default PostgresStorageAdapter;
module.exports = PostgresStorageAdapter; // Required for tests
@@ -0,0 +1,11 @@
CREATE OR REPLACE FUNCTION array_add_unique(
"array" jsonb,
"values" jsonb
)
RETURNS jsonb
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT array_to_json(ARRAY(SELECT DISTINCT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT DISTINCT jsonb_array_elements("values")))))::jsonb;
$function$;
@@ -0,0 +1,11 @@
CREATE OR REPLACE FUNCTION array_add(
"array" jsonb,
"values" jsonb
)
RETURNS jsonb
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT array_to_json(ARRAY(SELECT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT jsonb_array_elements("values")))))::jsonb;
$function$;
@@ -0,0 +1,11 @@
CREATE OR REPLACE FUNCTION array_contains_all(
"array" jsonb,
"values" jsonb
)
RETURNS boolean
LANGUAGE sql
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;
$function$;
@@ -0,0 +1,11 @@
CREATE OR REPLACE FUNCTION array_contains(
"array" jsonb,
"values" jsonb
)
RETURNS boolean
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT RES.CNT >= 1 FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES;
$function$;
@@ -0,0 +1,11 @@
CREATE OR REPLACE FUNCTION array_remove(
"array" jsonb,
"values" jsonb
)
RETURNS jsonb
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT array_to_json(ARRAY(SELECT * FROM jsonb_array_elements("array") as elt WHERE elt NOT IN (SELECT * FROM (SELECT jsonb_array_elements("values")) AS sub)))::jsonb;
$function$;
@@ -0,0 +1,32 @@
'use strict';
var QueryFile = require('pg-promise').QueryFile;
var path = require('path');
module.exports = {
array: {
add: sql('array/add.sql'),
addUnique: sql('array/add-unique.sql'),
contains: sql('array/contains.sql'),
containsAll: sql('array/contains-all.sql'),
remove: sql('array/remove.sql')
},
misc: {
jsonObjectSetKeys: sql('misc/json-object-set-keys.sql')
}
};
///////////////////////////////////////////////
// Helper for linking to external query files;
function sql(file) {
var fullPath = path.join(__dirname, file); // generating full path;
var qf = new QueryFile(fullPath, {minify: true});
if (qf.error) {
throw qf.error;
}
return qf;
}
@@ -0,0 +1,19 @@
-- Function to set a key on a nested JSON document
CREATE OR REPLACE FUNCTION json_object_set_key(
"json" jsonb,
key_to_set TEXT,
value_to_set anyelement
)
RETURNS jsonb
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT concat('{', string_agg(to_json("key") || ':' || "value", ','), '}')::jsonb
FROM (SELECT *
FROM jsonb_each("json")
WHERE key <> key_to_set
UNION ALL
SELECT key_to_set, to_json("value_to_set")::jsonb) AS fields
$function$;
+18 -5
View File
@@ -11,7 +11,7 @@ function removeTrailingSlash(str) {
return str;
}
if (str.endsWith("/")) {
str = str.substr(0, str.length-1);
str = str.substr(0, str.length - 1);
}
return str;
}
@@ -58,6 +58,9 @@ export class Config {
this.hooksController = cacheInfo.hooksController;
this.filesController = cacheInfo.filesController;
this.pushController = cacheInfo.pushController;
this.pushControllerQueue = cacheInfo.pushControllerQueue;
this.pushWorker = cacheInfo.pushWorker;
this.hasPushSupport = cacheInfo.hasPushSupport;
this.loggerController = cacheInfo.loggerController;
this.userController = cacheInfo.userController;
this.authDataManager = cacheInfo.authDataManager;
@@ -127,10 +130,16 @@ export class Config {
throw 'passwordPolicy.resetTokenValidityDuration must be a positive number';
}
if(passwordPolicy.validatorPattern && !(passwordPolicy.validatorPattern instanceof RegExp)) {
throw 'passwordPolicy.validatorPattern must be a RegExp.';
if(passwordPolicy.validatorPattern){
if(typeof(passwordPolicy.validatorPattern) === 'string') {
passwordPolicy.validatorPattern = new RegExp(passwordPolicy.validatorPattern);
}
else if(!(passwordPolicy.validatorPattern instanceof RegExp)){
throw 'passwordPolicy.validatorPattern must be a regex string or RegExp object.';
}
}
if(passwordPolicy.validatorCallback && typeof passwordPolicy.validatorCallback !== 'function') {
throw 'passwordPolicy.validatorCallback must be a function.';
}
@@ -201,7 +210,7 @@ export class Config {
return undefined;
}
var now = new Date();
return new Date(now.getTime() + (this.emailVerifyTokenValidityDuration*1000));
return new Date(now.getTime() + (this.emailVerifyTokenValidityDuration * 1000));
}
generatePasswordResetTokenExpiresAt() {
@@ -217,7 +226,7 @@ export class Config {
return undefined;
}
var now = new Date();
return new Date(now.getTime() + (this.sessionLength*1000));
return new Date(now.getTime() + (this.sessionLength * 1000));
}
get invalidLinkURL() {
@@ -240,6 +249,10 @@ export class Config {
return this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html`;
}
get parseFrameURL() {
return this.customPages.parseFrameURL;
}
get verifyEmailURL() {
return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`;
}
+6 -2
View File
@@ -38,11 +38,15 @@ export class AdaptableController {
}
validateAdapter(adapter) {
AdaptableController.validateAdapter(adapter, this);
}
static validateAdapter(adapter, self, ExpectedType) {
if (!adapter) {
throw new Error(this.constructor.name+" requires an adapter");
throw new Error(this.constructor.name + " requires an adapter");
}
const Type = this.expectedAdapterType();
const Type = ExpectedType || self.expectedAdapterType();
// Allow skipping for testing
if (!Type) {
return;
+40 -4
View File
@@ -18,7 +18,7 @@ function addWriteACL(query, acl) {
function addReadACL(query, acl) {
const newQuery = _.cloneDeep(query);
//Can't be any existing '_rperm' query, we don't allow client queries on that, no need to $and
newQuery._rperm = { "$in" : [null, "*", ...acl]};
newQuery._rperm = {"$in": [null, "*", ...acl]};
return newQuery;
}
@@ -56,6 +56,30 @@ const validateQuery = query => {
if (query.$or) {
if (query.$or instanceof Array) {
query.$or.forEach(validateQuery);
/* In MongoDB, $or queries which are not alone at the top level of the
* query can not make efficient use of indexes due to a long standing
* bug known as SERVER-13732.
*
* This block restructures queries in which $or is not the sole top
* level element by moving all other top-level predicates inside every
* subdocument of the $or predicate, allowing MongoDB's query planner
* to make full use of the most relevant indexes.
*
* EG: {$or: [{a: 1}, {a: 2}], b: 2}
* Becomes: {$or: [{a: 1, b: 2}, {a: 2, b: 2}]}
*
* https://jira.mongodb.org/browse/SERVER-13732
*/
Object.keys(query).forEach(key => {
const noCollisions = !query.$or.some(subq => subq.hasOwnProperty(key))
if (key != '$or' && noCollisions) {
query.$or.forEach(subquery => {
subquery[key] = query[key];
});
delete query[key];
}
});
} else {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.');
}
@@ -885,31 +909,43 @@ DatabaseController.prototype.addPointerPermissions = function(schema, className,
// have a Parse app without it having a _User collection.
DatabaseController.prototype.performInitialization = function() {
const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } };
const requiredRoleFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._Role } };
const userClassPromise = this.loadSchema()
.then(schema => schema.enforceClassExists('_User'))
const roleClassPromise = this.loadSchema()
.then(schema => schema.enforceClassExists('_Role'))
const usernameUniqueness = userClassPromise
.then(() => this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']))
.catch(error => {
logger.warn('Unable to ensure uniqueness for usernames: ', error);
return Promise.reject(error);
throw error;
});
const emailUniqueness = userClassPromise
.then(() => this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']))
.catch(error => {
logger.warn('Unable to ensure uniqueness for user email addresses: ', error);
return Promise.reject(error);
throw error;
});
const roleUniqueness = roleClassPromise
.then(() => this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']))
.catch(error => {
logger.warn('Unable to ensure uniqueness for role name: ', error);
throw error;
});
// Create tables for volatile classes
const adapterInit = this.adapter.performInitialization({ VolatileClassesSchemas: SchemaController.VolatileClassesSchemas });
return Promise.all([usernameUniqueness, emailUniqueness, adapterInit]);
return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness, adapterInit]);
}
function joinTableName(className, key) {
return `_Join:${key}:${className}`;
}
// Expose validateQuery for tests
DatabaseController._validateQuery = validateQuery;
module.exports = DatabaseController;
+2
View File
@@ -9,6 +9,8 @@ const DefaultHooksCollectionName = "_Hooks";
export class HooksController {
_applicationId:string;
_webhookKey:string;
database: any;
constructor(applicationId:string, databaseController, webhookKey) {
this._applicationId = applicationId;
+14 -3
View File
@@ -41,7 +41,18 @@ export class LoggerController extends AdaptableController {
// check the url
if (e.url) {
e.url = this.maskSensitiveUrl(e.url);
// for strings
if (typeof e.url === 'string') {
e.url = this.maskSensitiveUrl(e.url);
} else if (Array.isArray(e.url)) { // for strings in array
e.url = e.url.map(item => {
if (typeof item === 'string') {
return this.maskSensitiveUrl(item);
}
return item;
});
}
}
if (e.body) {
@@ -134,10 +145,10 @@ export class LoggerController extends AdaptableController {
// until (optional) End time for the search. Defaults to current time.
// order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”.
// size (optional) Number of rows returned by search. Defaults to 10
getLogs(options= {}) {
getLogs(options = {}) {
if (!this.adapter) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Logger adapter is not availabe');
'Logger adapter is not available');
}
if (typeof this.adapter.query !== 'function') {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
+3 -80
View File
@@ -1,54 +1,17 @@
import { Parse } from 'parse/node';
import rest from '../rest';
import AdaptableController from './AdaptableController';
import { PushAdapter } from '../Adapters/Push/PushAdapter';
import deepcopy from 'deepcopy';
import RestQuery from '../RestQuery';
import RestWrite from '../RestWrite';
import { master } from '../Auth';
import { pushStatusHandler } from '../StatusHandler';
const UNSUPPORTED_BADGE_KEY = "unsupported";
export class PushController extends AdaptableController {
/**
* Check whether the deviceType parameter in qury condition is valid or not.
* @param {Object} where A query condition
* @param {Array} validPushTypes An array of valid push types(string)
*/
static validatePushType(where = {}, validPushTypes = []) {
var deviceTypeField = where.deviceType || {};
var deviceTypes = [];
if (typeof deviceTypeField === 'string') {
deviceTypes.push(deviceTypeField);
} else if (Array.isArray(deviceTypeField['$in'])) {
deviceTypes.concat(deviceTypeField['$in']);
}
for (var i = 0; i < deviceTypes.length; i++) {
var deviceType = deviceTypes[i];
if (validPushTypes.indexOf(deviceType) < 0) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
deviceType + ' is not supported push type.');
}
}
}
get pushIsAvailable() {
return !!this.adapter;
}
export class PushController {
sendPush(body = {}, where = {}, config, auth, onPushStatusSaved = () => {}) {
var pushAdapter = this.adapter;
if (!this.pushIsAvailable) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push adapter is not available');
}
if (!this.options) {
if (!config.hasPushSupport) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Missing push configuration');
}
PushController.validatePushType(where, pushAdapter.getValidPushTypes());
// Replace the expiration_time with a valid Unix epoch milliseconds time
body['expiration_time'] = PushController.getExpirationTime(body);
// TODO: If the req can pass the checking, we return immediately instead of waiting
@@ -86,15 +49,7 @@ export class PushController extends AdaptableController {
onPushStatusSaved(pushStatus.objectId);
return badgeUpdate();
}).then(() => {
return rest.find(config, auth, '_Installation', where);
}).then((response) => {
if (!response.results) {
return Promise.reject({error: 'PushController: no results in query'})
}
pushStatus.setRunning(response.results);
return this.sendToAdapter(body, response.results, pushStatus, config);
}).then((results) => {
return pushStatus.complete(results);
return config.pushControllerQueue.enqueue(body, where, config, auth, pushStatus);
}).catch((err) => {
return pushStatus.fail(err).then(() => {
throw err;
@@ -102,34 +57,6 @@ export class PushController extends AdaptableController {
});
}
sendToAdapter(body, installations, pushStatus) {
if (body.data && body.data.badge && typeof body.data.badge == 'string' && body.data.badge.toLowerCase() == "increment") {
// Collect the badges to reduce the # of calls
const badgeInstallationsMap = installations.reduce((map, installation) => {
let badge = installation.badge;
if (installation.deviceType != "ios") {
badge = UNSUPPORTED_BADGE_KEY;
}
map[badge+''] = map[badge+''] || [];
map[badge+''].push(installation);
return map;
}, {});
// Map the on the badges count and return the send result
const promises = Object.keys(badgeInstallationsMap).map((badge) => {
const payload = deepcopy(body);
if (badge == UNSUPPORTED_BADGE_KEY) {
delete payload.data.badge;
} else {
payload.data.badge = parseInt(badge);
}
return this.adapter.send(payload, badgeInstallationsMap[badge], pushStatus.objectId);
});
return Promise.all(promises);
}
return this.adapter.send(body, installations, pushStatus.objectId);
}
/**
* Get expiration time from the request body.
* @param {Object} request A request object
@@ -157,10 +84,6 @@ export class PushController extends AdaptableController {
}
return expirationTime.valueOf();
}
expectedAdapterType() {
return PushAdapter;
}
}
export default PushController;
+8 -8
View File
@@ -21,10 +21,10 @@ export default class SchemaCache {
}
put(key, value) {
return this.cache.get(this.prefix+ALL_KEYS).then((allKeys) => {
return this.cache.get(this.prefix + ALL_KEYS).then((allKeys) => {
allKeys = allKeys || {};
allKeys[key] = true;
return Promise.all([this.cache.put(this.prefix+ALL_KEYS, allKeys, this.ttl), this.cache.put(key, value, this.ttl)]);
return Promise.all([this.cache.put(this.prefix + ALL_KEYS, allKeys, this.ttl), this.cache.put(key, value, this.ttl)]);
});
}
@@ -32,32 +32,32 @@ export default class SchemaCache {
if (!this.ttl) {
return Promise.resolve(null);
}
return this.cache.get(this.prefix+MAIN_SCHEMA);
return this.cache.get(this.prefix + MAIN_SCHEMA);
}
setAllClasses(schema) {
if (!this.ttl) {
return Promise.resolve(null);
}
return this.put(this.prefix+MAIN_SCHEMA, schema);
return this.put(this.prefix + MAIN_SCHEMA, schema);
}
setOneSchema(className, schema) {
if (!this.ttl) {
return Promise.resolve(null);
}
return this.put(this.prefix+className, schema);
return this.put(this.prefix + className, schema);
}
getOneSchema(className) {
if (!this.ttl) {
return Promise.resolve(null);
}
return this.cache.get(this.prefix+className).then((schema) => {
return this.cache.get(this.prefix + className).then((schema) => {
if (schema) {
return Promise.resolve(schema);
}
return this.cache.get(this.prefix+MAIN_SCHEMA).then((cachedSchemas) => {
return this.cache.get(this.prefix + MAIN_SCHEMA).then((cachedSchemas) => {
cachedSchemas = cachedSchemas || [];
schema = cachedSchemas.find((cachedSchema) => {
return cachedSchema.className === className;
@@ -72,7 +72,7 @@ export default class SchemaCache {
clear() {
// That clears all caches...
return this.cache.get(this.prefix+ALL_KEYS).then((allKeys) => {
return this.cache.get(this.prefix + ALL_KEYS).then((allKeys) => {
if (!allKeys) {
return;
}
+13 -8
View File
@@ -86,6 +86,7 @@ const defaultColumns = Object.freeze({
"errorMessage": {type:'Object'},
"sentPerType": {type:'Object'},
"failedPerType":{type:'Object'},
"count": {type:'Number'}
},
_JobStatus: {
"jobName": {type: 'String'},
@@ -333,25 +334,29 @@ export default class SchemaController {
if (this.reloadDataPromise && !options.clearCache) {
return this.reloadDataPromise;
}
this.data = {};
this.perms = {};
this.reloadDataPromise = promise.then(() => {
return this.getAllClasses(options);
})
.then(allSchemas => {
const data = {};
const perms = {};
allSchemas.forEach(schema => {
this.data[schema.className] = injectDefaultSchema(schema).fields;
this.perms[schema.className] = schema.classLevelPermissions;
data[schema.className] = injectDefaultSchema(schema).fields;
perms[schema.className] = schema.classLevelPermissions;
});
// Inject the in-memory classes
volatileClasses.forEach(className => {
const schema = injectDefaultSchema({ className });
this.data[className] = schema.fields;
this.perms[className] = schema.classLevelPermissions;
data[className] = schema.fields;
perms[className] = schema.classLevelPermissions;
});
this.data = data;
this.perms = perms;
delete this.reloadDataPromise;
}, (err) => {
this.data = {};
this.perms = {};
delete this.reloadDataPromise;
throw err;
});
@@ -742,7 +747,7 @@ export default class SchemaController {
if (missingColumns.length > 0) {
throw new Parse.Error(
Parse.Error.INCORRECT_TYPE,
missingColumns[0]+' is required.');
missingColumns[0] + ' is required.');
}
return Promise.resolve(this);
}
@@ -947,7 +952,7 @@ function getObjectType(obj) {
}
break;
}
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid "+obj.__type);
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid " + obj.__type);
}
if (obj['$ne']) {
return getObjectType(obj['$ne']);
+15 -2
View File
@@ -119,7 +119,8 @@ export class UserController extends AdaptableController {
// We may need to fetch the user in case of update email
this.getUserIfNeeded(user).then((user) => {
const username = encodeURIComponent(user.username);
const link = `${this.config.verifyEmailURL}?token=${token}&username=${username}`;
const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config);
const options = {
appName: this.config.appName,
link: link,
@@ -153,8 +154,8 @@ export class UserController extends AdaptableController {
.then(user => {
const token = encodeURIComponent(user._perishable_token);
const username = encodeURIComponent(user.username);
const link = `${this.config.requestResetPasswordURL}?token=${token}&username=${username}`
const link = buildEmailLink(this.config.requestResetPasswordURL, username, token, this.config);
const options = {
appName: this.config.appName,
link: link,
@@ -215,4 +216,16 @@ function updateUserPassword(userId, password, config) {
});
}
function buildEmailLink(destination, username, token, config) {
const usernameAndToken = `token=${token}&username=${username}`
if (config.parseFrameURL) {
const destinationWithoutHost = destination.replace(config.publicServerURL, '');
return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`;
} else {
return `${destination}?${usernameAndToken}`;
}
}
export default UserController;
+1 -2
View File
@@ -37,7 +37,6 @@ class ParseLiveQueryServer {
// Initialize Parse
Parse.Object.disableSingleInstance();
Parse.User.enableUnsafeCurrentUser();
const serverURL = config.serverURL || Parse.serverURL;
Parse.serverURL = serverURL;
@@ -363,7 +362,7 @@ class ParseLiveQueryServer {
// Then get the user's roles
var rolesQuery = new Parse.Query(Parse.Role);
rolesQuery.equalTo("users", user);
return rolesQuery.find();
return rolesQuery.find({useMasterKey:true});
}).
then((roles) => {
+8 -1
View File
@@ -1,12 +1,19 @@
import logger from '../logger';
const typeMap = new Map([['disconnect', 'close']]);
const getWS = function() {
try {
return require('uws');
} catch(e) {
return require('ws');
}
}
export class ParseWebSocketServer {
server: Object;
constructor(server: any, onConnect: Function, websocketTimeout: number = 10 * 1000) {
const WebSocketServer = require('ws').Server;
const WebSocketServer = getWS().Server;
const wss = new WebSocketServer({ server: server });
wss.on('listening', () => {
logger.info('Parse LiveQuery Server starts running');
+7
View File
@@ -133,6 +133,13 @@ function matchesKeyConstraints(object, key, constraints) {
if (constraints === null) {
return false;
}
if(key.indexOf(".") >= 0){
// Key references a subobject
var keyComponents = key.split(".");
var subObjectKey = keyComponents[0];
var keyRemainder = keyComponents.slice(1).join(".");
return matchesKeyConstraints(object[subObjectKey] || {}, keyRemainder, constraints);
}
var i;
if (key === '$or') {
for (i = 0; i < constraints.length; i++) {
+13 -2
View File
@@ -2,10 +2,21 @@ import Parse from 'parse/node';
import LRU from 'lru-cache';
import logger from '../logger';
function userForSessionToken(sessionToken){
var q = new Parse.Query("_Session");
q.equalTo("sessionToken", sessionToken);
return q.first({useMasterKey:true}).then(function(session){
if(!session){
return Parse.Promise.error("No session found for session token");
}
return session.get("user");
});
}
class SessionTokenCache {
cache: Object;
constructor(timeout: number = 30 * 24 * 60 *60 * 1000, maxSize: number = 10000) {
constructor(timeout: number = 30 * 24 * 60 * 60 * 1000, maxSize: number = 10000) {
this.cache = new LRU({
max: maxSize,
maxAge: timeout
@@ -21,7 +32,7 @@ class SessionTokenCache {
logger.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken);
return Parse.Promise.as(userId);
}
return Parse.User.become(sessionToken).then((user) => {
return userForSessionToken(sessionToken).then((user) => {
logger.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken);
const userId = user.id;
this.cache.set(sessionToken, userId);
+26
View File
@@ -0,0 +1,26 @@
import { loadAdapter } from './Adapters/AdapterLoader';
import {
EventEmitterMQ
} from './Adapters/MessageQueue/EventEmitterMQ';
const ParseMessageQueue = {};
ParseMessageQueue.createPublisher = function(config: any): any {
const adapter = loadAdapter(config.messageQueueAdapter, EventEmitterMQ, config);
if (typeof adapter.createPublisher !== 'function') {
throw 'pubSubAdapter should have createPublisher()';
}
return adapter.createPublisher(config);
}
ParseMessageQueue.createSubscriber = function(config: any): void {
const adapter = loadAdapter(config.messageQueueAdapter, EventEmitterMQ, config)
if (typeof adapter.createSubscriber !== 'function') {
throw 'messageQueueAdapter should have createSubscriber()';
}
return adapter.createSubscriber(config);
}
export {
ParseMessageQueue
}
+26 -4
View File
@@ -39,6 +39,8 @@ import { LogsRouter } from './Routers/LogsRouter';
import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer';
import { PublicAPIRouter } from './Routers/PublicAPIRouter';
import { PushController } from './Controllers/PushController';
import { PushQueue } from './Push/PushQueue';
import { PushWorker } from './Push/PushWorker';
import { PushRouter } from './Routers/PushRouter';
import { CloudCodeRouter } from './Routers/CloudCodeRouter';
import { RolesRouter } from './Routers/RolesRouter';
@@ -168,11 +170,28 @@ class ParseServer {
});
const filesController = new FilesController(filesControllerAdapter, appId);
const pushOptions = Object.assign({}, push);
const pushQueueOptions = pushOptions.queueOptions || {};
if (pushOptions.queueOptions) {
delete pushOptions.queueOptions;
}
// Pass the push options too as it works with the default
const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push || {});
// We pass the options and the base class for the adapter,
const pushAdapter = loadAdapter(pushOptions && pushOptions.adapter, ParsePushAdapter, pushOptions);
// We pass the options and the base class for the adatper,
// Note that passing an instance would work too
const pushController = new PushController(pushControllerAdapter, appId, push);
const pushController = new PushController();
const hasPushSupport = pushAdapter && push;
const {
disablePushWorker
} = pushQueueOptions;
const pushControllerQueue = new PushQueue(pushQueueOptions);
let pushWorker;
if (!disablePushWorker) {
pushWorker = new PushWorker(pushAdapter, pushQueueOptions);
}
const emailControllerAdapter = loadAdapter(emailAdapter);
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
@@ -237,7 +256,10 @@ class ParseServer {
databaseController,
schemaCacheTTL,
enableSingleSchemaCache,
userSensitiveFields
userSensitiveFields,
pushWorker,
pushControllerQueue,
hasPushSupport
});
Config.validate(AppCache.get(appId));
+1 -1
View File
@@ -174,7 +174,7 @@ function makeExpressHandler(appId, promiseHandler) {
// Override the default expressjs response
// as it double encodes %encoded chars in URL
if (!result.response) {
res.send('Found. Redirecting to '+result.location);
res.send('Found. Redirecting to ' + result.location);
return;
}
}
+60
View File
@@ -0,0 +1,60 @@
import { ParseMessageQueue } from '../ParseMessageQueue';
import rest from '../rest';
import { isPushIncrementing } from './utils';
const PUSH_CHANNEL = 'parse-server-push';
const DEFAULT_BATCH_SIZE = 100;
export class PushQueue {
parsePublisher: Object;
channel: String;
batchSize: Number;
// config object of the publisher, right now it only contains the redisURL,
// but we may extend it later.
constructor(config: any = {}) {
this.channel = config.channel || PUSH_CHANNEL;
this.batchSize = config.batchSize || DEFAULT_BATCH_SIZE;
this.parsePublisher = ParseMessageQueue.createPublisher(config);
}
static defaultPushChannel() {
return PUSH_CHANNEL;
}
enqueue(body, where, config, auth, pushStatus) {
const limit = this.batchSize;
// Order by badge (because the payload is badge dependant)
// and createdAt to fix the order
const order = isPushIncrementing(body) ? 'badge,createdAt' : 'createdAt';
return Promise.resolve().then(() => {
return rest.find(config,
auth,
'_Installation',
where,
{limit: 0, count: true});
}).then(({results, count}) => {
if (!results) {
return Promise.reject({error: 'PushController: no results in query'})
}
pushStatus.setRunning(count);
let skip = 0;
while (skip < count) {
const query = { where,
limit,
skip,
order };
const pushWorkItem = {
body,
query,
pushStatus: { objectId: pushStatus.objectId },
applicationId: config.applicationId
}
this.parsePublisher.publish(this.channel, JSON.stringify(pushWorkItem));
skip += limit;
}
});
}
}
+95
View File
@@ -0,0 +1,95 @@
// @flow
import deepcopy from 'deepcopy';
import AdaptableController from '../Controllers/AdaptableController';
import { master } from '../Auth';
import Config from '../Config';
import { PushAdapter } from '../Adapters/Push/PushAdapter';
import rest from '../rest';
import { pushStatusHandler } from '../StatusHandler';
import { isPushIncrementing } from './utils';
import { ParseMessageQueue } from '../ParseMessageQueue';
import { PushQueue } from './PushQueue';
const UNSUPPORTED_BADGE_KEY = "unsupported";
function groupByBadge(installations) {
return installations.reduce((map, installation) => {
let badge = installation.badge + '';
if (installation.deviceType != "ios") {
badge = UNSUPPORTED_BADGE_KEY;
}
map[badge] = map[badge] || [];
map[badge].push(installation);
return map;
}, {});
}
export class PushWorker {
subscriber: ?any;
adapter: any;
channel: string;
constructor(pushAdapter: PushAdapter, subscriberConfig: any = {}) {
AdaptableController.validateAdapter(pushAdapter, this, PushAdapter);
this.adapter = pushAdapter;
this.channel = subscriberConfig.channel || PushQueue.defaultPushChannel();
this.subscriber = ParseMessageQueue.createSubscriber(subscriberConfig);
if (this.subscriber) {
const subscriber = this.subscriber;
subscriber.subscribe(this.channel);
subscriber.on('message', (channel, messageStr) => {
const workItem = JSON.parse(messageStr);
this.run(workItem);
});
}
}
unsubscribe(): void {
if (this.subscriber) {
this.subscriber.unsubscribe(this.channel);
}
}
run({ body, query, pushStatus, applicationId }: any): Promise<*> {
const config = new Config(applicationId);
const auth = master(config);
const where = query.where;
delete query.where;
return rest.find(config, auth, '_Installation', where, query).then(({results}) => {
if (results.length == 0) {
return;
}
return this.sendToAdapter(body, results, pushStatus, config);
}, err => {
throw err;
});
}
sendToAdapter(body: any, installations: any[], pushStatus: any, config: Config): Promise<*> {
pushStatus = pushStatusHandler(config, pushStatus.objectId);
if (!isPushIncrementing(body)) {
return this.adapter.send(body, installations, pushStatus.objectId).then((results) => {
return pushStatus.trackSent(results);
});
}
// Collect the badges to reduce the # of calls
const badgeInstallationsMap = groupByBadge(installations);
// Map the on the badges count and return the send result
const promises = Object.keys(badgeInstallationsMap).map((badge) => {
const payload = deepcopy(body);
if (badge == UNSUPPORTED_BADGE_KEY) {
delete payload.data.badge;
} else {
payload.data.badge = parseInt(badge);
}
const installations = badgeInstallationsMap[badge];
return this.sendToAdapter(payload, installations, pushStatus, config);
});
return Promise.all(promises);
}
}
export default PushWorker;
+30
View File
@@ -0,0 +1,30 @@
import Parse from 'parse/node';
export function isPushIncrementing(body) {
return body.data &&
body.data.badge &&
typeof body.data.badge == 'string' &&
body.data.badge.toLowerCase() == "increment"
}
/**
* Check whether the deviceType parameter in qury condition is valid or not.
* @param {Object} where A query condition
* @param {Array} validPushTypes An array of valid push types(string)
*/
export function validatePushType(where = {}, validPushTypes = []) {
var deviceTypeField = where.deviceType || {};
var deviceTypes = [];
if (typeof deviceTypeField === 'string') {
deviceTypes.push(deviceTypeField);
} else if (Array.isArray(deviceTypeField['$in'])) {
deviceTypes.concat(deviceTypeField['$in']);
}
for (var i = 0; i < deviceTypes.length; i++) {
var deviceType = deviceTypes[i];
if (validPushTypes.indexOf(deviceType) < 0) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
deviceType + ' is not supported push type.');
}
}
}
+7 -7
View File
@@ -92,15 +92,15 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
break;
case 'order':
var fields = restOptions.order.split(',');
var sortMap = {};
for (var field of fields) {
this.findOptions.sort = fields.reduce((sortMap, field) => {
field = field.trim();
if (field[0] == '-') {
sortMap[field.slice(1)] = -1;
} else {
sortMap[field] = 1;
}
}
this.findOptions.sort = sortMap;
return sortMap;
}, {});
break;
case 'include': {
const paths = restOptions.include.split(',');
@@ -110,7 +110,7 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
// reduce to create all paths
// ([a,b,c] -> {a: true, 'a.b': true, 'a.b.c': true})
return path.split('.').reduce((memo, path, index, parts) => {
memo[parts.slice(0, index+1).join('.')] = true;
memo[parts.slice(0, index + 1).join('.')] = true;
return memo;
}, memo);
}, {});
@@ -552,8 +552,8 @@ function includePath(config, auth, response, path, restOptions = {}) {
const keys = new Set(restOptions.keys.split(','));
const keySet = Array.from(keys).reduce((set, key) => {
const keyPath = key.split('.');
let i=0;
for (i; i<path.length; i++) {
let i = 0;
for (i; i < path.length; i++) {
if (path[i] != keyPath[i]) {
return set;
}
+1 -1
View File
@@ -366,7 +366,7 @@ RestWrite.prototype.transformUser = function() {
return promise.then(() => {
// Transform the password
if (!this.data.password) {
if (this.data.password === undefined) { // ignore only if undefined. should proceed if empty ('')
return Promise.resolve();
}
+2 -2
View File
@@ -29,9 +29,9 @@ export class FeaturesRouter extends PromiseRouter {
from: true,
},
push: {
immediatePush: req.config.pushController.pushIsAvailable,
immediatePush: req.config.hasPushSupport,
scheduledPush: false,
storedPushData: req.config.pushController.pushIsAvailable,
storedPushData: req.config.hasPushSupport,
pushAudiences: false,
},
schemas: {
+4 -2
View File
@@ -4,6 +4,7 @@ import * as Middlewares from '../middlewares';
import Parse from 'parse/node';
import Config from '../Config';
import mime from 'mime';
import logger from '../logger';
export class FilesRouter {
@@ -87,7 +88,8 @@ export class FilesRouter {
res.status(201);
res.set('Location', result.url);
res.json(result);
}).catch(() => {
}).catch((e) => {
logger.error(e.message, e);
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Could not store file.'));
});
}
@@ -137,7 +139,7 @@ function handleFileStream(stream, req, res, contentType) {
}
if (!partialend) {
if (((stream.length-1) - start) < (buffer_size)) {
if (((stream.length - 1) - start) < (buffer_size)) {
end = stream.length - 1;
}else{
end = start + (buffer_size);
+36 -3
View File
@@ -191,7 +191,7 @@ export class UsersRouter extends ClassesRouter {
return Promise.resolve(success);
}
handleResetRequest(req) {
_throwOnBadEmailConfig(req) {
try {
Config.validateEmailConfiguration({
emailAdapter: req.config.userController.adapter,
@@ -202,11 +202,16 @@ export class UsersRouter extends ClassesRouter {
} catch (e) {
if (typeof e === 'string') {
// Maybe we need a Bad Configuration error, but the SDKs won't understand it. For now, Internal Server Error.
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'An appName, publicServerURL, and emailAdapter are required for password reset functionality.');
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.');
} else {
throw e;
}
}
}
handleResetRequest(req) {
this._throwOnBadEmailConfig(req);
const { email } = req.body;
if (!email) {
throw new Parse.Error(Parse.Error.EMAIL_MISSING, "you must provide an email");
@@ -228,6 +233,33 @@ export class UsersRouter extends ClassesRouter {
});
}
handleVerificationEmailRequest(req) {
this._throwOnBadEmailConfig(req);
const { email } = req.body;
if (!email) {
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
}
if (typeof email !== 'string') {
throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string');
}
return req.config.database.find('_User', { email: email }).then((results) => {
if (!results.length || results.length < 1) {
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`);
}
const user = results[0];
if (user.emailVerified) {
throw new Parse.Error(Parse.Error.OTHER_CAUSE, `Email ${email} is already verified.`);
}
const userController = req.config.userController;
userController.sendVerificationEmail(user);
return { response: {} };
});
}
mountRoutes() {
this.route('GET', '/users', req => { return this.handleFind(req); });
@@ -238,7 +270,8 @@ export class UsersRouter extends ClassesRouter {
this.route('DELETE', '/users/:objectId', req => { return this.handleDelete(req); });
this.route('GET', '/login', req => { return this.handleLogIn(req); });
this.route('POST', '/logout', req => { return this.handleLogOut(req); });
this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); })
this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); });
this.route('POST', '/verificationEmailRequest', req => { return this.handleVerificationEmailRequest(req); });
}
}
+55 -25
View File
@@ -4,15 +4,25 @@ import { logger } from './logger';
const PUSH_STATUS_COLLECTION = '_PushStatus';
const JOB_STATUS_COLLECTION = '_JobStatus';
const incrementOp = function(object = {}, key, amount = 1) {
if (!object[key]) {
object[key] = {__op: 'Increment', amount: amount}
} else {
object[key].amount += amount;
}
return object[key];
}
export function flatten(array) {
return array.reduce((memo, element) => {
if (Array.isArray(element)) {
memo = memo.concat(flatten(element));
var flattened = [];
for(var i = 0; i < array.length; i++) {
if(Array.isArray(array[i])) {
flattened = flattened.concat(flatten(array[i]));
} else {
memo = memo.concat(element);
flattened.push(array[i]);
}
return memo;
}, []);
}
return flattened;
}
function statusHandler(className, database) {
@@ -93,10 +103,9 @@ export function jobStatusHandler(config) {
});
}
export function pushStatusHandler(config) {
export function pushStatusHandler(config, objectId = newObjectId()) {
let pushStatus;
const objectId = newObjectId();
const database = config.database;
const handler = statusHandler(PUSH_STATUS_COLLECTION, database);
const setInitial = function(body = {}, where, options = {source: 'rest'}) {
@@ -135,18 +144,17 @@ export function pushStatusHandler(config) {
});
}
const setRunning = function(installations) {
logger.verbose('sending push to %d installations', installations.length);
const setRunning = function(count) {
logger.verbose(`_PushStatus ${objectId}: sending push to %d installations`, count);
return handler.update({status:"pending", objectId: objectId},
{status: "running", updatedAt: new Date() });
{status: "running", updatedAt: new Date(), count });
}
const complete = function(results) {
const trackSent = function(results) {
const update = {
status: 'succeeded',
updatedAt: new Date(),
numSent: 0,
numFailed: 0,
numFailed: 0
};
if (Array.isArray(results)) {
results = flatten(results);
@@ -156,23 +164,44 @@ export function pushStatusHandler(config) {
return memo;
}
const deviceType = result.device.deviceType;
if (result.transmitted)
{
const key = result.transmitted ? `sentPerType.${deviceType}` : `failedPerType.${deviceType}`;
memo[key] = incrementOp(memo, key);
if (result.transmitted) {
memo.numSent++;
memo.sentPerType = memo.sentPerType || {};
memo.sentPerType[deviceType] = memo.sentPerType[deviceType] || 0;
memo.sentPerType[deviceType]++;
} else {
memo.numFailed++;
memo.failedPerType = memo.failedPerType || {};
memo.failedPerType[deviceType] = memo.failedPerType[deviceType] || 0;
memo.failedPerType[deviceType]++;
}
return memo;
}, update);
incrementOp(update, 'count', -results.length);
}
logger.verbose('sent push! %d success, %d failures', update.numSent, update.numFailed);
return handler.update({status:"running", objectId }, update);
logger.verbose(`_PushStatus ${objectId}: sent push! %d success, %d failures`, update.numSent, update.numFailed);
['numSent', 'numFailed'].forEach((key) => {
if (update[key] > 0) {
update[key] = {
__op: 'Increment',
amount: update[key]
};
} else {
delete update[key];
}
});
return handler.update({ objectId }, update).then((res) => {
if (res && res.count === 0) {
return this.complete();
}
})
}
const complete = function() {
return handler.update({ objectId }, {
status: 'succeeded',
count: {__op: 'Delete'},
updatedAt: new Date()
});
}
const fail = function(err) {
@@ -181,7 +210,7 @@ export function pushStatusHandler(config) {
status: 'failed',
updatedAt: new Date()
}
logger.info('warning: error while sending push', err);
logger.warn(`_PushStatus ${objectId}: error while sending push`, err);
return handler.update({ objectId }, update);
}
@@ -189,6 +218,7 @@ export function pushStatusHandler(config) {
objectId,
setInitial,
setRunning,
trackSent,
complete,
fail
})
+1 -1
View File
@@ -20,7 +20,7 @@ function parseURL(URL) {
function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) {
serverURL = serverURL ? parseURL(serverURL) : undefined;
publicServerURL = publicServerURL ? parseURL(publicServerURL): undefined;
publicServerURL = publicServerURL ? parseURL(publicServerURL) : undefined;
const apiPrefixLength = originalUrl.length - batchPath.length;
let apiPrefix = originalUrl.slice(0, apiPrefixLength);
+4 -1
View File
@@ -178,7 +178,7 @@ export default {
},
"userSensitiveFields": {
help: "Personally identifiable information fields in the user table the should be removed for non-authorized users.",
default: "email"
default: ["email"]
},
"sessionLength": {
env: "PARSE_SERVER_SESSION_LENGTH",
@@ -249,4 +249,7 @@ export default {
help: "Live query server configuration options (will start the liveQuery server)",
action: objectParser
},
"middleware": {
help: "middleware for express server, can be string or function"
}
};
+15 -3
View File
@@ -5,6 +5,7 @@ import definitions from './definitions/parse-server';
import cluster from 'cluster';
import os from 'os';
import runner from './utils/runner';
const path = require("path");
const help = function(){
console.log(' Get Started guide:');
@@ -30,9 +31,20 @@ const help = function(){
function startServer(options, callback) {
const app = express();
if (options.middleware) {
let middleware;
if (typeof options.middleware == 'function') {
middleware = options.middleware;
} if (typeof options.middleware == 'string') {
middleware = require(path.resolve(process.cwd(), options.middleware));
} else {
throw "middleware should be a string or a function";
}
app.use(middleware);
}
const api = new ParseServer(options);
const sockets = {};
app.use(options.mountPath, api);
const server = app.listen(options.port, options.host, callback);
@@ -122,14 +134,14 @@ runner({
});
} else {
startServer(options, () => {
console.log('['+process.pid+'] parse-server running on '+options.serverURL);
console.log('[' + process.pid + '] parse-server running on ' + options.serverURL);
});
}
} else {
startServer(options, () => {
logOptions();
console.log('');
console.log('['+process.pid+'] parse-server running on '+options.serverURL);
console.log('[' + process.pid + '] parse-server running on ' + options.serverURL);
});
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ export function randomHexString(size: number): string {
if (size % 2 !== 0) {
throw new Error('randomHexString size must be divisible by 2.')
}
return randomBytes(size/2).toString('hex');
return randomBytes(size / 2).toString('hex');
}
// Returns a new random alphanumeric string of the given size.
+2 -1
View File
@@ -7,6 +7,7 @@ import RedisCacheAdapter from './Adapters/Cache/RedisCacheAdapter'
import * as TestUtils from './TestUtils';
import { useExternal } from './deprecated';
import { getLogger } from './logger';
import { PushWorker } from './Push/PushWorker';
// Factory function
const _ParseServer = function(options) {
@@ -23,4 +24,4 @@ Object.defineProperty(module.exports, 'logger', {
});
export default ParseServer;
export { S3Adapter, GCSAdapter, FileSystemAdapter, InMemoryCacheAdapter, NullCacheAdapter, RedisCacheAdapter, TestUtils, _ParseServer as ParseServer };
export { S3Adapter, GCSAdapter, FileSystemAdapter, InMemoryCacheAdapter, NullCacheAdapter, RedisCacheAdapter, TestUtils, PushWorker, _ParseServer as ParseServer };
+19 -16
View File
@@ -1,9 +1,9 @@
import AppCache from './cache';
import log from './logger';
import Parse from 'parse/node';
import auth from './Auth';
import Config from './Config';
import ClientSDK from './ClientSDK';
import AppCache from './cache';
import log from './logger';
import Parse from 'parse/node';
import auth from './Auth';
import Config from './Config';
import ClientSDK from './ClientSDK';
// Checks that the request is authorized for this app and checks user
// auth too.
@@ -122,10 +122,10 @@ export function handleParseHeaders(req, res, next) {
// to preserve original behavior.
const keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"];
const oneKeyConfigured = keys.some(function(key) {
return req.config[key];
return req.config[key] !== undefined;
});
const oneKeyMatches = keys.some(function(key){
return req.config[key] && info[key] == req.config[key];
return req.config[key] !== undefined && info[key] === req.config[key];
});
if (oneKeyConfigured && !oneKeyMatches) {
@@ -233,10 +233,8 @@ export function allowMethodOverride(req, res, next) {
}
export function handleParseErrors(err, req, res, next) {
// TODO: Add logging as those errors won't make it to the PromiseRouter
if (err instanceof Parse.Error) {
var httpStatus;
let httpStatus;
// TODO: fill out this mapping
switch (err.code) {
case Parse.Error.INTERNAL_SERVER_ERROR:
@@ -250,17 +248,22 @@ export function handleParseErrors(err, req, res, next) {
}
res.status(httpStatus);
res.json({code: err.code, error: err.message});
res.json({ code: err.code, error: err.message });
log.error(err.message, err);
} else if (err.status && err.message) {
res.status(err.status);
res.json({error: err.message});
res.json({ error: err.message });
next(err);
} else {
log.error('Uncaught internal server error.', err, err.stack);
res.status(500);
res.json({code: Parse.Error.INTERNAL_SERVER_ERROR,
message: 'Internal server error.'});
res.json({
code: Parse.Error.INTERNAL_SERVER_ERROR,
message: 'Internal server error.'
});
next(err);
}
next(err);
}
export function enforceMasterKeyAccess(req, res, next) {
+16 -7
View File
@@ -14,6 +14,16 @@ var RestQuery = require('./RestQuery');
var RestWrite = require('./RestWrite');
var triggers = require('./triggers');
function checkTriggers(className, config, types) {
return types.some((triggerType) => {
return triggers.getTrigger(className, triggers.Types[triggerType], config.applicationId);
});
}
function checkLiveQuery(className, config) {
return config.liveQueryController && config.liveQueryController.hasLiveQuery(className)
}
// Returns a promise for an object with optional keys 'results' and 'count'.
function find(config, auth, className, restWhere, restOptions, clientSDK) {
enforceRoleSecurity('find', className, auth);
@@ -49,10 +59,9 @@ function del(config, auth, className, objectId) {
var inflatedObject;
return Promise.resolve().then(() => {
if (triggers.getTrigger(className, triggers.Types.beforeDelete, config.applicationId) ||
triggers.getTrigger(className, triggers.Types.afterDelete, config.applicationId) ||
(config.liveQueryController && config.liveQueryController.hasLiveQuery(className)) ||
className == '_Session') {
const hasTriggers = checkTriggers(className, config, ['beforeDelete', 'afterDelete']);
const hasLiveQuery = checkLiveQuery(className, config);
if (hasTriggers || hasLiveQuery || className == '_Session') {
return find(config, Auth.master(config), className, {objectId: objectId})
.then((response) => {
if (response && response.results && response.results.length) {
@@ -108,9 +117,9 @@ function update(config, auth, className, objectId, restObject, clientSDK) {
enforceRoleSecurity('update', className, auth);
return Promise.resolve().then(() => {
if (triggers.getTrigger(className, triggers.Types.beforeSave, config.applicationId) ||
triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId) ||
(config.liveQueryController && config.liveQueryController.hasLiveQuery(className))) {
const hasTriggers = checkTriggers(className, config, ['beforeSave', 'afterSave']);
const hasLiveQuery = checkLiveQuery(className, config);
if (hasTriggers || hasLiveQuery) {
return find(config, Auth.master(config), className, {objectId: objectId});
}
return Promise.resolve({});

Some files were not shown because too many files have changed in this diff Show More