Compare commits

..
126 Commits
Author SHA1 Message Date
Florent Vilmart 302a0dda73 Prepare 2.3.8 release (#3710) 2017-04-12 16:02:13 -07:00
Florent Vilmart e7a45cecb7 Adds graceful shutdown handler for mongodb (#3706) 2017-04-11 09:26:25 -07:00
Florent Vilmart 906fb27389 Delete { 2017-04-08 18:24:44 -04:00
ren dong f3f81b692b support pg-promise init options (#3613)
* pg-promise init options

* add database init options

* Create PostgresInitOptions.spec.js

* Update PostgresInitOptions.spec.js

* Update PostgresInitOptions.spec.js

* add PostgresInitOptions test

* Add files via upload

* linebreaks CRLF to LF

* modify postgresURI to test environment

* modify pg error code to 42P01

* fix reconfigureServer callback
2017-04-07 07:50:55 -04:00
zhoul-HS 52828683ba Add __type property to GeoPoint fields in PostgresStorageAdapter (#3695)
* Add __type property to GeoPoint fields in PostgresStorageAdapter

* Add test to check __type on GeoPoint queries
2017-04-07 07:44:12 -04:00
Florent Vilmart 0add7c0c50 Fixes broken links
Fixes #3692
2017-04-06 08:27:00 -04:00
Jean-Michel Fayard 1bc74f4aa3 doc: Host sample application on flynn (#3691) 2017-04-06 07:44:36 -04:00
Florent Vilmart c2e3092385 Updates links 2017-04-05 17:22:45 -04:00
Kevin Lacker 79806c5eba transfer code 2017-04-05 13:44:18 -07:00
George Perez 48115349d4 Add -d flag to link parse with mgo container (#3677)
Needed to run docker run --name my-parse-server --link my-mongo:mongo -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY to link the containers
2017-04-03 12:39:05 -07:00
Greenkeeper 87085b35b6 chore(package): update cross-env to version 4.0.0 (#3680)
https://greenkeeper.io/
2017-04-03 12:36:42 -07:00
Greenkeeper 65a8a1fc56 chore(package): update ws to version 2.2.3 (#3686)
https://greenkeeper.io/
2017-04-03 12:35:34 -07:00
Greenkeeper f9ecebde3b chore(package): update babel-core to version 6.24.0 (#3628)
https://greenkeeper.io/
2017-03-28 15:28:41 -07:00
Benjamin Wilson Friedman e01b417d3f Added exception for Twitter and OAuth missing options (#3676)
* Added exception for Twitter and OAuth missing configuration information

* Updated error codes to INTERNAL_SERVER_ERROR, code 1
2017-03-28 18:16:47 -04:00
Zubeen Tolani a54f4d43fa Add install instructions for root user (#3419)
* Add install instructions for root user

* Reword the comment

* Update README.md

Commit adds a reference to solve the EACCES error while installation.

* remove trailing whitespace
2017-03-27 14:30:55 -07:00
Greenkeeper 7a43faa49c chore(package): update ws to version 2.2.2 (#3656)
https://greenkeeper.io/
2017-03-25 10:31:03 -07:00
Greenkeeper abf6828a7f chore(package): update babel-plugin-syntax-flow to version 6.18.0 (#3664)
https://greenkeeper.io/
2017-03-25 10:30:15 -07:00
Greenkeeper 2436a85de6 chore(package): update uws to version 0.14.0 (#3670)
https://greenkeeper.io/
2017-03-25 10:29:35 -07:00
Florent Vilmart 1f9a4edacd Ignore duplicate_object error on create _Schema if not exists (#3668) 2017-03-23 22:29:43 -04:00
Greenkeeper 6a2b972566 chore(package): update tv4 to version 1.3.0 (#3653)
https://greenkeeper.io/
2017-03-20 13:10:31 -04:00
Florent Vilmart f3765e0b8e use nyc for coverage (#3652)
* use nyc for coverage

* removes unnecessary logss

* Disable RedisCacheAdapter tests when not in specific environment
2017-03-20 09:58:36 -07:00
Greenkeeper eeb3a7028d chore(package): update mongodb to version 2.2.25 (#3649)
https://greenkeeper.io/
2017-03-17 18:57:36 -04:00
Wissam Abirached 2533a8cdb3 Do not create user if username or password is empty (#3650) 2017-03-17 18:57:21 -04:00
Greenkeeper ea94ae73f6 chore(package): update request-promise to version 4.2.0 (#3648)
https://greenkeeper.io/
2017-03-16 18:28:15 -04:00
Greenkeeper 1ec70b299b chore(package): update redis to version 2.7.1 (#3644)
https://greenkeeper.io/
2017-03-15 09:00:37 -04:00
Greenkeeper 45efe2f29c chore(package): update cross-env to version 3.2.4 (#3642)
https://greenkeeper.io/
2017-03-15 09:00:19 -04:00
Stefan Trauth 5e29336962 update push adapter to 1.3.0 (#3641) 2017-03-15 08:59:58 -04:00
jeacott1 82e0a59139 missing return in test. (#3645) 2017-03-15 08:59:19 -04:00
Greenkeeper 51b519355a chore(package): update ws to version 2.2.1 (#3631)
https://greenkeeper.io/
2017-03-13 15:48:20 -04:00
Greenkeeper 08e8a428b6 chore(package): update babel-cli to version 6.24.0 (#3627)
https://greenkeeper.io/
2017-03-13 15:48:09 -04:00
Greenkeeper 63bd333715 chore(package): update babel-preset-es2015 to version 6.24.0 (#3630)
https://greenkeeper.io/
2017-03-13 14:17:36 -04:00
Greenkeeper 11071dd015 chore(package): update babel-register to version 6.24.0 (#3629)
https://greenkeeper.io/
2017-03-13 14:17:22 -04:00
Greenkeeper 9b879dd371 chore(package): update winston-daily-rotate-file to version 1.4.6 (#3625)
https://greenkeeper.io/
2017-03-13 14:17:10 -04:00
Florent Vilmart eb1b84b556 CLI: Do not exit(0) so each module can close its connections (#3634) 2017-03-13 12:13:07 -04:00
Greenkeeper 80ec61f7ce chore(package): update redis to version 2.7.0 (#3620)
https://greenkeeper.io/
2017-03-12 21:28:01 -07:00
Greenkeeper 9f1077c9d3 chore(package): update pg-promise to version 5.6.4 (#3622)
https://greenkeeper.io/
2017-03-12 17:17:18 -07:00
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
110 changed files with 2039 additions and 664 deletions
+3 -2
View File
@@ -1,2 +1,3 @@
lib/*
coverage/*
lib
coverage
+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.
+1
View File
@@ -13,6 +13,7 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
-2
View File
@@ -1,2 +0,0 @@
instrumentation:
excludes: ["**/spec/**"]
+11
View File
@@ -0,0 +1,11 @@
{
"reporter": [
"lcov",
"text-summary"
],
"exclude": [
"**/spec/**",
"lib/"
]
}
+3 -5
View File
@@ -1,7 +1,7 @@
language: node_js
dist: trusty
node_js:
- '4.5'
- '4.6'
- '6.1'
services:
- postgresql
@@ -12,13 +12,13 @@ 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
env:
global:
- COVERAGE_OPTION='./node_modules/.bin/istanbul cover'
- COVERAGE_OPTION='./node_modules/.bin/nyc'
matrix:
- MONGODB_VERSION=3.0.8
- MONGODB_VERSION=3.2.6
@@ -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
+101 -3
View File
@@ -1,5 +1,103 @@
## Parse Server Changelog
### 2.3.8
[Full Changelog](https://github.com/ParsePlatform/parse-server/compare/2.3.7...2.3.8)
#### New Features
* Support for PG-Promise options, thanks to [ren dong](https://github.com/rendongsc)
#### Improvements
* Improves support for graceful shutdown, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Improves configuration validation for Twitter Authentication, thanks to [Benjamin Wilson Friedman](https://github.com/montymxb)
#### Bug Fixes
* Fixes issue affecting GeoPoint __type with Postgres, thanks to [zhoul-HS](https://github.com/zhoul-HS)
* Prevent user creation if username or password is empty, thanks to [Wissam Abirached](https://github.com/wabirached)
#### Dependency Updates:
* [cross-env@4.0.0 ](https://www.npmjs.com/package/cross-env)
* [ws@2.2.3](https://www.npmjs.com/package/ws)
* [babel-core@6.24.0](https://www.npmjs.com/package/babel-core)
* [uws@0.14.0](https://www.npmjs.com/package/uws)
* [babel-preset-es2015@6.24.0](https://www.npmjs.com/package/babel-preset-es2015)
* [babel-plugin-syntax-flow@6.18.0](https://www.npmjs.com/package/babel-plugin-syntax-flow)
* [babel-cli@6.24.0](https://www.npmjs.com/package/babel-cli)
* [babel-register@6.24.0](https://www.npmjs.com/package/babel-register)
* [winston-daily-rotate-file@1.4.6](https://www.npmjs.com/package/winston-daily-rotate-file)
* [mongodb@2.2.25](https://www.npmjs.com/package/mongodb)
* [redis@2.7.0](https://www.npmjs.com/package/redis)
* [pg-promise@5.6.4](https://www.npmjs.com/package/pg-promise)
* [parse-server-push-adapter@1.3.0](https://www.npmjs.com/package/parse-server-push-adapter)
### 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)
@@ -32,7 +130,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
@@ -85,7 +183,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
@@ -132,7 +230,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
View File
@@ -28,3 +28,7 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-----
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
+4
View File
@@ -31,3 +31,7 @@ necessarily infringed by the Software standing alone.
A "Patent Assertion" is any lawsuit or other action alleging direct, indirect,
or contributory infringement or inducement to infringe any patent, including a
cross-claim or counterclaim.
-----
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
+27 -23
View File
@@ -1,7 +1,7 @@
![Parse Server logo](.github/parse-server-logo.png?raw=true)
[![Build Status](https://img.shields.io/travis/ParsePlatform/parse-server/master.svg?style=flat)](https://travis-ci.org/ParsePlatform/parse-server)
[![Coverage Status](https://img.shields.io/codecov/c/github/ParsePlatform/parse-server/master.svg)](https://codecov.io/github/ParsePlatform/parse-server?branch=master)
[![Build Status](https://img.shields.io/travis/parse-community/parse-server/master.svg?style=flat)](https://travis-ci.org/parse-community/parse-server)
[![Coverage Status](https://img.shields.io/codecov/c/github/parse-community/parse-server/master.svg)](https://codecov.io/github/parse-community/parse-server?branch=master)
[![npm version](https://img.shields.io/npm/v/parse-server.svg?style=flat)](https://www.npmjs.com/package/parse-server)
[![Join Chat](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/ParsePlatform/Chat)
@@ -24,12 +24,14 @@ $ npm install -g parse-server mongodb-runner
$ mongodb-runner start
$ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://localhost/test
```
***Note:*** *If installation with* `-g` *fails due to permission problems* (`npm ERR! code 'EACCES'`), *please refer to [this link](https://docs.npmjs.com/getting-started/fixing-npm-permissions).*
### Inside a Docker container
```
$ docker build --tag parse-server .
$ docker run --name my-mongo -d mongo
$ docker run --name my-parse-server --link my-mongo:mongo parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test
$ docker run --name my-parse-server --link my-mongo:mongo -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test
```
You can use any arbitrary string as your application id and master key. These will be used by your clients to authenticate with the Parse Server.
@@ -40,7 +42,7 @@ That's it! You are now running a standalone version of Parse Server on your mach
### Saving your first object
Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](https://parse.com/docs/rest/guide), but you can easily do the same using any of the [Parse SDKs](https://parseplatform.github.io/#sdks). Run the following:
Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](https://docs.parseplatform.org/rest/guide), but you can easily do the same using any of the [Parse SDKs](https://parseplatform.github.io/#sdks). Run the following:
```bash
curl -X POST \
@@ -102,19 +104,19 @@ $ curl -X GET \
```
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](https://parse.com/docs).
To learn more about using saving and querying objects on Parse Server, check out the [Parse documentation](https://docs.parseplatform.org).
### Connect your app to Parse Server
Parse provides SDKs for all the major platforms. Refer to the Parse Server guide to [learn how to connect your app to Parse Server](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#using-parse-sdks-with-parse-server).
Parse provides SDKs for all the major platforms. Refer to the Parse Server guide to [learn how to connect your app to Parse Server](https://github.com/parse-community/parse-server/wiki/Parse-Server-Guide#using-parse-sdks-with-parse-server).
## Running Parse Server elsewhere
Once you have a better understanding of how the project works, please refer to the [Parse Server wiki](https://github.com/ParsePlatform/parse-server/wiki) for in-depth guides to deploy Parse Server to major infrastructure providers. Read on to learn more about additional ways of running Parse Server.
Once you have a better understanding of how the project works, please refer to the [Parse Server wiki](https://github.com/parse-community/parse-server/wiki) for in-depth guides to deploy Parse Server to major infrastructure providers. Read on to learn more about additional ways of running Parse Server.
### Parse Server Sample Application
We have provided a basic [Node.js application](https://github.com/ParsePlatform/parse-server-example) that uses the Parse Server module on Express and can be easily deployed to various infrastructure providers:
We have provided a basic [Node.js application](https://github.com/parse-community/parse-server-example) that uses the Parse Server module on Express and can be easily deployed to various infrastructure providers:
* [Heroku and mLab](https://devcenter.heroku.com/articles/deploying-a-parse-server-to-heroku)
* [AWS and Elastic Beanstalk](http://mobile.awsblog.com/post/TxCD57GZLM2JR/How-to-set-up-Parse-Server-on-AWS-using-AWS-Elastic-Beanstalk)
@@ -125,6 +127,7 @@ We have provided a basic [Node.js application](https://github.com/ParsePlatform/
* [Pivotal Web Services](https://github.com/cf-platform-eng/pws-parse-server)
* [Back4app](http://blog.back4app.com/2016/03/01/quick-wizard-migration/)
* [Gomix](https://gomix.com/#!/project/parse-server)
* [Flynn](https://flynn.io/blog/parse-apps-on-flynn)
### Parse Server + Express
@@ -156,7 +159,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
@@ -170,11 +173,7 @@ Logs are also be viewable in Parse Dashboard.
# Documentation
The full documentation for Parse Server is available in the [wiki](https://github.com/ParsePlatform/parse-server/wiki). The [Parse Server guide](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide) is a good place to get started. If you're interested in developing for Parse Server, the [Development guide](https://github.com/ParsePlatform/parse-server/wiki/Development-Guide) will help you get set up.
## Migrating an Existing Parse App
The hosted version of Parse will be fully retired on January 28th, 2017. If you are planning to migrate an app, you need to begin work as soon as possible. There are a few areas where Parse Server does not provide compatibility with the hosted version of Parse. Learn more in the [Migration guide](https://parse.com/migration).
The full documentation for Parse Server is available in the [wiki](https://github.com/parse-community/parse-server/wiki). The [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/) is a good place to get started. If you're interested in developing for Parse Server, the [Development guide](http://docs.parseplatform.org/parse-server/guide/#development-guide) will help you get set up.
## Configuration
@@ -190,7 +189,7 @@ For the full list of available options, run `parse-server --help`.
* `port` - The default port is 1337, specify this parameter to use a different port.
* `serverURL` - URL to your Parse Server (don't forget to specify http:// or https://). This URL will be used when making requests to Parse Server from Cloud Code.
* `cloud` - The absolute path to your cloud code `main.js` file.
* `push` - Configuration options for APNS and GCM push. See the [Push Notifications wiki entry](https://github.com/ParsePlatform/parse-server/wiki/Push).
* `push` - Configuration options for APNS and GCM push. See the [Push Notifications quick start](http://docs.parseplatform.org/parse-server/guide/#push-notifications_push-notifications-quick-start).
#### Client key options
@@ -206,17 +205,18 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
* `fileKey` - For migrated apps, this is necessary to provide access to files already hosted on Parse.
* `allowClientClassCreation` - Set to false to disable client class creation. Defaults to true.
* `enableAnonymousUsers` - Set to false to disable anonymous users. Defaults to true.
* `auth` - Used to configure support for [3rd party authentication](https://github.com/ParsePlatform/parse-server/wiki/OAuth--and-Custom-Authentication).
* `auth` - Used to configure support for [3rd party authentication](http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication).
* `facebookAppIds` - An array of valid Facebook application IDs that users may authenticate with.
* `mountPath` - Mount path for the server. Defaults to `/parse`.
* `filesAdapter` - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js)).
* `filesAdapter` - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/parse-community/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js)).
* `maxUploadSize` - Max file size for uploads. Defaults to 20 MB.
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)).
* `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/parse-community/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)).
* `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year).
* `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
@@ -336,19 +336,19 @@ Parse Server allows developers to choose from several options when hosting files
* `S3Adapter`, which is backed by [Amazon S3](https://aws.amazon.com/s3/); or
* `GCSAdapter`, which is backed by [Google Cloud Storage](https://cloud.google.com/storage/)
`GridStoreAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server wiki](https://github.com/ParsePlatform/parse-server/wiki/Configuring-File-Adapters).
`GridStoreAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters).
# Support
For implementation related questions or technical support, please refer to the [Stack Overflow](http://stackoverflow.com/questions/tagged/parse.com) and [Server Fault](https://serverfault.com/tags/parse) communities.
If you believe you've found an issue with Parse Server, make sure these boxes are checked before [reporting an issue](https://github.com/ParsePlatform/parse-server/issues):
If you believe you've found an issue with Parse Server, make sure these boxes are checked before [reporting an issue](https://github.com/parse-community/parse-server/issues):
- [ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites).
- [ ] You've met the [prerequisites](http://docs.parseplatform.org/parse-server/guide/#prerequisites).
- [ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server.
- [ ] You're running the [latest version](https://github.com/parse-community/parse-server/releases) of Parse Server.
- [ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
- [ ] You've searched through [existing issues](https://github.com/parse-community/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before.
# Want to ride the bleeding edge?
@@ -365,3 +365,7 @@ npm install parseplatform/parse-server.git#latest
# Contributing
We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md).
-----
As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code.
+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}"
+33 -32
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "2.3.2",
"version": "2.3.8",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -18,72 +18,73 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"bcryptjs": "2.4.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.4",
"lru-cache": "4.0.2",
"mime": "1.3.4",
"mongodb": "2.2.10",
"multer": "1.2.1",
"mongodb": "2.2.25",
"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.3.0",
"parse-server-s3-adapter": "1.0.6",
"parse-server-simple-mailgun-adapter": "1.0.0",
"pg-promise": "5.5.0",
"redis": "2.6.3",
"request": "2.79.0",
"pg-promise": "5.6.4",
"redis": "2.7.1",
"request": "2.81.0",
"semver": "5.2.0",
"tv4": "1.2.7",
"winston": "2.3.0",
"winston-daily-rotate-file": "1.4.2",
"ws": "1.1.1"
"tv4": "1.3.0",
"winston": "2.3.1",
"winston-daily-rotate-file": "1.4.6",
"ws": "2.2.3"
},
"devDependencies": {
"babel-cli": "6.18.0",
"babel-core": "6.21.0",
"babel-cli": "6.24.0",
"babel-core": "6.24.0",
"babel-eslint": "^7.1.1",
"babel-plugin-syntax-flow": "6.13.0",
"babel-plugin-transform-flow-strip-types": "6.21.0",
"babel-preset-es2015": "6.14.0",
"babel-preset-stage-0": "6.5.0",
"babel-register": "6.18.0",
"babel-plugin-syntax-flow": "6.18.0",
"babel-plugin-transform-flow-strip-types": "6.22.0",
"babel-preset-es2015": "6.24.0",
"babel-preset-stage-0": "6.22.0",
"babel-register": "6.24.0",
"bcrypt-nodejs": "0.0.3",
"cross-env": "3.1.4",
"cross-env": "4.0.0",
"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"
"nyc": "^10.1.2",
"request-promise": "4.2.0"
},
"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",
"coverage": "cross-env COVERAGE_OPTION='./node_modules/.bin/istanbul cover' npm test",
"coverage:win": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 node ./node_modules/istanbul/lib/cli.js cover ./node_modules/jasmine/bin/jasmine.js",
"coverage": "cross-env COVERAGE_OPTION='./node_modules/.bin/nyc' npm test",
"coverage:win": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 NODE_ENV=test TESTING=1 node ./node_modules/.bin/nyc ./node_modules/jasmine/bin/jasmine.js",
"start": "node ./bin/parse-server",
"prepublish": "npm run build"
},
"engines": {
"node": ">=4.5"
"node": ">=4.6"
},
"bin": {
"parse-server": "./bin/parse-server"
},
"optionalDependencies": {
"bcrypt": "1.0.2"
"bcrypt": "1.0.2",
"uws": "^0.14.0"
}
}
+1
View File
@@ -16,6 +16,7 @@
"notEqual": true,
"it_exclude_dbs": true,
"describe_only_db": true,
"describe_only": true,
"on_db": true,
"defaultConfiguration": true,
"expectSuccess": true,
+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', () => {
+11 -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"')
@@ -133,4 +133,14 @@ describe('OAuth', function() {
done();
})
});
it("Should fail with missing options", (done) => {
var options = undefined;
try {
new OAuth(options);
} catch (error) {
jequal(error.message, 'No options passed to OAuth');
done();
}
});
});
+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 -4
View File
@@ -755,14 +755,12 @@ describe('Parse.ACL', () => {
user2.set("password", "burger");
return user2.signUp();
}).then(() => {
console.log(user2.getSessionToken());
return object.destroy({sessionToken: user2.getSessionToken() });
}).then((res) => {
console.log(res);
}).then(() => {
fail('should not be able to destroy the object');
done();
}, (err) => {
console.error(err);
expect(err).not.toBeUndefined();
done();
});
});
+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) => {
+23
View File
@@ -26,6 +26,29 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('has the correct __type field in the json response', done => {
var point = new Parse.GeoPoint(44.0, -11.0);
var obj = new TestObject();
obj.set('location', point);
obj.set('name', 'Zhoul')
obj.save(null, {
success: (obj) => {
console.log(obj);
Parse.Cloud.httpRequest({
url: 'http://localhost:8378/1/classes/TestObject/' + obj.id,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test'
}
}).then(response => {
equal(response.data.location.__type, 'GeoPoint');
done();
})
}
})
});
it('geo point exception two fields', (done) => {
var point = new Parse.GeoPoint(20, 20);
var obj = new TestObject();
+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());
+24
View File
@@ -1,5 +1,7 @@
const ParseServerRESTController = require('../src/ParseServerRESTController').ParseServerRESTController;
const ParseServer = require('../src/ParseServer').default;
const Parse = require('parse/node').Parse;
let RESTController;
describe('ParseServerRESTController', () => {
@@ -103,6 +105,28 @@ describe('ParseServerRESTController', () => {
});
});
it('ensures no user is created when passing an empty username', (done) => {
RESTController.request("POST", "/classes/_User", {username: "", password: "world"}).then(() => {
jfail(new Error('Success callback should not be called when passing an empty username.'));
done();
}, (err) => {
expect(err.code).toBe(Parse.Error.USERNAME_MISSING);
expect(err.message).toBe('bad or missing username');
done();
});
});
it('ensures no user is created when passing an empty password', (done) => {
RESTController.request("POST", "/classes/_User", {username: "hello", password: ""}).then(() => {
jfail(new Error('Success callback should not be called when passing an empty password.'));
done();
}, (err) => {
expect(err.code).toBe(Parse.Error.PASSWORD_MISSING);
expect(err.message).toBe('password is required');
done();
});
});
it('ensures no session token is created on creating users', (done) => {
RESTController.request("POST", "/classes/_User", {username: "hello", password: "world"}).then((user) => {
expect(user.sessionToken).toBeUndefined();
+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)
});
+22
View File
@@ -219,6 +219,28 @@ describe("Password Policy: ", () => {
})
});
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({
+60
View File
@@ -0,0 +1,60 @@
const Parse = require('parse/node').Parse;
const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter');
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
//public schema
const databaseOptions1 = {
initOptions: {
connect: function (client, dc, isFresh) {
if (isFresh) {
client.query('SET search_path = public');
}
}
}
};
//not exists schema
const databaseOptions2 = {
initOptions: {
connect: function (client, dc, isFresh) {
if (isFresh) {
client.query('SET search_path = not_exists_schema');
}
}
}
};
const GameScore = Parse.Object.extend({
className: "GameScore"
});
describe('Postgres database init options', () => {
it('create server with public schema databaseOptions,shoud be ok', (done) => {
reconfigureServer({
databaseAdapter: new PostgresStorageAdapter({
uri: postgresURI, collectionPrefix: 'test_',
databaseOptions: databaseOptions1
})
}).then(done, fail);
});
it("save new GameScore in public schema", function (done) {
var score = new GameScore({ "score": 1337, "playerName": "Sean Plott", "cheatMode": false });
score.save().then(done, fail);
});
it('create server with not exists schema databaseOptions,shoud be fail', (done) => {
reconfigureServer({
databaseAdapter: new PostgresStorageAdapter({
uri: postgresURI, collectionPrefix: 'test_',
databaseOptions: databaseOptions2
})
}).then(() => {
done();
})
.catch(error => {
expect(error.code).toEqual('42P01');
done();
});
});
});
+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(() => {
return 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);
})
});
});
+56
View File
@@ -0,0 +1,56 @@
var RedisCacheAdapter = require('../src/Adapters/Cache/RedisCacheAdapter').default;
/*
To run this test part of the complete suite
set PARSE_SERVER_TEST_CACHE='redis'
and make sure a redis server is available on the default port
*/
describe_only(() => {
return process.env.PARSE_SERVER_TEST_CACHE === 'redis';
})('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');
});
});
+15 -1
View File
@@ -9,7 +9,7 @@ describe('Twitter Auth', () => {
consumer_key: 'hello'
}, {
consumer_key: 'world'
}]).consumer_key).toEqual('hello')
}]).consumer_key).toEqual('hello');
// Multiple options, consumer_key not found
expect(function(){
@@ -47,4 +47,18 @@ describe('Twitter Auth', () => {
consumer_key: 'hello'
}).consumer_key).toEqual('hello');
});
it("Should fail with missing options", (done) => {
try {
twitter.validateAuthData({
consumer_key: 'key',
consumer_secret: 'secret',
auth_token: 'token',
auth_token_secret: 'secret'
}, undefined);
} catch (error) {
jequal(error.message, 'Twitter auth configuration missing');
done();
}
});
});
+3 -3
View File
@@ -383,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();
});
})
@@ -414,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();
});
})
@@ -442,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();
});
})
+10 -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);
@@ -419,6 +419,14 @@ global.describe_only_db = db => {
}
}
global.describe_only = (validator) =>{
if (validator()) {
return describe;
} else {
return xdescribe;
}
};
var libraryCache = {};
jasmine.mockLibrary = function(library, name, mock) {
+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)
+10 -6
View File
@@ -1,7 +1,11 @@
var https = require('https'),
crypto = require('crypto');
var Parse = require('parse/node').Parse;
var OAuth = function(options) {
if(!options) {
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'No options passed to OAuth');
}
this.consumer_key = options.consumer_key;
this.consumer_secret = options.consumer_secret;
this.auth_token = options.auth_token;
@@ -36,7 +40,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 +122,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 +166,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 +176,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 +197,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 +214,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 = '';
+4 -1
View File
@@ -5,6 +5,9 @@ var logger = require('../../logger').default;
// Returns a promise that fulfills iff this user id is valid.
function validateAuthData(authData, options) {
if(!options) {
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Twitter auth configuration missing');
}
options = handleMultipleConfigurations(authData, options);
var client = new OAuth(options);
client.host = "api.twitter.com";
@@ -12,7 +15,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();
});
});
@@ -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;
@@ -97,6 +97,17 @@ export class MongoStorageAdapter {
// MaxTimeMS is not a global MongoDB client option, it is applied per operation.
this._maxTimeMS = mongoOptions.maxTimeMS;
process.on('SIGTERM', this.handleShutdown(this));
process.on('SIGINT', this.handleShutdown(this));
}
handleShutdown(storageAdapter) {
return () => {
if (!storageAdapter.database) {
return;
}
storageAdapter.database.close(false);
}
}
connect() {
+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,4 +1,4 @@
const pgp = require('pg-promise')();
const parser = require('./PostgresConfigParser');
export function createClient(uri, databaseOptions) {
@@ -13,11 +13,13 @@ export function createClient(uri, databaseOptions) {
dbOptions[key] = databaseOptions[key];
}
const initOptions = dbOptions.initOptions || {};
const pgp = require('pg-promise')(initOptions);
const client = pgp(dbOptions);
if (dbOptions.pgOptions) {
for (const key in dbOptions.pgOptions) {
client.pg.defaults[key] = dbOptions.pgOptions[key];
pgp.pg.defaults[key] = dbOptions.pgOptions[key];
}
}
@@ -1,17 +1,19 @@
import { createClient } from './PostgresClient';
import Parse from 'parse/node';
import _ from 'lodash';
import sql from './sql';
const PostgresRelationDoesNotExistError = '42P01';
const PostgresDuplicateRelationError = '42P07';
const PostgresDuplicateColumnError = '42701';
const PostgresDuplicateObjectError = '42710';
const PostgresUniqueIndexViolationError = '23505';
const PostgresTransactionAbortedError = '25P02';
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 +198,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 +279,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 +307,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 +325,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 +339,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 +359,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;
}
@@ -415,7 +417,9 @@ export class PostgresStorageAdapter {
conn = conn || this._client;
return conn.none('CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )')
.catch(error => {
if (error.code === PostgresDuplicateRelationError || error.code === PostgresUniqueIndexViolationError) {
if (error.code === PostgresDuplicateRelationError
|| error.code === PostgresUniqueIndexViolationError
|| error.code === PostgresDuplicateObjectError) {
// Table already exists, must have been created by a different request. Ignore error.
} else {
throw error;
@@ -490,11 +494,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 +622,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 +703,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 +766,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 +852,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 +930,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 +1019,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 +1046,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);
}
@@ -1052,6 +1074,7 @@ export class PostgresStorageAdapter {
}
if (object[fieldName] && schema.fields[fieldName].type === 'GeoPoint') {
object[fieldName] = {
__type: "GeoPoint",
latitude: object[fieldName].y,
longitude: object[fieldName].x
}
@@ -1141,41 +1164,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 +1269,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$;
+6 -3
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;
@@ -207,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() {
@@ -223,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() {
+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;
+13 -2
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,7 +145,7 @@ 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 available');
+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']);
+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');
+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;
}
+3 -3
View File
@@ -204,11 +204,11 @@ RestWrite.prototype.validateAuthData = function() {
}
if (!this.query && !this.data.authData) {
if (typeof this.data.username !== 'string') {
if (typeof this.data.username !== 'string' || _.isEmpty(this.data.username)) {
throw new Parse.Error(Parse.Error.USERNAME_MISSING,
'bad or missing username');
}
if (typeof this.data.password !== 'string') {
if (typeof this.data.password !== 'string' || _.isEmpty(this.data.password)) {
throw new Parse.Error(Parse.Error.PASSWORD_MISSING,
'password is required');
}
@@ -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);

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