Compare commits

..
317 Commits
Author SHA1 Message Date
Florent Vilmart c32ed52f04 Changelog 2.2.14 (#2148) 2016-06-25 15:22:06 -04:00
Florent Vilmart 3e413683f7 Makes HTTPResponse serializable (#2143)
* Use the callback body instead of response.body that may not be set

* Adds test to handle undefined responses

* Adds toJSON method to properly serialize HTTPResponse

* Use ES5 defineProperty to make keys enumerable

* removes body key from serialization

* Indent nits
2016-06-25 10:56:02 -07:00
Greenkeeper 35b4c063fb chore(package): update pg-promise to version 4.8.0 (#2129)
https://greenkeeper.io/
2016-06-23 09:25:17 -07:00
Greenkeeper 29ffbabd3d chore(package): update babel-core to version 6.10.4 (#2119)
https://greenkeeper.io/
2016-06-21 18:53:56 -07:00
Greenkeeper 57cecd800c chore(package): update body-parser to version 1.15.2 (#2107)
https://greenkeeper.io/
2016-06-21 12:54:16 -07:00
Greenkeeper c36ca0c44e chore(package): update pg-promise to version 4.7.5 (#2111)
https://greenkeeper.io/
2016-06-21 10:59:28 -07:00
Nikita Lutsenko a96b0afac5 Merge pull request #2118 from ParsePlatform/greenkeeper-babel-core-6.9.1
Update babel-core to version 6.9.1 🚀
2016-06-21 10:36:42 -07:00
greenkeeperio-bot 1feedef21d chore(package): update babel-core to version 6.9.1
https://greenkeeper.io/
2016-06-21 09:54:14 -07:00
Drew 006540cda5 Implement pointer types in postgres. (#2086)
* Support pointers in postgres

* implement count

* Fix bug in createClass
2016-06-17 11:09:42 -07:00
Drew ab06055369 Postgres exclude failing tests (#2081)
* reload the right data

More passing postgres tests

Handle schema updates, and $in for non array columns

remove authdata from user and implement ensureUniqueness

Make some tests work, detect existing classes

Throw proper error for unique index violation

fix findOneAndUpdate

Support more types

support more type

Support boolean, fix _rperm/_wperm, add TODO

Support string types and also simplify tests

Move operator flattening into Parse Server and out of mongo adapters

Move authdata transform for create into Parse Server

Move authdata transforms completely in to Parse Server

Fix test setup

inline addSchema

Inject default schema to response from DB adapter

* Mark tests that don't work in Postgres

* Exclude one more test

* Exclude some more failing tests

* Exclude more tests
2016-06-17 12:59:16 -04:00
Vitaly Tomilov 7da4debbe0 Update PostgresStorageAdapter.js (#2087)
1. Deleting tables in a transaction, as opposed to just a task.
2. Added transaction where it was supposed to be. However, it is not enough, the logic is still broken there...

First, do not use `.catch` before `.then`. It is dangerous without good understanding how it actually works.

Check this out:
```js
        .catch(error => {
          if (error.code === PostgresRelationDoesNotExistError) {
            return this.createClass(className, {fields: {[fieldName]: type}}) // this gets into the following `.then`
          } else if (error.code === PostgresDuplicateColumnError) {
            // Column already exists, created by other request. Carry on to
            // See if it's the right type.
           
// this will get the following `.then` with `undefined` as the value

          } else {
            throw error;
          }
        })
```
2016-06-17 00:04:58 -07:00
Drew c37a4ea1a1 Incorporate vitaly-t changes (#2085) 2016-06-16 19:34:00 -07:00
Drew 9d078003a6 Pin pg-postgres version (#2084) 2016-06-16 18:44:31 -07:00
Vitaly Tomilov a685ed8814 Update package.json (#2083)
upgrading pg-promise version.
2016-06-16 18:43:08 -07:00
Drew 0ff35e18f0 Add more postgres support (#2080)
* reload the right data

More passing postgres tests

Handle schema updates, and $in for non array columns

remove authdata from user and implement ensureUniqueness

Make some tests work, detect existing classes

Throw proper error for unique index violation

* fix findOneAndUpdate
2016-06-16 15:39:05 -07:00
Drew 1a75101146 Fix flaky test (#2078)
* Debug flaky test

* create new object instead of modifying and assigning existing object

* use getOneSchema instead of this.data when updating fields

* Remove debug stuff

* Don't try to validate existing fields

* run just one test

* Verbose test all

* Use schema instead of this.data

* Switch to all tests
2016-06-16 15:34:33 -07:00
Greenkeeper 627b4164bb chore(package): update express to version 4.14.0 (#2079)
https://greenkeeper.io/
2016-06-16 10:39:13 -07:00
Fosco Marotto d1f77a9a98 Merge pull request #2053 from drew-gross/postgres-3
Continue with postgres support
2016-06-16 10:19:07 -07:00
Greenkeeper 35ed09a7b4 chore(package): update redis to version 2.6.2 (#2076)
https://greenkeeper.io/
2016-06-16 08:53:30 -07:00
Blayne Chard 2eb861db66 Fixing typo in _loadRoles which prevents caching of roles. (#2063) 2016-06-15 22:22:23 -07:00
Drew ae0ba7b5a9 Pin packages (#2073)
* Pin all package versions

Closes #2040

* Pin packages to a working version

* Drop extra stuff
2016-06-15 23:28:07 -04:00
Drew 726c866a4d Remove console.log 2016-06-15 17:25:17 -07:00
Drew f7f5bab75d Remove attempt to fix tests 2016-06-15 17:24:00 -07:00
Drew 9ed3fcc536 Remove duplicate test 2016-06-15 17:23:29 -07:00
Drew Gross 8130658583 Fix purge 2016-06-15 15:01:21 -07:00
Drew Gross f23d80ec9a Ignore coverage requirements for postgres adapter until I can figure out how to run coverage on it. 2016-06-15 13:11:51 -07:00
Drew Gross 510a890a03 centralize mongo schema handling logic 2016-06-15 13:11:51 -07:00
Drew Gross a69a88f3a4 Fix some stuff 2016-06-15 13:11:51 -07:00
Drew Gross f796d69d19 Move $options validation into Parse Server 2016-06-15 13:11:51 -07:00
Drew Gross 452b737be4 WIP 2016-06-15 13:11:51 -07:00
Drew Gross 634d672ad1 passing another test 2016-06-15 13:11:51 -07:00
Drew Gross 803b9be97a Add unique indexing for username/email
WIP

Notes on how to upgrade to 2.3.0 safely

index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)"

reconfigure username/email tests

Start dealing with test shittyness

most tests passing

Make specific server config for tests async

Fix more tests

Save callback to variable

undo

remove uses of _collection

reorder some params

reorder find() arguments

finishsh touching up argument order

Accept a database adapter as a parameter

First passing test with postgres!

Fix tests

Setup travis

sudo maybe?

use postgres username

reorder find() arguments

Build objects with default fields correctly

Don't tell adapter about ACL

WIP

Passing postgres test with user

Fix up createdAt, updatedAt, nad _hashed_password handling
2016-06-15 13:11:51 -07:00
Drew Gross 2cafd6919f Passing postgres test with user 2016-06-15 13:11:51 -07:00
Drew Gross f75c8b3a4d index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)"
reconfigure username/email tests

Fix broken cloud code

Save callback to variable

undo

Fix all tests where connections are left open after server closes.

Fix issues caused by missing gridstore adapter

remove uses of _collection

reorder find() arguments

Accept a database adapter as a parameter

sudo maybe?

use postgres username

reorder find() arguments

Build objects with default fields correctly

Don't tell adapter about ACL

WIP
2016-06-15 13:11:51 -07:00
Lemuel Barango 693707b33c fixed typo in comment (#2070) 2016-06-15 13:09:20 -07:00
Florent Vilmart 39f11a5134 Update .travis.yml (#2065)
Runs coverage no matter what build result.
2016-06-14 20:10:48 -07:00
Marco Cheung b2a5092627 Endpoint for purging all objects in class (#2032)
* Endpoint for purging all objects in class

* Use deleteObjectsByQuery

* Standalone handling function and purge cache

* Change endpoint url
2016-06-14 15:52:49 -07:00
Marco Cheung d9f8486790 Fix role test (#2057) 2016-06-14 10:17:15 -07:00
Héctor Ramos 5838a49396 Move logging docs to Advanced configuration (#1895) 2016-06-13 13:05:33 -07:00
Greenkeeper 63e2ceeb02 chore(package): update deepcopy to version 0.6.3 (#2049)
https://greenkeeper.io/
2016-06-13 09:36:50 -07:00
Marco Cheung 7c8ad7e071 Throw error when push is missing configuration (#2035) 2016-06-13 09:26:02 -07:00
Marco Cheung 0ec78d478b Fix checking existent class for allowClientClassCreation (#2051) 2016-06-13 09:21:52 -07:00
Drew 2cc1b0cfa9 Remove unused function (#2046) 2016-06-13 07:34:57 -04:00
Vitaly Tomilov 324cd85775 Update PostgresStorageAdapter.js (#2045)
1. removing `return` where it does nothing;
2. changing repeated queries to be executed as a task. See [Chaining Queries](https://github.com/vitaly-t/pg-promise/wiki/chaining-queries).
2016-06-12 17:27:02 -07:00
Drew 5518edc2a5 Postgres adapter (#2012)
* Remove adaptiveCollection

* Remove an adaptiveCollection use

* Remove an adaptiveCollection

* make adaptiveCollection private

* Remove collection from mongoadapter

* Move schema collection usage into mongo adapter

* stop relying on mongo format for removing join tables

* reduce usage of schemaCollection

* remove uses of _collection

* Move CLP setting into mongo adapter

* remove all uses of schemaCollection

* make schemaCollection private

* remove transform from schemaCollection

* rename some stuff

* Tweak paramaters and stuff

* reorder some params

* reorder find() arguments

* finishsh touching up argument order

* Accept a database adapter as a parameter

* First passing test with postgres!

* Actually use the provided className

* index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)"

* Start dealing with test shittyness

* Make specific server config for tests async

* Fix email validation

* Fix broken cloud code

* Save callback to variable

* undo

* Fix tests

* Setup travis

* fix travis maybe

* try removing db user

* indentation?

* remove postgres version setting

* sudo maybe?

* use postgres username

* fix check for _PushStatus

* excludes

* remove db=mongo

* allow postgres to fail

* Fix allow failure

* postgres 9.4

* Remove mongo implementations and fix test

* Fix test leaving behind connections
2016-06-12 16:35:13 -07:00
Drew d559cb2382 Move transform acl (#2021)
* Move ACL transforming into Parse Server

For the database adapters, it will be more performant and easier to work with _rperm and _wperm than with the ACL object. This way we can type it as an array and so on, and once we have stronger validations in Parse Server, we can type it as an array containing strings of length < x, which will be much much better in sql databases.

* Use destructuring
2016-06-12 13:39:41 -07:00
Drew 5baa53d858 Changelog 2.2.13 (#2039) 2016-06-12 13:06:08 -07:00
Drew 729a0c101d Pin deepcopy (#2038) 2016-06-12 15:59:23 -04:00
Yaroslav Syrytsia afbc662dcd fix gettings user roles from cache (#2034) 2016-06-12 02:43:11 -07:00
Drew 7e868b2dcc Unique indexes (#1971)
* Add unique indexing

* Add unique indexing for username/email

* WIP

* Finish unique indexes

* Notes on how to upgrade to 2.3.0 safely

* index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)"

* reconfigure username/email tests

* Start dealing with test shittyness

* Remove tests for files that we are removing

* most tests passing

* fix failing test

* Make specific server config for tests async

* Fix more tests

* fix more tests

* Fix another test

* fix more tests

* Fix email validation

* move some stuff around

* Destroy server to ensure all connections are gone

* Fix broken cloud code

* Save callback to variable

* no need to delete non existant cloud

* undo

* Fix all tests where connections are left open after server closes.

* Fix issues caused by missing gridstore adapter

* Update guide for 2.3.0 and fix final tests

* use strict

* don't use features that won't work in node 4

* Fix syntax error

* Fix typos

* Add duplicate finding command

* Update 2.3.0.md
2016-06-10 20:27:21 -07:00
Michael Chan 6415a35433 Update google.js (#2023)
Updated the authentication method of Google Sign-in (from v1 to v3)
2016-06-10 16:25:25 -07:00
Dan Huang 11301d9590 Fix an issue in MongoTransform where transforming object type fields with null values fails (#2029) 2016-06-10 13:44:41 -07:00
Drew a00d795404 Changelog 2.2.12 (#2019)
* Changelog 2.2.12

* Update CHANGELOG.md
2016-06-09 18:41:02 -07:00
Marco Cheung 2e50416cba Recursive parse date in cloud function (#2014)
* Recursive parse date in cloud function

* Use _.mapValues
2016-06-09 12:37:05 -07:00
Drew fcd914bdfd Revert "Log objects rather than JSON stringified objects" (#1995) 2016-06-08 10:11:08 -07:00
Patrick Pelletier db76aa376f Merge pull request #2001 from skinp/legacy-files
Support file objects in the legacy bucket: files.parse.com
2016-06-07 18:36:24 -07:00
Patrick Pelletier 65518cdd64 Support file objects in the legacy bucket: files.parse.com 2016-06-07 15:59:45 -07:00
Florent Vilmart e7e2369132 Fixes for Class Level and Pointer Permissions (#1989)
* Fixes for Pointer Permissions

- Fix bug that would leave public CLP when setting a new set of permissions
- Sets empty permissions if missing to match parse.com API
- Updates tests to reflect changes

* Adds regression test for #1991

* Fit -> It
2016-06-06 09:31:50 -07:00
Manuel ac705a8da6 Fixed 3rd party OAuth link (#1986)
Fixed bad OAuth link in wiki [ci-skip]
2016-06-04 12:37:40 -04:00
Drew aeb6880b85 Allow installation get (#1980) 2016-06-04 12:37:15 -04:00
Sen Palanisami bea655e61b Log objects rather than JSON stringified objects (#1922) 2016-06-01 18:35:54 -07:00
Tyler Brock abeecad907 Ensure beforeSave hook response is object before stripping (#1969) 2016-06-01 12:01:35 -07:00
Florent Vilmart 38f02af511 Update .travis.yml (#1967)
Fixes tags not running when having more than a digit
2016-06-01 13:49:53 -04:00
Jeremy Pease c6c9c97b54 Allow custom error codes with response.error from Cloud Code functions and before/after hooks (#1955) 2016-06-01 10:28:06 -04:00
Drew 103839ce60 Changelog 2.2.11 (#1962) 2016-05-31 17:01:10 -07:00
Tyler Brock 04172eda5a Strip read only fields from serialized beforeSave response objects (#1964) 2016-05-31 17:00:41 -07:00
Florent Vilmart 5176efbe16 Patch failing delete (#1960)
* Patch failing delete

* Fixing unit test
2016-05-31 13:42:56 -07:00
Blayne Chard 3997b1aa5a Fixing #1900 JS SDK file upload (#1935)
* Fixing #1900 JS SDK file upload

JS SDK file upload uses req.body._ContentType to specify the upload content type

* Fixing import statements

* Dont clear the cache just delete the new entry that the test added.

* adding E2E test for _ContentType support
2016-05-30 21:42:45 -04:00
Drew cd525802a6 Remove mongoFind and mostly remove adaptiveCollection (#1924)
* Use adapter.count

* use adapter.upsertOneObject

* Use adapter.deleteObjectsByQuery

* Use adapter.find

* use adapter.find

* Update tests to avoid mongoFind

* Fix a test to not use mongoFind

* Fix a test to not use mongoFind

* remove some mongoFind

* Remove some mongoFind

* Remove some mongoFind

* Remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* remove more mongoFind

* Restore update ios device token with duplicate device token to original

* remove a mongoFind

* remove a mongoFind

* formatting

* formatting

* remove a mongoFind

* remove a mongoFind

* remove a mongoFind

* kill mongoFind

* Fix tests

* Fix tests

* fix syntax

* Fix test
2016-05-28 12:25:09 -04:00
Tyler Brock 17374eff8d Round out webhookKey support in cli, docs, and Config (#1936) 2016-05-28 12:21:55 -04:00
Madhav Bhagat 181b12771d Make beforeSave work when object is passed to success (#1937) 2016-05-28 12:21:22 -04:00
Drew 1c162101be Fix database URI (#1911)
* Fix database URI

* Use database name that is less likely to collide with people's actual databases
2016-05-28 02:27:59 -07:00
Emad Ehsan 7d2929abe2 Changed deprecated res.send(status) to res.sendStatus(status) (#1912) 2016-05-28 02:26:53 -07:00
Tyler Brock 1487a73f16 Fix expected webhook external response format (#1934) 2016-05-27 19:04:49 -07:00
Drew 50b9ae4803 Fix #1919 (#1926) 2016-05-26 19:15:54 -04:00
Tyler Brock c5e3be8b64 Strip objectId out of responses from HooksController (#1925) 2016-05-26 15:33:37 -07:00
Jeremy Pease dd1d18dbe6 Set push.storedPushData in serverInfo features if push is available (#1923)
It's assumed that when push is available, the pushHandler is used and data is saved in _PushStatus.
This allows Parse Dashboard push history to be enabled: ParsePlatform/parse-dashboard#378
2016-05-26 14:02:13 -07:00
Tyler Brock 2561987d20 Add webhookKey support (#1920) 2016-05-26 11:17:24 -07:00
Drew 0850c184d3 Fixes #1649 (#1650)
* Regression test #1649

* Address comments

* Comment

* Change emails to help debug flaky test failures

* More logging info to debug flaky tests
2016-05-25 19:48:18 -04:00
Héctor Ramos 98f8db36e8 Update issue template (#1899)
Make it clearer what kind of information we are looking for.
2016-05-25 19:33:57 -04:00
Drew c9a138d9ca Break schemaController dependency. (#1901)
* Break dependency on MongoCollection for updateMany

* Move transformWhere usage into MongoTransform

* Pass parse schema into transformUpdate

* break dependency on schemaController

* remove schema parameter

* move key name validation up one level

* Move validation out of mongo adapter

* Move validation into Parse Server and transformUpdate in Mongo Adapter

* Update mongo adapter

* Use adapter API

* use and fix mongo adapter api

* Remove/rename stuff

* Kill transform in DBController

* better imports for transform

* Tidy ConfigRouter

* Remove schemaController in more places

* Remove comment
2016-05-24 17:21:20 -07:00
Peter J. Shin c4499d25e0 Merge pull request #1888 from drew-gross/finish-moving-query-logic
Finish moving query logic
2016-05-24 16:38:14 -07:00
Drew Gross 0896f33824 Remove unnecessary null check 2016-05-24 16:30:43 -07:00
Jeremy Pease 4864518315 Update schema mismatch error to include type string (#1898) 2016-05-24 13:52:45 -07:00
Drew Gross 3ed3c7b62f Move more mongo specific stuff into mongo adapter 2016-05-23 21:16:03 -07:00
Drew Gross 05ae010b91 Kill mongoOptions 2016-05-23 20:28:12 -07:00
Drew Gross d428041a83 Consistent parameter order 2016-05-23 20:22:04 -07:00
Drew Gross a763f7c2fc move transformWhere into mongo adapter 2016-05-23 20:19:03 -07:00
Drew Gross 135b0e0254 Remove options from count 2016-05-23 20:15:51 -07:00
Drew Gross e444ca8425 Can't sort a count 2016-05-23 20:12:03 -07:00
Drew Gross aa072dabff Remove limit from count 2016-05-23 20:09:59 -07:00
Drew Gross c9be5a3aac Destructure mongo options 2016-05-23 20:07:16 -07:00
Drew Gross cf0a4b246f remove adaptive collection call 2016-05-23 20:04:10 -07:00
Drew Gross 14938bbe7a create + use adapter count instead of collection count 2016-05-23 19:00:58 -07:00
Drew Gross 1ae1d42c32 transformWhere in MongoAdapter 2016-05-23 18:39:34 -07:00
Drew Gross 4052470820 remove schemaController paramater 2016-05-23 18:39:34 -07:00
Drew Gross 7dca7e20b0 Remove schemaController parameter 2016-05-23 18:39:34 -07:00
Drew Gross e440046be4 Remove getRelationFields 2016-05-23 18:39:34 -07:00
Drew Gross f4b1f7b951 Remove all dependencies on schemaController 2016-05-23 18:39:34 -07:00
Drew Gross d944255e4e remove another use of schemaController 2016-05-23 18:39:34 -07:00
Drew Gross a55b2b6209 remove another use of schemaController 2016-05-23 18:39:34 -07:00
Drew Gross 00de555ecb remove one use of schemaController 2016-05-23 18:39:34 -07:00
Drew Gross 474a893a22 Pass the Parse Schema into untransform 2016-05-23 18:39:34 -07:00
Drew Gross fe8160449c Revert changes to find 2016-05-23 18:39:34 -07:00
Drew Gross 74ee8613d8 Fix inner keys named _rperm, _wperm 2016-05-23 18:39:19 -07:00
Drew Gross c928dcc118 Prevent untransforming inner object keys named _p_* 2016-05-23 18:38:48 -07:00
Drew Gross 9f149e6db5 Don't mess with inner object keys called _auth_data_* 2016-05-23 18:38:16 -07:00
Drew Gross 69d5a2f87c Make find() in MongoStorageAdapter 2016-05-23 18:35:38 -07:00
Drew 3a96140103 Fix issue with pointers getting un-hydrated when there is a beforeSave (#1884)
* Add failing test and simplify RestWrite

* simplify and add test stubs

* Fix issue
2016-05-23 17:13:32 -07:00
Drew 614e1ac8e5 Move query logic into mongo (#1885)
* Move Parse Server logic into Parse Server and out of MongoAdapter

* Move untransforming up one level

* Make find() in MongoStorageAdapter

* Put nested object untransforming into it's own function

* Simplfy nested untransform

* Don't mess with inner object keys called _auth_data_*

* Prevent untransforming inner object keys named _p_*

* Fix inner keys named _rperm, _wperm

* Fix bugs with inner objects behaving strange when other fields have same name as key in specific circumstances

* remove params from untransform nested object

* Revert changes to find
2016-05-23 19:31:51 -04:00
Adrian Brink 88fa7bad92 Update README.md (#1886)
Add link to the parse-server-module repo for easier reference.
2016-05-23 15:49:06 -07:00
Peter J. Shin 6b28075194 Merge pull request #1834 from drew-gross/move-stuff
Move query format validation into Parse Server
2016-05-23 12:46:44 -07:00
Steven Shipton 8b43d2644f Change existsSync to exception handler (#1879) 2016-05-23 11:49:43 -07:00
Peter J. Shin 361d4a34ce Merge pull request #1876 from ParsePlatform/pin-mongodb
Pin mongodb package
2016-05-23 10:44:53 -07:00
Drew e93a1ad7fc Pin mongodb package
Fixes #1855
2016-05-23 09:15:32 -07:00
Drew 392102eb97 Cache users by objectID, and clear cache when updated via master key (fixes #1836) (#1844)
* Cache users by objectID, and clear cache when updated via master key

* Go back to caching by session token. Clear out cache by querying _Session when user is modified with Master Key (ew, hopefully that can be improved later)

* Fix issue with user updates from different sessions causing stale reads

* Tests aren't transpiled...

* Still not transpiled
2016-05-22 09:59:36 -07:00
Drew eefa2ccac7 Handle "bytes" type in DB. Fixes #1821. (#1866) 2016-05-22 09:33:59 -04:00
Jeremy Pease fab8cfdfc7 Add additional default fields to _Installation class (#1852)
Fields are appVersion, appName, appIdentifier, and parseVersion.
These fields are sent by Android and iOS SDKs.
2016-05-20 18:15:47 -07:00
Tyler Brock 9bad87905f Add MongoDB 3.2 to test matrix (#1842)
* Add MongoDB 3.2 to test matrix

 - Updated mongodb-runner to support specifying storage engine
 - Specifying mmapv1 explictly because of new 3.2 default

* Increase jasmine.DEFAULT_TIMEOUT_INTERVAL to 3 seconds

* Use fixed mongodb-runner

* Increase jasmine.DEFAULT_TIMEOUT_INTERVAL to 15 seconds

* Update to mongodb-runner 3.3.2 upon release
2016-05-20 14:18:41 -07:00
Drew b2183680be Refactor cloud code tests (#1837)
* Move cloud code tests

* Remove _removeHook calls that are no longer necessary

* Use Strict
2016-05-19 13:38:16 -07:00
benishak fece2a4b47 change logger.error to logger.info to prevent pm2 from crashing (#1830) 2016-05-19 09:52:44 -07:00
Hussam Moqhim 03108e6347 add support for http basic auth (#1706)
* add support for http basic auth

* update http auth per flovilmart feedback
2016-05-18 20:06:37 -07:00
Drew Gross c416cad43f remove extra special keys list 2016-05-18 18:59:52 -07:00
Drew Gross 5165c80570 Remove validate parameter from deleteObjectsByQuery 2016-05-18 18:56:47 -07:00
Drew Gross 643bdc8227 Move query validation out of mongo adapter 2016-05-18 18:56:47 -07:00
Drew Gross 15fc186a51 Extract query validation logic 2016-05-18 18:56:47 -07:00
Drew Gross ea09213511 lift query key validation out of transformWhere 2016-05-18 18:56:47 -07:00
Drew Gross 559205bc64 Lift no-query-ACL validation out of transformWhere 2016-05-18 18:56:47 -07:00
Drew Gross 66b8a8474e Lift query key validation out of transformQueryKeyValue 2016-05-18 18:56:47 -07:00
Drew Gross b24ff151d5 Correct validation of inner keys 2016-05-18 18:56:47 -07:00
Drew Gross 0d856c1f23 remove extra lines 2016-05-18 18:54:06 -07:00
Drew 4d4361451c Refactor MongoTransform.js (#1823)
* Split transformAtom into transfromTopLevelAtom and transformInteriorAtom

* Use single param for inArray and inObject

* Tidyness in transformKeyValue

* Add transformInteriorKeyValue

* Remove update from tranformInteriorKeyValue

* Split out transform update

* Move validation out of transfromUpdate

* Remove force paramater from transformTopLevelAtom throw error after if necessary

* Turn transformKeyValue into transfromKey since it is only used for that purpose

* Remove unnecessary stuff from transformKey

* convert transformKey to use parse format schema

* interior keys fixes

* Add test for interior keys with special names

* Correct validation of inner keys
2016-05-18 18:14:54 -07:00
Tyler Brock d7d4699832 Fix logic for missing geo index error message check (#1824) 2016-05-18 12:33:55 -07:00
Blayne Chard 8c09c3dae1 Adding Caching Adapter, allows caching of _Role and _User queries (fixes #168) (#1664)
* Adding Caching Adapter, allows caching of _Role and _User queries.
2016-05-18 12:12:30 +12:00
KartikeyaRokde 5d887e18f0 FIX #1572 - Accepting LOGS_FOLDER as env variable (#1757)
* FIX #1572 - Accepting LOGS_FOLDER as env variable

* Changed env variable LOGS_FOLDER to PARSE_SERVER_LOGS_FOLDER

* Added Note for starting parse-server with PARSE_SERVER_LOGS_FOLDER env variable
2016-05-17 17:02:28 -07:00
Marco Cheung 40965186c0 Mask sensitive information when logging (#1790) 2016-05-17 13:15:44 -07:00
Drew Gross b40e16647b Changelog for version 2.2.10 2016-05-16 16:39:41 -07:00
Tyler Brock 3b4ae2d0a0 Write old ACL format in _acl in addition to new format (#1810) 2016-05-16 14:41:25 -07:00
Tyler Brock 1854928fe7 Add test to ensure you can set ACL in beforeSave (#1772) 2016-05-13 18:17:22 -07:00
Drew e4998c256a Move field name validation logic out of mongo (#1752)
* Remove transformKey(...)

* Move validation logic into Parse Server and out of Mongo Adapter

* Fix nits
2016-05-13 15:28:14 -07:00
Peter J. Shin 4bfe2c5014 Merge pull request #1774 from drew-gross/pointer-perms-in-dash
Tell the dashboard that Pointer Permissions are implemented so it can let you edit them.
2016-05-13 14:47:24 -07:00
Marco Cheung d0c3535a39 Fix error when unset user email (#1778) 2016-05-13 10:51:01 -07:00
Drew Gross c2cfa14627 Tell dashboard that the feature exits 2016-05-12 16:30:38 -07:00
Marco Cheung 19e7407f55 Return correct error when violating unique index (#1763) 2016-05-11 17:24:15 -07:00
North 6cfcb4ddf8 Fix #1755 (#1756)
* Add condition at limit = 0

* Add tests for installations with limit and count parameters
2016-05-11 15:22:29 -07:00
Drew 10ee229e60 Update dependencies and support node 6.1 (#1754)
* Update dependencies and support node 6.1

* Remove gcs-adapter, which depends on grpc, which fails to build on node 6

* Remove GCS adapter usage
2016-05-11 08:54:23 -04:00
Sebastián Balay 1291f70a16 Fix malformed config.json in bootstrap.sh
The bootstrap bash script generates a wrong config.json file.
Instead of having a databaseURI property, it has a mongodbURI prop.
2016-05-10 18:13:06 -04:00
Drew 26df50924b Update to 2.2.9 (#1747) 2016-05-10 10:50:05 -07:00
Drew e698f61a58 Handle null param in cloud code. Fixes #1472 (#1746) 2016-05-10 10:43:02 -07:00
Drew e60901de6a Version 2.2.8 (#1738) 2016-05-09 16:56:32 -07:00
Fosco Marotto b2f36e192a Merge pull request #1644 from drew-gross/refactor-query-transform
Break dependency of deleteObjectsByQuery on schemaController
2016-05-09 11:21:22 -07:00
andrewchae 10d2988985 Pass dates to cloud code functions (#655) (#1697) 2016-05-09 10:45:57 -07:00
Steven Shipton 37c502bed0 Add config.expireInactiveSession to add support for non-expiring inactive sessions (#1536)
* Create non-expiring session when sessionLength is zero

* Introduce expireInactiveSessions setting
2016-05-06 12:50:45 -07:00
Héctor Ramos b00572de65 Merge pull request #1696 from ParsePlatform/issue-template
Update issue template
2016-05-06 12:46:41 -07:00
Héctor Ramos da5b5ccefa Update ISSUE_TEMPLATE.md 2016-05-06 12:45:47 -07:00
Steven Shipton 7c850fc1ad Fix for #1642 - copy query parameters to request body (#1687)
* Fix for #1642 - copy query parameters to request body

* Add missing request.query to pass test
2016-05-06 12:36:05 -07:00
Héctor Ramos 90df7841ef Update issue template
Additional guidance for what type of details should be included with a bug report.
2016-05-03 12:08:23 -07:00
Héctor Ramos 0b88302bea Link to new migration guide 2016-04-29 21:34:33 -07:00
Héctor Ramos 583bb34995 Link to bug tool for migration issues 2016-04-28 12:56:04 -07:00
Drew Gross 71ae7bee94 better names and comments 2016-04-26 13:08:58 -07:00
Drew Gross 449ca115b1 Break object deletions's dependency on schemaController 2016-04-26 10:23:14 -07:00
Drew Gross d4bd21fcbc remove schema from transformWhere 2016-04-26 10:20:17 -07:00
Drew Gross a926712951 Start using parse format schema in transformQueryKeyValue 2016-04-26 10:12:45 -07:00
Drew Gross 5f564f32f5 simplify a little 2016-04-25 23:39:42 -07:00
Drew Gross 874d10fc74 Check expected type is array from Parse Format Schema 2016-04-25 23:36:25 -07:00
Drew Gross 4371ca164c Pass parse format schema to transformWhere 2016-04-25 22:45:16 -07:00
Drew Gross 608cba9e8c Clearer names in DatabaseController 2016-04-25 22:12:03 -07:00
Drew Gross 5cbf3eb8dd Tidy up db controller 2016-04-25 22:01:50 -07:00
Drew Gross 37953d146b some cleanup 2016-04-25 21:53:57 -07:00
Drew Gross a3179e43fb No need to handle object or reject arrays when transforming queries 2016-04-25 21:13:34 -07:00
Drew Gross 7b431ad064 nits 2016-04-25 20:54:47 -07:00
Aidan Musnitzky 3d7589c1bc Update README.md (#1637) 2016-04-25 20:51:52 -07:00
Drew Gross ee8a3c94d5 No need to transform post-transform keys in mongo adapter 2016-04-25 20:42:19 -07:00
Drew Gross e2a3213351 Remove update logic from query 2016-04-25 19:34:07 -07:00
Drew Gross 5c7b346292 Remove unnecessary logic 2016-04-25 18:48:22 -07:00
Florent Vilmart 0542f860f4 Test case to ensure dashboard.parse.com won't break (#1636)
* Test case to ensure dashboard.parse.com won't break

* Adds volatile classes concept for _PushStatus

* Fixes test
2016-04-25 21:33:11 -04:00
Drew Gross e0a9f19895 Split logic for transforming queries into it's own function 2016-04-25 18:22:20 -07:00
Drew Gross 21e73ac276 Destructuring 2016-04-25 17:21:24 -07:00
Drew 957b5927b1 Ignore _RevoableSession "header" that is sent by JS SDK. Fixes #1548. (#1627) 2016-04-25 15:52:21 -04:00
Drew 234d0093ff Cleanup delete schema (#1604)
* Some cleanup for deleting one schema

* tidyness

* Remove _allCollections as Parse Server doesn't need it.
2016-04-25 14:47:57 -04:00
Drew d14d451028 Move acl adding into parse server (#1601)
* Move writeACL knowledge out of mongoAdapter

* Remove write ACL from mongo adapter

* Remove readACL from Mongo Transform
2016-04-22 21:44:03 -04:00
Drew d33dd68cc5 Add revokeSessionOnPasswordReset option. Closes #1584 (#1597)
* Add revokeSessionOnPasswordReset option

* Fix nits
2016-04-22 18:21:50 -04:00
Drew 0d094767cf Cleanup update (#1590)
* destructuring in DB controller

* deleteObject in db adapter

* Turns out we can't have delete by object ID because of ACLs...

* Fix tests

* destructure acl

* Don't reject with object
2016-04-22 17:05:21 -04:00
Florent Vilmart ab827e3c2f Forces delete resolution (#1595) 2016-04-22 08:46:27 -07:00
Florent Vilmart d49d539a81 _PushStatus uses proper Parse format (#1580)
* removes mongo style props from _PushStatus

* removes log

* Update MongoTransform.js

Removes _id case
2016-04-21 21:36:15 -04:00
Blayne Chard 6e9529f81c Exposes the loggerAdapter as log to Cloud Functions and Triggers (#1565)
This allows access to logging inside cloud code and triggers via
    request.log.info
    request.log.error
2016-04-21 16:20:14 -04:00
Florent Vilmart 2d94a885d2 regression test for #1554 2016-04-20 21:51:48 -04:00
Florent Vilmart b7b57f7ea7 Adds support for Pointer Permissions
* WIP: Initial pointer permissions

* Process Pointer perms when no matching CLP are found

* Additional tests with read lockdown

* Create operation lockdown with pointer permissions,

on parse.com, when an class is locked down with write pointer perm, users can't create objects even if they set their own as the pointer permission key

* Adds test case for multiple write PointerPerms

* Adds validation for pointer permissions when setting

* Adds tests for validating pointer permissions column types

* Adds tests for complex ACL/CLP/PP hierarchy

* Restores power of the master

* Adds validation of borked fields

* Adds complex test for find

* Adds more variations around PointerPermissions tests
2016-04-20 21:51:11 -04:00
Drew b185508508 Remove flaky test
This test depends on external services and is super flaky. Lets kill it.
2016-04-20 19:03:56 -04:00
Drew 7b91ee98ed Fixes #1568 (#1574) 2016-04-20 18:42:18 -04:00
Drew 9776362ab2 Begin isolating object creation code into an externalizable API. (#1569)
* Tidy up transformKeyValue

* Specialize transformKeyValue for object creation

* remove keys that never appear in creation requests

* rename function

* remove local var

* early exit for simple keys

* Refactor create

* Force class creation when creating an object

* Pass parameters to key value transformer

* No need to check for array in this func

* start using Parse Format schema in MongoTransform

* Remove call to getExpectedType

* add tests to ensure client can't see _PushStatus
2016-04-20 13:35:48 -07:00
Florent Vilmart 59b4047de8 Defers the session creation after DB operation (#1561) 2016-04-20 11:57:38 -04:00
Blayne Chard 54b21c2f73 Compress the ACL queries into a single $in query (#1566) 2016-04-20 09:03:44 -04:00
Drew 5d37755d88 Test facebook unlinking (regression test #1532) (#1538) 2016-04-20 00:31:15 -07:00
Florent Vilmart ddd01181be Safely require babel-polyfill (#1558)
(https://github.com/shakacode/bootstrap-loader/pull/29/files)
2016-04-19 22:29:00 -07:00
Drew bf47a0b2b6 Remove acceptor parameter (#1553) 2016-04-19 14:29:05 -07:00
Florent Vilmart 3945e0c680 Update pushStatusHandler.js (#1543)
Fixes error thrown by #1526
2016-04-18 19:30:19 -07:00
Drew 61b62e4e9f Rename Schema => SchemaController (#1542)
* Rename Schema to SchemaController

* Rename Schema => SchemaController

* Move to controllers folder

* Move SchemasController to Controllers folder

* remove ./..
2016-04-18 18:59:57 -07:00
Drew 0708af17d7 Move "No two geopoints" logic into mongo adapter (#1491)
* Move "No two geopoints" logic into mongo adapter

* Semicolon
2016-04-18 17:10:30 -07:00
Drew cecb2a1cb1 Move 'ns not found' into mongo adapter. (#1541) 2016-04-18 17:06:51 -07:00
Drew ab0ea09067 Schema.js cleanup (#1540)
* Tidy up schemas router

* de-duplicate logic for injecting default schemas

* Remove no-op promise

* use hasClass

* Make getAllSchemas part of SchemaController

* Move getOneSchema logic onto schema controller

* remove log
2016-04-18 17:06:00 -07:00
Drew 414ee6740d Regression test for #1489 (#1511) 2016-04-18 19:45:48 -04:00
Fosco Marotto 65bd59d95f Add link to screencast blog in readme. 2016-04-18 13:08:28 -07:00
Drew ac839f75c7 Remove mongo object create format from Parse Server (#1516) 2016-04-18 12:45:48 -04:00
Drew c57c6b33b1 Add link to mandrill adapter. 2016-04-15 14:33:12 -07:00
Fosco Marotto 4c2636e23c Merge pull request #1510 from ParsePlatform/fosco.227
Changelog and update to 2.2.7
2016-04-15 12:49:26 -07:00
Fosco Marotto 2f51e751de Changelog and update to 2.2.7 2016-04-15 12:46:25 -07:00
Seiji Akiyama a727e1ccd3 Adds limit = 0 as a valid parameter for queries (#1493)
* Remove results if limit = 0;

* Adds tests for limit=0 and count=1.

* Improves readability.
2016-04-15 11:17:53 -07:00
Drew f076078bf1 Add note about URL encoding special characters in passwords. 2016-04-15 10:50:46 -07:00
Drew 3f66c302b7 Enable logs (#1502) 2016-04-14 17:44:50 -07:00
Florent Vilmart eef51cf9ad Hotfix for tests (#1503) 2016-04-14 17:25:02 -07:00
Florent Vilmart 1023baf20d DBController refactoring (#1228)
* Moves transform to MongoTransform

- Adds ACL query injection in MongoTransform

* Removes adaptiveCollection from DatabaseController

- All collections manipulations are now handled by a DBController
- Adds optional flags to configure an unsafe databaseController for direct
  access
- Adds ability to configure RestWrite with multiple writes
- Moves some transfirmations to MongoTransform as they output specific code

* Renames Unsafe to WithoutValidation
2016-04-14 19:24:56 -04:00
Tyler Brock 51970fb470 Do some triple equals for great justice (#1499) 2016-04-14 15:44:22 -07:00
Tyler Brock d57e384dcb Apply credential stripping to all untransforms for _User (#1498) 2016-04-14 14:50:16 -07:00
Florent Vilmart 34851c0ae5 Makes sure we don't delete Installations at large when updating a token (#1475) (#1486) 2016-04-14 12:59:59 -07:00
Simonas Karuzas 686cc4ab95 Checking if object has defined key for Pointer constraints in liveQuery
* Removing sessionToken and authData from _User objects included in a query

This bug caused sessionToken to be replaced on client side to some old
sessionToken from DB.

* Removing dangling variable that is never used

* Checking if object has defined key for Pointer constraints in liveQuery

If there is a liveQuery subscription, with Pointer type constrains (e.g
query.equalTo('user', Parse.User.current())), and new object has
undefined value for that field, we get this error:

    error: Uncaught internal server error. [TypeError: Cannot read
property 'className' of undefined] TypeError: Cannot read property
'className' of undefined
    at matchesKeyConstraints
(…/node_modules/parse-server/lib/LiveQuery/QueryTools.js:145:51)
2016-04-14 10:24:04 -04:00
Drew fc1cdd4408 Remove collection prefix and default mongo URI (#1479)
* Remove collection prefix from DB Controller

* Remove collection prefix from cache

* Revert "Remove collection prefix from cache"

This reverts commit 529d67dd61.

* Remove knowledge of default mongo URI from Parse Server

* Remove adaptive collection paramater from deleteFields

* Tidy up DBAdapter.js
2016-04-13 19:45:07 -04:00
Drew 3fb3ce1ab7 Store collection prefix in mongo adapter, and clean up adapter interface (#1472) 2016-04-13 08:21:53 -04:00
Drew 512251eb58 Move field deletion logic into mongo adapter (#1471) 2016-04-12 22:06:58 -04:00
Florent Vilmart f312f81cbd Adds support for Long and Double mongodb types (fixes #1316) (#1470)
* Adds support for Long and Double mongodb types (fixes #1316)

* Use bigger numbers
2016-04-12 21:19:47 -04:00
Florent Vilmart af30f669cc Push status nits (#1462)
* Sets _id to objectId in _PushStatus (fixes #1457)

* _PushStatus stores serialized payload (fixes #1458)
2016-04-12 17:43:46 -04:00
Florent Vilmart c050a65d49 Schema.js database agnostic (#1468)
* Schema.js database agnostic

* nits
2016-04-12 17:39:27 -04:00
Drew c419106a38 Remove console.log (#1465) 2016-04-12 10:46:33 -07:00
Drew 99ecbb39f5 Move mongo field type logic into mongoadapter (#1432) 2016-04-12 08:06:00 -04:00
Simonas Karuzas e9e561f5e8 Removing sessionToken and authData from _User objects included in a query (#1450)
* Removing sessionToken and authData from _User objects included in a query

This bug caused sessionToken to be replaced on client side to some old
sessionToken from DB.

* Removing dangling variable that is never used
2016-04-12 07:57:31 -04:00
Florent Vilmart 73a3db44ab Fixes #1444 (#1451) 2016-04-11 11:27:55 -07:00
Drew 281568edd2 Fixes #1417
* Fixes #1417

* Cleanup

* Perf improvement

* Hoist constant array

* Improve tests
2016-04-08 19:06:52 -04:00
Florent Vilmart b433fb9b4e Prevents _User lock out when setting ACL on signup or afterwards (#1429) 2016-04-08 13:19:41 -04:00
Steven Shipton 30197a7d84 Expose DatabaseAdapter to simplify application tests (#1121)
* Move helper.clearData to DatabaseAdapter. Expose DatabaseAdapter in index

* fix indentation

* Export DatabaseAdapter in index.js

* Rename clearData to destroyAllDataPermanently. Only export destroyAllDataPermanently from DatabaseAdapter. Update helper

* Expose wrapped TestUtils from index.js. TestUtils exposed select functions from other modules, only in test environment
2016-04-08 10:00:16 -07:00
Florent Vilmart ab6925a5e2 Update .travis.yml (#1428)
Adds regular expression to match tags (yes, match tag in branches...)
Tags should follow:

1.0.0
or
1.0.0-some
2016-04-08 08:02:54 -04:00
Florent Vilmart a770c4cba8 Adds relation fields to objects 2016-04-08 07:42:02 -04:00
Florent Vilmart de2a2ee245 Update .travis.yml (#1423)
* Update .travis.yml

This will prevent the additional build we're seeing, as well as hopefully keep the tagged deploys
Only run the deployment when $MONGODB_VERSION = '3.0.8' (one part of the build matrix)
2016-04-07 23:11:06 -04:00
Florent Vilmart c22bafb951 Sets the defaultSchemas keys in the SchemaCollection (#1421)
* Sets the defaultSchemas keys in the SchemaCollection

* Moves defaultSchema injection logic to router

* maps outside the function

* fixes test

* conciseness
2016-04-07 22:13:07 -04:00
Florent Vilmart bc96f0beb1 Adds X-Parse-Push-Status-Id header (#1412)
* Adds X-Parse-Push-Status-Id header

* Waits for _PushStatus to be stored
2016-04-07 18:08:09 -04:00
Florent Vilmart cedac3fda6 Adds support for --verbose and verbose option when running ParseServer (#1414) 2016-04-07 18:07:52 -04:00
Blayne Chard 616ac71623 Untransform should treat Array's as nested objects (#1416) 2016-04-07 14:43:48 -07:00
Drew 325d09ca4c Schema format cleanup 2016-04-07 07:47:47 -04:00
Florent Vilmart ef92a79818 Updates the publicServerURL option [ci skip] 2016-04-07 07:45:22 -04:00
Drew 91ace4e718 Progres towards moving mongo logic into its adapter 2016-04-05 21:16:39 -07:00
Florent Vilmart cbbd66964a Huge performance improvement on roles queries 2016-04-05 20:44:26 -04:00
0xA455 1bd804693c Fix exception with non-expiring session tokens.
Session tokens generated by Parse with with "Expire inactive session" set

to No leave the the expiresAt field undefined. This fixes the TypeError

"Cannot read property 'iso' of undefined'" exception thrown when trying

to use a session token with undefined expiresAt.
2016-04-05 16:12:15 -07:00
Florent Vilmart 1eb2a87f9f Removes GCS Adapter from provided adapters
* 💣 removes exposed GCS files adapter

- Should fix problems with GCS
- Throws error that shows the right repo
2016-04-05 18:18:49 -04:00
Fosco Marotto 0b2dc12143 Merge pull request #1226 from 1nput0utput/master
Spotify authentication
2016-04-05 10:49:18 -07:00
Fosco Marotto 213bfea62d Merge pull request #1379 from ParsePlatform/fosco.226
Changelog and update to 2.2.6
2016-04-05 10:22:31 -07:00
Fosco Marotto a17e54c397 Changelog and update to 2.2.6 2016-04-05 10:20:48 -07:00
Florent Vilmart 5773143973 Disables find on installation from clients
- fixes #1372
2016-04-05 13:09:40 -04:00
Florent Vilmart 406f237119 Adds missing options to the CLI 2016-04-05 08:53:35 -04:00
Florent Vilmart bc384e97e1 Let travis build all branches 2016-04-05 08:53:17 -04:00
Blayne Chard 18906f17b4 Auth._loadRoles should not query the same role twice. 2016-04-04 21:32:41 -04:00
Fosco Marotto 442c723350 Merge pull request #1364 from ParsePlatform/fosco.225
Changelog and update to 2.2.5
2016-04-04 18:01:27 -07:00
Fosco Marotto 9053b941ee Changelog and update to 2.2.5 2016-04-04 17:47:20 -07:00
Fosco Marotto 7e52c18893 Merge pull request #1363 from ParsePlatform/issue/1319
Improves config loading and tests
2016-04-04 17:30:59 -07:00
Florent Vilmart 8c92b80b8f Improves config loading and tests 2016-04-04 20:02:34 -04:00
Fosco Marotto 27dd2ebf5d Merge pull request #1361 from ParsePlatform/fosco.npm
Adds travis configuration to deploy NPM on new version tags
2016-04-04 13:50:50 -07:00
Fosco Marotto c1ef0440b0 Merge pull request #1362 from peterbytes/patch-1
The Heroku link redirects to the wiki index
2016-04-04 13:50:31 -07:00
peterbytes f4889a3393 The Heroku link redirects to the wiki index
Changed the link so that it goes to Heroku Dev Center guide instead
2016-04-04 13:40:47 -07:00
Fosco Marotto 19da14f3df Adds travis configuration to deploy NPM on new version tags 2016-04-04 12:44:22 -07:00
Florent Vilmart 91de7504fe 🎉 fixes #1288
* ⚡ regression test for #1288

* 🎉 fixes #1288
2016-04-04 14:27:11 -04:00
Florent Vilmart ec6a39718d Adds console transport when testing with VERBOSE=1 2016-04-04 11:07:25 -07:00
Florent Vilmart 0e3636d880 Make notEqual work on relations
* ⚡ regression tests for #1349

* 🎉 fixes #1349

* Adds support for multiple constraints on the same key
2016-04-04 11:05:03 -07:00
Drew 89df778ac8 Merge pull request #1357 from ParsePlatform/schema-defaults
🚧 Inject the default schemas properties when loading it
2016-04-04 08:56:41 -07:00
Tommy Goode acc23d0828 Fix for #1334: using relative cloud code files broken
* Adding tests for absolute and relative cloud code file loading.

* Fixes #1334 by resolving relative cloud code file paths to the process' working directory.
2016-04-04 08:43:06 -04:00
Florent Vilmart 3a28352c96 🎉 fixes #1355
- injects default schema upon schema load
2016-04-04 08:36:40 -04:00
Jeremy May f99b5588ab Added session length option for session tokens to server configuration 2016-04-02 11:36:47 -04:00
Fosco Marotto 51664c8f33 Merge pull request #1268 from ParsePlatform/flovilmart.noLogsWhileTesting
Removes all stdout/err logs while testing
2016-04-01 13:42:50 -07:00
Fosco Marotto 8c12f80d28 Merge pull request #1269 from ParsePlatform/flovilmart.reportPushError
Improves report for Push error in logs and _PushStatus
2016-04-01 13:42:01 -07:00
Drew c346e15a18 Merge pull request #1280 from jeremyjackson89/master
Single object queries to use include and keys
2016-04-01 09:06:54 -07:00
Jeremy 4fd67a51fb Put the allowed query keys for get in a constant. 2016-04-01 11:51:42 -04:00
Jeremy fffc83c0d9 Added tests for include and key query parameters on a specific object. 2016-04-01 11:35:40 -04:00
Jeremy 04e8e3e233 Merge remote-tracking branch 'upstream/master' 2016-04-01 11:33:11 -04:00
Florent Vilmart 02af61dc9a Merge pull request #1315 from drew-gross/bool-only
Accept only bool for $exists in LiveQuery
2016-04-01 10:57:00 -04:00
Drew 57156cf52b Merge pull request #1314 from ParsePlatform/flovilmart.issue1302
Fixes issue #1302
2016-03-31 19:00:55 -07:00
Drew Gross 01af755d18 Accept only bool for $exists in LiveQuery 2016-03-31 18:49:58 -07:00
Drew 3f8a1aae02 Merge pull request #1250 from andrecardoso/live-query-server-matching-does-not-exist-queries
Matching queries with doesNotExist constraint
2016-03-31 18:42:17 -07:00
Florent Vilmart edfa4092c0 😎 fixes #1302
- when including elements from an array of pointers, filters unaccessible/missing objects
2016-03-31 20:39:09 -04:00
Florent Vilmart 431d864ac3 ⚡ reproduces #1302 2016-03-31 20:38:45 -04:00
Drew 647987534f Merge pull request #1312 from ParsePlatform/flovilmart.includesArray
Fixes bug related to include in queries
2016-03-31 16:49:15 -07:00
Florent Vilmart ca7d8580e3 👍 fixes #1298 2016-03-31 19:32:24 -04:00
Florent Vilmart 9c528c6fe8 🎉 regression test for #1298 2016-03-31 19:11:42 -04:00
Florent Vilmart 2b3bf7c2b7 ⚡ troubleshoot #1293 2016-03-31 19:01:42 -04:00
Florent Vilmart 6540ad1483 Merge pull request #1305 from ParsePlatform/flovilmart.emailAdapterConfig
Adds more options when using CLI/config
2016-03-31 16:20:44 -04:00
Florent Vilmart b5625bc2e5 more configuration options in the CLI 2016-03-31 11:53:25 -04:00
Florent Vilmart 09279d1987 Allows loading cli adapters from module path 2016-03-31 11:49:15 -04:00
Florent Vilmart dbf64a6d83 Merge pull request #1297 from drew-gross/error-copy
Update error message
2016-03-31 09:16:55 -04:00
Drew Gross eeb3331167 Update error message 2016-03-31 00:21:21 -07:00
Drew 8a6903aa8d Merge pull request #1287 from ParsePlatform/flovilmart.dynamicConfigMount
Adds ability to override mount with publicServerURL for production uses
2016-03-30 20:29:37 -07:00
Drew 434ac91129 Merge branch 'master' into flovilmart.dynamicConfigMount 2016-03-30 20:29:30 -07:00
Florent Vilmart f0ebb7bae6 Merge pull request #1295 from drew-gross/test-1259
Fixes #1271
2016-03-30 23:14:17 -04:00
Drew 781e81d36b Merge pull request #1291 from ParsePlatform/flovilmart.issue1257
Properly let masterKey add fields
2016-03-30 19:54:38 -07:00
Drew Gross 73bca3b64c Improve comments 2016-03-30 19:48:33 -07:00
Drew Gross 6311c95785 Fixes #1271 2016-03-30 19:35:54 -07:00
Florent Vilmart efa7366250 Merge pull request #1286 from drew-gross/test-1259
Regression test for #1259
2016-03-30 20:31:05 -04:00
Florent Vilmart 6b6e684d55 Merge pull request #1290 from ParsePlatform/drew-gross-patch-2
Point to #1271 as how to write a good issue report
2016-03-30 20:27:43 -04:00
Florent Vilmart 5d99075663 Properly let masterKey add fields 2016-03-30 20:27:12 -04:00
Drew c5636e1b77 Point to #1271 as how to write a good issue report 2016-03-30 17:09:11 -07:00
Florent Vilmart ab1858616b Adds ability to override mount with publicServerURL for production uses 2016-03-30 20:09:10 -04:00
Drew Gross 632c8054da Regression test for #1259 2016-03-30 16:46:08 -07:00
Florent Vilmart 7afc08abe4 Merge pull request #1283 from drew-gross/test-871
Regression test for #871
2016-03-30 19:17:17 -04:00
Florent Vilmart 33c9730be2 Merge pull request #1281 from drew-gross/test-701
Add a test to repro #701
2016-03-30 19:10:20 -04:00
Drew Gross 97d3deb73b Regression test for #871 2016-03-30 16:04:37 -07:00
Drew Gross a0e7739526 Add a test to repro #701 2016-03-30 11:43:47 -07:00
Jeremy 7064f1d3ed Allow queries on specific objects to user the include and keys query parameters. 2016-03-30 13:50:36 -04:00
Florent Vilmart 4774e90937 Removes all stdout/err logs while testing 2016-03-29 22:52:00 -04:00
Florent Vilmart 6055f2a552 Improves report for Push error in logs and _PushStatus 2016-03-29 22:42:37 -04:00
Fosco Marotto b2819df7e5 Merge pull request #1264 from ParsePlatform/fosco.224
Changelog and update to 2.2.4
2016-03-29 14:36:26 -08:00
Fosco Marotto 5dc5ef239e Merge pull request #1265 from drew-gross/ignore-thrown-message
Fix test issue cause by env variables
2016-03-29 14:36:07 -08:00
Fosco Marotto f25bcc490a Updated changelog 2016-03-29 15:31:36 -07:00
Florent Vilmart aa5ebd49f0 Merge pull request #1199 from yuzeh/fix-authData-null-value
Clean authData of null values on _User update
2016-03-29 18:28:17 -04:00
Fosco Marotto 106ac95622 Fix adapter imports 2016-03-29 15:27:43 -07:00
Fosco Marotto 1f4393cd4d Changelog and update to 2.2.4 2016-03-29 15:10:55 -07:00
Fosco Marotto e361ccf5a5 Merge pull request #1263 from drew-gross/fix-adapters
Fix adapter imports
2016-03-29 14:08:46 -08:00
Drew Gross 342a3a92fe Fix adapter imports 2016-03-29 15:05:03 -07:00
Dan Huang 507fa54570 Merge branch 'master' into fix-authData-null-value 2016-03-29 14:43:19 -07:00
yuzeh 1b8e613dfb Clean authData of null values on _User update
Adds a step to the RestWrite#execute chain: it cleans the response
authData object of null values.

For example, this:

{"authData": {"anonymous": null}, "updatedAt", ...}

will be transformed to this:

{"updatedAt", ...}

And this:

{"authData": {"anonymous": null, "twitter": ...}, "updatedAt", ...}

will be transformed to this:

{"authData": {"twitter": ...}, "updatedAt", ...}

Fixing this issue will fix anonymous user upgrades from the Android SDK.
2016-03-29 14:42:37 -07:00
Florent Vilmart f259a3b17e Supports increment as well as Increment 2016-03-29 14:42:37 -07:00
André Luiz Cardoso 3dccd61222 Matching queries with doesNotExist constraint 2016-03-29 11:28:45 -03:00
Rajinder Ramgarhia cbf3f3c1be Spotify authentication 2016-03-28 02:07:10 -04:00
128 changed files with 12602 additions and 6323 deletions
+57 -7
View File
@@ -1,17 +1,67 @@
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.
Please read the following instructions carefully.
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!
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
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've met the prerequisites: https://github.com/ParsePlatform/parse-server/wiki/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 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=). Chances are that your issue has been reported or resolved before.
- [ ] 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.
#### Environment Setup
- [ ] You have filled out every section below. Issues without sufficient information are more likely to be closed.
--
#### Steps to reproduce
### Issue Description
[DELETE EVERYTHING ABOVE THIS LINE BEFORE SUBMITTING YOUR ISSUE]
#### Logs/Trace
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]
#### Actual Outcome
[FILL THIS OUT]
### Environment Setup
- **Server**
- parse-server version: [FILL THIS OUT]
- Operating System: [FILL THIS OUT]
- Hardware: [FILL THIS OUT]
- Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): [FILL THIS OUT]
- **Database**
- MongoDB version: [FILL THIS OUT]
- Storage engine: [FILL THIS OUT]
- Hardware: [FILL THIS OUT]
- Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): [FILL THIS OUT]
### Logs/Trace
You can turn on additional logging by configuring VERBOSE=1 in your environment.
[FILL THIS OUT]
+33 -10
View File
@@ -1,16 +1,39 @@
branches:
only:
- master
language: node_js
node_js:
- "4.3"
- '4.3'
- '6.1'
services:
- postgresql
addons:
postgresql: '9.4'
before_script:
- psql -c 'create database parse_server_postgres_adapter_test_database;' -U postgres
env:
global:
- COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**'
global:
- COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**'
matrix:
- MONGODB_VERSION=2.6.11
- MONGODB_VERSION=3.0.8
- PARSE_SERVER_TEST_DB=postgres
- MONGODB_VERSION=2.6.11
- MONGODB_VERSION=3.0.8
- MONGODB_VERSION=3.2.6
matrix:
fast_finish: true,
branches:
only:
- master
- /^[0-9]+.[0-9]+.[0-9]+(-.*)?$/
cache:
directories:
- $HOME/.mongodb/versions/downloads
after_success: ./node_modules/.bin/codecov
- "$HOME/.mongodb/versions/downloads"
after_script: "./node_modules/.bin/codecov"
deploy:
provider: npm
email:
secure: T3LwSJFcu632SDfCoavcKL39NktWLEOOFzonAjeHAglmMyDp9hvU8xHwQ4JJy2CRA03c+ezAe2gH3Va+jfxvh1JtFUan+p88vaCHHKuNwPSp4WZBPt1ZTDmG+ACG6j+ZWHK+eP7hLvtlCi/M9/v4/bVojpm7v01LLiM+MRhnE9E7eLemQC4zb6RVtf2oGQ3mX51kMeya218kEm5bsQPpRJElI9jwletFa8qYK5AFgaCHpll059RRHQTTE9MTNcN58P/Kf6Qb3KGpKNoaHTXoOD4U6LcXLWvdHONVB6AzGefxg2b/dvtaO4nd5lDooMBR3u9kWpopXkyAuf+fO/8je9JTxO6CDUtDjHRcR2mCEXWX6rculIAbNXOv1ccRYCTQ8aFtYnFlMSb1+QIAdyT6HHmeT5ktk3+XJRuEv0PJJvqHTo+j7tPngjiv5sPNutgGFlOFO90omTbzEkIT/D/LsgXbWm2QKYWejbLiuSINg+FiFiZN90doCf4aCpm2y1Q/9GzvK+eXcpqzPCGxOykE7EuPZK3+S/ZU2oaWL2uSpbKPtC0qxebrneR307aVEa7C85HCkTMMBzE4tjUr8h5HhLqtWvqmDcnBo3PfQhf9cEO6jQLd3qhEVJmcwKz/yOCfgPXlCbRqiKk7j94perpNCIalXtMI9ySYjJNjHsB4600=
api_key:
secure: WLjhHVAdUkDy6UdNWveTpZqphw9olN0alCpyPpU0cJjlN/hk7YxPP+YHOSVnMZaRZoHM0LL11jPKLf970nymBfvJHDWxKk71c/5xyTX8pBtSxmGmGy23a5g2VrHVMd2JxsI7NEAH500tlFz+01t4E3Steo0NpAkHR3Q51kj01gQy8IumsksfhRc9uvjZ7zjm99Yk4L+cxyei79v4esfpx7Bgm0HTfAAAG/19F+r0hvvFiV517SREDU/YtcX/rIjVepEy1uiLKEohnmYtVIRSA3Hjh6VlHVhdj2WDd/dYrxi/Ioysq2zOM9pZVLamO9asB1e6JrTEipWE9jSZXxsbnfFzuNLxcOjKv1wd3OzQKF/7pGKGiWRTv5Xm19D/FrXoE/ULs6bHcC/Ke8Gs2RxPlOCdvAFehCkyf5P4HOPCQdR7o8Yiuvt+5JWKBflElNbSd4nWgwUOo3Yv8vC4Vj53fwmM+Uqfu3IgYFWktYxCm4RWIKMfB/gtmjcF6QYFfjvEMwAvRfBV81kuynCsnubXWzQeBE/b3JOhBfpGciKCjKfy+tS6bZfFjCtQV98hMMiCPre8Y7PahHDYc65wU9Ake+ZE+dDaSbeV3DZ5JeifLJHzKW2J2dWeRANkOaiSwO9VBC9/rht5ulK5qQ1pB2+sKDToAaiKy6RMlB/HgIoYqsU=
on:
tags: true
all_branches: true
condition: "$MONGODB_VERSION = '3.0.8'"
repo: ParsePlatform/parse-server
+82
View File
@@ -0,0 +1,82 @@
# Upgrading Parse Server to version 2.3.0
Parse Server version 2.3.0 begins using unique indexes to ensure User's username and email are unique. This is not a backwards incompatable change, but it may in some cases cause a significant performance regression until the index finishes building. Building the unique index before upgrading your Parse Server version will eliminate the performance impact, and is a recommended step before upgrading any app to Parse Server 2.3.0. New apps starting with version 2.3.0 do not need to take any steps before beginning their project.
If you are using MongoDB in Cluster or Replica Set mode, we recommend reading Mongo's [documentation on index building](https://docs.mongodb.com/v3.0/tutorial/build-indexes-on-replica-sets/) first. If you are not using these features, you can execute the following commands from the Mongo shell to build the unique index. You may also want to create a backup first.
```js
// Select the database that your Parse App uses
use parse;
// Select the collection your Parse App uses for users. For migrated apps, this probably includes a collectionPrefix.
var coll = db['your_prefix:_User'];
// You can check if the indexes already exists by running coll.getIndexes()
coll.getIndexes();
// The indexes you want should look like this. If they already exist, you can skip creating them.
{
"v" : 1,
"unique" : true,
"key" : {
"username" : 1
},
"name" : "username_1",
"ns" : "parse.your_prefix:_User",
"background" : true,
"sparse" : true
}
{
"v" : 1,
"unique" : true,
"key" : {
"email" : 1
},
"name" : "email_1",
"ns" : "parse.your_prefix:_User",
"background" : true,
"sparse" : true
}
// Create the username index.
// "background: true" is mandatory and avoids downtime while the index builds.
// "sparse: true" is also mandatory because Parse Server uses sparse indexes.
coll.ensureIndex({ username: 1 }, { background: true, unique: true, sparse: true });
// Create the email index.
// "background: true" is still mandatory.
// "sparse: true" is also mandatory both because Parse Server uses sparse indexes, and because email addresses are not required by the Parse API.
coll.ensureIndex({ email: 1 }, { background: true, unique: true, sparse: true });
```
There are some issues you may run into during this process:
## Mongo complains that the index already exists, but with different options
In this case, you will need to remove the incorrect index. If your app relies on the existence of the index in order to be performant, you can create a new index, with "-1" for the direction of the field, so that it counts as different options. Then, drop the conflicting index, and create the unique index.
## There is already non-unique data in the username or email field
This is possible if you have explicitly set some user's emails to null. If this is bogus data, and those null fields shoud be unset, you can unset the null emails with this command. If your app relies on the difference between null and unset emails, you will need to upgrade your app to treat null and unset emails the same before building the index and upgrading to Parse Server 2.3.0.
```js
coll.update({ email: { $exists: true, $eq: null } }, { $unset: { email: '' } }, { multi: true })
```
## There is already non-unique data in the username or email field, and it's not nulls
This is possible due to a race condition in previous versions of Parse Server. If you have this problem, it is unlikely that you have a lot of rows with duplicate data. We recommend you clean up the data manually, by removing or modifying the offending rows.
This command, can be used to find the duplicate data:
```js
coll.aggregate([
{$match: {"username": {"$ne": null}}},
{$group: {_id: "$username", uniqueIds: {$addToSet: "$_id"}, count: {$sum: 1}}},
{$match: {count: {"$gt": 1}}},
{$project: {id: "$uniqueIds", username: "$_id", _id : 0} },
{$unwind: "$id" },
{$out: '_duplicates'} // Save the list of duplicates to a new, "_duplicates" collection. Remove this line to just output the list.
], {allowDiskUse:true})
```
+154 -1
View File
@@ -1,5 +1,158 @@
## Parse Server Changelog
### 2.2.14 (6/25/2016)
* Hotfix: Fix Parse.Cloud.HTTPResponse serialization
### 2.2.13 (6/12/2016)
* Hotfix: Pin version of deepcopy
### 2.2.12 (6/9/2016)
* New: Custom error codes in cloud code response.error, thanks to [Jeremy Pease](https://github.com/JeremyPlease)
* Fix: Crash in beforeSave when response is not an object, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Allow "get" on installations
* Fix: Fix overly restrictive Class Level Permissions, thanks to [Florent Vilmart](https://github.com/flovilmart)
* Fix: Fix nested date parsing in Cloud Code, thanks to [Marco Cheung](https://github.com/Marco129)
* Fix: Support very old file formats from Parse.com
### 2.2.11 (5/31/2016)
* Security: Censor user password in logs, thanks to [Marco Cheung](https://github.com/Marco129)
* New: Add PARSE_SERVER_LOGS_FOLDER env var for setting log folder, thanks to [KartikeyaRokde](https://github.com/KartikeyaRokde)
* New: Webhook key support, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Perf: Add cache adapter and default caching of certain objects, thanks to [Blayne Chard](https://github.com/blacha)
* Improvement: Better error messages for schema type mismatches, thanks to [Jeremy Pease](https://github.com/JeremyPlease)
* Improvement: Better error messages for reset password emails
* Improvement: Webhook key support in CLI, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Remove read only fields when using beforeSave, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Use content type provided by JS SDK, thanks to [Blayne Chard](https://github.com/blacha) and [Florent Vilmart](https://github.com/flovilmart)
* Fix: Tell the dashboard the stored push data is available, thanks to [Jeremy Pease](https://github.com/JeremyPlease)
* Fix: Add support for HTTP Basic Auth, thanks to [Hussam Moqhim](https://github.com/hmoqhim)
* Fix: Support for MongoDB version 3.2.6, (note: do not use MongoDB 3.2 with migrated apps that still have traffic on Parse.com), thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Prevent `pm2` from crashing when push notifications fail, thanks to [benishak](https://github.com/benishak)
* Fix: Add full list of default _Installation fields, thanks to [Jeremy Pease](https://github.com/JeremyPlease)
* Fix: Strip objectId out of hooks responses, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Fix external webhook response format, thanks to [Tyler Brock](https://github.com/TylerBrock)
* Fix: Fix beforeSave when object is passed to `success`, thanks to [Madhav Bhagat](https://github.com/codebreach)
* Fix: Remove use of deprecated APIs, thanks to [Emad Ehsan](https://github.com/emadehsan)
* Fix: Crash when multiple Parse Servers on the same machine try to write to the same logs folder, thanks to [Steven Shipton](https://github.com/steven-supersolid)
* Fix: Various issues with key names in `Parse.Object`s
* Fix: Treat Bytes type properly
* Fix: Caching bugs that caused writes by masterKey or other session token to not show up to users reading with a different session token
* Fix: Pin mongo driver version, preventing a regression in version 2.1.19
* Fix: Various issues with pointer fields not being treated properly
* Fix: Issues with pointed getting un-fetched due to changes in beforeSave
* Fix: Fixed crash when deleting classes that have CLPs
### 2.2.10 (5/15/2016)
* Fix: Write legacy ACLs to Mongo so that clients that still go through Parse.com can read them, thanks to [Tyler Brock](https://github.com/TylerBrock) and [carmenlau](https://github.com/carmenlau)
* Fix: Querying installations with limit = 0 and count = 1 now works, thanks to [ssk7833](https://github.com/ssk7833)
* Fix: Return correct error when violating unique index, thanks to [Marco Cheung](https://github.com/Marco129)
* Fix: Allow unsetting user's email, thanks to [Marco Cheung](https://github.com/Marco129)
* New: Support for Node 6.1
### 2.2.9 (5/9/2016)
* Fix: Fix a regression that caused Parse Server to crash when a null parameter is passed to a Cloud function
### 2.2.8 (5/8/2016)
* New: Support for Pointer Permissions
* New: Expose logger in Cloud Code
* New: Option to revoke sessions on password reset
* New: Option to expire inactive sessions
* Perf: Improvements in ACL checking query
* Fix: Issues when sending pushes to list of devices that contains invalid values
* Fix: Issues caused by using babel-polyfill outside of Parse Server, but in the same express app
* Fix: Remove creation of extra session tokens
* Fix: Return authData when querying with master key
* Fix: Bugs when deleting webhooks
* Fix: Ignore _RevocableSession header, which might be sent by the JS SDK
* Fix: Issues with querying via URL params
* Fix: Properly encode "Date" parameters to cloud code functions
### 2.2.7 (4/15/2016)
* Adds support for --verbose and verbose option when running ParseServer [\#1414](https://github.com/ParsePlatform/parse-server/pull/1414) ([flovilmart](https://github.com/flovilmart))
* Adds limit = 0 as a valid parameter for queries [\#1493](https://github.com/ParsePlatform/parse-server/pull/1493) ([seijiakiyama](https://github.com/seijiakiyama))
* Makes sure we preserve Installations when updating a token \(\#1475\) [\#1486](https://github.com/ParsePlatform/parse-server/pull/1486) ([flovilmart](https://github.com/flovilmart))
* Hotfix for tests [\#1503](https://github.com/ParsePlatform/parse-server/pull/1503) ([flovilmart](https://github.com/flovilmart))
* Enable logs [\#1502](https://github.com/ParsePlatform/parse-server/pull/1502) ([drew-gross](https://github.com/drew-gross))
* Do some triple equals for great justice [\#1499](https://github.com/ParsePlatform/parse-server/pull/1499) ([TylerBrock](https://github.com/TylerBrock))
* Apply credential stripping to all untransforms for \_User [\#1498](https://github.com/ParsePlatform/parse-server/pull/1498) ([TylerBrock](https://github.com/TylerBrock))
* Checking if object has defined key for Pointer constraints in liveQuery [\#1487](https://github.com/ParsePlatform/parse-server/pull/1487) ([simonas-notcat](https://github.com/simonas-notcat))
* Remove collection prefix and default mongo URI [\#1479](https://github.com/ParsePlatform/parse-server/pull/1479) ([drew-gross](https://github.com/drew-gross))
* Store collection prefix in mongo adapter, and clean up adapter interface [\#1472](https://github.com/ParsePlatform/parse-server/pull/1472) ([drew-gross](https://github.com/drew-gross))
* Move field deletion logic into mongo adapter [\#1471](https://github.com/ParsePlatform/parse-server/pull/1471) ([drew-gross](https://github.com/drew-gross))
* Adds support for Long and Double mongodb types \(fixes \#1316\) [\#1470](https://github.com/ParsePlatform/parse-server/pull/1470) ([flovilmart](https://github.com/flovilmart))
* Schema.js database agnostic [\#1468](https://github.com/ParsePlatform/parse-server/pull/1468) ([flovilmart](https://github.com/flovilmart))
* Remove console.log [\#1465](https://github.com/ParsePlatform/parse-server/pull/1465) ([drew-gross](https://github.com/drew-gross))
* Push status nits [\#1462](https://github.com/ParsePlatform/parse-server/pull/1462) ([flovilmart](https://github.com/flovilmart))
* Fixes \#1444 [\#1451](https://github.com/ParsePlatform/parse-server/pull/1451) ([flovilmart](https://github.com/flovilmart))
* Removing sessionToken and authData from \_User objects included in a query [\#1450](https://github.com/ParsePlatform/parse-server/pull/1450) ([simonas-notcat](https://github.com/simonas-notcat))
* Move mongo field type logic into mongoadapter [\#1432](https://github.com/ParsePlatform/parse-server/pull/1432) ([drew-gross](https://github.com/drew-gross))
* Prevents \_User lock out when setting ACL on signup or afterwards [\#1429](https://github.com/ParsePlatform/parse-server/pull/1429) ([flovilmart](https://github.com/flovilmart))
* Update .travis.yml [\#1428](https://github.com/ParsePlatform/parse-server/pull/1428) ([flovilmart](https://github.com/flovilmart))
* Adds relation fields to objects [\#1424](https://github.com/ParsePlatform/parse-server/pull/1424) ([flovilmart](https://github.com/flovilmart))
* Update .travis.yml [\#1423](https://github.com/ParsePlatform/parse-server/pull/1423) ([flovilmart](https://github.com/flovilmart))
* Sets the defaultSchemas keys in the SchemaCollection [\#1421](https://github.com/ParsePlatform/parse-server/pull/1421) ([flovilmart](https://github.com/flovilmart))
* Fixes \#1417 [\#1420](https://github.com/ParsePlatform/parse-server/pull/1420) ([drew-gross](https://github.com/drew-gross))
* Untransform should treat Array's as nested objects [\#1416](https://github.com/ParsePlatform/parse-server/pull/1416) ([blacha](https://github.com/blacha))
* Adds X-Parse-Push-Status-Id header [\#1412](https://github.com/ParsePlatform/parse-server/pull/1412) ([flovilmart](https://github.com/flovilmart))
* Schema format cleanup [\#1407](https://github.com/ParsePlatform/parse-server/pull/1407) ([drew-gross](https://github.com/drew-gross))
* Updates the publicServerURL option [\#1397](https://github.com/ParsePlatform/parse-server/pull/1397) ([flovilmart](https://github.com/flovilmart))
* Fix exception with non-expiring session tokens. [\#1386](https://github.com/ParsePlatform/parse-server/pull/1386) ([0x18B2EE](https://github.com/0x18B2EE))
* Move mongo schema format related logic into mongo adapter [\#1385](https://github.com/ParsePlatform/parse-server/pull/1385) ([drew-gross](https://github.com/drew-gross))
* WIP: Huge performance improvement on roles queries [\#1383](https://github.com/ParsePlatform/parse-server/pull/1383) ([flovilmart](https://github.com/flovilmart))
* Removes GCS Adapter from provided adapters [\#1339](https://github.com/ParsePlatform/parse-server/pull/1339) ([flovilmart](https://github.com/flovilmart))
* DBController refactoring [\#1228](https://github.com/ParsePlatform/parse-server/pull/1228) ([flovilmart](https://github.com/flovilmart))
* Spotify authentication [\#1226](https://github.com/ParsePlatform/parse-server/pull/1226) ([1nput0utput](https://github.com/1nput0utput))
* Expose DatabaseAdapter to simplify application tests [\#1121](https://github.com/ParsePlatform/parse-server/pull/1121) ([steven-supersolid](https://github.com/steven-supersolid))
### 2.2.6 (4/5/2016)
* Important Fix: Disables find on installation from clients [\#1374](https://github.com/ParsePlatform/parse-server/pull/1374) ([flovilmart](https://github.com/flovilmart))
* Adds missing options to the CLI [\#1368](https://github.com/ParsePlatform/parse-server/pull/1368) ([flovilmart](https://github.com/flovilmart))
* Removes only master on travis [\#1367](https://github.com/ParsePlatform/parse-server/pull/1367) ([flovilmart](https://github.com/flovilmart))
* Auth.\_loadRoles should not query the same role twice. [\#1366](https://github.com/ParsePlatform/parse-server/pull/1366) ([blacha](https://github.com/blacha))
### 2.2.5 (4/4/2016)
* Improves config loading and tests [\#1363](https://github.com/ParsePlatform/parse-server/pull/1363) ([flovilmart](https://github.com/flovilmart))
* Adds travis configuration to deploy NPM on new version tags [\#1361](https://github.com/ParsePlatform/parse-server/pull/1361) ([gfosco](https://github.com/gfosco))
* Inject the default schemas properties when loading it [\#1357](https://github.com/ParsePlatform/parse-server/pull/1357) ([flovilmart](https://github.com/flovilmart))
* Adds console transport when testing with VERBOSE=1 [\#1351](https://github.com/ParsePlatform/parse-server/pull/1351) ([flovilmart](https://github.com/flovilmart))
* Make notEqual work on relations [\#1350](https://github.com/ParsePlatform/parse-server/pull/1350) ([flovilmart](https://github.com/flovilmart))
* Accept only bool for $exists in LiveQuery [\#1315](https://github.com/ParsePlatform/parse-server/pull/1315) ([drew-gross](https://github.com/drew-gross))
* Adds more options when using CLI/config [\#1305](https://github.com/ParsePlatform/parse-server/pull/1305) ([flovilmart](https://github.com/flovilmart))
* Update error message [\#1297](https://github.com/ParsePlatform/parse-server/pull/1297) ([drew-gross](https://github.com/drew-gross))
* Properly let masterKey add fields [\#1291](https://github.com/ParsePlatform/parse-server/pull/1291) ([flovilmart](https://github.com/flovilmart))
* Point to \#1271 as how to write a good issue report [\#1290](https://github.com/ParsePlatform/parse-server/pull/1290) ([drew-gross](https://github.com/drew-gross))
* Adds ability to override mount with publicServerURL for production uses [\#1287](https://github.com/ParsePlatform/parse-server/pull/1287) ([flovilmart](https://github.com/flovilmart))
* Single object queries to use include and keys [\#1280](https://github.com/ParsePlatform/parse-server/pull/1280) ([jeremyjackson89](https://github.com/jeremyjackson89))
* Improves report for Push error in logs and \_PushStatus [\#1269](https://github.com/ParsePlatform/parse-server/pull/1269) ([flovilmart](https://github.com/flovilmart))
* Removes all stdout/err logs while testing [\#1268](https://github.com/ParsePlatform/parse-server/pull/1268) ([flovilmart](https://github.com/flovilmart))
* Matching queries with doesNotExist constraint [\#1250](https://github.com/ParsePlatform/parse-server/pull/1250) ([andrecardoso](https://github.com/andrecardoso))
* Added session length option for session tokens to server configuration [\#997](https://github.com/ParsePlatform/parse-server/pull/997) ([Kenishi](https://github.com/Kenishi))
* Regression test for \#1259 [\#1286](https://github.com/ParsePlatform/parse-server/pull/1286) ([drew-gross](https://github.com/drew-gross))
* Regression test for \#871 [\#1283](https://github.com/ParsePlatform/parse-server/pull/1283) ([drew-gross](https://github.com/drew-gross))
* Add a test to repro \#701 [\#1281](https://github.com/ParsePlatform/parse-server/pull/1281) ([drew-gross](https://github.com/drew-gross))
* Fix for \#1334: using relative cloud code files broken [\#1353](https://github.com/ParsePlatform/parse-server/pull/1353) ([airdrummingfool](https://github.com/airdrummingfool))
* Fix Issue/1288 [\#1346](https://github.com/ParsePlatform/parse-server/pull/1346) ([flovilmart](https://github.com/flovilmart))
* Fixes \#1271 [\#1295](https://github.com/ParsePlatform/parse-server/pull/1295) ([drew-gross](https://github.com/drew-gross))
* Fixes issue \#1302 [\#1314](https://github.com/ParsePlatform/parse-server/pull/1314) ([flovilmart](https://github.com/flovilmart))
* Fixes bug related to include in queries [\#1312](https://github.com/ParsePlatform/parse-server/pull/1312) ([flovilmart](https://github.com/flovilmart))
### 2.2.4 (3/29/2016)
* Hotfix: fixed imports issue for S3Adapter, GCSAdapter, FileSystemAdapter [\#1263](https://github.com/ParsePlatform/parse-server/pull/1263) ([drew-gross](https://github.com/drew-gross)
* Fix: Clean null authData values on _User update [\#1199](https://github.com/ParsePlatform/parse-server/pull/1199) ([yuzeh](https://github.com/yuzeh))
### 2.2.3 (3/29/2016)
* Fixed bug with invalid email verification link on email update. [\#1253](https://github.com/ParsePlatform/parse-server/pull/1253) ([kzielonka](https://github.com/kzielonka))
@@ -20,7 +173,7 @@
* Important Fix: Mounts createLiveQueryServer, fix babel induced problem [\#1153](https://github.com/ParsePlatform/parse-server/pull/1153) (flovilmart)
* Move ParseServer to it's own file [\#1166](https://github.com/ParsePlatform/parse-server/pull/1166) (flovilmart)
* Update README.md - remove deploy buttons - replace with community links [\#1139](https://github.com/ParsePlatform/parse-server/pull/1139) (drew-gross)
* Update README.md * remove deploy buttons * replace with community links [\#1139](https://github.com/ParsePlatform/parse-server/pull/1139) (drew-gross)
* Adds bootstrap.sh [\#1138](https://github.com/ParsePlatform/parse-server/pull/1138) (flovilmart)
* Fix: Do not override username [\#1142](https://github.com/ParsePlatform/parse-server/pull/1142) (flovilmart)
* Fix: Add pushId back to GCM payload [\#1168](https://github.com/ParsePlatform/parse-server/pull/1168) (wangmengyan95)
+24 -7
View File
@@ -10,6 +10,8 @@ Parse Server works with the Express web application framework. It can be added t
# Getting Started
April 2016 - We created a series of video screencasts, please check them out here: [http://blog.parse.com/learn/parse-server-video-series-april-2016/](http://blog.parse.com/learn/parse-server-video-series-april-2016/)
The fastest and easiest way to get started is to run MongoDB and Parse Server locally.
## Running Parse Server locally
@@ -104,7 +106,7 @@ Once you have a better understanding of how the project works, please refer to t
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:
* [Heroku and mLab](https://github.com/ParsePlatform/parse-server/wiki/Deploying-Parse-Server#deploying-to-heroku-and-mLab)
* [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)
* [Digital Ocean](https://www.digitalocean.com/community/tutorials/how-to-run-parse-server-on-ubuntu-14-04)
* [NodeChef](https://nodechef.com/blog/post/6/migrate-from-parse-to-nodechef%E2%80%99s-managed-parse-server)
@@ -147,7 +149,7 @@ The full documentation for Parse Server is available in the [wiki](https://githu
## 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://github.com/ParsePlatform/parse-server/wiki/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).
## Configuration
@@ -159,7 +161,7 @@ For the full list of available options, run `parse-server --help`.
* `appId` **(required)** - The application id to host with this server instance. You can use any arbitrary string. For migrated apps, this should match your hosted Parse app.
* `masterKey` **(required)** - The master key to use for overriding ACL security. You can use any arbitrary string. Keep it secret! For migrated apps, this should match your hosted Parse app.
* `databaseURI` **(required)** - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`.
* `databaseURI` **(required)** - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`. Be sure to [URL encode your password](https://app.zencoder.com/docs/guides/getting-started/special-characters-in-usernames-and-passwords) if your password has special characters.
* `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.
@@ -179,13 +181,24 @@ 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.
* `oauth` - Used to configure support for [3rd party authentication](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth).
* `oauth` - Used to configure support for [3rd party authentication](https://github.com/ParsePlatform/parse-server/wiki/OAuth).
* `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)).
* `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)).
* `databaseAdapter` - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`). Defaults to `MongoStorageAdapter`.
* `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.
##### Logging
Use the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server` to save your server logfiles to the specified folder.
Usage:
```
PARSE_SERVER_LOGS_FOLDER='<path-to-logs-folder>' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
```
##### Email verification and password reset
@@ -198,7 +211,8 @@ var server = ParseServer({
verifyUserEmails: true,
// The public URL of your app.
// This will appear in the link that is used to verify email addresses and reset passwords.
publicServerURL: 'https://example.com',
// Set the mount path as it is in serverURL
publicServerURL: 'https://example.com/parse',
// Your apps name. This will appear in the subject and body of the emails that are sent.
appName: 'Parse App',
// The email adapter
@@ -216,7 +230,7 @@ var server = ParseServer({
});
```
You can also use other email adapters contributed by the community such as [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter).
You can also use other email adapters contributed by the community such as [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter) or [parse-server-mandrill-adapter](https://github.com/back4app/parse-server-mandrill-adapter).
### Using environment variables to configure Parse Server
@@ -239,6 +253,9 @@ $ PORT=8080 parse-server --appId APPLICATION_ID --masterKey MASTER_KEY
For the full list of configurable environment variables, run `parse-server --help`.
### Available Adapters
[Parse Server Modules (Adapters)](https://github.com/parse-server-modules)
### Configuring File Adapters
Parse Server allows developers to choose from several options when hosting files:
+1 -1
View File
@@ -103,7 +103,7 @@ cat > ./config.json << EOF
"masterKey": "$MASTER_KEY",
"appName": "$APP_NAME",
"cloud": "./cloud/main",
"mongodbURI": "$MONGODB_URI"
"databaseURI": "$MONGODB_URI"
}
EOF
echo "${CHECK} Created config.json"
+44 -42
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "2.2.3",
"version": "2.2.14",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -18,54 +18,56 @@
],
"license": "BSD-3-Clause",
"dependencies": {
"babel-polyfill": "^6.5.0",
"babel-runtime": "^6.5.0",
"babel-polyfill": "6.8.0",
"babel-runtime": "6.6.1",
"bcrypt-nodejs": "0.0.3",
"body-parser": "^1.14.2",
"colors": "^1.1.2",
"commander": "^2.9.0",
"deepcopy": "^0.6.1",
"express": "^4.13.4",
"lru-cache": "^4.0.0",
"mailgun-js": "^0.7.7",
"mime": "^1.3.4",
"mongodb": "~2.1.0",
"multer": "^1.1.0",
"parse": "^1.8.0",
"parse-server-fs-adapter": "^1.0.0",
"parse-server-gcs-adapter": "^1.0.0",
"parse-server-push-adapter": "^1.0.0",
"parse-server-s3-adapter": "^1.0.0",
"parse-server-simple-mailgun-adapter": "^1.0.0",
"redis": "^2.5.0-1",
"request": "^2.65.0",
"tv4": "^1.2.7",
"winston": "^2.1.1",
"winston-daily-rotate-file": "^1.0.1",
"ws": "^1.0.1"
"body-parser": "1.15.2",
"colors": "1.1.2",
"commander": "2.9.0",
"deepcopy": "0.6.3",
"express": "4.14.0",
"intersect": "1.0.1",
"lodash": "4.12.0",
"lru-cache": "4.0.1",
"mailgun-js": "0.7.10",
"mime": "1.3.4",
"mongodb": "2.1.18",
"multer": "1.1.0",
"parse": "1.8.5",
"parse-server-fs-adapter": "1.0.0",
"parse-server-push-adapter": "1.0.4",
"parse-server-s3-adapter": "1.0.3",
"parse-server-simple-mailgun-adapter": "1.0.0",
"pg-promise": "4.8.0",
"redis": "2.6.2",
"request": "2.72.0",
"request-promise": "3.0.0",
"tv4": "1.2.7",
"winston": "2.2.0",
"winston-daily-rotate-file": "1.0.1",
"ws": "1.0.1"
},
"devDependencies": {
"babel-cli": "^6.5.1",
"babel-core": "^6.5.1",
"babel-istanbul": "^0.6.0",
"babel-plugin-syntax-flow": "^6.5.0",
"babel-plugin-transform-flow-strip-types": "^6.5.0",
"babel-preset-es2015": "^6.5.0",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.5.1",
"codecov": "^1.0.1",
"cross-env": "^1.0.7",
"deep-diff": "^0.3.3",
"flow-bin": "^0.22.0",
"gaze": "^0.5.2",
"jasmine": "^2.3.2",
"mongodb-runner": "3.1.15",
"nodemon": "^1.8.1"
"babel-cli": "6.8.0",
"babel-core": "6.10.4",
"babel-istanbul": "0.6.1",
"babel-plugin-syntax-flow": "6.8.0",
"babel-plugin-transform-flow-strip-types": "6.8.0",
"babel-preset-es2015": "6.6.0",
"babel-preset-stage-0": "6.5.0",
"babel-register": "6.8.0",
"codecov": "1.0.1",
"cross-env": "1.0.7",
"deep-diff": "0.3.4",
"gaze": "1.0.0",
"jasmine": "2.4.1",
"mongodb-runner": "3.3.2",
"nodemon": "1.9.2"
},
"scripts": {
"dev": "npm run build && node bin/dev",
"build": "./node_modules/.bin/babel src/ -d lib/",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./node_modules/.bin/mongodb-runner start",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 ./node_modules/.bin/mongodb-runner start",
"test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js",
"test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js && npm run posttest",
"posttest": "./node_modules/.bin/mongodb-runner stop",
+8 -9
View File
@@ -10,7 +10,6 @@ MockController.prototype = Object.create(AdaptableController.prototype);
MockController.prototype.constructor = AdaptableController;
describe("AdaptableController", ()=>{
it("should use the provided adapter", (done) => {
var adapter = new FilesAdapter();
var controller = new FilesController(adapter);
@@ -22,7 +21,7 @@ describe("AdaptableController", ()=>{
expect(controller.adapter).toBe(adapter);
done();
});
it("should throw when creating a new mock controller", (done) => {
var adapter = new FilesAdapter();
expect(() => {
@@ -30,7 +29,7 @@ describe("AdaptableController", ()=>{
}).toThrow();
done();
});
it("should fail setting the wrong adapter to the controller", (done) => {
function WrongAdapter() {};
var adapter = new FilesAdapter();
@@ -41,7 +40,7 @@ describe("AdaptableController", ()=>{
}).toThrow();
done();
});
it("should fail to instantiate a controller with wrong adapter", (done) => {
function WrongAdapter() {};
var adapter = new WrongAdapter();
@@ -50,14 +49,14 @@ describe("AdaptableController", ()=>{
}).toThrow();
done();
});
it("should fail to instantiate a controller without an adapter", (done) => {
expect(() => {
new FilesController();
}).toThrow();
done();
});
it("should accept an object adapter", (done) => {
var adapter = {
createFile: function(config, filename, data) { },
@@ -70,18 +69,18 @@ describe("AdaptableController", ()=>{
}).not.toThrow();
done();
});
it("should accept an object adapter", (done) => {
function AGoodAdapter() {};
AGoodAdapter.prototype.createFile = function(config, filename, data) { };
AGoodAdapter.prototype.deleteFile = function(config, filename) { };
AGoodAdapter.prototype.getFileData = function(config, filename) { };
AGoodAdapter.prototype.getFileLocation = function(config, filename) { };
var adapter = new AGoodAdapter();
expect(() => {
new FilesController(adapter);
}).not.toThrow();
done();
});
});
});
-10
View File
@@ -2,7 +2,6 @@
var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
var FilesAdapter = require("parse-server-fs-adapter").default;
var S3Adapter = require("parse-server-s3-adapter").default;
var GCSAdapter = require("parse-server-gcs-adapter").default;
var ParsePushAdapter = require("parse-server-push-adapter").default;
describe("AdapterLoader", ()=>{
@@ -114,13 +113,4 @@ describe("AdapterLoader", ()=>{
}).not.toThrow();
done();
})
it("should load GCSAdapter from direct passing", (done) => {
var gcsAdapter = new GCSAdapter("projectId", "path/to/keyfile", "bucket")
expect(() => {
var adapter = loadAdapter(gcsAdapter, FilesAdapter);
expect(adapter).toBe(gcsAdapter);
}).not.toThrow();
done();
})
});
+83
View File
@@ -0,0 +1,83 @@
describe('Auth', () => {
var Auth = require('../src/Auth.js').Auth;
describe('getUserRoles', () => {
var auth;
var config;
var cacheController;
var currentRoles = null;
var currentUserId = 'userId';
beforeEach(() => {
currentRoles = ['role:userId'];
config = {
cacheController: {
role: {
get: () => Promise.resolve(currentRoles),
set: jasmine.createSpy('set')
}
}
}
spyOn(config.cacheController.role, 'get').and.callThrough();
auth = new Auth({
config: config,
isMaster: false,
user: {
id: currentUserId
},
installationId: 'installationId'
});
});
it('should get user roles from the cache', (done) => {
auth.getUserRoles()
.then((roles) => {
var firstSet = config.cacheController.role.set.calls.first();
expect(firstSet).toEqual(undefined);
var firstGet = config.cacheController.role.get.calls.first();
expect(firstGet.args[0]).toEqual(currentUserId);
expect(roles).toEqual(currentRoles);
done();
});
});
it('should only query the roles once', (done) => {
var loadRolesSpy = spyOn(auth, '_loadRoles').and.callThrough();
auth.getUserRoles()
.then((roles) => {
expect(roles).toEqual(currentRoles);
return auth.getUserRoles()
})
.then((roles) => auth.getUserRoles())
.then((roles) => auth.getUserRoles())
.then((roles) => {
// Should only call the cache adapter once.
expect(config.cacheController.role.get.calls.count()).toEqual(1);
expect(loadRolesSpy.calls.count()).toEqual(1);
var firstGet = config.cacheController.role.get.calls.first();
expect(firstGet.args[0]).toEqual(currentUserId);
expect(roles).toEqual(currentRoles);
done();
});
});
it('should not have any roles with no user', (done) => {
auth.user = null
auth.getUserRoles()
.then((roles) => expect(roles).toEqual([]))
.then(() => done());
});
it('should not have any user roles with master', (done) => {
auth.isMaster = true
auth.getUserRoles()
.then((roles) => expect(roles).toEqual([]))
.then(() => done());
})
});
});
+59 -8
View File
@@ -1,3 +1,4 @@
'use strict';
var commander = require("../src/cli/utils/commander").default;
var definitions = {
@@ -11,7 +12,7 @@ var definitions = {
action: function(value) {
var value = parseInt(value);
if (!Number.isInteger(value)) {
throw "port is invalid";
throw "arg2 is invalid";
}
return value;
}
@@ -23,7 +24,6 @@ var definitions = {
}
describe("commander additions", () => {
afterEach((done) => {
commander.options = [];
delete commander.arg0;
@@ -33,7 +33,7 @@ describe("commander additions", () => {
delete commander.arg4;
done();
})
it("should load properly definitions from args", (done) => {
commander.loadDefinitions(definitions);
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "--arg1", "arg1Value", "--arg2", "2", "--arg3", "some"]);
@@ -44,7 +44,7 @@ describe("commander additions", () => {
expect(commander.arg4).toEqual("arg4Value");
done();
});
it("should load properly definitions from env", (done) => {
commander.loadDefinitions(definitions);
commander.parse([], {
@@ -58,7 +58,7 @@ describe("commander additions", () => {
expect(commander.arg4).toEqual("arg4Value");
done();
});
it("should load properly use args over env", (done) => {
commander.loadDefinitions(definitions);
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "--arg4", "anotherArg4"], {
@@ -72,7 +72,7 @@ describe("commander additions", () => {
expect(commander.arg4).toEqual("anotherArg4");
done();
});
it("should fail in action as port is invalid", (done) => {
commander.loadDefinitions(definitions);
expect(()=> {
@@ -81,7 +81,58 @@ describe("commander additions", () => {
"PROGRAM_ARG_1": "arg1ENVValue",
"PROGRAM_ARG_2": "hello",
});
}).toThrow("port is invalid");
}).toThrow("arg2 is invalid");
done();
});
});
it("should not override config.json", (done) => {
commander.loadDefinitions(definitions);
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "./spec/configs/CLIConfig.json"], {
"PROGRAM_ARG_0": "arg0ENVValue",
"PROGRAM_ARG_1": "arg1ENVValue",
});
let options = commander.getOptions();
expect(options.arg2).toBe(8888);
expect(options.arg3).toBe("hello"); //config value
expect(options.arg4).toBe('/1');
done();
});
it("should fail with invalid values in JSON", (done) => {
commander.loadDefinitions(definitions);
expect(() => {
commander.parse(["node","./CLI.spec.js","--arg0", "arg0Value", "./spec/configs/CLIConfigFail.json"], {
"PROGRAM_ARG_0": "arg0ENVValue",
"PROGRAM_ARG_1": "arg1ENVValue",
});
}).toThrow("arg2 is invalid")
done();
});
it("should fail when too many apps are set", (done) => {
commander.loadDefinitions(definitions);
expect(() => {
commander.parse(["node","./CLI.spec.js","./spec/configs/CLIConfigFailTooManyApps.json"]);
}).toThrow("Multiple apps are not supported")
done();
});
it("should load config from apps", (done) => {
commander.loadDefinitions(definitions);
commander.parse(["node", "./CLI.spec.js", "./spec/configs/CLIConfigApps.json"]);
let options = commander.getOptions();
expect(options.arg1).toBe("my_app");
expect(options.arg2).toBe(8888);
expect(options.arg3).toBe("hello"); //config value
expect(options.arg4).toBe('/1');
done();
});
it("should fail when passing an invalid arguement", (done) => {
commander.loadDefinitions(definitions);
expect(() => {
commander.parse(["node", "./CLI.spec.js", "./spec/configs/CLIConfigUnknownArg.json"]);
}).toThrow('error: unknown option myArg')
done();
});
});
+74
View File
@@ -0,0 +1,74 @@
var CacheController = require('../src/Controllers/CacheController.js').default;
describe('CacheController', function() {
var FakeCacheAdapter;
var FakeAppID = 'foo';
var KEY = 'hello';
beforeEach(() => {
FakeCacheAdapter = {
get: () => Promise.resolve(null),
put: jasmine.createSpy('put'),
del: jasmine.createSpy('del'),
clear: jasmine.createSpy('clear')
}
spyOn(FakeCacheAdapter, 'get').and.callThrough();
});
it('should expose role and user caches', (done) => {
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
expect(cache.role).not.toEqual(null);
expect(cache.role.get).not.toEqual(null);
expect(cache.user).not.toEqual(null);
expect(cache.user.get).not.toEqual(null);
done();
});
['role', 'user'].forEach((cacheName) => {
it('should prefix ' + cacheName + ' cache', () => {
var cache = new CacheController(FakeCacheAdapter, FakeAppID)[cacheName];
cache.put(KEY, 'world');
var firstPut = FakeCacheAdapter.put.calls.first();
expect(firstPut.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
cache.get(KEY);
var firstGet = FakeCacheAdapter.get.calls.first();
expect(firstGet.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
cache.del(KEY);
var firstDel = FakeCacheAdapter.del.calls.first();
expect(firstDel.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':'));
});
});
it('should clear the entire cache', () => {
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
cache.clear();
expect(FakeCacheAdapter.clear.calls.count()).toEqual(1);
cache.user.clear();
expect(FakeCacheAdapter.clear.calls.count()).toEqual(2);
cache.role.clear();
expect(FakeCacheAdapter.clear.calls.count()).toEqual(3);
});
it('should handle cache rejections', (done) => {
FakeCacheAdapter.get = () => Promise.reject();
var cache = new CacheController(FakeCacheAdapter, FakeAppID);
cache.get('foo').then(done, () => {
fail('Promise should not be rejected.');
});
});
});
-1
View File
@@ -2,7 +2,6 @@ var Client = require('../src/LiveQuery/Client').Client;
var ParseWebSocket = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocket;
describe('Client', function() {
it('can be initialized', function() {
var parseWebSocket = new ParseWebSocket({});
var client = new Client(1, parseWebSocket);
+669
View File
@@ -0,0 +1,669 @@
"use strict"
const Parse = require("parse/node");
const request = require('request');
const rp = require('request-promise');
const InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').InMemoryCacheAdapter;
describe('Cloud Code', () => {
it('can load absolute cloud code file', done => {
reconfigureServer({ cloud: __dirname + '/cloud/cloudCodeRelativeFile.js' })
.then(() => {
Parse.Cloud.run('cloudCodeInFile', {}, result => {
expect(result).toEqual('It is possible to define cloud code in a file.');
done();
});
})
});
it('can load relative cloud code file', done => {
reconfigureServer({ cloud: './spec/cloud/cloudCodeAbsoluteFile.js' })
.then(() => {
Parse.Cloud.run('cloudCodeInFile', {}, result => {
expect(result).toEqual('It is possible to define cloud code in a file.');
done();
});
})
});
it('can create functions', done => {
Parse.Cloud.define('hello', (req, res) => {
res.success('Hello world!');
});
Parse.Cloud.run('hello', {}, result => {
expect(result).toEqual('Hello world!');
done();
});
});
it('is cleared cleared after the previous test', done => {
Parse.Cloud.run('hello', {})
.catch(error => {
expect(error.code).toEqual(141);
done();
});
});
it('basic beforeSave rejection', function(done) {
Parse.Cloud.beforeSave('BeforeSaveFail', function(req, res) {
res.error('You shall not pass!');
});
var obj = new Parse.Object('BeforeSaveFail');
obj.set('foo', 'bar');
obj.save().then(() => {
fail('Should not have been able to save BeforeSaveFailure class.');
done();
}, () => {
done();
})
});
it('beforeSave rejection with custom error code', function(done) {
Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function (req, res) {
res.error(999, 'Nope');
});
var obj = new Parse.Object('BeforeSaveFailWithErrorCode');
obj.set('foo', 'bar');
obj.save().then(function() {
fail('Should not have been able to save BeforeSaveFailWithErrorCode class.');
done();
}, function(error) {
expect(error.code).toEqual(999);
expect(error.message).toEqual('Nope');
done();
});
});
it('basic beforeSave rejection via promise', function(done) {
Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', function (req, res) {
var query = new Parse.Query('Yolo');
query.find().then(() => {
res.error('Nope');
}, () => {
res.success();
});
});
var obj = new Parse.Object('BeforeSaveFailWithPromise');
obj.set('foo', 'bar');
obj.save().then(function() {
fail('Should not have been able to save BeforeSaveFailure class.');
done();
}, function(error) {
expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED);
expect(error.message).toEqual('Nope');
done();
})
});
it('test beforeSave changed object success', function(done) {
Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) {
req.object.set('foo', 'baz');
res.success();
});
var obj = new Parse.Object('BeforeSaveChanged');
obj.set('foo', 'bar');
obj.save().then(function() {
var query = new Parse.Query('BeforeSaveChanged');
query.get(obj.id).then(function(objAgain) {
expect(objAgain.get('foo')).toEqual('baz');
done();
}, function(error) {
fail(error);
done();
});
}, function(error) {
fail(error);
done();
});
});
it('test beforeSave returns value on create and update', (done) => {
Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) {
req.object.set('foo', 'baz');
res.success();
});
var obj = new Parse.Object('BeforeSaveChanged');
obj.set('foo', 'bing');
obj.save().then(() => {
expect(obj.get('foo')).toEqual('baz');
obj.set('foo', 'bar');
return obj.save().then(() => {
expect(obj.get('foo')).toEqual('baz');
done();
})
})
});
it('test afterSave ran and created an object', function(done) {
Parse.Cloud.afterSave('AfterSaveTest', function(req) {
var obj = new Parse.Object('AfterSaveProof');
obj.set('proof', req.object.id);
obj.save();
});
var obj = new Parse.Object('AfterSaveTest');
obj.save();
setTimeout(function() {
var query = new Parse.Query('AfterSaveProof');
query.equalTo('proof', obj.id);
query.find().then(function(results) {
expect(results.length).toEqual(1);
done();
}, function(error) {
fail(error);
done();
});
}, 500);
});
it('test beforeSave happens on update', function(done) {
Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) {
req.object.set('foo', 'baz');
res.success();
});
var obj = new Parse.Object('BeforeSaveChanged');
obj.set('foo', 'bar');
obj.save().then(function() {
obj.set('foo', 'bar');
return obj.save();
}).then(function() {
var query = new Parse.Query('BeforeSaveChanged');
return query.get(obj.id).then(function(objAgain) {
expect(objAgain.get('foo')).toEqual('baz');
done();
});
}, function(error) {
fail(error);
done();
});
});
it('test beforeDelete failure', function(done) {
Parse.Cloud.beforeDelete('BeforeDeleteFail', function(req, res) {
res.error('Nope');
});
var obj = new Parse.Object('BeforeDeleteFail');
var id;
obj.set('foo', 'bar');
obj.save().then(() => {
id = obj.id;
return obj.destroy();
}).then(() => {
fail('obj.destroy() should have failed, but it succeeded');
done();
}, (error) => {
expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED);
expect(error.message).toEqual('Nope');
var objAgain = new Parse.Object('BeforeDeleteFail', {objectId: id});
return objAgain.fetch();
}).then((objAgain) => {
if (objAgain) {
expect(objAgain.get('foo')).toEqual('bar');
} else {
fail("unable to fetch the object ", id);
}
done();
}, (error) => {
// We should have been able to fetch the object again
fail(error);
});
});
it('basic beforeDelete rejection via promise', function(done) {
Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', function (req, res) {
var query = new Parse.Query('Yolo');
query.find().then(() => {
res.error('Nope');
}, () => {
res.success();
});
});
var obj = new Parse.Object('BeforeDeleteFailWithPromise');
obj.set('foo', 'bar');
obj.save().then(function() {
fail('Should not have been able to save BeforeSaveFailure class.');
done();
}, function(error) {
expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED);
expect(error.message).toEqual('Nope');
done();
})
});
it('test afterDelete ran and created an object', function(done) {
Parse.Cloud.afterDelete('AfterDeleteTest', function(req) {
var obj = new Parse.Object('AfterDeleteProof');
obj.set('proof', req.object.id);
obj.save();
});
var obj = new Parse.Object('AfterDeleteTest');
obj.save().then(function() {
obj.destroy();
});
setTimeout(function() {
var query = new Parse.Query('AfterDeleteProof');
query.equalTo('proof', obj.id);
query.find().then(function(results) {
expect(results.length).toEqual(1);
done();
}, function(error) {
fail(error);
done();
});
}, 500);
});
it('test cloud function return types', function(done) {
Parse.Cloud.define('foo', function(req, res) {
res.success({
object: {
__type: 'Object',
className: 'Foo',
objectId: '123',
x: 2,
relation: {
__type: 'Object',
className: 'Bar',
objectId: '234',
x: 3
}
},
array: [{
__type: 'Object',
className: 'Bar',
objectId: '345',
x: 2
}],
a: 2
});
});
Parse.Cloud.run('foo').then((result) => {
expect(result.object instanceof Parse.Object).toBeTruthy();
if (!result.object) {
fail("Unable to run foo");
done();
return;
}
expect(result.object.className).toEqual('Foo');
expect(result.object.get('x')).toEqual(2);
var bar = result.object.get('relation');
expect(bar instanceof Parse.Object).toBeTruthy();
expect(bar.className).toEqual('Bar');
expect(bar.get('x')).toEqual(3);
expect(Array.isArray(result.array)).toEqual(true);
expect(result.array[0] instanceof Parse.Object).toBeTruthy();
expect(result.array[0].get('x')).toEqual(2);
done();
});
});
it('test cloud function request params types', function(done) {
Parse.Cloud.define('params', function(req, res) {
expect(req.params.date instanceof Date).toBe(true);
expect(req.params.date.getTime()).toBe(1463907600000);
expect(req.params.dateList[0] instanceof Date).toBe(true);
expect(req.params.dateList[0].getTime()).toBe(1463907600000);
expect(req.params.complexStructure.date[0] instanceof Date).toBe(true);
expect(req.params.complexStructure.date[0].getTime()).toBe(1463907600000);
expect(req.params.complexStructure.deepDate.date[0] instanceof Date).toBe(true);
expect(req.params.complexStructure.deepDate.date[0].getTime()).toBe(1463907600000);
expect(req.params.complexStructure.deepDate2[0].date instanceof Date).toBe(true);
expect(req.params.complexStructure.deepDate2[0].date.getTime()).toBe(1463907600000);
return res.success({});
});
let params = {
'date': {
'__type': 'Date',
'iso': '2016-05-22T09:00:00.000Z'
},
'dateList': [
{
'__type': 'Date',
'iso': '2016-05-22T09:00:00.000Z'
}
],
'lol': 'hello',
'complexStructure': {
'date': [
{
'__type': 'Date',
'iso': '2016-05-22T09:00:00.000Z'
}
],
'deepDate': {
'date': [
{
'__type': 'Date',
'iso': '2016-05-22T09:00:00.000Z'
}
]
},
'deepDate2': [
{
'date': {
'__type': 'Date',
'iso': '2016-05-22T09:00:00.000Z'
}
}
]
}
};
Parse.Cloud.run('params', params).then((result) => {
done();
});
});
it('test cloud function should echo keys', function(done) {
Parse.Cloud.define('echoKeys', function(req, res){
return res.success({
applicationId: Parse.applicationId,
masterKey: Parse.masterKey,
javascriptKey: Parse.javascriptKey
})
});
Parse.Cloud.run('echoKeys').then((result) => {
expect(result.applicationId).toEqual(Parse.applicationId);
expect(result.masterKey).toEqual(Parse.masterKey);
expect(result.javascriptKey).toEqual(Parse.javascriptKey);
done();
});
});
it('should properly create an object in before save', done => {
Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) {
req.object.set('foo', 'baz');
res.success();
});
Parse.Cloud.define('createBeforeSaveChangedObject', function(req, res){
var obj = new Parse.Object('BeforeSaveChanged');
obj.save().then(() => {
res.success(obj);
})
})
Parse.Cloud.run('createBeforeSaveChangedObject').then((res) => {
expect(res.get('foo')).toEqual('baz');
done();
});
});
it('dirtyKeys are set on update', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.beforeSave('GameScore', (req, res) => {
var object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
expect(object.get('fooAgain')).toEqual('barAgain');
if (triggerTime == 0) {
// Create
expect(object.get('foo')).toEqual('bar');
} else if (triggerTime == 1) {
// Update
expect(object.dirtyKeys()).toEqual(['foo']);
expect(object.dirty('foo')).toBeTruthy();
expect(object.get('foo')).toEqual('baz');
} else {
res.error();
}
triggerTime++;
res.success();
});
let obj = new Parse.Object('GameScore');
obj.set('foo', 'bar');
obj.set('fooAgain', 'barAgain');
obj.save().then(() => {
// We only update foo
obj.set('foo', 'baz');
return obj.save();
}).then(() => {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
done();
}, function(error) {
fail(error);
done();
});
});
it('test beforeSave unchanged success', function(done) {
Parse.Cloud.beforeSave('BeforeSaveUnchanged', function(req, res) {
res.success();
});
var obj = new Parse.Object('BeforeSaveUnchanged');
obj.set('foo', 'bar');
obj.save().then(function() {
done();
}, function(error) {
fail(error);
done();
});
});
it('test beforeDelete success', function(done) {
Parse.Cloud.beforeDelete('BeforeDeleteTest', function(req, res) {
res.success();
});
var obj = new Parse.Object('BeforeDeleteTest');
obj.set('foo', 'bar');
obj.save().then(function() {
return obj.destroy();
}).then(function() {
var objAgain = new Parse.Object('BeforeDeleteTest', obj.id);
return objAgain.fetch().then(fail, done);
}, function(error) {
fail(error);
done();
});
});
it('test save triggers get user', function(done) {
Parse.Cloud.beforeSave('SaveTriggerUser', function(req, res) {
if (req.user && req.user.id) {
res.success();
} else {
res.error('No user present on request object for beforeSave.');
}
});
Parse.Cloud.afterSave('SaveTriggerUser', function(req) {
if (!req.user || !req.user.id) {
console.log('No user present on request object for afterSave.');
}
});
var user = new Parse.User();
user.set("password", "asdf");
user.set("email", "asdf@example.com");
user.set("username", "zxcv");
user.signUp(null, {
success: function() {
var obj = new Parse.Object('SaveTriggerUser');
obj.save().then(function() {
done();
}, function(error) {
fail(error);
done();
});
}
});
});
it('beforeSave change propagates through the save response', (done) => {
Parse.Cloud.beforeSave('ChangingObject', function(request, response) {
request.object.set('foo', 'baz');
response.success();
});
let obj = new Parse.Object('ChangingObject');
obj.save({ foo: 'bar' }).then((objAgain) => {
expect(objAgain.get('foo')).toEqual('baz');
done();
}, (e) => {
fail('Should not have failed to save.');
done();
});
});
it('test cloud function parameter validation success', (done) => {
// Register a function with validation
Parse.Cloud.define('functionWithParameterValidation', (req, res) => {
res.success('works');
}, (request) => {
return request.params.success === 100;
});
Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => {
done();
}, (e) => {
fail('Validation should not have failed.');
done();
});
});
it('doesnt receive stale user in cloud code functions after user has been updated with master key (regression test for #1836)', done => {
Parse.Cloud.define('testQuery', function(request, response) {
response.success(request.user.get('data'));
});
Parse.User.signUp('user', 'pass')
.then(user => {
user.set('data', 'AAA');
return user.save();
})
.then(() => Parse.Cloud.run('testQuery'))
.then(result => {
expect(result).toEqual('AAA');
Parse.User.current().set('data', 'BBB');
return Parse.User.current().save(null, {useMasterKey: true});
})
.then(() => Parse.Cloud.run('testQuery'))
.then(result => {
expect(result).toEqual('BBB');
done();
});
});
it('clears out the user cache for all sessions when the user is changed', done => {
let session1;
let session2;
let user;
const cacheAdapter = new InMemoryCacheAdapter({ ttl: 100000000 });
reconfigureServer({ cacheAdapter })
.then(() => {
Parse.Cloud.define('checkStaleUser', (request, response) => {
response.success(request.user.get('data'));
});
user = new Parse.User();
user.set('username', 'test');
user.set('password', 'moon-y');
user.set('data', 'first data');
return user.signUp();
})
.then(user => {
session1 = user.getSessionToken();
return rp({
uri: 'http://localhost:8378/1/login?username=test&password=moon-y',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
})
})
.then(body => {
session2 = body.sessionToken;
//Ensure both session tokens are in the cache
return Parse.Cloud.run('checkStaleUser')
})
.then(() => rp({
method: 'POST',
uri: 'http://localhost:8378/1/functions/checkStaleUser',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Session-Token': session2,
}
}))
.then(() => Parse.Promise.all([cacheAdapter.get('test:user:' + session1), cacheAdapter.get('test:user:' + session2)]))
.then(cachedVals => {
expect(cachedVals[0].objectId).toEqual(user.id);
expect(cachedVals[1].objectId).toEqual(user.id);
//Change with session 1 and then read with session 2.
user.set('data', 'second data');
return user.save()
})
.then(() => rp({
method: 'POST',
uri: 'http://localhost:8378/1/functions/checkStaleUser',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Session-Token': session2,
}
}))
.then(body => {
expect(body.result).toEqual('second data');
done();
})
.catch(error => {
fail(JSON.stringify(error));
done();
});
});
it('trivial beforeSave should not affect fetched pointers (regression test for #1238)', done => {
Parse.Cloud.beforeSave('BeforeSaveUnchanged', (req, res) => {
res.success();
});
var TestObject = Parse.Object.extend("TestObject");
var NoBeforeSaveObject = Parse.Object.extend("NoBeforeSave");
var BeforeSaveObject = Parse.Object.extend("BeforeSaveUnchanged");
var aTestObject = new TestObject();
aTestObject.set("foo", "bar");
aTestObject.save()
.then(aTestObject => {
var aNoBeforeSaveObj = new NoBeforeSaveObject();
aNoBeforeSaveObj.set("aTestObject", aTestObject);
expect(aNoBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar");
return aNoBeforeSaveObj.save();
})
.then(aNoBeforeSaveObj => {
expect(aNoBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar");
var aBeforeSaveObj = new BeforeSaveObject();
aBeforeSaveObj.set("aTestObject", aTestObject);
expect(aBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar");
return aBeforeSaveObj.save();
})
.then(aBeforeSaveObj => {
expect(aBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar");
done();
});
});
});
+58
View File
@@ -0,0 +1,58 @@
'use strict';
var LoggerController = require('../src/Controllers/LoggerController').LoggerController;
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
describe("Cloud Code Logger", () => {
it("should expose log to functions", (done) => {
var logController = new LoggerController(new FileLoggerAdapter());
Parse.Cloud.define("loggerTest", (req, res) => {
req.log.info('logTest', 'info log', {info: 'some log' });
req.log.error('logTest','error log', {error: 'there was an error'});
res.success({});
});
Parse.Cloud.run('loggerTest').then(() => {
return logController.getLogs({from: Date.now() - 500, size: 1000});
}).then((res) => {
expect(res.length).not.toBe(0);
let lastLogs = res.slice(0, 2);
let errorMessage = lastLogs[0];
let infoMessage = lastLogs[1];
expect(errorMessage.level).toBe('error');
expect(errorMessage.error).toBe('there was an error');
expect(errorMessage.message).toBe('logTest error log');
expect(infoMessage.level).toBe('info');
expect(infoMessage.info).toBe('some log');
expect(infoMessage.message).toBe('logTest info log');
done();
});
});
it("should expose log to trigger", (done) => {
var logController = new LoggerController(new FileLoggerAdapter());
Parse.Cloud.beforeSave("MyObject", (req, res) => {
req.log.info('beforeSave MyObject', 'info log', {info: 'some log' });
req.log.error('beforeSave MyObject','error log', {error: 'there was an error'});
res.success({});
});
let obj = new Parse.Object('MyObject');
obj.save().then(() => {
return logController.getLogs({from: Date.now() - 500, size: 1000})
}).then((res) => {
expect(res.length).not.toBe(0);
let lastLogs = res.slice(0, 2);
let errorMessage = lastLogs[0];
let infoMessage = lastLogs[1];
expect(errorMessage.level).toBe('error');
expect(errorMessage.error).toBe('there was an error');
expect(errorMessage.message).toBe('beforeSave MyObject error log');
expect(infoMessage.level).toBe('info');
expect(infoMessage.info).toBe('some log');
expect(infoMessage.message).toBe('beforeSave MyObject info log');
done();
});
});
});
-23
View File
@@ -1,23 +0,0 @@
'use strict';
let DatabaseAdapter = require('../src/DatabaseAdapter');
describe('DatabaseAdapter', () => {
it('options and URI are available to adapter', done => {
DatabaseAdapter.setAppDatabaseURI('optionsTest', 'mongodb://localhost:27017/optionsTest');
DatabaseAdapter.setAppDatabaseOptions('optionsTest', {foo: "bar"});
let optionsTestDatabaseConnection = DatabaseAdapter.getDatabaseConnection('optionsTest');
expect(optionsTestDatabaseConnection instanceof Object).toBe(true);
expect(optionsTestDatabaseConnection.adapter._options instanceof Object).toBe(true);
expect(optionsTestDatabaseConnection.adapter._options.foo).toBe("bar");
DatabaseAdapter.setAppDatabaseURI('noOptionsTest', 'mongodb://localhost:27017/noOptionsTest');
let noOptionsTestDatabaseConnection = DatabaseAdapter.getDatabaseConnection('noOptionsTest');
expect(noOptionsTestDatabaseConnection instanceof Object).toBe(true);
expect(noOptionsTestDatabaseConnection.adapter._options instanceof Object).toBe(false);
done();
});
});
-17
View File
@@ -1,17 +0,0 @@
'use strict';
let DatabaseController = require('../src/Controllers/DatabaseController');
let MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
describe('DatabaseController', () => {
it('can be constructed', done => {
let adapter = new MongoStorageAdapter('mongodb://localhost:27017/test');
let databaseController = new DatabaseController(adapter, {
collectionPrefix: 'test_'
});
databaseController.connect().then(done, error => {
console.log('error', error.stack);
fail();
});
});
});
-1
View File
@@ -1,7 +1,6 @@
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
describe('EventEmitterPubSub', function() {
it('can publish and subscribe', function() {
var publisher = EventEmitterPubSub.createPublisher();
var subscriber = EventEmitterPubSub.createSubscriber();
+58 -4
View File
@@ -1,13 +1,16 @@
'use strict';
var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter;
var Parse = require('parse/node').Parse;
var request = require('request');
describe('info logs', () => {
it("Verify INFO logs", (done) => {
var fileLoggerAdapter = new FileLoggerAdapter();
fileLoggerAdapter.info('testing info logs', () => {
fileLoggerAdapter.query({
size: 1,
from: new Date(Date.now() - 500),
size: 100,
level: 'info'
}, (results) => {
if(results.length == 0) {
@@ -23,12 +26,12 @@ describe('info logs', () => {
});
describe('error logs', () => {
it("Verify ERROR logs", (done) => {
var fileLoggerAdapter = new FileLoggerAdapter();
fileLoggerAdapter.error('testing error logs', () => {
fileLoggerAdapter.query({
size: 1,
from: new Date(Date.now() - 500),
size: 100,
level: 'error'
}, (results) => {
if(results.length == 0) {
@@ -43,3 +46,54 @@ describe('error logs', () => {
});
});
});
describe('verbose logs', () => {
it("mask sensitive information in _User class", (done) => {
reconfigureServer({ verbose: true })
.then(() => createTestUser())
.then(() => {
let fileLoggerAdapter = new FileLoggerAdapter();
return fileLoggerAdapter.query({
from: new Date(Date.now() - 500),
size: 100,
level: 'verbose'
});
}).then((results) => {
expect(results[1].message.includes('"password": "********"')).toEqual(true);
var headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
};
request.get({
headers: headers,
url: 'http://localhost:8378/1/login?username=test&password=moon-y'
}, (error, response, body) => {
let fileLoggerAdapter = new FileLoggerAdapter();
return fileLoggerAdapter.query({
from: new Date(Date.now() - 500),
size: 100,
level: 'verbose'
}).then((results) => {
expect(results[1].message.includes('password=********')).toEqual(true);
done();
});
});
});
});
it("should not mask information in non _User class", (done) => {
let obj = new Parse.Object('users');
obj.set('password', 'pw');
obj.save().then(() => {
let fileLoggerAdapter = new FileLoggerAdapter();
return fileLoggerAdapter.query({
from: new Date(Date.now() - 500),
size: 100,
level: 'verbose'
});
}).then((results) => {
expect(results[1].message.includes('"password": "pw"')).toEqual(true);
done();
});
});
});
+95 -29
View File
@@ -1,6 +1,7 @@
'use strict';
var httpRequest = require("../src/cloud-code/httpRequest"),
HTTPResponse = require('../src/cloud-code/HTTPResponse').default,
bodyParser = require('body-parser'),
express = require("express");
@@ -36,7 +37,6 @@ app.listen(13371);
describe("httpRequest", () => {
it("should do /hello", (done) => {
httpRequest({
url: httpRequestServer+"/hello"
@@ -51,7 +51,7 @@ describe("httpRequest", () => {
done();
})
});
it("should do /hello with callback and promises", (done) => {
var calls = 0;
httpRequest({
@@ -70,7 +70,7 @@ describe("httpRequest", () => {
done();
})
});
it("should do not follow redirects by default", (done) => {
httpRequest({
@@ -83,7 +83,7 @@ describe("httpRequest", () => {
done();
})
});
it("should follow redirects when set", (done) => {
httpRequest({
@@ -100,17 +100,17 @@ describe("httpRequest", () => {
done();
})
});
it("should fail on 404", (done) => {
var calls = 0;
httpRequest({
url: httpRequestServer+"/404",
success: function() {
success: function() {
calls++;
fail("should not succeed");
done();
},
error: function(httpResponse) {
error: function(httpResponse) {
calls++;
expect(calls).toBe(1);
expect(httpResponse.status).toBe(404);
@@ -121,7 +121,7 @@ describe("httpRequest", () => {
}
});
})
it("should fail on 404", (done) => {
httpRequest({
url: httpRequestServer+"/404",
@@ -136,7 +136,7 @@ describe("httpRequest", () => {
done();
})
})
it("should post on echo", (done) => {
var calls = 0;
httpRequest({
@@ -160,27 +160,27 @@ describe("httpRequest", () => {
done();
})
});
it("should encode a query string body by default", (done) => {
let options = {
body: {"foo": "bar"},
body: {"foo": "bar"},
}
let result = httpRequest.encodeBody(options);
expect(result.body).toEqual('foo=bar');
expect(result.headers['Content-Type']).toEqual('application/x-www-form-urlencoded');
done();
})
it("should encode a JSON body", (done) => {
let options = {
body: {"foo": "bar"},
body: {"foo": "bar"},
headers: {'Content-Type': 'application/json'}
}
let result = httpRequest.encodeBody(options);
expect(result.body).toEqual('{"foo":"bar"}');
done();
})
it("should encode a www-form body", (done) => {
let options = {
@@ -193,7 +193,7 @@ describe("httpRequest", () => {
});
it("should not encode a wrong content type", (done) => {
let options = {
body:{"foo": "bar", "bar": "baz"},
body:{"foo": "bar", "bar": "baz"},
headers: {'cOntent-tYpe': 'mime/jpeg'}
}
let result = httpRequest.encodeBody(options);
@@ -204,28 +204,17 @@ describe("httpRequest", () => {
it("should fail gracefully", (done) => {
httpRequest({
url: "http://not a good url",
success: function() {
success: function() {
fail("should not succeed");
done();
},
error: function(error) {
error: function(error) {
expect(error).not.toBeUndefined();
expect(error).not.toBeNull();
done();
}
});
});
it('should get a cat image', (done) => {
httpRequest({
url: 'http://thecatapi.com/api/images/get?format=src&type=jpg',
followRedirects: true
}).then((res) => {
expect(res.buffer).not.toBe(null);
expect(res.text).not.toBe(null);
done();
})
})
it("should params object to query string", (done) => {
httpRequest({
@@ -257,4 +246,81 @@ describe("httpRequest", () => {
})
});
it('should not crash with undefined body', () => {
let httpResponse = new HTTPResponse({});
expect(httpResponse.body).toBeUndefined();
expect(httpResponse.data).toBeUndefined();
expect(httpResponse.text).toBeUndefined();
expect(httpResponse.buffer).toBeUndefined();
});
it('serialized httpResponse correctly with body string', () => {
let httpResponse = new HTTPResponse({}, 'hello');
expect(httpResponse.text).toBe('hello');
expect(httpResponse.data).toBe(undefined);
expect(httpResponse.body).toBe('hello');
let serialized = JSON.stringify(httpResponse);
let result = JSON.parse(serialized);
expect(result.text).toBe('hello');
expect(result.data).toBe(undefined);
expect(result.body).toBe(undefined);
});
it('serialized httpResponse correctly with body object', () => {
let httpResponse = new HTTPResponse({}, {foo: "bar"});
let encodedResponse = Parse._encode(httpResponse);
let serialized = JSON.stringify(httpResponse);
let result = JSON.parse(serialized);
expect(httpResponse.text).toEqual('{"foo":"bar"}');
expect(httpResponse.data).toEqual({foo: 'bar'});
expect(httpResponse.body).toEqual({foo: 'bar'});
expect(result.text).toEqual('{"foo":"bar"}');
expect(result.data).toEqual({foo: 'bar'});
expect(result.body).toEqual(undefined);
});
it('serialized httpResponse correctly with body buffer string', () => {
let httpResponse = new HTTPResponse({}, new Buffer('hello'));
expect(httpResponse.text).toBe('hello');
expect(httpResponse.data).toBe(undefined);
let serialized = JSON.stringify(httpResponse);
let result = JSON.parse(serialized);
expect(result.text).toBe('hello');
expect(result.data).toBe(undefined);
});
it('serialized httpResponse correctly with body buffer JSON Object', () => {
let json = '{"foo":"bar"}';
let httpResponse = new HTTPResponse({}, new Buffer(json));
let serialized = JSON.stringify(httpResponse);
let result = JSON.parse(serialized);
expect(result.text).toEqual('{"foo":"bar"}');
expect(result.data).toEqual({foo: 'bar'});
});
it('serialized httpResponse with Parse._encode should be allright', () => {
let json = '{"foo":"bar"}';
let httpResponse = new HTTPResponse({}, new Buffer(json));
let encoded = Parse._encode(httpResponse);
let foundData, foundText, foundBody = false;
for(var key in encoded) {
if (key == 'data') {
foundData = true;
}
if (key == 'text') {
foundText = true;
}
if (key == 'body') {
foundBody = true;
}
}
expect(foundData).toBe(true);
expect(foundText).toBe(true);
expect(foundBody).toBe(false);
});
});
+74
View File
@@ -0,0 +1,74 @@
const InMemoryCache = require('../src/Adapters/Cache/InMemoryCache').default;
describe('InMemoryCache', function() {
var BASE_TTL = {
ttl: 10
};
var NO_EXPIRE_TTL = {
ttl: NaN
};
var KEY = 'hello';
var KEY_2 = KEY + '_2';
var VALUE = 'world';
function wait(sleep) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, sleep);
})
}
it('should destroy a expire items in the cache', (done) => {
var cache = new InMemoryCache(BASE_TTL);
cache.put(KEY, VALUE);
var value = cache.get(KEY);
expect(value).toEqual(VALUE);
wait(BASE_TTL.ttl * 5).then(() => {
value = cache.get(KEY)
expect(value).toEqual(null);
done();
});
});
it('should delete items', (done) => {
var cache = new InMemoryCache(NO_EXPIRE_TTL);
cache.put(KEY, VALUE);
cache.put(KEY_2, VALUE);
expect(cache.get(KEY)).toEqual(VALUE);
expect(cache.get(KEY_2)).toEqual(VALUE);
cache.del(KEY);
expect(cache.get(KEY)).toEqual(null);
expect(cache.get(KEY_2)).toEqual(VALUE);
cache.del(KEY_2);
expect(cache.get(KEY)).toEqual(null);
expect(cache.get(KEY_2)).toEqual(null);
done();
});
it('should clear all items', (done) => {
var cache = new InMemoryCache(NO_EXPIRE_TTL);
cache.put(KEY, VALUE);
cache.put(KEY_2, VALUE);
expect(cache.get(KEY)).toEqual(VALUE);
expect(cache.get(KEY_2)).toEqual(VALUE);
cache.clear();
expect(cache.get(KEY)).toEqual(null);
expect(cache.get(KEY_2)).toEqual(null);
done();
});
it('should deafult TTL to 5 seconds', () => {
var cache = new InMemoryCache({});
expect(cache.ttl).toEqual(5 * 1000);
});
});
+59
View File
@@ -0,0 +1,59 @@
var InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').default;
describe('InMemoryCacheAdapter', function() {
var KEY = 'hello';
var VALUE = 'world';
function wait(sleep) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, sleep);
})
}
it('should expose promisifyed methods', (done) => {
var cache = new InMemoryCacheAdapter({
ttl: NaN
});
var noop = () => {};
// Verify all methods return promises.
Promise.all([
cache.put(KEY, VALUE),
cache.del(KEY),
cache.get(KEY),
cache.clear()
]).then(() => {
done();
});
});
it('should get/set/clear', (done) => {
var cache = new InMemoryCacheAdapter({
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 InMemoryCacheAdapter({
ttl: 10
});
cache.put(KEY, VALUE)
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(VALUE))
.then(wait.bind(null, 50))
.then(() => cache.get(KEY))
.then((value) => expect(value).toEqual(null))
.then(done);
})
});
+172
View File
@@ -0,0 +1,172 @@
var auth = require('../src/Auth');
var Config = require('../src/Config');
var rest = require('../src/rest');
var InstallationsRouter = require('../src/Routers/InstallationsRouter').InstallationsRouter;
var config = new Config('test');
describe('InstallationsRouter', () => {
it('uses find condition from request.body', (done) => {
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {
where: {
deviceType: 'android'
}
},
query: {}
};
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
}).then(() => {
return router.handleFind(request);
}).then((res) => {
var results = res.response.results;
expect(results.length).toEqual(1);
done();
});
});
it('uses find condition from request.query', (done) => {
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
where: {
deviceType: 'android'
}
}
};
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
}).then(() => {
return router.handleFind(request);
}).then((res) => {
var results = res.response.results;
expect(results.length).toEqual(1);
done();
});
});
it('query installations with limit = 0', (done) => {
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
limit: 0
}
};
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
}).then(() => {
return router.handleFind(request);
}).then((res) => {
var response = res.response;
expect(response.results.length).toEqual(0);
done();
});
});
it('query installations with count = 1', done => {
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
count: 1
}
};
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest))
.then(() => router.handleFind(request))
.then((res) => {
var response = res.response;
expect(response.results.length).toEqual(2);
expect(response.count).toEqual(2);
done();
})
.catch(error => {
fail(JSON.stringify(error));
done();
})
});
it('query installations with limit = 0 and count = 1', (done) => {
var androidDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android'
};
var iosDeviceRequest = {
'installationId': '12345678-abcd-abcd-abcd-123456789abd',
'deviceType': 'ios'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
limit: 0,
count: 1
}
};
var router = new InstallationsRouter();
rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest);
}).then(() => {
return router.handleFind(request);
}).then((res) => {
var response = res.response;
expect(response.results.length).toEqual(0);
expect(response.count).toEqual(2);
done();
});
});
});
+69
View File
@@ -0,0 +1,69 @@
var middlewares = require('../src/middlewares');
var AppCache = require('../src/cache').AppCache;
describe('middlewares', () => {
var fakeReq, fakeRes;
beforeEach(() => {
fakeReq = {
originalUrl: 'http://example.com/parse/',
url: 'http://example.com/',
body: {
_ApplicationId: 'FakeAppId'
},
headers: {},
get: (key) => {
return fakeReq.headers[key.toLowerCase()]
}
};
AppCache.put(fakeReq.body._ApplicationId, {});
});
afterEach(() => {
AppCache.del(fakeReq.body._ApplicationId);
});
it('should use _ContentType if provided', (done) => {
expect(fakeReq.headers['content-type']).toEqual(undefined);
var contentType = 'image/jpeg';
fakeReq.body._ContentType = contentType;
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.headers['content-type']).toEqual(contentType);
expect(fakeReq.body._ContentType).toEqual(undefined);
done()
});
});
const BodyParams = {
clientVersion: '_ClientVersion',
installationId: '_InstallationId',
sessionToken: '_SessionToken',
masterKey: '_MasterKey',
javascriptKey: '_JavaScriptKey'
};
const BodyKeys = Object.keys(BodyParams);
BodyKeys.forEach((infoKey) => {
const bodyKey = BodyParams[infoKey];
const keyValue = 'Fake' + bodyKey;
// javascriptKey is the only one that gets defaulted,
const otherKeys = BodyKeys.filter((otherKey) => otherKey !== infoKey && otherKey !== 'javascriptKey');
it(`it should pull ${bodyKey} into req.info`, (done) => {
fakeReq.body[bodyKey] = keyValue;
middlewares.handleParseHeaders(fakeReq, fakeRes, () => {
expect(fakeReq.body[bodyKey]).toEqual(undefined);
expect(fakeReq.info[infoKey]).toEqual(keyValue);
otherKeys.forEach((otherKey) => {
expect(fakeReq.info[otherKey]).toEqual(undefined);
});
done();
});
});
});
});
+73
View File
@@ -0,0 +1,73 @@
'use strict';
const MongoSchemaCollection = require('../src/Adapters/Storage/Mongo/MongoSchemaCollection').default;
describe('MongoSchemaCollection', () => {
it('can transform legacy _client_permissions keys to parse format', done => {
expect(MongoSchemaCollection._TESTmongoSchemaToParseSchema({
"_id":"_Installation",
"_client_permissions":{
"get":true,
"find":true,
"update":true,
"create":true,
"delete":true,
},
"_metadata":{
"class_permissions":{
"get":{"*":true},
"find":{"*":true},
"update":{"*":true},
"create":{"*":true},
"delete":{"*":true},
"addField":{"*":true},
}
},
"installationId":"string",
"deviceToken":"string",
"deviceType":"string",
"channels":"array",
"user":"*_User",
"pushType":"string",
"GCMSenderId":"string",
"timeZone":"string",
"localeIdentifier":"string",
"badge":"number",
"appVersion":"string",
"appName":"string",
"appIdentifier":"string",
"parseVersion":"string",
})).toEqual({
className: '_Installation',
fields: {
installationId: { type: 'String' },
deviceToken: { type: 'String' },
deviceType: { type: 'String' },
channels: { type: 'Array' },
user: { type: 'Pointer', targetClass: '_User' },
pushType: { type: 'String' },
GCMSenderId: { type: 'String' },
timeZone: { type: 'String' },
localeIdentifier: { type: 'String' },
badge: { type: 'Number' },
appVersion: { type: 'String' },
appName: { type: 'String' },
appIdentifier: { type: 'String' },
parseVersion: { type: 'String' },
ACL: { type: 'ACL' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
objectId: { type: 'String' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
}
});
done();
});
});
+124 -6
View File
@@ -2,12 +2,22 @@
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
const MongoClient = require('mongodb').MongoClient;
const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
// These tests are specific to the mongo storage adapter + mongo storage format
// and will eventually be moved into their own repo
describe('MongoStorageAdapter', () => {
beforeEach(done => {
new MongoStorageAdapter({ uri: databaseURI })
.deleteAllClasses()
.then(done, fail);
});
it('auto-escapes symbols in auth information', () => {
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
new MongoStorageAdapter('mongodb://user!with@+ symbols:password!with@+ symbols@localhost:1234/parse', {})
.connect();
new MongoStorageAdapter({
uri: 'mongodb://user!with@+ symbols:password!with@+ symbols@localhost:1234/parse'
}).connect();
expect(MongoClient.connect).toHaveBeenCalledWith(
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
jasmine.any(Object)
@@ -16,8 +26,9 @@ describe('MongoStorageAdapter', () => {
it("doesn't double escape already URI-encoded information", () => {
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
new MongoStorageAdapter('mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse', {})
.connect();
new MongoStorageAdapter({
uri: 'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse'
}).connect();
expect(MongoClient.connect).toHaveBeenCalledWith(
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
jasmine.any(Object)
@@ -27,11 +38,118 @@ describe('MongoStorageAdapter', () => {
// https://github.com/ParsePlatform/parse-server/pull/148#issuecomment-180407057
it('preserves replica sets', () => {
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null));
new MongoStorageAdapter('mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415', {})
.connect();
new MongoStorageAdapter({
uri: 'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415'
}).connect();
expect(MongoClient.connect).toHaveBeenCalledWith(
'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415',
jasmine.any(Object)
);
});
it('stores objectId in _id', done => {
let adapter = new MongoStorageAdapter({ uri: databaseURI });
adapter.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
.then(() => adapter._rawFind('Foo', {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj._id).toEqual('abcde');
expect(obj.objectId).toBeUndefined();
done();
});
});
it('stores pointers with a _p_ prefix', (done) => {
let obj = {
objectId: 'bar',
aPointer: {
__type: 'Pointer',
className: 'JustThePointer',
objectId: 'qwerty'
}
};
let adapter = new MongoStorageAdapter({ uri: databaseURI });
adapter.createObject('APointerDarkly', { fields: {
objectId: { type: 'String' },
aPointer: { type: 'Pointer', targetClass: 'JustThePointer' },
}}, obj)
.then(() => adapter._rawFind('APointerDarkly', {}))
.then(results => {
expect(results.length).toEqual(1);
let output = results[0];
expect(typeof output._id).toEqual('string');
expect(typeof output._p_aPointer).toEqual('string');
expect(output._p_aPointer).toEqual('JustThePointer$qwerty');
expect(output.aPointer).toBeUndefined();
done();
});
});
it('handles object and subdocument', done => {
let adapter = new MongoStorageAdapter({ uri: databaseURI });
let schema = { fields : { subdoc: { type: 'Object' } } };
let obj = { subdoc: {foo: 'bar', wu: 'tan'} };
adapter.createObject('MyClass', schema, obj)
.then(() => adapter._rawFind('MyClass', {}))
.then(results => {
expect(results.length).toEqual(1);
let mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('tan');
let obj = { 'subdoc.wu': 'clan' };
return adapter.findOneAndUpdate('MyClass', schema, {}, obj);
})
.then(() => adapter._rawFind('MyClass', {}))
.then(results => {
expect(results.length).toEqual(1);
let mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('clan');
done();
});
});
it('handles array, object, date', (done) => {
let adapter = new MongoStorageAdapter({ uri: databaseURI });
let obj = {
array: [1, 2, 3],
object: {foo: 'bar'},
date: {
__type: 'Date',
iso: '2016-05-26T20:55:01.154Z',
},
};
let schema = { fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
} };
adapter.createObject('MyClass', schema, obj)
.then(() => adapter._rawFind('MyClass', {}))
.then(results => {
expect(results.length).toEqual(1);
let mob = results[0];
expect(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date instanceof Date).toBe(true);
return adapter.find('MyClass', schema, {}, {});
})
.then(results => {
expect(results.length).toEqual(1);
let mob = results[0];
expect(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date.__type).toBe('Date');
expect(mob.date.iso).toBe('2016-05-26T20:55:01.154Z');
done();
})
.catch(error => {
console.log(error);
fail();
done();
});
});
});
+253
View File
@@ -0,0 +1,253 @@
// These tests are unit tests designed to only test transform.js.
"use strict";
let transform = require('../src/Adapters/Storage/Mongo/MongoTransform');
let dd = require('deep-diff');
let mongodb = require('mongodb');
describe('parseObjectToMongoObjectForCreate', () => {
it('a basic number', (done) => {
var input = {five: 5};
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
fields: {five: {type: 'Number'}}
});
jequal(input, output);
done();
});
it('an object with null values', (done) => {
var input = {objectWithNullValues: {isNull: null, notNull: 3}};
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
fields: {objectWithNullValues: {type: 'object'}}
});
jequal(input, output);
done();
});
it('built-in timestamps', (done) => {
var input = {
createdAt: "2015-10-06T21:24:50.332Z",
updatedAt: "2015-10-06T21:24:50.332Z"
};
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
expect(output._created_at instanceof Date).toBe(true);
expect(output._updated_at instanceof Date).toBe(true);
done();
});
it('array of pointers', (done) => {
var pointer = {
__type: 'Pointer',
objectId: 'myId',
className: 'Blah',
};
var out = transform.parseObjectToMongoObjectForCreate(null, {pointers: [pointer]},{
fields: {pointers: {type: 'Array'}}
});
jequal([pointer], out.pointers);
done();
});
//TODO: object creation requests shouldn't be seeing __op delete, it makes no sense to
//have __op delete in a new object. Figure out what this should actually be testing.
xit('a delete op', (done) => {
var input = {deleteMe: {__op: 'Delete'}};
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
jequal(output, {});
done();
});
it('Doesnt allow ACL, as Parse Server should tranform ACL to _wperm + _rperm', done => {
var input = {ACL: {'0123': {'read': true, 'write': true}}};
expect(() => transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} })).toThrow();
done();
});
it('plain', (done) => {
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
var out = transform.parseObjectToMongoObjectForCreate(null, {location: geoPoint},{
fields: {location: {type: 'GeoPoint'}}
});
expect(out.location).toEqual([180, -180]);
done();
});
it('in array', (done) => {
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
var out = transform.parseObjectToMongoObjectForCreate(null, {locations: [geoPoint, geoPoint]},{
fields: {locations: {type: 'Array'}}
});
expect(out.locations).toEqual([geoPoint, geoPoint]);
done();
});
it('in sub-object', (done) => {
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
var out = transform.parseObjectToMongoObjectForCreate(null, { locations: { start: geoPoint }},{
fields: {locations: {type: 'Object'}}
});
expect(out).toEqual({ locations: { start: geoPoint } });
done();
});
it('objectId', (done) => {
var out = transform.transformWhere(null, {objectId: 'foo'});
expect(out._id).toEqual('foo');
done();
});
it('objectId in a list', (done) => {
var input = {
objectId: {'$in': ['one', 'two', 'three']},
};
var output = transform.transformWhere(null, input);
jequal(input.objectId, output._id);
done();
});
it('built-in timestamps', (done) => {
var input = {createdAt: new Date(), updatedAt: new Date()};
var output = transform.mongoObjectToParseObject(null, input, { fields: {} });
expect(typeof output.createdAt).toEqual('string');
expect(typeof output.updatedAt).toEqual('string');
done();
});
it('pointer', (done) => {
var input = {_p_userPointer: '_User$123'};
var output = transform.mongoObjectToParseObject(null, input, {
fields: { userPointer: { type: 'Pointer', targetClass: '_User' } },
});
expect(typeof output.userPointer).toEqual('object');
expect(output.userPointer).toEqual(
{__type: 'Pointer', className: '_User', objectId: '123'}
);
done();
});
it('null pointer', (done) => {
var input = {_p_userPointer: null};
var output = transform.mongoObjectToParseObject(null, input, {
fields: { userPointer: { type: 'Pointer', targetClass: '_User' } },
});
expect(output.userPointer).toBeUndefined();
done();
});
it('file', (done) => {
var input = {picture: 'pic.jpg'};
var output = transform.mongoObjectToParseObject(null, input, {
fields: { picture: { type: 'File' }},
});
expect(typeof output.picture).toEqual('object');
expect(output.picture).toEqual({__type: 'File', name: 'pic.jpg'});
done();
});
it('geopoint', (done) => {
var input = {location: [180, -180]};
var output = transform.mongoObjectToParseObject(null, input, {
fields: { location: { type: 'GeoPoint' }},
});
expect(typeof output.location).toEqual('object');
expect(output.location).toEqual(
{__type: 'GeoPoint', longitude: 180, latitude: -180}
);
done();
});
it('nested array', (done) => {
var input = {arr: [{_testKey: 'testValue' }]};
var output = transform.mongoObjectToParseObject(null, input, {
fields: { arr: { type: 'Array' } },
});
expect(Array.isArray(output.arr)).toEqual(true);
expect(output.arr).toEqual([{ _testKey: 'testValue'}]);
done();
});
it('untransforms objects containing nested special keys', done => {
let input = {array: [{
_id: "Test ID",
_hashed_password: "I Don't know why you would name a key this, but if you do it should work",
_tombstone: {
_updated_at: "I'm sure people will nest keys like this",
_acl: 7,
_id: { someString: "str", someNumber: 7},
regularKey: { moreContents: [1, 2, 3] },
},
regularKey: "some data",
}]}
let output = transform.mongoObjectToParseObject(null, input, {
fields: { array: { type: 'Array' }},
});
expect(dd(output, input)).toEqual(undefined);
done();
});
it('changes new pointer key', (done) => {
var input = {
somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'}
};
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
fields: {somePointer: {type: 'Pointer'}}
});
expect(typeof output._p_somePointer).toEqual('string');
expect(output._p_somePointer).toEqual('Micro$oft');
done();
});
it('changes existing pointer keys', (done) => {
var input = {
userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'}
};
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
fields: {userPointer: {type: 'Pointer'}}
});
expect(typeof output._p_userPointer).toEqual('string');
expect(output._p_userPointer).toEqual('_User$qwerty');
done();
});
it('writes the old ACL format in addition to rperm and wperm', (done) => {
var input = {
_rperm: ['*'],
_wperm: ['Kevin'],
};
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
expect(typeof output._acl).toEqual('object');
expect(output._acl["Kevin"].w).toBeTruthy();
expect(output._acl["Kevin"].r).toBeUndefined();
done();
})
it('untransforms from _rperm and _wperm to ACL', (done) => {
var input = {
_rperm: ["*"],
_wperm: ["Kevin"]
};
var output = transform.mongoObjectToParseObject(null, input, { fields: {} });
expect(output._rperm).toEqual(['*']);
expect(output._wperm).toEqual(['Kevin']);
expect(output.ACL).toBeUndefined()
done();
});
it('untransforms mongodb number types', (done) => {
var input = {
long: mongodb.Long.fromNumber(Number.MAX_SAFE_INTEGER),
double: new mongodb.Double(Number.MAX_VALUE)
}
var output = transform.mongoObjectToParseObject(null, input, {
fields: {
long: { type: 'Number' },
double: { type: 'Number' },
},
});
expect(output.long).toBe(Number.MAX_SAFE_INTEGER);
expect(output.double).toBe(Number.MAX_VALUE);
done();
});
});
+34 -36
View File
@@ -1,9 +1,9 @@
var OAuth = require("../src/authDataManager/OAuth1Client");
var request = require('request');
var Config = require("../src/Config");
var defaultColumns = require('../src/Controllers/SchemaController').defaultColumns;
describe('OAuth', function() {
it("Nonce should have right length", (done) => {
jequal(OAuth.nonce().length, 30);
done();
@@ -217,50 +217,47 @@ describe('OAuth', function() {
return request.post(options, callback);
}
it("should create user with REST API", (done) => {
it_exclude_dbs(['postgres'])("should create user with REST API", done => {
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
ok(b.sessionToken);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
var sessionToken = b.sessionToken;
var q = new Parse.Query("_Session");
q.equalTo('sessionToken', sessionToken);
q.first({useMasterKey: true}).then((res) => {
expect(res.get("installationId")).toEqual('yolo');
done();
}).fail((err) => {
fail('should not fail fetching the session');
done();
})
});
expect(error).toBe(null);
var b = JSON.parse(body);
ok(b.sessionToken);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
var sessionToken = b.sessionToken;
var q = new Parse.Query("_Session");
q.equalTo('sessionToken', sessionToken);
q.first({useMasterKey: true}).then((res) => {
expect(res.get("installationId")).toEqual('yolo');
done();
}).fail((err) => {
fail('should not fail fetching the session');
done();
})
});
});
it("should only create a single user with REST API", (done) => {
it_exclude_dbs(['postgres'])("should only create a single user with REST API", (done) => {
var objectId;
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
objectId = b.objectId;
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
objectId = b.objectId;
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
expect(b.objectId).toBe(objectId);
done();
});
expect(b.objectId).toBe(objectId);
done();
});
});
});
it("unlink and link with custom provider", (done) => {
it_exclude_dbs(['postgres'])("unlink and link with custom provider", (done) => {
var provider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("myoauth", {
@@ -284,9 +281,10 @@ describe('OAuth', function() {
"Expiration should be cleared");
// make sure the auth data is properly deleted
var config = new Config(Parse.applicationId);
config.database.mongoFind('_User', {
_id: model.id
}).then((res) => {
config.database.adapter.find('_User', {
fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation),
}, { objectId: model.id }, {})
.then(res => {
expect(res.length).toBe(1);
expect(res[0]._auth_data_myoauth).toBeUndefined();
expect(res[0]._auth_data_myoauth).not.toBeNull();
+90 -16
View File
@@ -1,8 +1,8 @@
'use strict';
let request = require('request');
describe('Parse.Push', () => {
var setup = function() {
var pushAdapter = {
send: function(body, installations) {
@@ -27,29 +27,30 @@ describe('Parse.Push', () => {
}
}
setServerConfiguration({
return reconfigureServer({
appId: Parse.applicationId,
masterKey: Parse.masterKey,
serverURL: Parse.serverURL,
push: {
adapter: pushAdapter
}
})
.then(() => {
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);
}
return Parse.Object.saveAll(installations);
});
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);
}
return Parse.Object.saveAll(installations);
}
it('should properly send push', (done) => {
it_exclude_dbs(['postgres'])('should properly send push', (done) => {
return setup().then(() => {
return Parse.Push.send({
where: {
@@ -70,7 +71,7 @@ describe('Parse.Push', () => {
});
});
it('should properly send push with lowercaseIncrement', (done) => {
it_exclude_dbs(['postgres'])('should properly send push with lowercaseIncrement', (done) => {
return setup().then(() => {
return Parse.Push.send({
where: {
@@ -89,4 +90,77 @@ describe('Parse.Push', () => {
done();
});
});
it_exclude_dbs(['postgres'])('should not allow clients to query _PushStatus', done => {
setup()
.then(() => Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'increment',
alert: 'Hello world!'
}
}, {useMasterKey: true}))
.then(() => {
request.get({
url: 'http://localhost:8378/1/classes/_PushStatus',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
},
}, (error, response, body) => {
expect(body.error).toEqual('unauthorized');
done();
});
});
});
it_exclude_dbs(['postgres'])('should allow master key to query _PushStatus', done => {
setup()
.then(() => Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'increment',
alert: 'Hello world!'
}
}, {useMasterKey: true}))
.then(() => {
request.get({
url: 'http://localhost:8378/1/classes/_PushStatus',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
}, (error, response, body) => {
expect(body.results.length).toEqual(1);
expect(body.results[0].query).toEqual('{"deviceType":"ios"}');
expect(body.results[0].payload).toEqual('{"badge":"increment","alert":"Hello world!"}');
done();
});
});
});
it_exclude_dbs(['postgres'])('should throw error if missing push configuration', done => {
reconfigureServer({push: null})
.then(() => {
return Parse.Push.send({
where: {
deviceType: 'ios'
},
data: {
badge: 'increment',
alert: 'Hello world!'
}
}, {useMasterKey: true})
}).then((response) => {
fail('should not succeed');
}, (err) => {
expect(err.code).toEqual(Parse.Error.PUSH_MISCONFIGURED);
done();
});
});
});
+622 -550
View File
File diff suppressed because it is too large Load Diff
+713 -491
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -2,7 +2,6 @@ var ParseCloudCodePublisher = require('../src/LiveQuery/ParseCloudCodePublisher'
var Parse = require('parse/node');
describe('ParseCloudCodePublisher', function() {
beforeEach(function(done) {
// Mock ParsePubSub
var mockParsePubSub = {
+52 -7
View File
@@ -36,6 +36,31 @@ describe('Parse.File testing', () => {
});
});
it('works with _ContentType', done => {
request.post({
url: 'http://localhost:8378/1/files/file',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'text/html',
base64: 'PGh0bWw+PC9odG1sPgo='
})
}, (error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.name).toMatch(/_file.html/);
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/);
request.get(b.url, (error, response, body) => {
expect(response.headers['content-type']).toMatch('^text/html');
expect(error).toBe(null);
expect(body).toEqual('<html></html>\n');
done();
});
});
});
it('works without Content-Type', done => {
var headers = {
'X-Parse-Application-Id': 'test',
@@ -185,7 +210,7 @@ describe('Parse.File testing', () => {
}));
});
it("save file in object", done => {
it_exclude_dbs(['postgres'])("save file in object", done => {
var file = new Parse.File("hello.txt", data, "text/plain");
ok(!file.url());
file.save(expectSuccess({
@@ -212,7 +237,7 @@ describe('Parse.File testing', () => {
}));
});
it("save file in object with escaped characters in filename", done => {
it_exclude_dbs(['postgres'])("save file in object with escaped characters in filename", done => {
var file = new Parse.File("hello . txt", data, "text/plain");
ok(!file.url());
file.save(expectSuccess({
@@ -240,7 +265,7 @@ describe('Parse.File testing', () => {
}));
});
it("autosave file in object", done => {
it_exclude_dbs(['postgres'])("autosave file in object", done => {
var file = new Parse.File("hello.txt", data, "text/plain");
ok(!file.url());
var object = new Parse.Object("TestObject");
@@ -262,7 +287,7 @@ describe('Parse.File testing', () => {
}));
});
it("autosave file in object in object", done => {
it_exclude_dbs(['postgres'])("autosave file in object in object", done => {
var file = new Parse.File("hello.txt", data, "text/plain");
ok(!file.url());
@@ -330,7 +355,7 @@ describe('Parse.File testing', () => {
});
});
it("file toJSON testing", done => {
it_exclude_dbs(['postgres'])("file toJSON testing", done => {
var file = new Parse.File("hello.txt", data, "text/plain");
ok(!file.url());
var object = new Parse.Object("TestObject");
@@ -466,7 +491,7 @@ describe('Parse.File testing', () => {
});
});
it('creates correct url for old files hosted on parse', done => {
it_exclude_dbs(['postgres'])('creates correct url for old files hosted on files.parsetfss.com', done => {
var file = {
__type: 'File',
url: 'http://irrelevant.elephant/',
@@ -486,7 +511,27 @@ describe('Parse.File testing', () => {
});
});
it('supports files in objects without urls', done => {
it_exclude_dbs(['postgres'])('creates correct url for old files hosted on files.parse.com', done => {
var file = {
__type: 'File',
url: 'http://irrelevant.elephant/',
name: 'd6e80979-a128-4c57-a167-302f874700dc-123.txt'
};
var obj = new Parse.Object('OldFileTest');
obj.set('oldfile', file);
obj.save().then(() => {
var query = new Parse.Query('OldFileTest');
return query.first();
}).then((result) => {
var fileAgain = result.get('oldfile');
expect(fileAgain.url()).toEqual(
'http://files.parse.com/test/d6e80979-a128-4c57-a167-302f874700dc-123.txt'
);
done();
});
});
it_exclude_dbs(['postgres'])('supports files in objects without urls', done => {
var file = {
__type: 'File',
name: '123.txt'
+17 -17
View File
@@ -4,7 +4,7 @@
var TestObject = Parse.Object.extend('TestObject');
describe('Parse.GeoPoint testing', () => {
it('geo point roundtrip', (done) => {
it_exclude_dbs(['postgres'])('geo point roundtrip', (done) => {
var point = new Parse.GeoPoint(44.0, -11.0);
var obj = new TestObject();
obj.set('location', point);
@@ -26,7 +26,7 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('geo point exception two fields', (done) => {
it_exclude_dbs(['postgres'])('geo point exception two fields', (done) => {
var point = new Parse.GeoPoint(20, 20);
var obj = new TestObject();
obj.set('locationOne', point);
@@ -39,7 +39,7 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('geo line', (done) => {
it_exclude_dbs(['postgres'])('geo line', (done) => {
var line = [];
for (var i = 0; i < 10; ++i) {
var obj = new TestObject();
@@ -67,7 +67,7 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('geo max distance large', (done) => {
it_exclude_dbs(['postgres'])('geo max distance large', (done) => {
var objects = [];
[0, 1, 2].map(function(i) {
var obj = new TestObject();
@@ -85,12 +85,12 @@ describe('Parse.GeoPoint testing', () => {
equal(results.length, 3);
done();
}, (err) => {
console.log(err);
fail();
fail("Couldn't query GeoPoint");
fail(err)
});
});
it('geo max distance medium', (done) => {
it_exclude_dbs(['postgres'])('geo max distance medium', (done) => {
var objects = [];
[0, 1, 2].map(function(i) {
var obj = new TestObject();
@@ -114,7 +114,7 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('geo max distance small', (done) => {
it_exclude_dbs(['postgres'])('geo max distance small', (done) => {
var objects = [];
[0, 1, 2].map(function(i) {
var obj = new TestObject();
@@ -153,7 +153,7 @@ describe('Parse.GeoPoint testing', () => {
Parse.Object.saveAll([sacramento, sf, honolulu], callback);
};
it('geo max distance in km everywhere', (done) => {
it_exclude_dbs(['postgres'])('geo max distance in km everywhere', (done) => {
makeSomeGeoPoints(function(list) {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
@@ -167,7 +167,7 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('geo max distance in km california', (done) => {
it_exclude_dbs(['postgres'])('geo max distance in km california', (done) => {
makeSomeGeoPoints(function(list) {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
@@ -183,7 +183,7 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('geo max distance in km bay area', (done) => {
it_exclude_dbs(['postgres'])('geo max distance in km bay area', (done) => {
makeSomeGeoPoints(function(list) {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
@@ -198,7 +198,7 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('geo max distance in km mid peninsula', (done) => {
it_exclude_dbs(['postgres'])('geo max distance in km mid peninsula', (done) => {
makeSomeGeoPoints(function(list) {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
@@ -212,7 +212,7 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('geo max distance in miles everywhere', (done) => {
it_exclude_dbs(['postgres'])('geo max distance in miles everywhere', (done) => {
makeSomeGeoPoints(function(list) {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
@@ -226,7 +226,7 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('geo max distance in miles california', (done) => {
it_exclude_dbs(['postgres'])('geo max distance in miles california', (done) => {
makeSomeGeoPoints(function(list) {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
@@ -242,7 +242,7 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('geo max distance in miles bay area', (done) => {
it_exclude_dbs(['postgres'])('geo max distance in miles bay area', (done) => {
makeSomeGeoPoints(function(list) {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
@@ -257,7 +257,7 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('geo max distance in miles mid peninsula', (done) => {
it_exclude_dbs(['postgres'])('geo max distance in miles mid peninsula', (done) => {
makeSomeGeoPoints(function(list) {
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
var query = new Parse.Query(TestObject);
@@ -271,7 +271,7 @@ describe('Parse.GeoPoint testing', () => {
});
});
it('works with geobox queries', (done) => {
it_exclude_dbs(['postgres'])('works with geobox queries', (done) => {
var inSF = new Parse.GeoPoint(37.75, -122.4);
var southwestOfSF = new Parse.GeoPoint(37.708813, -122.526398);
var northeastOfSF = new Parse.GeoPoint(37.822802, -122.373962);
+56 -23
View File
@@ -7,12 +7,15 @@ let Config = require('../src/Config');
describe('a GlobalConfig', () => {
beforeEach(done => {
let config = new Config('test');
config.database.adaptiveCollection('_GlobalConfig')
.then(coll => coll.upsertOne({ '_id': 1 }, { $set: { params: { companies: ['US', 'DK'] } } }))
.then(() => { done(); });
config.database.adapter.upsertOneObject(
'_GlobalConfig',
{ fields: {} },
{ objectId: 1 },
{ params: { companies: ['US', 'DK'] } }
).then(done);
});
it('can be retrieved', (done) => {
it_exclude_dbs(['postgres'])('can be retrieved', (done) => {
request.get({
url : 'http://localhost:8378/1/config',
json : true,
@@ -27,7 +30,7 @@ describe('a GlobalConfig', () => {
});
});
it('can be updated when a master key exists', (done) => {
it_exclude_dbs(['postgres'])('can be updated when a master key exists', (done) => {
request.put({
url : 'http://localhost:8378/1/config',
json : true,
@@ -43,7 +46,36 @@ describe('a GlobalConfig', () => {
});
});
it('fail to update if master key is missing', (done) => {
it_exclude_dbs(['postgres'])('properly handles delete op', (done) => {
request.put({
url : 'http://localhost:8378/1/config',
json : true,
body : { params: { companies: {__op: 'Delete'}, foo: 'bar' } },
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.result).toEqual(true);
request.get({
url : 'http://localhost:8378/1/config',
json : true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.params.companies).toBeUndefined();
expect(body.params.foo).toBe('bar');
expect(Object.keys(body.params).length).toBe(1);
done();
});
});
});
it_exclude_dbs(['postgres'])('fail to update if master key is missing', (done) => {
request.put({
url : 'http://localhost:8378/1/config',
json : true,
@@ -59,24 +91,25 @@ describe('a GlobalConfig', () => {
});
});
it('failed getting config when it is missing', (done) => {
it_exclude_dbs(['postgres'])('failed getting config when it is missing', (done) => {
let config = new Config('test');
config.database.adaptiveCollection('_GlobalConfig')
.then(coll => coll.deleteOne({ '_id': 1 }))
.then(() => {
request.get({
url : 'http://localhost:8378/1/config',
json : true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.params).toEqual({});
done();
});
config.database.adapter.deleteObjectsByQuery(
'_GlobalConfig',
{ fields: { params: { __type: 'String' } } },
{ objectId: 1 }
).then(() => {
request.get({
url : 'http://localhost:8378/1/config',
json : true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key' : 'test'
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body.params).toEqual({});
done();
});
});
});
});
+161 -90
View File
@@ -1,3 +1,4 @@
"use strict";
/* global describe, it, expect, fail, Parse */
var request = require('request');
var triggers = require('../src/triggers');
@@ -9,15 +10,14 @@ Parse.Hooks = require("../src/cloud-code/Parse.Hooks");
var port = 12345;
var hookServerURL = "http://localhost:"+port;
let AppCache = require('../src/cache').AppCache;
var app = express();
app.use(bodyParser.json({ 'type': '*/*' }))
app.listen(12345);
describe('Hooks', () => {
it("should have some hooks registered", (done) => {
it_exclude_dbs(['postgres'])("should have no hooks registered", (done) => {
Parse.Hooks.getFunctions().then((res) => {
expect(res.constructor).toBe(Array.prototype.constructor);
done();
@@ -26,8 +26,8 @@ describe('Hooks', () => {
done();
});
});
it("should have some triggers registered", (done) => {
it_exclude_dbs(['postgres'])("should have no triggers registered", (done) => {
Parse.Hooks.getTriggers().then( (res) => {
expect(res.constructor).toBe(Array.prototype.constructor);
done();
@@ -37,52 +37,48 @@ describe('Hooks', () => {
});
});
it("should CRUD a function registration", (done) => {
// Create
Parse.Hooks.createFunction("My-Test-Function", "http://someurl").then((res) => {
expect(res.functionName).toBe("My-Test-Function");
expect(res.url).toBe("http://someurl")
// Find
return Parse.Hooks.getFunction("My-Test-Function");
}, (err) => {
fail(err);
done();
}).then((res) => {
expect(res).not.toBe(null);
expect(res).not.toBe(undefined);
expect(res.url).toBe("http://someurl");
// delete
return Parse.Hooks.updateFunction("My-Test-Function", "http://anotherurl");
}, (err) => {
fail(err);
done();
}).then((res) => {
expect(res.functionName).toBe("My-Test-Function");
expect(res.url).toBe("http://anotherurl")
return Parse.Hooks.deleteFunction("My-Test-Function");
}, (err) => {
fail(err);
done();
}).then((res) => {
// Find again! but should be deleted
return Parse.Hooks.getFunction("My-Test-Function");
}, (err) => {
fail(err);
done();
}).then((res) => {
fail("Should not succeed")
done();
}, (err) => {
expect(err).not.toBe(null);
expect(err).not.toBe(undefined);
expect(err.code).toBe(143);
expect(err.error).toBe("no function named: My-Test-Function is defined")
done();
})
});
it("should CRUD a trigger registration", (done) => {
it_exclude_dbs(['postgres'])("should CRUD a function registration", (done) => {
// Create
Parse.Hooks.createFunction("My-Test-Function", "http://someurl")
.then(response => {
expect(response.functionName).toBe("My-Test-Function");
expect(response.url).toBe("http://someurl")
// Find
return Parse.Hooks.getFunction("My-Test-Function")
}).then(response => {
expect(response.objectId).toBeUndefined();
expect(response.url).toBe("http://someurl");
return Parse.Hooks.updateFunction("My-Test-Function", "http://anotherurl");
})
.then((res) => {
expect(res.objectId).toBeUndefined();
expect(res.functionName).toBe("My-Test-Function");
expect(res.url).toBe("http://anotherurl")
// delete
return Parse.Hooks.deleteFunction("My-Test-Function")
})
.then((res) => {
// Find again! but should be deleted
return Parse.Hooks.getFunction("My-Test-Function")
.then(res => {
fail("Failed to delete hook")
fail(res)
done();
return Promise.resolve();
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe("no function named: My-Test-Function is defined")
done();
return Promise.resolve();
})
})
.catch(error => {
fail(error);
done();
})
});
it_exclude_dbs(['postgres'])("should CRUD a trigger registration", (done) => {
// Create
Parse.Hooks.createTrigger("MyClass","beforeDelete", "http://someurl").then((res) => {
expect(res.className).toBe("MyClass");
@@ -96,6 +92,7 @@ describe('Hooks', () => {
}).then((res) => {
expect(res).not.toBe(null);
expect(res).not.toBe(undefined);
expect(res.objectId).toBeUndefined();
expect(res.url).toBe("http://someurl");
// delete
return Parse.Hooks.updateTrigger("MyClass","beforeDelete", "http://anotherurl");
@@ -105,7 +102,8 @@ describe('Hooks', () => {
}).then((res) => {
expect(res.className).toBe("MyClass");
expect(res.url).toBe("http://anotherurl")
expect(res.objectId).toBeUndefined();
return Parse.Hooks.deleteTrigger("MyClass","beforeDelete");
}, (err) => {
fail(err);
@@ -127,7 +125,7 @@ describe('Hooks', () => {
done();
});
});
it("should fail to register hooks without Master Key", (done) => {
request.post(Parse.serverURL+"/hooks/functions", {
headers: {
@@ -141,8 +139,8 @@ describe('Hooks', () => {
done();
})
});
it("should fail trying to create two times the same function", (done) => {
it_exclude_dbs(['postgres'])("should fail trying to create two times the same function", (done) => {
Parse.Hooks.createFunction("my_new_function", "http://url.com").then( () => {
return Parse.Hooks.createFunction("my_new_function", "http://url.com")
}, () => {
@@ -162,8 +160,8 @@ describe('Hooks', () => {
done();
})
});
it("should fail trying to create two times the same trigger", (done) => {
it_exclude_dbs(['postgres'])("should fail trying to create two times the same trigger", (done) => {
Parse.Hooks.createTrigger("MyClass", "beforeSave", "http://url.com").then( () => {
return Parse.Hooks.createTrigger("MyClass", "beforeSave", "http://url.com")
}, () => {
@@ -181,8 +179,8 @@ describe('Hooks', () => {
done();
})
});
it("should fail trying to update a function that don't exist", (done) => {
it_exclude_dbs(['postgres'])("should fail trying to update a function that don't exist", (done) => {
Parse.Hooks.updateFunction("A_COOL_FUNCTION", "http://url.com").then( () => {
fail("Should not succeed")
}, (err) => {
@@ -198,8 +196,8 @@ describe('Hooks', () => {
done();
});
});
it("should fail trying to update a trigger that don't exist", (done) => {
it_exclude_dbs(['postgres'])("should fail trying to update a trigger that don't exist", (done) => {
Parse.Hooks.updateTrigger("AClassName","beforeSave", "http://url.com").then( () => {
fail("Should not succeed")
}, (err) => {
@@ -215,8 +213,8 @@ describe('Hooks', () => {
done();
});
});
it("should fail trying to create a malformed function", (done) => {
Parse.Hooks.createFunction("MyFunction").then( (res) => {
fail(res);
@@ -226,7 +224,7 @@ describe('Hooks', () => {
done();
});
});
it("should fail trying to create a malformed function (REST)", (done) => {
request.post(Parse.serverURL+"/hooks/functions", {
headers: {
@@ -241,16 +239,16 @@ describe('Hooks', () => {
done();
})
});
it("should create hooks and properly preload them", (done) => {
it_exclude_dbs(['postgres'])("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));
}
Parse.Promise.when(promises).then(function(results){
for (var i=0; i<5; i++) {
// Delete everything from memory, as the server just started
@@ -259,11 +257,11 @@ describe('Hooks', () => {
expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).toBeUndefined();
expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).toBeUndefined();
}
const hooksController = new HooksController(Parse.applicationId);
const hooksController = new HooksController(Parse.applicationId, AppCache.get('test').databaseController);
return hooksController.load()
}, (err) => {
console.error(err);
fail();
fail('Should properly create all hooks');
done();
}).then(function() {
for (var i=0; i<5; i++) {
@@ -273,17 +271,17 @@ describe('Hooks', () => {
done();
}, (err) => {
console.error(err);
fail();
fail('should properly load all hooks');
done();
})
});
it("should run the function on the test server", (done) => {
it_exclude_dbs(['postgres'])("should run the function on the test server", (done) => {
app.post("/SomeFunction", function(req, res) {
res.json({success:"OK!"});
});
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunction").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
@@ -297,15 +295,15 @@ describe('Hooks', () => {
console.error(err);
fail("Should not fail calling a function");
done();
})
});
});
it("should run the function on the test server", (done) => {
it_exclude_dbs(['postgres'])("should run the function on the test server", (done) => {
app.post("/SomeFunctionError", function(req, res) {
res.json({error: {code: 1337, error: "hacking that one!"}});
});
// The function is delete as the DB is dropped between calls
// The function is deleted as the DB is dropped between calls
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunctionError").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
@@ -322,9 +320,62 @@ describe('Hooks', () => {
done();
});
});
it("should run the beforeSave hook on the test server", (done) => {
it_exclude_dbs(['postgres'])("should provide X-Parse-Webhook-Key when defined", (done) => {
app.post("/ExpectingKey", function(req, res) {
if (req.get('X-Parse-Webhook-Key') === 'hook') {
res.json({success: "correct key provided"});
} else {
res.json({error: "incorrect key provided"});
}
});
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/ExpectingKey").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
console.error(err);
fail("Should not fail creating a function");
done();
}).then(function(res){
expect(res).toBe("correct key provided");
done();
}, (err) => {
console.error(err);
fail("Should not fail calling a function");
done();
});
});
it_exclude_dbs(['postgres'])("should not pass X-Parse-Webhook-Key if not provided", (done) => {
reconfigureServer({ webhookKey: undefined })
.then(() => {
app.post("/ExpectingKeyAlso", function(req, res) {
if (req.get('X-Parse-Webhook-Key') === 'hook') {
res.json({success: "correct key provided"});
} else {
res.json({error: "incorrect key provided"});
}
});
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/ExpectingKeyAlso").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
console.error(err);
fail("Should not fail creating a function");
done();
}).then(function(res){
fail("Should not succeed calling that function");
done();
}, (err) => {
expect(err.code).toBe(141);
expect(err.message).toEqual("incorrect key provided");
done();
});
});
});
it_exclude_dbs(['postgres'])("should run the beforeSave hook on the test server", (done) => {
var triggerCount = 0;
app.post("/BeforeSaveSome", function(req, res) {
triggerCount++;
@@ -332,16 +383,16 @@ describe('Hooks', () => {
object.hello = "world";
// Would need parse cloud express to set much more
// But this should override the key upon return
res.json({success: {object: object}});
res.json({success: object});
});
// The function is delete 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){
}).then(function(res) {
expect(triggerCount).toBe(1);
return res.fetch();
}).then(function(res){
}).then(function(res) {
expect(res.get("hello")).toEqual("world");
done();
}).fail((err) => {
@@ -350,8 +401,28 @@ describe('Hooks', () => {
done();
});
});
it("should run the afterSave hook on the test server", (done) => {
it_exclude_dbs(['postgres'])("beforeSave hooks should correctly handle responses containing entire object", (done) => {
app.post("/BeforeSaveSome2", function(req, res) {
var object = Parse.Object.fromJSON(req.body.object);
object.set('hello', "world");
res.json({success: object});
});
Parse.Hooks.createTrigger("SomeRandomObject2", "beforeSave" ,hookServerURL+"/BeforeSaveSome2").then(function(){
const obj = new Parse.Object("SomeRandomObject2");
return obj.save();
}).then(function(res) {
return res.save();
}).then(function(res) {
expect(res.get("hello")).toEqual("world");
done();
}).fail((err) => {
fail(`Should not fail: ${JSON.stringify(err)}`);
done();
});
});
it_exclude_dbs(['postgres'])("should run the afterSave hook on the test server", (done) => {
var triggerCount = 0;
var newObjectId;
app.post("/AfterSaveSome", function(req, res) {
@@ -387,4 +458,4 @@ describe('Hooks', () => {
done();
});
});
});
});
+255 -216
View File
@@ -2,19 +2,21 @@
// These tests check the Installations functionality of the REST API.
// Ported from installation_collection_test.go
var auth = require('../src/Auth');
var cache = require('../src/cache');
var Config = require('../src/Config');
var DatabaseAdapter = require('../src/DatabaseAdapter');
var Parse = require('parse/node').Parse;
var rest = require('../src/rest');
let auth = require('../src/Auth');
let cache = require('../src/cache');
let Config = require('../src/Config');
let Parse = require('parse/node').Parse;
let rest = require('../src/rest');
let request = require("request");
var config = new Config('test');
let database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
let config = new Config('test');
let database = config.database;
let defaultColumns = require('../src/Controllers/SchemaController').defaultColumns;
const installationSchema = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation) };
describe('Installations', () => {
it('creates an android installation with ids', (done) => {
it_exclude_dbs(['postgres'])('creates an android installation with ids', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var device = 'android';
var input = {
@@ -22,9 +24,8 @@ describe('Installations', () => {
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj.installationId).toEqual(installId);
@@ -33,7 +34,7 @@ describe('Installations', () => {
}).catch((error) => { console.log(error); });
});
it('creates an ios installation with ids', (done) => {
it_exclude_dbs(['postgres'])('creates an ios installation with ids', (done) => {
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
var device = 'ios';
var input = {
@@ -41,9 +42,8 @@ describe('Installations', () => {
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj.deviceToken).toEqual(t);
@@ -52,7 +52,7 @@ describe('Installations', () => {
}).catch((error) => { console.log(error); });
});
it('creates an embedded installation with ids', (done) => {
it_exclude_dbs(['postgres'])('creates an embedded installation with ids', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var device = 'embedded';
var input = {
@@ -60,9 +60,8 @@ describe('Installations', () => {
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj.installationId).toEqual(installId);
@@ -71,7 +70,7 @@ describe('Installations', () => {
}).catch((error) => { console.log(error); });
});
it('creates an android installation with all fields', (done) => {
it_exclude_dbs(['postgres'])('creates an android installation with all fields', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var device = 'android';
var input = {
@@ -80,9 +79,8 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj.installationId).toEqual(installId);
@@ -95,7 +93,7 @@ describe('Installations', () => {
}).catch((error) => { console.log(error); });
});
it('creates an ios installation with all fields', (done) => {
it_exclude_dbs(['postgres'])('creates an ios installation with all fields', (done) => {
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
var device = 'ios';
var input = {
@@ -104,9 +102,8 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj.deviceToken).toEqual(t);
@@ -119,6 +116,50 @@ describe('Installations', () => {
}).catch((error) => { console.log(error); });
});
it('should properly fail queying installations', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var device = 'android';
var input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
let query = new Parse.Query(Parse.Installation);
return query.find()
}).then((results) => {
fail('Should not succeed!');
done();
}).catch((error) => {
expect(error.code).toBe(119);
expect(error.message).toBe('Clients aren\'t allowed to perform the find operation on the installation collection.')
done();
});
});
it_exclude_dbs(['postgres'])('should properly queying installations with masterKey', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var device = 'android';
var input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
let query = new Parse.Query(Parse.Installation);
return query.find({useMasterKey: true});
}).then((results) => {
expect(results.length).toEqual(1);
var obj = results[0].toJSON();
expect(obj.installationId).toEqual(installId);
expect(obj.deviceType).toEqual(device);
done();
}).catch((error) => {
fail('Should not fail');
done();
});
});
it('fails with missing ids', (done) => {
var input = {
'deviceType': 'android',
@@ -150,7 +191,7 @@ describe('Installations', () => {
});
});
it('creates an object with custom fields', (done) => {
it_exclude_dbs(['postgres'])('creates an object with custom fields', (done) => {
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
var input = {
'deviceToken': t,
@@ -159,9 +200,8 @@ describe('Installations', () => {
'custom': 'allowed'
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(obj.custom).toEqual('allowed');
@@ -171,7 +211,7 @@ describe('Installations', () => {
// Note: did not port test 'TestObjectIDForIdentifiers'
it('merging when installationId already exists', (done) => {
it_exclude_dbs(['postgres'])('merging when installationId already exists', (done) => {
var installId1 = '12345678-abcd-abcd-abcd-123456789abc';
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
var installId2 = '12345678-abcd-abcd-abcd-123456789abd';
@@ -184,18 +224,17 @@ describe('Installations', () => {
var firstObject;
var secondObject;
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
firstObject = results[0];
delete input.deviceToken;
delete input.channels;
input['foo'] = 'bar';
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
secondObject = results[0];
expect(firstObject._id).toEqual(secondObject._id);
@@ -205,7 +244,7 @@ describe('Installations', () => {
}).catch((error) => { console.log(error); });
});
it('merging when two objects both only have one id', (done) => {
it_exclude_dbs(['postgres'])('merging when two objects both only have one id', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
var input1 = {
@@ -224,15 +263,14 @@ describe('Installations', () => {
var firstObject;
var secondObject;
rest.create(config, auth.nobody(config), '_Installation', input1)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
firstObject = results[0];
return rest.create(config, auth.nobody(config), '_Installation', input2);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(2);
if (results[0]['_id'] == firstObject._id) {
secondObject = results[1];
@@ -240,16 +278,16 @@ describe('Installations', () => {
secondObject = results[0];
}
return rest.create(config, auth.nobody(config), '_Installation', input3);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0]['_id']).toEqual(secondObject._id);
done();
}).catch((error) => { console.log(error); });
});
notWorking('creating multiple devices with same device token works', (done) => {
xit('creating multiple devices with same device token works', (done) => {
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
var installId3 = '33333333-abcd-abcd-abcd-123456789abc';
@@ -266,51 +304,50 @@ describe('Installations', () => {
}).then(() => {
input.installationId = installId3;
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation',
{installationId: installId1}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', {installationId: installId1}, installationSchema, {}))
.then(results => {
expect(results.length).toEqual(1);
return database.mongoFind('_Installation',
{installationId: installId2}, {});
}).then((results) => {
return database.adapter.find('_Installation', {installationId: installId2}, installationSchema, {});
}).then(results => {
expect(results.length).toEqual(1);
return database.mongoFind('_Installation',
{installationId: installId3}, {});
return database.adapter.find('_Installation', {installationId: installId3}, installationSchema, {});
}).then((results) => {
expect(results.length).toEqual(1);
done();
}).catch((error) => { console.log(error); });
});
it('updating with new channels', (done) => {
it_exclude_dbs(['postgres'])('updating with new channels', (done) => {
var input = {
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
'deviceType': 'android',
'channels': ['foo', 'bar']
installationId: '12345678-abcd-abcd-abcd-123456789abc',
deviceType: 'android',
channels: ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var id = results[0]['_id'];
var id = results[0].objectId;
var update = {
'channels': ['baz']
};
return rest.update(config, auth.nobody(config),
'_Installation', id, update);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', id, update);
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].channels.length).toEqual(1);
expect(results[0].channels[0]).toEqual('baz');
done();
}).catch((error) => { console.log(error); });
}).catch(error => {
console.log(error);
fail();
done();
});
});
it('update android fails with new installation id', (done) => {
it_exclude_dbs(['postgres'])('update android fails with new installation id', (done) => {
var installId1 = '12345678-abcd-abcd-abcd-123456789abc';
var installId2 = '87654321-abcd-abcd-abcd-123456789abc';
var input = {
@@ -319,15 +356,11 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'installationId': installId2
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
input = { 'installationId': installId2 };
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
}).then(() => {
fail('Updating the installation should have failed.');
done();
@@ -337,7 +370,7 @@ describe('Installations', () => {
});
});
it('update ios fails with new deviceToken and no installationId', (done) => {
it_exclude_dbs(['postgres'])('update ios fails with new deviceToken and no installationId', (done) => {
var a = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
var b = '91433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
var input = {
@@ -346,15 +379,11 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'deviceToken': b
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
input = { 'deviceToken': b };
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
}).then(() => {
fail('Updating the installation should have failed.');
}).catch((error) => {
@@ -363,7 +392,7 @@ describe('Installations', () => {
});
});
it('update ios updates device token', (done) => {
it_exclude_dbs(['postgres'])('update ios updates device token', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
var u = '91433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
@@ -374,27 +403,25 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'installationId': installId,
'deviceToken': u,
'deviceType': 'ios'
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].deviceToken).toEqual(u);
done();
});
});
it('update fails to change deviceType', (done) => {
it_exclude_dbs(['postgres'])('update fails to change deviceType', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var input = {
'installationId': installId,
@@ -402,15 +429,13 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'deviceType': 'ios'
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
}).then(() => {
fail('Should not have been able to update Installation.');
done();
@@ -420,7 +445,7 @@ describe('Installations', () => {
});
});
it('update android with custom field', (done) => {
it_exclude_dbs(['postgres'])('update android with custom field', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var input = {
'installationId': installId,
@@ -428,25 +453,23 @@ describe('Installations', () => {
'channels': ['foo', 'bar']
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'custom': 'allowed'
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0]['custom']).toEqual('allowed');
done();
});
});
it('update android device token with duplicate device token', (done) => {
it_exclude_dbs(['postgres'])('update android device token with duplicate device token', (done) => {
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
@@ -458,41 +481,37 @@ describe('Installations', () => {
var firstObject;
var secondObject;
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
input = {
'installationId': installId2,
'deviceType': 'android'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation',
{installationId: installId1}, {});
}).then((results) => {
expect(results.length).toEqual(1);
.then(() => {
input = {
'installationId': installId2,
'deviceType': 'android'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId1}, {}))
.then(results => {
firstObject = results[0];
return database.mongoFind('_Installation',
{installationId: installId2}, {});
}).then((results) => {
expect(results.length).toEqual(1);
return database.adapter.find('_Installation', installationSchema, {installationId: installId2}, {});
}).then(results => {
expect(results.length).toEqual(1);
secondObject = results[0];
// Update second installation to conflict with first installation
input = {
'objectId': secondObject._id,
'objectId': secondObject.objectId,
'deviceToken': t
};
return rest.update(config, auth.nobody(config), '_Installation',
secondObject._id, input);
}).then(() => {
return rest.update(config, auth.nobody(config), '_Installation', secondObject.objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {objectId: firstObject.objectId}, {}))
.then(results => {
// The first object should have been deleted
return database.mongoFind('_Installation', {_id: firstObject._id}, {});
}).then((results) => {
expect(results.length).toEqual(0);
done();
}).catch((error) => { console.log(error); });
});
it('update ios device token with duplicate device token', (done) => {
it_exclude_dbs(['postgres'])('update ios device token with duplicate device token', (done) => {
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
@@ -510,15 +529,14 @@ describe('Installations', () => {
'deviceType': 'ios'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation',
{installationId: installId1}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId1}, {}))
.then((results) => {
expect(results.length).toEqual(1);
firstObject = results[0];
return database.mongoFind('_Installation',
{installationId: installId2}, {});
}).then((results) => {
return database.adapter.find('_Installation', installationSchema, {installationId: installId2}, {});
})
.then(results => {
expect(results.length).toEqual(1);
secondObject = results[0];
// Update second installation to conflict with first installation id
@@ -526,18 +544,17 @@ describe('Installations', () => {
'installationId': installId2,
'deviceToken': t
};
return rest.update(config, auth.nobody(config), '_Installation',
secondObject._id, input);
}).then(() => {
return rest.update(config, auth.nobody(config), '_Installation', secondObject.objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {objectId: firstObject.objectId}, {}))
.then(results => {
// The first object should have been deleted
return database.mongoFind('_Installation', {_id: firstObject._id}, {});
}).then((results) => {
expect(results.length).toEqual(0);
done();
}).catch((error) => { console.log(error); });
});
notWorking('update ios device token with duplicate token different app', (done) => {
xit('update ios device token with duplicate token different app', (done) => {
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
@@ -552,9 +569,9 @@ describe('Installations', () => {
input.installationId = installId2;
input.appIdentifier = 'bar';
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
// The first object should have been deleted during merge
expect(results.length).toEqual(1);
expect(results[0].installationId).toEqual(installId2);
@@ -562,7 +579,7 @@ describe('Installations', () => {
});
});
it('update ios token and channels', (done) => {
it_exclude_dbs(['postgres'])('update ios token and channels', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
var input = {
@@ -570,19 +587,17 @@ describe('Installations', () => {
'deviceType': 'ios'
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'deviceToken': t,
'channels': []
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].installationId).toEqual(installId);
expect(results[0].deviceToken).toEqual(t);
@@ -591,7 +606,7 @@ describe('Installations', () => {
});
});
it('update ios linking two existing objects', (done) => {
it_exclude_dbs(['postgres'])('update ios linking two existing objects', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
var input = {
@@ -605,21 +620,19 @@ describe('Installations', () => {
'deviceType': 'ios'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation',
{deviceToken: t}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'deviceToken': t,
'installationId': installId,
'deviceType': 'ios'
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].installationId).toEqual(installId);
expect(results[0].deviceToken).toEqual(t);
@@ -628,7 +641,7 @@ describe('Installations', () => {
});
});
it('update is linking two existing objects w/ increment', (done) => {
it_exclude_dbs(['postgres'])('update is linking two existing objects w/ increment', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
var input = {
@@ -642,10 +655,9 @@ describe('Installations', () => {
'deviceType': 'ios'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation',
{deviceToken: t}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'deviceToken': t,
@@ -656,11 +668,10 @@ describe('Installations', () => {
'amount': 1
}
};
return rest.update(config, auth.nobody(config), '_Installation',
results[0]['_id'], input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', results[0].objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].installationId).toEqual(installId);
expect(results[0].deviceToken).toEqual(t);
@@ -670,7 +681,7 @@ describe('Installations', () => {
});
});
it('update is linking two existing with installation id', (done) => {
it_exclude_dbs(['postgres'])('update is linking two existing with installation id', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
var input = {
@@ -680,9 +691,8 @@ describe('Installations', () => {
var installObj;
var tokenObj;
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
installObj = results[0];
input = {
@@ -690,9 +700,9 @@ describe('Installations', () => {
'deviceType': 'ios'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation', {deviceToken: t}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}))
.then(results => {
expect(results.length).toEqual(1);
tokenObj = results[0];
input = {
@@ -700,11 +710,10 @@ describe('Installations', () => {
'deviceToken': t,
'deviceType': 'ios'
};
return rest.update(config, auth.nobody(config), '_Installation',
installObj._id, input);
}).then(() => {
return database.mongoFind('_Installation', {_id: tokenObj._id}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', installObj.objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, { objectId: tokenObj.objectId }, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].installationId).toEqual(installId);
expect(results[0].deviceToken).toEqual(t);
@@ -712,7 +721,7 @@ describe('Installations', () => {
}).catch((error) => { console.log(error); });
});
it('update is linking two existing with installation id w/ op', (done) => {
it_exclude_dbs(['postgres'])('update is linking two existing with installation id w/ op', (done) => {
var installId = '12345678-abcd-abcd-abcd-123456789abc';
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
var input = {
@@ -722,9 +731,8 @@ describe('Installations', () => {
var installObj;
var tokenObj;
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
installObj = results[0];
input = {
@@ -732,9 +740,9 @@ describe('Installations', () => {
'deviceType': 'ios'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation', {deviceToken: t}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}))
.then(results => {
expect(results.length).toEqual(1);
tokenObj = results[0];
input = {
@@ -746,11 +754,10 @@ describe('Installations', () => {
'amount': 1
}
};
return rest.update(config, auth.nobody(config), '_Installation',
installObj._id, input);
}).then(() => {
return database.mongoFind('_Installation', {_id: tokenObj._id}, {});
}).then((results) => {
return rest.update(config, auth.nobody(config), '_Installation', installObj.objectId, input);
})
.then(() => database.adapter.find('_Installation', installationSchema, { objectId: tokenObj.objectId }, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].installationId).toEqual(installId);
expect(results[0].deviceToken).toEqual(t);
@@ -759,7 +766,7 @@ describe('Installations', () => {
}).catch((error) => { console.log(error); });
});
it('ios merge existing same token no installation id', (done) => {
it_exclude_dbs(['postgres'])('ios merge existing same token no installation id', (done) => {
// Test creating installation when there is an existing object with the
// same device token but no installation ID. This is possible when
// developers import device tokens from another push provider; the import
@@ -777,9 +784,8 @@ describe('Installations', () => {
'deviceType': 'ios'
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
input = {
'installationId': installId,
@@ -787,18 +793,51 @@ describe('Installations', () => {
'deviceType': 'ios'
};
return rest.create(config, auth.nobody(config), '_Installation', input);
}).then(() => {
return database.mongoFind('_Installation', {}, {});
}).then((results) => {
})
.then(() => database.adapter.find('_Installation', installationSchema, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
expect(results[0].deviceToken).toEqual(t);
expect(results[0].installationId).toEqual(installId);
done();
})
.catch(error => {
console.log(error);
fail();
done();
});
});
it('allows you to get your own installation (regression test for #1718)', done => {
let installId = '12345678-abcd-abcd-abcd-123456789abc';
let device = 'android';
let input = {
'installationId': installId,
'deviceType': device
};
rest.create(config, auth.nobody(config), '_Installation', input)
.then(createResult => {
let headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request.get({
headers: headers,
url: 'http://localhost:8378/1/installations/' + createResult.response.objectId,
json: true,
}, (error, response, body) => {
expect(body.objectId).toEqual(createResult.response.objectId);
done();
});
})
.catch(error => {
console.log(error);
fail('failed');
done();
});
});
// TODO: Look at additional tests from installation_collection_test.go:882
// TODO: Do we need to support _tombstone disabling of installations?
// TODO: Test deletion, badge increments
});
-1
View File
@@ -7,7 +7,6 @@ var testUserId = 'userId';
var testClassName = 'TestObject';
describe('ParseLiveQueryServer', function() {
beforeEach(function(done) {
// Mock ParseWebSocketServer
var mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer');
+26 -26
View File
@@ -49,7 +49,7 @@ describe('Parse.Object testing', () => {
});
});
it("save cycle", function(done) {
it("save cycle", done => {
var a = new Parse.Object("TestObject");
var b = new Parse.Object("TestObject");
a.set("b", b);
@@ -117,7 +117,7 @@ describe('Parse.Object testing', () => {
});
});
it("relational fields", function(done) {
it_exclude_dbs(['postgres'])("relational fields", function(done) {
var item = new Item();
item.set("property", "x");
var container = new Container();
@@ -205,7 +205,7 @@ describe('Parse.Object testing', () => {
});
});
it("createdAt doesn't change", function(done) {
it_exclude_dbs(['postgres'])("createdAt doesn't change", function(done) {
var object = new TestObject({ foo: "bar" });
object.save(null, {
success: function() {
@@ -268,7 +268,7 @@ describe('Parse.Object testing', () => {
});
});
it("can set null", function(done) {
it_exclude_dbs(['postgres'])("can set null", function(done) {
var obj = new Parse.Object("TestObject");
obj.set("foo", null);
obj.save(null, {
@@ -365,7 +365,7 @@ describe('Parse.Object testing', () => {
}).then(fail, err => next(0));
});
it("simple field deletion", function(done) {
it_exclude_dbs(['postgres'])("simple field deletion", function(done) {
var simple = new Parse.Object("SimpleObject");
simple.save({
foo: "bar"
@@ -439,7 +439,7 @@ describe('Parse.Object testing', () => {
});
});
it("relation deletion", function(done) {
it_exclude_dbs(['postgres'])("relation deletion", function(done) {
var simple = new Parse.Object("SimpleObject");
var child = new Parse.Object("Child");
simple.save({
@@ -578,7 +578,7 @@ describe('Parse.Object testing', () => {
});
});
it("addUnique", function(done) {
it_exclude_dbs(['postgres'])("addUnique", function(done) {
var x1 = new Parse.Object('X');
x1.set('stuff', [1, 2]);
x1.save().then(() => {
@@ -600,7 +600,7 @@ describe('Parse.Object testing', () => {
});
});
it("addUnique with object", function(done) {
it_exclude_dbs(['postgres'])("addUnique with object", function(done) {
var x1 = new Parse.Object('X');
x1.set('stuff', [ 1, {'hello': 'world'}, {'foo': 'bar'}]);
x1.save().then(() => {
@@ -622,7 +622,7 @@ describe('Parse.Object testing', () => {
});
});
it("removes with object", function(done) {
it_exclude_dbs(['postgres'])("removes with object", function(done) {
var x1 = new Parse.Object('X');
x1.set('stuff', [ 1, {'hello': 'world'}, {'foo': 'bar'}]);
x1.save().then(() => {
@@ -668,7 +668,7 @@ describe('Parse.Object testing', () => {
});
});
it("dirty keys", function(done) {
it_exclude_dbs(['postgres'])("dirty keys", function(done) {
var object = new Parse.Object("TestObject");
object.set("gogo", "good");
object.set("sito", "sexy");
@@ -763,7 +763,7 @@ describe('Parse.Object testing', () => {
});
});
it("old attribute unset then unset", function(done) {
it_exclude_dbs(['postgres'])("old attribute unset then unset", function(done) {
var TestObject = Parse.Object.extend("TestObject");
var obj = new TestObject();
obj.set("x", 3);
@@ -832,7 +832,7 @@ describe('Parse.Object testing', () => {
});
});
it("old attribute unset then clear", function(done) {
it_exclude_dbs(['postgres'])("old attribute unset then clear", function(done) {
var TestObject = Parse.Object.extend("TestObject");
var obj = new TestObject();
obj.set("x", 3);
@@ -901,7 +901,7 @@ describe('Parse.Object testing', () => {
});
});
it("old attribute clear then unset", function(done) {
it_exclude_dbs(['postgres'])("old attribute clear then unset", function(done) {
var TestObject = Parse.Object.extend("TestObject");
var obj = new TestObject();
obj.set("x", 3);
@@ -970,7 +970,7 @@ describe('Parse.Object testing', () => {
});
});
it("old attribute clear then clear", function(done) {
it_exclude_dbs(['postgres'])("old attribute clear then clear", function(done) {
var TestObject = Parse.Object.extend("TestObject");
var obj = new TestObject();
obj.set("x", 3);
@@ -1039,7 +1039,7 @@ describe('Parse.Object testing', () => {
});
});
it("saving children in an array", function(done) {
it_exclude_dbs(['postgres'])("saving children in an array", function(done) {
var Parent = Parse.Object.extend("Parent");
var Child = Parse.Object.extend("Child");
@@ -1304,7 +1304,7 @@ describe('Parse.Object testing', () => {
});
});
it("bytes work", function(done) {
it_exclude_dbs(['postgres'])("bytes work", function(done) {
Parse.Promise.as().then(function() {
var obj = new TestObject();
obj.set("bytes", { __type: "Bytes", base64: "ZnJveW8=" });
@@ -1342,7 +1342,7 @@ describe('Parse.Object testing', () => {
});
});
it("fetchAll", function(done) {
it_exclude_dbs(['postgres'])("fetchAll", function(done) {
var numItems = 11;
var container = new Container();
var items = [];
@@ -1389,7 +1389,7 @@ describe('Parse.Object testing', () => {
});
});
it("fetchAll updates dates", function(done) {
it_exclude_dbs(['postgres'])("fetchAll updates dates", function(done) {
var updatedObject;
var object = new TestObject();
object.set("x", 7);
@@ -1409,7 +1409,7 @@ describe('Parse.Object testing', () => {
});
});
it("fetchAll backbone-style callbacks", function(done) {
it_exclude_dbs(['postgres'])("fetchAll backbone-style callbacks", function(done) {
var numItems = 11;
var container = new Container();
var items = [];
@@ -1478,7 +1478,7 @@ describe('Parse.Object testing', () => {
expectError(Parse.Error.MISSING_OBJECT_ID, done));
});
it("fetchAll error on deleted object", function(done) {
it_exclude_dbs(['postgres'])("fetchAll error on deleted object", function(done) {
var numItems = 11;
var container = new Container();
var subContainer = new Container();
@@ -1504,7 +1504,7 @@ describe('Parse.Object testing', () => {
// TODO: Verify that with Sessions, this test is wrong... A fetch on
// user should not bring down a session token.
notWorking("fetchAll User attributes get merged", function(done) {
xit("fetchAll User attributes get merged", function(done) {
var sameUser;
var user = new Parse.User();
user.set("username", "asdf");
@@ -1536,7 +1536,7 @@ describe('Parse.Object testing', () => {
});
});
it("fetchAllIfNeeded", function(done) {
it_exclude_dbs(['postgres'])("fetchAllIfNeeded", function(done) {
var numItems = 11;
var container = new Container();
var items = [];
@@ -1574,7 +1574,7 @@ describe('Parse.Object testing', () => {
});
});
it("fetchAllIfNeeded backbone-style callbacks", function(done) {
it_exclude_dbs(['postgres'])("fetchAllIfNeeded backbone-style callbacks", function(done) {
var numItems = 11;
var container = new Container();
var items = [];
@@ -1778,7 +1778,7 @@ describe('Parse.Object testing', () => {
});
});
it('dictionary fetched pointers do not lose data on fetch', (done) => {
it_exclude_dbs(['postgres'])('dictionary fetched pointers do not lose data on fetch', (done) => {
var parent = new Parse.Object('Parent');
var dict = {};
for (var i = 0; i < 5; i++) {
@@ -1838,7 +1838,7 @@ describe('Parse.Object testing', () => {
});
});
it('should have undefined includes when object is missing', (done) => {
it_exclude_dbs(['postgres'])('should have undefined includes when object is missing', (done) => {
let obj1 = new Parse.Object("AnObject");
let obj2 = new Parse.Object("AnObject");
@@ -1867,7 +1867,7 @@ describe('Parse.Object testing', () => {
})
});
it('should have undefined includes when object is missing on deeper path', (done) => {
it_exclude_dbs(['postgres'])('should have undefined includes when object is missing on deeper path', (done) => {
let obj1 = new Parse.Object("AnObject");
let obj2 = new Parse.Object("AnObject");
let obj3 = new Parse.Object("AnObject");
+391 -71
View File
@@ -23,6 +23,105 @@ describe('Parse.Query testing', () => {
});
});
it_exclude_dbs(['postgres'])("notEqualTo with Relation is working", function(done) {
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
var user1 = new Parse.User();
user1.setPassword("asdf");
user1.setUsername("qwerty");
var user2 = new Parse.User();
user2.setPassword("asdf");
user2.setUsername("asdf");
var Cake = Parse.Object.extend("Cake");
var cake1 = new Cake();
var cake2 = new Cake();
var cake3 = new Cake();
user.signUp().then(function(){
return user1.signUp();
}).then(function(){
return user2.signUp();
}).then(function(){
var relLike1 = cake1.relation("liker");
relLike1.add([user, user1]);
var relDislike1 = cake1.relation("hater");
relDislike1.add(user2);
return cake1.save();
}).then(function(){
var rellike2 = cake2.relation("liker");
rellike2.add([user, user1]);
var relDislike2 = cake2.relation("hater");
relDislike2.add(user2);
return cake2.save();
}).then(function(){
var rellike3 = cake3.relation("liker");
rellike3.add(user);
var relDislike3 = cake3.relation("hater");
relDislike3.add([user1, user2]);
return cake3.save();
}).then(function(){
var query = new Parse.Query(Cake);
// User2 likes nothing so we should receive 0
query.equalTo("liker", user2);
return query.find().then(function(results){
equal(results.length, 0);
});
}).then(function(){
var query = new Parse.Query(Cake);
// User1 likes two of three cakes
query.equalTo("liker", user1);
return query.find().then(function(results){
// It should return 2 -> cake 1 and cake 2
equal(results.length, 2);
});
}).then(function(){
var query = new Parse.Query(Cake);
// We want to know which cake the user1 is not appreciating -> cake3
query.notEqualTo("liker", user1);
return query.find().then(function(results){
// Should return 1 -> the cake 3
equal(results.length, 1);
});
}).then(function(){
var query = new Parse.Query(Cake);
// User2 is a hater of everything so we should receive 0
query.notEqualTo("hater", user2);
return query.find().then(function(results){
equal(results.length, 0);
});
}).then(function(){
var query = new Parse.Query(Cake);
// Only cake3 is liked by user
query.notContainedIn("liker", [user1]);
return query.find().then(function(results){
equal(results.length, 1);
});
}).then(function(){
var query = new Parse.Query(Cake);
// All the users
query.containedIn("liker", [user, user1, user2]);
// Exclude user 1
query.notEqualTo("liker", user1);
// Only cake3 is liked only by user1
return query.find().then(function(results){
equal(results.length, 1);
let cake = results[0];
expect(cake.id).toBe(cake3.id);
});
}).then(function(){
done();
})
});
it("query with limit", function(done) {
var baz = new TestObject({ foo: 'baz' });
var qux = new TestObject({ foo: 'qux' });
@@ -38,7 +137,7 @@ describe('Parse.Query testing', () => {
});
});
it("containedIn object array queries", function(done) {
it_exclude_dbs(['postgres'])("containedIn object array queries", function(done) {
var messageList = [];
for (var i = 0; i < 4; ++i) {
var message = new TestObject({});
@@ -73,7 +172,7 @@ describe('Parse.Query testing', () => {
});
});
it("containsAll number array queries", function(done) {
it_exclude_dbs(['postgres'])("containsAll number array queries", function(done) {
var NumberSet = Parse.Object.extend({ className: "NumberSet" });
var objectsList = [];
@@ -96,7 +195,7 @@ describe('Parse.Query testing', () => {
});
});
it("containsAll string array queries", function(done) {
it_exclude_dbs(['postgres'])("containsAll string array queries", function(done) {
var StringSet = Parse.Object.extend({ className: "StringSet" });
var objectsList = [];
@@ -115,7 +214,7 @@ describe('Parse.Query testing', () => {
});
});
it("containsAll date array queries", function(done) {
it_exclude_dbs(['postgres'])("containsAll date array queries", function(done) {
var DateSet = Parse.Object.extend({ className: "DateSet" });
function parseDate(iso8601) {
@@ -171,7 +270,7 @@ describe('Parse.Query testing', () => {
});
});
it("containsAll object array queries", function(done) {
it_exclude_dbs(['postgres'])("containsAll object array queries", function(done) {
var MessageSet = Parse.Object.extend({ className: "MessageSet" });
@@ -213,7 +312,7 @@ describe('Parse.Query testing', () => {
className: "BoxedNumber"
});
it("equalTo queries", function(done) {
it_exclude_dbs(['postgres'])("equalTo queries", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -230,7 +329,7 @@ describe('Parse.Query testing', () => {
});
});
it("equalTo undefined", function(done) {
it_exclude_dbs(['postgres'])("equalTo undefined", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -247,7 +346,7 @@ describe('Parse.Query testing', () => {
});
});
it("lessThan queries", function(done) {
it_exclude_dbs(['postgres'])("lessThan queries", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -264,7 +363,7 @@ describe('Parse.Query testing', () => {
});
});
it("lessThanOrEqualTo queries", function(done) {
it_exclude_dbs(['postgres'])("lessThanOrEqualTo queries", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -282,7 +381,7 @@ describe('Parse.Query testing', () => {
});
});
it("greaterThan queries", function(done) {
it_exclude_dbs(['postgres'])("greaterThan queries", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -300,7 +399,7 @@ describe('Parse.Query testing', () => {
});
});
it("greaterThanOrEqualTo queries", function(done) {
it_exclude_dbs(['postgres'])("greaterThanOrEqualTo queries", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -318,7 +417,7 @@ describe('Parse.Query testing', () => {
});
});
it("lessThanOrEqualTo greaterThanOrEqualTo queries", function(done) {
it_exclude_dbs(['postgres'])("lessThanOrEqualTo greaterThanOrEqualTo queries", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -337,7 +436,7 @@ describe('Parse.Query testing', () => {
});
});
it("lessThan greaterThan queries", function(done) {
it_exclude_dbs(['postgres'])("lessThan greaterThan queries", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -374,7 +473,7 @@ describe('Parse.Query testing', () => {
});
});
it("containedIn queries", function(done) {
it_exclude_dbs(['postgres'])("containedIn queries", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -392,7 +491,7 @@ describe('Parse.Query testing', () => {
});
});
it("notContainedIn queries", function(done) {
it_exclude_dbs(['postgres'])("notContainedIn queries", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -411,7 +510,7 @@ describe('Parse.Query testing', () => {
});
it("objectId containedIn queries", function(done) {
it_exclude_dbs(['postgres'])("objectId containedIn queries", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -438,7 +537,7 @@ describe('Parse.Query testing', () => {
});
});
it("objectId equalTo queries", function(done) {
it_exclude_dbs(['postgres'])("objectId equalTo queries", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -461,7 +560,7 @@ describe('Parse.Query testing', () => {
});
});
it("find no elements", function(done) {
it_exclude_dbs(['postgres'])("find no elements", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -582,7 +681,7 @@ describe('Parse.Query testing', () => {
className: "Container"
});
it("notEqualTo object", function(done) {
it_exclude_dbs(['postgres'])("notEqualTo object", function(done) {
var item1 = new TestObject();
var item2 = new TestObject();
var container1 = new Container({item: item1});
@@ -599,7 +698,7 @@ describe('Parse.Query testing', () => {
});
});
it("skip", function(done) {
it_exclude_dbs(['postgres'])("skip", function(done) {
Parse.Object.saveAll([new TestObject(), new TestObject()], function() {
var query = new Parse.Query(TestObject);
query.skip(1);
@@ -618,7 +717,7 @@ describe('Parse.Query testing', () => {
});
});
it("skip doesn't affect count", function(done) {
it_exclude_dbs(['postgres'])("skip doesn't affect count", function(done) {
Parse.Object.saveAll([new TestObject(), new TestObject()], function() {
var query = new Parse.Query(TestObject);
query.count({
@@ -642,7 +741,7 @@ describe('Parse.Query testing', () => {
});
});
it("count", function(done) {
it_exclude_dbs(['postgres'])("count", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -660,7 +759,7 @@ describe('Parse.Query testing', () => {
});
});
it("order by ascending number", function(done) {
it_exclude_dbs(['postgres'])("order by ascending number", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -679,7 +778,7 @@ describe('Parse.Query testing', () => {
});
});
it("order by descending number", function(done) {
it_exclude_dbs(['postgres'])("order by descending number", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -698,7 +797,7 @@ describe('Parse.Query testing', () => {
});
});
it("order by ascending number then descending string", function(done) {
it_exclude_dbs(['postgres'])("order by ascending number then descending string", function(done) {
var strings = ["a", "b", "c", "d"];
var makeBoxedNumber = function(num, i) {
return new BoxedNumber({ number: num, string: strings[i] });
@@ -725,7 +824,7 @@ describe('Parse.Query testing', () => {
});
});
it("order by descending number then ascending string", function(done) {
it_exclude_dbs(['postgres'])("order by descending number then ascending string", function(done) {
var strings = ["a", "b", "c", "d"];
var makeBoxedNumber = function(num, i) {
return new BoxedNumber({ number: num, string: strings[i] });
@@ -751,7 +850,7 @@ describe('Parse.Query testing', () => {
});
});
it("order by descending number and string", function(done) {
it_exclude_dbs(['postgres'])("order by descending number and string", function(done) {
var strings = ["a", "b", "c", "d"];
var makeBoxedNumber = function(num, i) {
return new BoxedNumber({ number: num, string: strings[i] });
@@ -777,7 +876,7 @@ describe('Parse.Query testing', () => {
});
});
it("order by descending number and string, with space", function(done) {
it_exclude_dbs(['postgres'])("order by descending number and string, with space", function(done) {
var strings = ["a", "b", "c", "d"];
var makeBoxedNumber = function(num, i) {
return new BoxedNumber({ number: num, string: strings[i] });
@@ -803,7 +902,7 @@ describe('Parse.Query testing', () => {
});
});
it("order by descending number and string, with array arg", function(done) {
it_exclude_dbs(['postgres'])("order by descending number and string, with array arg", function(done) {
var strings = ["a", "b", "c", "d"];
var makeBoxedNumber = function(num, i) {
return new BoxedNumber({ number: num, string: strings[i] });
@@ -829,7 +928,7 @@ describe('Parse.Query testing', () => {
});
});
it("order by descending number and string, with multiple args", function(done) {
it_exclude_dbs(['postgres'])("order by descending number and string, with multiple args", function(done) {
var strings = ["a", "b", "c", "d"];
var makeBoxedNumber = function(num, i) {
return new BoxedNumber({ number: num, string: strings[i] });
@@ -855,7 +954,7 @@ describe('Parse.Query testing', () => {
});
});
it("can't order by password", function(done) {
it_exclude_dbs(['postgres'])("can't order by password", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -894,7 +993,7 @@ describe('Parse.Query testing', () => {
});
});
it("order by createdAt", function(done) {
it_exclude_dbs(['postgres'])("order by createdAt", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -918,7 +1017,7 @@ describe('Parse.Query testing', () => {
});
});
it("order by _updated_at", function(done) {
it_exclude_dbs(['postgres'])("order by _updated_at", function(done) {
var makeBoxedNumber = function(i) {
return new BoxedNumber({ number: i });
};
@@ -947,7 +1046,7 @@ describe('Parse.Query testing', () => {
});
});
it("order by updatedAt", function(done) {
it_exclude_dbs(['postgres'])("order by updatedAt", function(done) {
var makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); };
var numbers = [3, 1, 2].map(makeBoxedNumber);
numbers[0].save().then(() => {
@@ -998,7 +1097,7 @@ describe('Parse.Query testing', () => {
});
}
it("time equality", function(done) {
it_exclude_dbs(['postgres'])("time equality", function(done) {
makeThreeTimeObjects().then(function(list) {
var query = new Parse.Query(TestObject);
query.equalTo("time", list[1].get("time"));
@@ -1012,7 +1111,7 @@ describe('Parse.Query testing', () => {
});
});
it("time lessThan", function(done) {
it_exclude_dbs(['postgres'])("time lessThan", function(done) {
makeThreeTimeObjects().then(function(list) {
var query = new Parse.Query(TestObject);
query.lessThan("time", list[2].get("time"));
@@ -1026,7 +1125,7 @@ describe('Parse.Query testing', () => {
});
// This test requires Date objects to be consistently stored as a Date.
it("time createdAt", function(done) {
it_exclude_dbs(['postgres'])("time createdAt", function(done) {
makeThreeTimeObjects().then(function(list) {
var query = new Parse.Query(TestObject);
query.greaterThanOrEqualTo("createdAt", list[0].createdAt);
@@ -1039,7 +1138,7 @@ describe('Parse.Query testing', () => {
});
});
it("matches string", function(done) {
it_exclude_dbs(['postgres'])("matches string", function(done) {
var thing1 = new TestObject();
thing1.set("myString", "football");
var thing2 = new TestObject();
@@ -1056,7 +1155,7 @@ describe('Parse.Query testing', () => {
});
});
it("matches regex", function(done) {
it_exclude_dbs(['postgres'])("matches regex", function(done) {
var thing1 = new TestObject();
thing1.set("myString", "football");
var thing2 = new TestObject();
@@ -1073,7 +1172,7 @@ describe('Parse.Query testing', () => {
});
});
it("case insensitive regex success", function(done) {
it_exclude_dbs(['postgres'])("case insensitive regex success", function(done) {
var thing = new TestObject();
thing.set("myString", "football");
Parse.Object.saveAll([thing], function() {
@@ -1087,13 +1186,13 @@ describe('Parse.Query testing', () => {
});
});
it("regexes with invalid options fail", function(done) {
it_exclude_dbs(['postgres'])("regexes with invalid options fail", function(done) {
var query = new Parse.Query(TestObject);
query.matches("myString", "FootBall", "some invalid option");
query.find(expectError(Parse.Error.INVALID_QUERY, done));
});
it("Use a regex that requires all modifiers", function(done) {
it_exclude_dbs(['postgres'])("Use a regex that requires all modifiers", function(done) {
var thing = new TestObject();
thing.set("myString", "PArSe\nCom");
Parse.Object.saveAll([thing], function() {
@@ -1113,7 +1212,7 @@ describe('Parse.Query testing', () => {
});
});
it("Regular expression constructor includes modifiers inline", function(done) {
it_exclude_dbs(['postgres'])("Regular expression constructor includes modifiers inline", function(done) {
var thing = new TestObject();
thing.set("myString", "\n\nbuffer\n\nparse.COM");
Parse.Object.saveAll([thing], function() {
@@ -1131,7 +1230,7 @@ describe('Parse.Query testing', () => {
var someAscii = "\\E' !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTU" +
"VWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'";
it("contains", function(done) {
it_exclude_dbs(['postgres'])("contains", function(done) {
Parse.Object.saveAll([new TestObject({myString: "zax" + someAscii + "qub"}),
new TestObject({myString: "start" + someAscii}),
new TestObject({myString: someAscii + "end"}),
@@ -1147,7 +1246,7 @@ describe('Parse.Query testing', () => {
});
});
it("startsWith", function(done) {
it_exclude_dbs(['postgres'])("startsWith", function(done) {
Parse.Object.saveAll([new TestObject({myString: "zax" + someAscii + "qub"}),
new TestObject({myString: "start" + someAscii}),
new TestObject({myString: someAscii + "end"}),
@@ -1163,7 +1262,7 @@ describe('Parse.Query testing', () => {
});
});
it("endsWith", function(done) {
it_exclude_dbs(['postgres'])("endsWith", function(done) {
Parse.Object.saveAll([new TestObject({myString: "zax" + someAscii + "qub"}),
new TestObject({myString: "start" + someAscii}),
new TestObject({myString: someAscii + "end"}),
@@ -1179,7 +1278,7 @@ describe('Parse.Query testing', () => {
});
});
it("exists", function(done) {
it_exclude_dbs(['postgres'])("exists", function(done) {
var objects = [];
for (var i of [0, 1, 2, 3, 4, 5, 6, 7, 8]) {
var item = new TestObject();
@@ -1205,7 +1304,7 @@ describe('Parse.Query testing', () => {
});
});
it("doesNotExist", function(done) {
it_exclude_dbs(['postgres'])("doesNotExist", function(done) {
var objects = [];
for (var i of [0, 1, 2, 3, 4, 5, 6, 7, 8]) {
var item = new TestObject();
@@ -1231,7 +1330,7 @@ describe('Parse.Query testing', () => {
});
});
it("exists relation", function(done) {
it_exclude_dbs(['postgres'])("exists relation", function(done) {
var objects = [];
for (var i of [0, 1, 2, 3, 4, 5, 6, 7, 8]) {
var container = new Container();
@@ -1260,7 +1359,7 @@ describe('Parse.Query testing', () => {
});
});
it("doesNotExist relation", function(done) {
it_exclude_dbs(['postgres'])("doesNotExist relation", function(done) {
var objects = [];
for (var i of [0, 1, 2, 3, 4, 5, 6, 7]) {
var container = new Container();
@@ -1289,7 +1388,7 @@ describe('Parse.Query testing', () => {
});
});
it("don't include by default", function(done) {
it_exclude_dbs(['postgres'])("don't include by default", function(done) {
var child = new TestObject();
var parent = new Container();
child.set("foo", "bar");
@@ -1422,6 +1521,153 @@ describe('Parse.Query testing', () => {
});
});
it('properly includes array', (done) => {
let objects = [];
let total = 0;
while(objects.length != 5) {
let object = new Parse.Object('AnObject');
object.set('key', objects.length);
total += objects.length;
objects.push(object);
}
Parse.Object.saveAll(objects).then(() => {
let object = new Parse.Object("AContainer");
object.set('objects', objects);
return object.save();
}).then(() => {
let query = new Parse.Query('AContainer');
query.include('objects');
return query.find()
}).then((results) => {
expect(results.length).toBe(1);
let res = results[0];
let objects = res.get('objects');
expect(objects.length).toBe(5);
objects.forEach((object) => {
total -= object.get('key');
});
expect(total).toBe(0);
done()
}, () => {
fail('should not fail');
done();
})
});
it('properly includes array of mixed objects', (done) => {
let objects = [];
let total = 0;
while(objects.length != 5) {
let object = new Parse.Object('AnObject');
object.set('key', objects.length);
total += objects.length;
objects.push(object);
}
while(objects.length != 10) {
let object = new Parse.Object('AnotherObject');
object.set('key', objects.length);
total += objects.length;
objects.push(object);
}
Parse.Object.saveAll(objects).then(() => {
let object = new Parse.Object("AContainer");
object.set('objects', objects);
return object.save();
}).then(() => {
let query = new Parse.Query('AContainer');
query.include('objects');
return query.find()
}).then((results) => {
expect(results.length).toBe(1);
let res = results[0];
let objects = res.get('objects');
expect(objects.length).toBe(10);
objects.forEach((object) => {
total -= object.get('key');
});
expect(total).toBe(0);
done()
}, (err) => {
fail('should not fail');
done();
})
});
it('properly nested array of mixed objects with bad ids', (done) => {
let objects = [];
let total = 0;
while(objects.length != 5) {
let object = new Parse.Object('AnObject');
object.set('key', objects.length);
objects.push(object);
}
while(objects.length != 10) {
let object = new Parse.Object('AnotherObject');
object.set('key', objects.length);
objects.push(object);
}
Parse.Object.saveAll(objects).then(() => {
let object = new Parse.Object("AContainer");
for (var i=0; i<objects.length; i++) {
if (i%2 == 0) {
objects[i].id = 'randomThing'
} else {
total += objects[i].get('key');
}
}
object.set('objects', objects);
return object.save();
}).then(() => {
let query = new Parse.Query('AContainer');
query.include('objects');
return query.find()
}).then((results) => {
expect(results.length).toBe(1);
let res = results[0];
let objects = res.get('objects');
expect(objects.length).toBe(5);
objects.forEach((object) => {
total -= object.get('key');
});
expect(total).toBe(0);
done()
}, (err) => {
console.error(err);
fail('should not fail');
done();
})
});
it_exclude_dbs(['postgres'])('properly fetches nested pointers', (done) =>  {
let color = new Parse.Object('Color');
color.set('hex','#133733');
let circle = new Parse.Object('Circle');
circle.set('radius', 1337);
Parse.Object.saveAll([color, circle]).then(() => {
circle.set('color', color);
let badCircle = new Parse.Object('Circle');
badCircle.id = 'badId';
let complexFigure = new Parse.Object('ComplexFigure');
complexFigure.set('consistsOf', [circle, badCircle]);
return complexFigure.save();
}).then(() => {
let q = new Parse.Query('ComplexFigure');
q.include('consistsOf.color');
return q.find()
}).then((results) => {
expect(results.length).toBe(1);
let figure = results[0];
expect(figure.get('consistsOf').length).toBe(1);
expect(figure.get('consistsOf')[0].get('color').get('hex')).toBe('#133733');
done();
}, (err) => {
fail('should not fail');
done();
})
});
it("result object creation uses current extension", function(done) {
var ParentObject = Parse.Object.extend({ className: "ParentObject" });
// Add a foo() method to ChildObject.
@@ -1457,7 +1703,7 @@ describe('Parse.Query testing', () => {
});
});
it("matches query", function(done) {
it_exclude_dbs(['postgres'])("matches query", function(done) {
var ParentObject = Parse.Object.extend("ParentObject");
var ChildObject = Parse.Object.extend("ChildObject");
var objects = [];
@@ -1496,7 +1742,7 @@ describe('Parse.Query testing', () => {
});
});
it("select query", function(done) {
it_exclude_dbs(['postgres'])("select query", function(done) {
var RestaurantObject = Parse.Object.extend("Restaurant");
var PersonObject = Parse.Object.extend("Person");
var objects = [
@@ -1522,7 +1768,7 @@ describe('Parse.Query testing', () => {
});
});
it('$select inside $or', (done) => {
it_exclude_dbs(['postgres'])('$select inside $or', (done) => {
var Restaurant = Parse.Object.extend('Restaurant');
var Person = Parse.Object.extend('Person');
var objects = [
@@ -1551,7 +1797,7 @@ describe('Parse.Query testing', () => {
});
});
it("dontSelect query", function(done) {
it_exclude_dbs(['postgres'])("dontSelect query", function(done) {
var RestaurantObject = Parse.Object.extend("Restaurant");
var PersonObject = Parse.Object.extend("Person");
var objects = [
@@ -1577,7 +1823,7 @@ describe('Parse.Query testing', () => {
});
});
it("dontSelect query without conditions", function(done) {
it_exclude_dbs(['postgres'])("dontSelect query without conditions", function(done) {
const RestaurantObject = Parse.Object.extend("Restaurant");
const PersonObject = Parse.Object.extend("Person");
const objects = [
@@ -1664,7 +1910,7 @@ describe('Parse.Query testing', () => {
});
});
it("or queries", function(done) {
it_exclude_dbs(['postgres'])("or queries", function(done) {
var objects = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function(x) {
var object = new Parse.Object('BoxedNumber');
object.set('x', x);
@@ -1691,7 +1937,7 @@ describe('Parse.Query testing', () => {
});
// This relies on matchesQuery aka the $inQuery operator
it("or complex queries", function(done) {
it_exclude_dbs(['postgres'])("or complex queries", function(done) {
var objects = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function(x) {
var child = new Parse.Object('Child');
child.set('x', x);
@@ -1720,7 +1966,7 @@ describe('Parse.Query testing', () => {
}));
});
it("async methods", function(done) {
it_exclude_dbs(['postgres'])("async methods", function(done) {
var saves = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function(x) {
var obj = new Parse.Object("TestObject");
obj.set("x", x + 1);
@@ -1757,7 +2003,7 @@ describe('Parse.Query testing', () => {
});
});
it("query.each", function(done) {
it_exclude_dbs(['postgres'])("query.each", function(done) {
var TOTAL = 50;
var COUNT = 25;
@@ -1792,7 +2038,7 @@ describe('Parse.Query testing', () => {
});
});
it("query.each async", function(done) {
it_exclude_dbs(['postgres'])("query.each async", function(done) {
var TOTAL = 50;
var COUNT = 25;
@@ -1999,7 +2245,7 @@ describe('Parse.Query testing', () => {
});
});
it('notEqual with array of pointers', (done) => {
it_exclude_dbs(['postgres'])('notEqual with array of pointers', (done) => {
var children = [];
var parents = [];
var promises = [];
@@ -2029,7 +2275,7 @@ describe('Parse.Query testing', () => {
}).catch((error) => { console.log(error); });
});
it('querying for null value', (done) => {
it_exclude_dbs(['postgres'])('querying for null value', (done) => {
var obj = new Parse.Object('TestObject');
obj.set('aNull', null);
obj.save().then(() => {
@@ -2043,7 +2289,7 @@ describe('Parse.Query testing', () => {
})
});
it('query within dictionary', (done) => {
it_exclude_dbs(['postgres'])('query within dictionary', (done) => {
var objs = [];
var promises = [];
for (var i = 0; i < 2; i++) {
@@ -2082,7 +2328,7 @@ describe('Parse.Query testing', () => {
});
});
it('query match on array with single object', (done) => {
it_exclude_dbs(['postgres'])('query match on array with single object', (done) => {
var target = {__type: 'Pointer', className: 'TestObject', objectId: 'abc123'};
var obj = new Parse.Object('TestObject');
obj.set('someObjs', [target]);
@@ -2098,7 +2344,7 @@ describe('Parse.Query testing', () => {
});
});
it('query match on array with multiple objects', (done) => {
it_exclude_dbs(['postgres'])('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');
@@ -2116,7 +2362,7 @@ describe('Parse.Query testing', () => {
});
// #371
it('should properly interpret a query', (done) => {
it_exclude_dbs(['postgres'])('should properly interpret a query v1', (done) => {
var query = new Parse.Query("C1");
var auxQuery = new Parse.Query("C1");
query.matchesKeyInQuery("A1", "A2", auxQuery);
@@ -2131,7 +2377,7 @@ describe('Parse.Query testing', () => {
})
});
it('should properly interpret a query', (done) => {
it_exclude_dbs(['postgres'])('should properly interpret a query v2', (done) => {
var user = new Parse.User();
user.set("username", "foo");
user.set("password", "bar");
@@ -2166,11 +2412,9 @@ describe('Parse.Query testing', () => {
fail("should not fail");
done();
});
});
it('should find objects with array of pointers', (done) => {
it_exclude_dbs(['postgres'])('should find objects with array of pointers', (done) => {
var objects = [];
while(objects.length != 5) {
var object = new Parse.Object('ContainedObject');
@@ -2209,4 +2453,80 @@ describe('Parse.Query testing', () => {
})
})
it_exclude_dbs(['postgres'])('query with two OR subqueries (regression test #1259)', done => {
let relatedObject = new Parse.Object('Class2');
relatedObject.save().then(relatedObject => {
let anObject = new Parse.Object('Class1');
let relation = anObject.relation('relation');
relation.add(relatedObject);
return anObject.save();
}).then(anObject => {
let q1 = anObject.relation('relation').query();
q1.doesNotExist('nonExistantKey1');
let q2 = anObject.relation('relation').query();
q2.doesNotExist('nonExistantKey2');
let orQuery = Parse.Query.or(q1, q2).find().then(results => {
expect(results.length).toEqual(1);
expect(results[0].objectId).toEqual(q1.objectId);
done();
});
});
});
it('objectId containedIn with multiple large array', done => {
let obj = new Parse.Object('MyClass');
obj.save().then(obj => {
let longListOfStrings = [];
for (let i = 0; i < 130; i++) {
longListOfStrings.push(i.toString());
}
longListOfStrings.push(obj.id);
let q = new Parse.Query('MyClass');
q.containedIn('objectId', longListOfStrings);
q.containedIn('objectId', longListOfStrings);
return q.find();
}).then(results => {
expect(results.length).toEqual(1);
done();
});
});
it('include for specific object', function(done){
var child = new Parse.Object('Child');
var parent = new Parse.Object('Parent');
child.set('foo', 'bar');
parent.set('child', child);
Parse.Object.saveAll([child, parent], function(response){
var savedParent = response[1];
var parentQuery = new Parse.Query('Parent');
parentQuery.include('child');
parentQuery.get(savedParent.id, {
success: function(parentObj) {
var childPointer = parentObj.get('child');
ok(childPointer);
equal(childPointer.get('foo'), 'bar');
done();
}
});
});
});
it('select keys for specific object', function(done){
var Foobar = new Parse.Object('Foobar');
Foobar.set('foo', 'bar');
Foobar.set('fizz', 'buzz');
Foobar.save({
success: function(savedFoobar){
var foobarQuery = new Parse.Query('Foobar');
foobarQuery.select('fizz');
foobarQuery.get(savedFoobar.id,{
success: function(foobarObj){
equal(foobarObj.get('fizz'), 'buzz');
equal(foobarObj.get('foo'), undefined);
done();
}
});
}
})
});
});
+132 -46
View File
@@ -6,7 +6,7 @@ var ChildObject = Parse.Object.extend({className: "ChildObject"});
var ParentObject = Parse.Object.extend({className: "ParentObject"});
describe('Parse.Relation testing', () => {
it("simple add and remove relation", (done) => {
it_exclude_dbs(['postgres'])("simple add and remove relation", (done) => {
var child = new ChildObject();
child.set("x", 2);
var parent = new ParentObject();
@@ -41,7 +41,7 @@ describe('Parse.Relation testing', () => {
});
});
it("query relation without schema", (done) => {
it_exclude_dbs(['postgres'])("query relation without schema", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
@@ -75,7 +75,7 @@ describe('Parse.Relation testing', () => {
}));
});
it("relations are constructed right from query", (done) => {
it_exclude_dbs(['postgres'])("relations are constructed right from query", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
@@ -121,7 +121,7 @@ describe('Parse.Relation testing', () => {
});
it("compound add and remove relation", (done) => {
it_exclude_dbs(['postgres'])("compound add and remove relation", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
@@ -164,7 +164,7 @@ describe('Parse.Relation testing', () => {
});
it("queries with relations", (done) => {
it_exclude_dbs(['postgres'])("queries with relations", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
@@ -202,7 +202,7 @@ describe('Parse.Relation testing', () => {
});
});
it("queries on relation fields", (done) => {
it_exclude_dbs(['postgres'])("queries on relation fields", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
@@ -248,51 +248,55 @@ describe('Parse.Relation testing', () => {
});
});
it("queries on relation fields with multiple ins", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
it_exclude_dbs(['postgres'])("queries on relation fields with multiple containedIn (regression test for #1271)", (done) => {
let ChildObject = Parse.Object.extend("ChildObject");
let childObjects = [];
for (let i = 0; i < 10; i++) {
childObjects.push(new ChildObject({x: i}));
}
Parse.Object.saveAll(childObjects).then(() => {
var ParentObject = Parse.Object.extend("ParentObject");
var parent = new ParentObject();
let ParentObject = Parse.Object.extend("ParentObject");
let parent = new ParentObject();
parent.set("x", 4);
var relation = parent.relation("child");
relation.add(childObjects[0]);
relation.add(childObjects[1]);
relation.add(childObjects[2]);
var parent2 = new ParentObject();
let parent1Children = parent.relation("child");
parent1Children.add(childObjects[0]);
parent1Children.add(childObjects[1]);
parent1Children.add(childObjects[2]);
let parent2 = new ParentObject();
parent2.set("x", 3);
var relation2 = parent2.relation("child");
relation2.add(childObjects[4]);
relation2.add(childObjects[5]);
relation2.add(childObjects[6]);
let parent2Children = parent2.relation("child");
parent2Children.add(childObjects[4]);
parent2Children.add(childObjects[5]);
parent2Children.add(childObjects[6]);
var otherChild2 = parent2.relation("otherChild");
otherChild2.add(childObjects[0]);
otherChild2.add(childObjects[1]);
otherChild2.add(childObjects[2]);
let parent2OtherChildren = parent2.relation("otherChild");
parent2OtherChildren.add(childObjects[0]);
parent2OtherChildren.add(childObjects[1]);
parent2OtherChildren.add(childObjects[2]);
var parents = [];
parents.push(parent);
parents.push(parent2);
return Parse.Object.saveAll(parents);
return Parse.Object.saveAll([parent, parent2]);
}).then(() => {
var query = new Parse.Query(ParentObject);
var objects = [];
objects.push(childObjects[0]);
query.containedIn("child", objects);
query.containedIn("otherChild", [childObjects[0]]);
return query.find();
}).then((list) => {
equal(list.length, 2, "There should be 2 results");
let objectsWithChild0InBothChildren = new Parse.Query(ParentObject);
objectsWithChild0InBothChildren.containedIn("child", [childObjects[0]]);
objectsWithChild0InBothChildren.containedIn("otherChild", [childObjects[0]]);
return objectsWithChild0InBothChildren.find();
}).then(objectsWithChild0InBothChildren => {
//No parent has child 0 in both it's "child" and "otherChild" field;
expect(objectsWithChild0InBothChildren.length).toEqual(0);
}).then(() => {
let objectsWithChild4andOtherChild1 = new Parse.Query(ParentObject);
objectsWithChild4andOtherChild1.containedIn("child", [childObjects[4]]);
objectsWithChild4andOtherChild1.containedIn("otherChild", [childObjects[1]]);
return objectsWithChild4andOtherChild1.find();
}).then(objects => {
// parent2 has child 4 and otherChild 1
expect(objects.length).toEqual(1);
done();
});
});
it("query on pointer and relation fields with equal", (done) => {
it_exclude_dbs(['postgres'])("query on pointer and relation fields with equal", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
@@ -330,7 +334,7 @@ describe('Parse.Relation testing', () => {
});
});
it("query on pointer and relation fields with equal bis", (done) => {
it_exclude_dbs(['postgres'])("query on pointer and relation fields with equal bis", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
@@ -370,7 +374,7 @@ describe('Parse.Relation testing', () => {
});
});
it("or queries on pointer and relation fields", (done) => {
it_exclude_dbs(['postgres'])("or queries on pointer and relation fields", (done) => {
var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = [];
for (var i = 0; i < 10; i++) {
@@ -415,7 +419,7 @@ describe('Parse.Relation testing', () => {
});
it("Get query on relation using un-fetched parent object", (done) => {
it_exclude_dbs(['postgres'])("Get query on relation using un-fetched parent object", (done) => {
// Setup data model
var Wheel = Parse.Object.extend('Wheel');
var Car = Parse.Object.extend('Car');
@@ -448,7 +452,7 @@ describe('Parse.Relation testing', () => {
});
});
it("Find query on relation using un-fetched parent object", (done) => {
it_exclude_dbs(['postgres'])("Find query on relation using un-fetched parent object", (done) => {
// Setup data model
var Wheel = Parse.Object.extend('Wheel');
var Car = Parse.Object.extend('Car');
@@ -482,7 +486,7 @@ describe('Parse.Relation testing', () => {
});
});
it('Find objects with a related object using equalTo', (done) => {
it_exclude_dbs(['postgres'])('Find objects with a related object using equalTo', (done) => {
// Setup the objects
var Card = Parse.Object.extend('Card');
var House = Parse.Object.extend('House');
@@ -502,7 +506,7 @@ describe('Parse.Relation testing', () => {
});
});
it('should properly get related objects with unfetched queries', (done) => {
it_exclude_dbs(['postgres'])('should properly get related objects with unfetched queries', (done) => {
let objects = [];
let owners = [];
let allObjects = [];
@@ -571,7 +575,7 @@ describe('Parse.Relation testing', () => {
})
});
it("select query", function(done) {
it_exclude_dbs(['postgres'])("select query", function(done) {
var RestaurantObject = Parse.Object.extend("Restaurant");
var PersonObject = Parse.Object.extend("Person");
var OwnerObject = Parse.Object.extend('Owner');
@@ -611,7 +615,7 @@ describe('Parse.Relation testing', () => {
});
});
it("dontSelect query", function(done) {
it_exclude_dbs(['postgres'])("dontSelect query", function(done) {
var RestaurantObject = Parse.Object.extend("Restaurant");
var PersonObject = Parse.Object.extend("Person");
var OwnerObject = Parse.Object.extend('Owner');
@@ -652,4 +656,86 @@ describe('Parse.Relation testing', () => {
}));
});
});
it_exclude_dbs(['postgres'])('relations are not bidirectional (regression test for #871)', done => {
let PersonObject = Parse.Object.extend("Person");
let p1 = new PersonObject();
let p2 = new PersonObject();
Parse.Object.saveAll([p1, p2]).then(results => {
let p1 = results[0];
let p2 = results[1];
let relation = p1.relation('relation');
relation.add(p2);
p1.save().then(() => {
let query = new Parse.Query(PersonObject);
query.equalTo('relation', p1);
query.find().then(results => {
expect(results.length).toEqual(0);
let query = new Parse.Query(PersonObject);
query.equalTo('relation', p2);
query.find().then(results => {
expect(results.length).toEqual(1);
expect(results[0].objectId).toEqual(p1.objectId);
done();
});
});
})
});
});
it_exclude_dbs(['postgres'])('can query roles in Cloud Code (regession test #1489)', done => {
Parse.Cloud.define('isAdmin', (request, response) => {
let query = new Parse.Query(Parse.Role);
query.equalTo('name', 'admin');
query.first({ useMasterKey: true })
.then(role => {
let relation = new Parse.Relation(role, 'users');
let admins = relation.query();
admins.equalTo('username', request.user.get('username'));
admins.first({ useMasterKey: true })
.then(user => {
if (user) {
response.success(user);
done();
} else {
fail('Should have found admin user, found nothing instead');
done();
}
}, error => {
fail('User not admin');
done();
})
}, error => {
fail('Should have found admin user, errored instead');
fail(error);
done();
});
});
let adminUser = new Parse.User();
adminUser.set('username', 'name');
adminUser.set('password', 'pass');
adminUser.signUp()
.then(adminUser => {
let adminACL = new Parse.ACL();
adminACL.setPublicReadAccess(true);
// Create admin role
let adminRole = new Parse.Role('admin', adminACL);
adminRole.getUsers().add(adminUser);
adminRole.save()
.then(() => {
Parse.Cloud.run('isAdmin');
}, error => {
fail('failed to save role');
fail(error);
done()
});
}, error => {
fail('failed to sign up');
fail(error);
done();
});
});
});
+112 -42
View File
@@ -2,20 +2,23 @@
// Roles are not accessible without the master key, so they are not intended
// for use by clients. We can manually test them using the master key.
var RestQuery = require("../src/RestQuery");
var Auth = require("../src/Auth").Auth;
var Config = require("../src/Config");
describe('Parse Role testing', () => {
it('Do a bunch of basic role testing', (done) => {
it_exclude_dbs(['postgres'])('Do a bunch of basic role testing', done => {
var user;
var role;
createTestUser().then((x) => {
user = x;
let acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(false);
role = new Parse.Object('_Role');
role.set('name', 'Foos');
role.setACL(acl);
var users = role.relation('users');
users.add(user);
return role.save({}, { useMasterKey: true });
@@ -55,26 +58,92 @@ describe('Parse Role testing', () => {
}).then((x) => {
fail('Should not have been able to save.');
}, (e) => {
expect(e.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
done();
});
});
it("should recursively load roles", (done) => {
var rolesNames = ["FooRole", "BarRole", "BazRole"];
var createRole = function(name, sibling, user) {
var role = new Parse.Role(name, new Parse.ACL());
if (user) {
var users = role.relation('users');
users.add(user);
}
if (sibling) {
role.relation('roles').add(sibling);
}
return role.save({}, { useMasterKey: true });
var createRole = function(name, sibling, user) {
var role = new Parse.Role(name, new Parse.ACL());
if (user) {
var users = role.relation('users');
users.add(user);
}
if (sibling) {
role.relation('roles').add(sibling);
}
return role.save({}, { useMasterKey: true });
};
it_exclude_dbs(['postgres'])("should not recursively load the same role multiple times", (done) => {
var rootRole = "RootRole";
var roleNames = ["FooRole", "BarRole", "BazRole"];
var allRoles = [rootRole].concat(roleNames);
var roleObjs = {};
var createAllRoles = function(user) {
var promises = allRoles.map(function(roleName) {
return createRole(roleName, null, user)
.then(function(roleObj) {
roleObjs[roleName] = roleObj;
return roleObj;
});
});
return Promise.all(promises);
};
var restExecute = spyOn(RestQuery.prototype, "execute").and.callThrough();
var user,
auth,
getAllRolesSpy;
createTestUser().then( (newUser) => {
user = newUser;
return createAllRoles(user);
}).then ( (roles) => {
var rootRoleObj = roleObjs[rootRole];
roles.forEach(function(role, i) {
// Add all roles to the RootRole
if (role.id !== rootRoleObj.id) {
role.relation("roles").add(rootRoleObj);
}
// Add all "roleNames" roles to the previous role
if (i > 0) {
role.relation("roles").add(roles[i - 1]);
}
});
return Parse.Object.saveAll(roles, { useMasterKey: true });
}).then( () => {
auth = new Auth({config: new Config("test"), isMaster: true, user: user});
getAllRolesSpy = spyOn(auth, "_getAllRolesNamesForRoleIds").and.callThrough();
return auth._loadRoles();
}).then ( (roles) => {
expect(roles.length).toEqual(4);
allRoles.forEach(function(name) {
expect(roles.indexOf("role:"+name)).not.toBe(-1);
});
// 1 Query for the initial setup
// 1 query for the parent roles
expect(restExecute.calls.count()).toEqual(2);
// 1 call for the 1st layer of roles
// 1 call for the 2nd layer
expect(getAllRolesSpy.calls.count()).toEqual(2);
done()
}).catch( (err) => {
fail("should succeed");
done();
});
});
it_exclude_dbs(['postgres'])("should recursively load roles", (done) => {
var rolesNames = ["FooRole", "BarRole", "BazRole"];
var roleIds = {};
createTestUser().then( (user) => {
// Put the user on the 1st role
@@ -97,7 +166,7 @@ describe('Parse Role testing', () => {
expect(roles.length).toEqual(3);
rolesNames.forEach( (name) => {
expect(roles.indexOf('role:'+name)).not.toBe(-1);
})
});
done();
}, function(err){
fail("should succeed")
@@ -105,7 +174,7 @@ describe('Parse Role testing', () => {
});
});
it("_Role object should not save without name.", (done) => {
it_exclude_dbs(['postgres'])("_Role object should not save without name.", (done) => {
var role = new Parse.Role();
role.save(null,{useMasterKey:true})
.then((r) => {
@@ -122,7 +191,7 @@ describe('Parse Role testing', () => {
});
});
});
it("Should properly resolve roles", (done) => {
let admin = new Parse.Role("Admin", new Parse.ACL());
let moderator = new Parse.Role("Moderator", new Parse.ACL());
@@ -134,50 +203,49 @@ describe('Parse Role testing', () => {
moderator.getRoles().add([admin, superModerator]);
superContentManager.getRoles().add(superModerator);
return Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true});
}).then(() => {
}).then(() => {
var auth = new Auth({ config: new Config("test"), isMaster: true });
// For each role, fetch their sibling, what they inherit
// return with result and roleId for later comparison
let promises = [admin, moderator, contentManager, superModerator].map((role) => {
return auth._getAllRoleNamesForId(role.id).then((result) => {
return auth._getAllRolesNamesForRoleIds([role.id]).then((result) => {
return Parse.Promise.as({
id: role.id,
name: role.get('name'),
roleIds: result
roleNames: result
});
})
});
return Parse.Promise.when(promises);
}).then((results) => {
results.forEach((result) => {
let id = result.id;
let roleIds = result.roleIds;
let roleNames = result.roleNames;
if (id == admin.id) {
expect(roleIds.length).toBe(2);
expect(roleIds.indexOf(moderator.id)).not.toBe(-1);
expect(roleIds.indexOf(contentManager.id)).not.toBe(-1);
expect(roleNames.length).toBe(2);
expect(roleNames.indexOf("Moderator")).not.toBe(-1);
expect(roleNames.indexOf("ContentManager")).not.toBe(-1);
} else if (id == moderator.id) {
expect(roleIds.length).toBe(1);
expect(roleIds.indexOf(contentManager.id)).toBe(0);
expect(roleNames.length).toBe(1);
expect(roleNames.indexOf("ContentManager")).toBe(0);
} else if (id == contentManager.id) {
expect(roleIds.length).toBe(0);
expect(roleNames.length).toBe(0);
} else if (id == superModerator.id) {
expect(roleIds.length).toBe(3);
expect(roleIds.indexOf(moderator.id)).not.toBe(-1);
expect(roleIds.indexOf(contentManager.id)).not.toBe(-1);
expect(roleIds.indexOf(superContentManager.id)).not.toBe(-1);
expect(roleNames.length).toBe(3);
expect(roleNames.indexOf("Moderator")).not.toBe(-1);
expect(roleNames.indexOf("ContentManager")).not.toBe(-1);
expect(roleNames.indexOf("SuperContentManager")).not.toBe(-1);
}
});
done();
}).fail((err) => {
console.error(err);
done();
})
});
it('can create role and query empty users', (done)=> {
it_exclude_dbs(['postgres'])('can create role and query empty users', (done)=> {
var roleACL = new Parse.ACL();
roleACL.setPublicReadAccess(true);
var role = new Parse.Role('subscribers', roleACL);
@@ -192,13 +260,12 @@ describe('Parse Role testing', () => {
done();
});
}, (e) => {
console.log(e);
fail('should not have errored');
});
});
// Based on various scenarios described in issues #827 and #683,
it('should properly handle role permissions on objects', (done) => {
it_exclude_dbs(['postgres'])('should properly handle role permissions on objects', (done) => {
var user, user2, user3;
var role, role2, role3;
var obj, obj2;
@@ -271,10 +338,13 @@ describe('Parse Role testing', () => {
fail('Customer user should not have been able to save.');
done();
}, (e) => {
expect(e.code).toEqual(101);
if (e) {
expect(e.code).toEqual(101);
} else {
fail('should return an error');
}
done();
})
});
});
+368 -59
View File
@@ -10,6 +10,7 @@
var request = require('request');
var passwordCrypto = require('../src/password');
var Config = require('../src/Config');
const rp = require('request-promise');
function verifyACL(user) {
const ACL = user.getACL();
@@ -87,7 +88,56 @@ describe('Parse.User testing', () => {
});
});
it("user login with files", (done) => {
it_exclude_dbs(['postgres'])('should respect ACL without locking user out', (done) => {
let user = new Parse.User();
let ACL = new Parse.ACL();
ACL.setPublicReadAccess(false);
ACL.setPublicWriteAccess(false);
user.setUsername('asdf');
user.setPassword('zxcv');
user.setACL(ACL);
user.signUp().then((user) => {
return Parse.User.logIn("asdf", "zxcv");
}).then((user) => {
equal(user.get("username"), "asdf");
const ACL = user.getACL();
expect(ACL.getReadAccess(user)).toBe(true);
expect(ACL.getWriteAccess(user)).toBe(true);
expect(ACL.getPublicReadAccess()).toBe(false);
expect(ACL.getPublicWriteAccess()).toBe(false);
const perms = ACL.permissionsById;
expect(Object.keys(perms).length).toBe(1);
expect(perms[user.id].read).toBe(true);
expect(perms[user.id].write).toBe(true);
expect(perms['*']).toBeUndefined();
// Try to lock out user
let newACL = new Parse.ACL();
newACL.setReadAccess(user.id, false);
newACL.setWriteAccess(user.id, false);
user.setACL(newACL);
return user.save();
}).then((user) => {
return Parse.User.logIn("asdf", "zxcv");
}).then((user) => {
equal(user.get("username"), "asdf");
const ACL = user.getACL();
expect(ACL.getReadAccess(user)).toBe(true);
expect(ACL.getWriteAccess(user)).toBe(true);
expect(ACL.getPublicReadAccess()).toBe(false);
expect(ACL.getPublicWriteAccess()).toBe(false);
const perms = ACL.permissionsById;
expect(Object.keys(perms).length).toBe(1);
expect(perms[user.id].read).toBe(true);
expect(perms[user.id].write).toBe(true);
expect(perms['*']).toBeUndefined();
done();
}).catch((err) => {
fail("Should not fail");
done();
})
});
it_exclude_dbs(['postgres'])("user login with files", (done) => {
let file = new Parse.File("yolo.txt", [1,2,3], "text/plain");
file.save().then((file) => {
return Parse.User.signUp("asdf", "zxcv", { "file" : file });
@@ -101,30 +151,28 @@ describe('Parse.User testing', () => {
});
});
describe('become', () => {
it('sends token back', done => {
let user = null;
var sessionToken = null;
it_exclude_dbs(['postgres'])('become sends token back', done => {
let user = null;
var sessionToken = null;
Parse.User.signUp('Jason', 'Parse', { 'code': 'red' }).then(newUser => {
user = newUser;
expect(user.get('code'), 'red');
Parse.User.signUp('Jason', 'Parse', { 'code': 'red' }).then(newUser => {
user = newUser;
expect(user.get('code'), 'red');
sessionToken = newUser.getSessionToken();
expect(sessionToken).toBeDefined();
sessionToken = newUser.getSessionToken();
expect(sessionToken).toBeDefined();
return Parse.User.become(sessionToken);
}).then(newUser => {
expect(newUser.id).toEqual(user.id);
expect(newUser.get('username'), 'Jason');
expect(newUser.get('code'), 'red');
expect(newUser.getSessionToken()).toEqual(sessionToken);
}).then(() => {
done();
}, error => {
fail(error);
done();
});
return Parse.User.become(sessionToken);
}).then(newUser => {
expect(newUser.id).toEqual(user.id);
expect(newUser.get('username'), 'Jason');
expect(newUser.get('code'), 'red');
expect(newUser.getSessionToken()).toEqual(sessionToken);
}).then(() => {
done();
}, error => {
fail(error);
done();
});
});
@@ -247,7 +295,7 @@ describe('Parse.User testing', () => {
});
});
it("cannot saveAll with non-authed user", (done) => {
it_exclude_dbs(['postgres'])("cannot saveAll with non-authed user", (done) => {
var user = new Parse.User();
user.signUp({
"password": "asdf",
@@ -440,7 +488,7 @@ describe('Parse.User testing', () => {
return promise._thenRunCallbacks(optionsOrCallback);
}
it("contained in user array queries", (done) => {
it_exclude_dbs(['postgres'])("contained in user array queries", (done) => {
var USERS = 4;
var MESSAGES = 5;
@@ -535,7 +583,7 @@ describe('Parse.User testing', () => {
});
});
it("count users", (done) => {
it_exclude_dbs(['postgres'])("count users", (done) => {
var james = new Parse.User();
james.set("username", "james");
james.set("password", "mypass");
@@ -663,7 +711,7 @@ describe('Parse.User testing', () => {
});
});
it("saving user after browser refresh", (done) => {
it_exclude_dbs(['postgres'])("saving user after browser refresh", (done) => {
var _ = Parse._;
var id;
@@ -811,8 +859,7 @@ describe('Parse.User testing', () => {
});
});
it("user on disk gets updated after save", (done) => {
it_exclude_dbs(['postgres'])("user on disk gets updated after save", (done) => {
var SuperUser = Parse.User.extend({
isSuper: function() {
return true;
@@ -956,7 +1003,7 @@ describe('Parse.User testing', () => {
}
});
it("log in with provider", (done) => {
it_exclude_dbs(['postgres'])("log in with provider", (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
@@ -978,7 +1025,24 @@ describe('Parse.User testing', () => {
});
});
it('log in with provider with files', done => {
it_exclude_dbs(['postgres'])('returns authData when authed and logged in with provider (regression test for #1498)', done => {
Parse.Object.enableSingleInstance();
let provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith('facebook', {
success: user => {
let userQuery = new Parse.Query(Parse.User);
userQuery.get(user.id)
.then(user => {
expect(user.get('authData')).not.toBeUndefined();
Parse.Object.disableSingleInstance();
done();
});
}
});
});
it_exclude_dbs(['postgres'])('log in with provider with files', done => {
let provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
let file = new Parse.File("yolo.txt", [1, 2, 3], "text/plain");
@@ -1001,7 +1065,7 @@ describe('Parse.User testing', () => {
});
});
it("log in with provider twice", (done) => {
it_exclude_dbs(['postgres'])("log in with provider twice", (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
@@ -1076,7 +1140,7 @@ describe('Parse.User testing', () => {
});
});
it("login with provider should not call beforeSave trigger", (done) => {
it_exclude_dbs(['postgres'])("login with provider should not call beforeSave trigger", (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
@@ -1089,12 +1153,10 @@ describe('Parse.User testing', () => {
Parse.User._logInWith("facebook", {
success: function(innerModel) {
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
done();
},
error: function(model, error) {
ok(undefined, error);
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
done();
}
});
@@ -1102,7 +1164,7 @@ describe('Parse.User testing', () => {
});
});
it("link with provider", (done) => {
it_exclude_dbs(['postgres'])("link with provider", (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
var user = new Parse.User();
@@ -1135,7 +1197,7 @@ describe('Parse.User testing', () => {
// What this means is, only one Parse User can be linked to a
// particular Facebook account.
it("link with provider for already linked user", (done) => {
it_exclude_dbs(['postgres'])("link with provider for already linked user", (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
var user = new Parse.User();
@@ -1242,7 +1304,7 @@ describe('Parse.User testing', () => {
});
});
it("unlink with provider", (done) => {
it_exclude_dbs(['postgres'])("unlink with provider", (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
@@ -1278,7 +1340,7 @@ describe('Parse.User testing', () => {
});
});
it("unlink and link", (done) => {
it_exclude_dbs(['postgres'])("unlink and link", (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
@@ -1330,7 +1392,7 @@ describe('Parse.User testing', () => {
});
});
it("link multiple providers", (done) => {
it_exclude_dbs(['postgres'])("link multiple providers", (done) => {
var provider = getMockFacebookProvider();
var mockProvider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
@@ -1366,7 +1428,7 @@ describe('Parse.User testing', () => {
});
});
it("link multiple providers and update token", (done) => {
it_exclude_dbs(['postgres'])("link multiple providers and update token", (done) => {
var provider = getMockFacebookProvider();
var mockProvider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
@@ -1412,7 +1474,7 @@ describe('Parse.User testing', () => {
});
});
it('should fail linking with existing', (done) => {
it_exclude_dbs(['postgres'])('should fail linking with existing', (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
@@ -1438,7 +1500,7 @@ describe('Parse.User testing', () => {
});
});
it('should fail linking with existing', (done) => {
it_exclude_dbs(['postgres'])('should fail linking with existing', (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
@@ -1464,7 +1526,30 @@ describe('Parse.User testing', () => {
});
});
it('should have authData in beforeSave and afterSave', (done) => {
it_exclude_dbs(['postgres'])('should properly error when password is missing', (done) => {
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
success: function(user) {
user.set('username', 'myUser');
user.set('email', 'foo@example.com');
user.save().then(() => {
return Parse.User.logOut();
}).then(() => {
return Parse.User.logIn('myUser', 'password');
}).then(() => {
fail('should not succeed');
done();
}, (err) => {
expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
expect(err.message).toEqual('Invalid username/password.');
done();
})
}
});
});
it_exclude_dbs(['postgres'])('should have authData in beforeSave and afterSave', (done) => {
Parse.Cloud.beforeSave('_User', (request, response) => {
let authData = request.object.get('authData');
@@ -1494,8 +1579,6 @@ describe('Parse.User testing', () => {
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", {
success: function(model) {
Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className);
Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className);
done();
}
});
@@ -1529,7 +1612,7 @@ describe('Parse.User testing', () => {
}));
});
it("log in with explicit facebook auth data", (done) => {
it_exclude_dbs(['postgres'])("log in with explicit facebook auth data", (done) => {
Parse.FacebookUtils.logIn({
id: "8675309",
access_token: "jenny",
@@ -1537,7 +1620,7 @@ describe('Parse.User testing', () => {
}, expectSuccess({success: done}));
});
it("log in async with explicit facebook auth data", (done) => {
it_exclude_dbs(['postgres'])("log in async with explicit facebook auth data", (done) => {
Parse.FacebookUtils.logIn({
id: "8675309",
access_token: "jenny",
@@ -1550,7 +1633,7 @@ describe('Parse.User testing', () => {
});
});
it("link with explicit facebook auth data", (done) => {
it_exclude_dbs(['postgres'])("link with explicit facebook auth data", (done) => {
Parse.User.signUp("mask", "open sesame", null, expectSuccess({
success: function(user) {
Parse.FacebookUtils.link(user, {
@@ -1565,7 +1648,7 @@ describe('Parse.User testing', () => {
}));
});
it("link async with explicit facebook auth data", (done) => {
it_exclude_dbs(['postgres'])("link async with explicit facebook auth data", (done) => {
Parse.User.signUp("mask", "open sesame", null, expectSuccess({
success: function(user) {
Parse.FacebookUtils.link(user, {
@@ -1620,7 +1703,7 @@ describe('Parse.User testing', () => {
});
});
notWorking("querying for users doesn't get session tokens", (done) => {
xit("querying for users doesn't get session tokens", (done) => {
Parse.Promise.as().then(function() {
return Parse.User.signUp("finn", "human", { foo: "bar" });
@@ -1744,6 +1827,22 @@ describe('Parse.User testing', () => {
});
});
it_exclude_dbs(['postgres'])('unset user email', (done) => {
var user = new Parse.User();
user.set('username', 'test');
user.set('password', 'test');
user.set('email', 'test@test.com');
user.signUp().then(() => {
user.unset('email');
return user.save();
}).then(() => {
return Parse.User.logIn('test', 'test');
}).then((user) => {
expect(user.getEmail()).toBeUndefined();
done();
});
});
it('create session from user', (done) => {
Parse.Promise.as().then(() => {
return Parse.User.signUp("finn", "human", { foo: "bar" });
@@ -1850,7 +1949,7 @@ describe('Parse.User testing', () => {
});
});
it('get session only for current user', (done) => {
it_exclude_dbs(['postgres'])('get session only for current user', (done) => {
Parse.Promise.as().then(() => {
return Parse.User.signUp("test1", "test", { foo: "bar" });
}).then(() => {
@@ -1874,7 +1973,7 @@ describe('Parse.User testing', () => {
});
});
it('delete session by object', (done) => {
it_exclude_dbs(['postgres'])('delete session by object', (done) => {
Parse.Promise.as().then(() => {
return Parse.User.signUp("test1", "test", { foo: "bar" });
}).then(() => {
@@ -1954,7 +2053,7 @@ describe('Parse.User testing', () => {
});
});
it('test parse user become', (done) => {
it_exclude_dbs(['postgres'])('test parse user become', (done) => {
var sessionToken = null;
Parse.Promise.as().then(function() {
return Parse.User.signUp("flessard", "folo",{'foo':1});
@@ -2007,7 +2106,7 @@ describe('Parse.User testing', () => {
});
});
it("session expiresAt correct format", (done) => {
it_exclude_dbs(['postgres'])("session expiresAt correct format", (done) => {
Parse.User.signUp("asdf", "zxcv", null, {
success: function(user) {
request.get({
@@ -2025,13 +2124,11 @@ describe('Parse.User testing', () => {
});
});
// Sometimes the authData still has null on that keys
// https://github.com/ParsePlatform/parse-server/issues/935
it('should cleanup null authData keys', (done) => {
it_exclude_dbs(['postgres'])('should cleanup null authData keys (regression test for #935)', (done) => {
let database = new Config(Parse.applicationId).database;
database.create('_User', {
username: 'user',
password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie',
_hashed_password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie',
_auth_data_facebook: null
}, {}).then(() => {
return new Promise((resolve, reject) => {
@@ -2061,6 +2158,62 @@ describe('Parse.User testing', () => {
})
});
it('should cleanup null authData keys ParseUser update (regression test for #1198)', (done) => {
Parse.Cloud.beforeSave('_User', (req, res) => {
req.object.set('foo', 'bar');
res.success();
});
// Simulate anonymous user save
new Promise((resolve, reject) => {
request.post({
url: 'http://localhost:8378/1/classes/_User',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
}, (err, res, body) => {
if (err) {
reject(err);
} else {
resolve(body);
}
});
}).then((user) => {
// Simulate registration
return new Promise((resolve, reject) => {
request.put({
url: 'http://localhost:8378/1/classes/_User/' + user.objectId,
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Session-Token': user.sessionToken,
'X-Parse-REST-API-Key': 'rest',
},
json: {
authData: {anonymous: null},
user: 'user',
password: 'password',
}
}, (err, res, body) => {
if (err) {
reject(err);
} else {
resolve(body);
}
});
});
}).then((user) => {
expect(typeof user).toEqual('object');
expect(user.authData).toBeUndefined();
done();
}).catch((err) => {
fail('no request should fail: ' + JSON.stringify(err));
done();
});
});
it('should aftersave with full object', (done) => {
var hit = 0;
Parse.Cloud.afterSave('_User', (req, res) => {
@@ -2075,7 +2228,6 @@ describe('Parse.User testing', () => {
user.set('hello', 'world');
return user.save();
}).then(() => {
Parse.Cloud._removeHook('Triggers', 'afterSave', '_User');
done();
});
});
@@ -2101,4 +2253,161 @@ describe('Parse.User testing', () => {
});
});
it_exclude_dbs(['postgres'])('should fail to become user with expired token', (done) => {
let token;
Parse.User.signUp("auser", "somepass", null)
.then(user => rp({
method: 'GET',
url: 'http://localhost:8378/1/classes/_Session',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
}))
.then(body => {
var id = body.results[0].objectId;
var expiresAt = new Date((new Date()).setYear(2015));
token = body.results[0].sessionToken;
return rp({
method: 'PUT',
url: "http://localhost:8378/1/classes/_Session/" + id,
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
body: {
expiresAt: { __type: "Date", iso: expiresAt.toISOString() },
},
})
})
.then(() => Parse.User.become(token))
.then(() => {
fail("Should not have succeded")
done();
}, error => {
expect(error.code).toEqual(209);
expect(error.message).toEqual("Session token is expired.");
done();
})
});
it('should not create extraneous session tokens', (done) => {
let config = new Config(Parse.applicationId);
config.database.loadSchema().then((s) => {
// Lock down the _User class for creation
return s.addClassIfNotExists('_User', {}, {create: {}})
}).then((res) => {
let user = new Parse.User();
return user.save({'username': 'user', 'password': 'pass'});
}).then(() => {
fail('should not be able to save the user');
}, (err) => {
return Promise.resolve();
}).then(() => {
let q = new Parse.Query('_Session');
return q.find({useMasterKey: true})
}).then((res) => {
// We should have no session created
expect(res.length).toBe(0);
done();
}, (err) => {
fail('should not fail');
done();
});
});
it_exclude_dbs(['postgres'])('should not overwrite username when unlinking facebook user (regression test for #1532)', done => {
Parse.Object.disableSingleInstance();
var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider);
var user = new Parse.User();
user.set("username", "testLinkWithProvider");
user.set("password", "mypass");
user.signUp()
.then(user => user._linkWith("facebook", {
success: user => {
expect(user.get('username')).toEqual('testLinkWithProvider');
expect(Parse.FacebookUtils.isLinked(user)).toBeTruthy();
return user._unlinkFrom('facebook')
.then(() => user.fetch())
.then(user => {
expect(user.get('username')).toEqual('testLinkWithProvider');
expect(Parse.FacebookUtils.isLinked(user)).toBeFalsy();
done();
});
},
error: error => {
fail('Unexpected failure testing linking');
fail(JSON.stringify(error));
done();
}
}))
.catch(error => {
fail('Unexpected failure testing in unlink user test');
fail(error);
done();
});
});
it_exclude_dbs(['postgres'])('should revoke sessions when converting anonymous user to "normal" user', done => {
request.post({
url: 'http://localhost:8378/1/classes/_User',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
}, (err, res, body) => {
Parse.User.become(body.sessionToken)
.then(user => {
let obj = new Parse.Object('TestObject');
obj.setACL(new Parse.ACL(user));
return obj.save()
.then(() => {
// Change password, revoking session
user.set('username', 'no longer anonymous');
user.set('password', 'password');
return user.save()
})
.then(() => obj.fetch())
.catch(error => {
expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND);
done();
});
})
});
});
it_exclude_dbs(['postgres'])('should not revoke session tokens if the server is configures to not revoke session tokens', done => {
reconfigureServer({ revokeSessionOnPasswordReset: false })
.then(() => {
request.post({
url: 'http://localhost:8378/1/classes/_User',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
},
json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}}
}, (err, res, body) => {
Parse.User.become(body.sessionToken)
.then(user => {
let obj = new Parse.Object('TestObject');
obj.setACL(new Parse.ACL(user));
return obj.save()
.then(() => {
// Change password, revoking session
user.set('username', 'no longer anonymous');
user.set('password', 'password');
return user.save()
})
.then(() => obj.fetch())
// fetch should succeed as we still have our session token
.then(done, fail);
})
});
});
})
});
+693
View File
@@ -0,0 +1,693 @@
'use strict';
var Schema = require('../src/Controllers/SchemaController');
var Config = require('../src/Config');
describe('Pointer Permissions', () => {
it_exclude_dbs(['postgres'])('should work with find', (done) => {
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
let obj = new Parse.Object('AnObject');
let obj2 = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
obj.set('owner', user);
obj2.set('owner', user2);
return Parse.Object.saveAll([obj, obj2]);
}).then(() => {
return config.database.loadSchema().then((schema) => {
return schema.updateClass('AnObject', {}, {readUserFields: ['owner']})
});
}).then(() => {
return Parse.User.logIn('user1', 'password');
}).then(() => {
let q = new Parse.Query('AnObject');
return q.find();
}).then((res) => {
expect(res.length).toBe(1);
expect(res[0].id).toBe(obj.id);
done();
}).catch(error => {
fail(JSON.stringify(error));
done();
});
});
it_exclude_dbs(['postgres'])('should work with write', (done) => {
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
let obj = new Parse.Object('AnObject');
let obj2 = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
obj.set('owner', user);
obj.set('reader', user2);
obj2.set('owner', user2);
obj2.set('reader', user);
return Parse.Object.saveAll([obj, obj2]);
}).then(() => {
return config.database.loadSchema().then((schema) => {
return schema.updateClass('AnObject', {}, {writeUserFields: ['owner'], readUserFields: ['reader', 'owner']});
});
}).then(() => {
return Parse.User.logIn('user1', 'password');
}).then(() => {
obj2.set('hello', 'world');
return obj2.save();
}).then((res) => {
fail('User should not be able to update obj2');
}, (err) => {
// User 1 should not be able to update obj2
expect(err.code).toBe(101);
return Promise.resolve();
}).then(()=> {
obj.set('hello', 'world');
return obj.save();
}).then(() => {
return Parse.User.logIn('user2', 'password');
}, (err) => {
fail('User should be able to update');
return Promise.resolve();
}).then(() => {
let q = new Parse.Query('AnObject');
return q.find();
}, (err) => {
fail('should login with user 2');
}).then((res) => {
expect(res.length).toBe(2);
res.forEach((result) => {
if (result.id == obj.id) {
expect(result.get('hello')).toBe('world');
} else {
expect(result.id).toBe(obj2.id);
}
})
done();
}, (err) =>  {
fail("failed");
done();
})
});
it_exclude_dbs(['postgres'])('should let a proper user find', (done) => {
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
let obj = new Parse.Object('AnObject');
let obj2 = new Parse.Object('AnObject');
user.signUp().then(() => {
return user2.signUp()
}).then(() => {
Parse.User.logOut();
}).then(() => {
obj.set('owner', user);
return Parse.Object.saveAll([obj, obj2]);
}).then(() => {
return config.database.loadSchema().then((schema) => {
return schema.updateClass('AnObject', {}, {find: {}, get:{}, readUserFields: ['owner']})
});
}).then(() => {
let q = new Parse.Query('AnObject');
return q.find();
}).then((res) => {
expect(res.length).toBe(0);
}).then(() => {
return Parse.User.logIn('user2', 'password');
}).then(() => {
let q = new Parse.Query('AnObject');
return q.find();
}).then((res) => {
expect(res.length).toBe(0);
let q = new Parse.Query('AnObject');
return q.get(obj.id);
}).then(() => {
fail('User 2 should not get the obj1 object');
}, (err) => {
expect(err.code).toBe(101);
expect(err.message).toBe('Object not found.');
return Promise.resolve();
}).then(() => {
return Parse.User.logIn('user1', 'password');
}).then(() => {
let q = new Parse.Query('AnObject');
return q.find();
}).then((res) => {
expect(res.length).toBe(1);
done();
}).catch((err) => {
console.error(err);
fail('should not fail');
done();
})
});
it_exclude_dbs(['postgres'])('should not allow creating objects', (done) => {
let config = new Config(Parse.applicationId);
let user = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
let obj = new Parse.Object('AnObject');
user.save().then(() => {
return config.database.loadSchema().then((schema) => {
return schema.addClassIfNotExists('AnObject', {owner: {type:'Pointer', targetClass: '_User'}}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user1', 'password');
}).then(() => {
obj.set('owner', user);
return obj.save();
}).then(() => {
fail('should not succeed');
done();
}, (err) => {
expect(err.code).toBe(119);
done();
})
});
it_exclude_dbs(['postgres'])('should handle multiple writeUserFields', done => {
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
let obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2])
.then(() => {
obj.set('owner', user);
obj.set('otherOwner', user2);
return obj.save();
})
.then(() => config.database.loadSchema())
.then(schema => schema.updateClass('AnObject', {}, {find: {"*": true},writeUserFields: ['owner', 'otherOwner']}))
.then(() => Parse.User.logIn('user1', 'password'))
.then(() => obj.save({hello: 'fromUser1'}))
.then(() => Parse.User.logIn('user2', 'password'))
.then(() => obj.save({hello: 'fromUser2'}))
.then(() => Parse.User.logOut())
.then(() => {
let q = new Parse.Query('AnObject');
return q.first();
})
.then(result => {
expect(result.get('hello')).toBe('fromUser2');
done();
}).catch(err => {
fail('should not fail');
done();
})
});
it('should prevent creating pointer permission on missing field', (done) => {
let config = new Config(Parse.applicationId);
config.database.loadSchema().then((schema) => {
return schema.addClassIfNotExists('AnObject', {}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
}).then(() => {
fail('should not succeed');
}).catch((err) => {
expect(err.code).toBe(107);
expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields");
done();
})
});
it('should prevent creating pointer permission on bad field', (done) => {
let config = new Config(Parse.applicationId);
config.database.loadSchema().then((schema) => {
return schema.addClassIfNotExists('AnObject', {owner: {type: 'String'}}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
}).then(() => {
fail('should not succeed');
}).catch((err) => {
expect(err.code).toBe(107);
expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields");
done();
})
});
it('should prevent creating pointer permission on bad field', (done) => {
let config = new Config(Parse.applicationId);
let object = new Parse.Object('AnObject');
object.set('owner', 'value');
object.save().then(() => {
return config.database.loadSchema();
}).then((schema) => {
return schema.updateClass('AnObject', {}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']});
}).then(() => {
fail('should not succeed');
}).catch((err) => {
expect(err.code).toBe(107);
expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields");
done();
})
});
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL write (PP Locked)', (done) => {
/*
tests:
CLP: update closed ({})
PointerPerm: "owner"
ACL: logged in user has access
The owner is another user than the ACL
*/
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
let obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
let ACL = new Parse.ACL();
ACL.setReadAccess(user, true);
ACL.setWriteAccess(user, true);
obj.setACL(ACL);
obj.set('owner', user2);
return obj.save();
}).then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {}, {update: {}, writeUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user1', 'password');
}).then(() => {
// user1 has ACL read/write but should be blocked by PP
return obj.save({key: 'value'});
}).then(() => {
fail('Should not succeed saving');
done();
}, (err) => {
expect(err.code).toBe(101);
done();
});
});
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL write (ACL Locked)', (done) => {
/*
tests:
CLP: update closed ({})
PointerPerm: "owner"
ACL: logged in user has access
*/
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
let obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
let ACL = new Parse.ACL();
ACL.setReadAccess(user, true);
ACL.setWriteAccess(user, true);
obj.setACL(ACL);
obj.set('owner', user2);
return obj.save();
}).then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {}, {update: {}, writeUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user2', 'password');
}).then(() => {
// user1 has ACL read/write but should be blocked by ACL
return obj.save({key: 'value'});
}).then(() => {
fail('Should not succeed saving');
done();
}, (err) => {
expect(err.code).toBe(101);
done();
});
});
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL write (ACL/PP OK)', (done) => {
/*
tests:
CLP: update closed ({})
PointerPerm: "owner"
ACL: logged in user has access
*/
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
let obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
let ACL = new Parse.ACL();
ACL.setWriteAccess(user, true);
ACL.setWriteAccess(user2, true);
obj.setACL(ACL);
obj.set('owner', user2);
return obj.save();
}).then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {}, {update: {}, writeUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user2', 'password');
}).then(() => {
// user1 has ACL read/write but should be blocked by ACL
return obj.save({key: 'value'});
}).then((objAgain) => {
expect(objAgain.get('key')).toBe('value');
done();
}, (err) => {
fail('Should not fail saving');
done();
});
});
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL read (PP locked)', (done) => {
/*
tests:
CLP: find/get open ({})
PointerPerm: "owner" : read
ACL: logged in user has access
The owner is another user than the ACL
*/
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
let obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
let ACL = new Parse.ACL();
ACL.setReadAccess(user, true);
ACL.setWriteAccess(user, true);
obj.setACL(ACL);
obj.set('owner', user2);
return obj.save();
}).then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {}, {find: {}, get: {}, readUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user1', 'password');
}).then(() => {
// user1 has ACL read/write but should be block
return obj.fetch();
}).then(() => {
fail('Should not succeed saving');
done();
}, (err) => {
expect(err.code).toBe(101);
done();
});
});
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL read (PP/ACL OK)', (done) => {
/*
tests:
CLP: find/get open ({"*": true})
PointerPerm: "owner" : read
ACL: logged in user has access
*/
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
let obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
let ACL = new Parse.ACL();
ACL.setReadAccess(user, true);
ACL.setWriteAccess(user, true);
ACL.setReadAccess(user2, true);
ACL.setWriteAccess(user2, true);
obj.setACL(ACL);
obj.set('owner', user2);
return obj.save();
}).then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {}, {find: {"*": true}, get: {"*": true}, readUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user2', 'password');
}).then(() => {
// user1 has ACL read/write but should be block
return obj.fetch();
}).then((objAgain) => {
expect(objAgain.id).toBe(obj.id);
done();
}, (err) => {
fail('Should not fail fetching');
done();
});
});
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL read (ACL locked)', (done) => {
/*
tests:
CLP: find/get open ({"*": true})
PointerPerm: "owner" : read // proper owner
ACL: logged in user has not access
*/
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let user2 = new Parse.User();
user.set({
username: 'user1',
password: 'password'
});
user2.set({
username: 'user2',
password: 'password'
});
let obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, user2]).then(() => {
let ACL = new Parse.ACL();
ACL.setReadAccess(user, true);
ACL.setWriteAccess(user, true);
obj.setACL(ACL);
obj.set('owner', user2);
return obj.save();
}).then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {}, {find: {"*": true}, get: {"*": true}, readUserFields: ['owner']});
});
}).then(() => {
return Parse.User.logIn('user2', 'password');
}).then(() => {
// user2 has ACL read/write but should be block by ACL
return obj.fetch();
}).then(() => {
fail('Should not succeed saving');
done();
}, (err) => {
expect(err.code).toBe(101);
done();
});
});
it_exclude_dbs(['postgres'])('should let master key find objects', (done) => {
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let object = new Parse.Object('AnObject');
object.set('hello', 'world');
return object.save().then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {find: {}, get: {}, readUserFields: ['owner']});
});
}).then(() => {
let q = new Parse.Query('AnObject');
return q.find();
}).then(() => {
}, (err) => {
expect(err.code).toBe(101);
return Promise.resolve();
}).then(() => {
let q = new Parse.Query('AnObject');
return q.find({useMasterKey: true});
}).then((objects) => {
expect(objects.length).toBe(1);
done();
}, (err) => {
fail('master key should find the object');
done();
})
});
it_exclude_dbs(['postgres'])('should let master key get objects', (done) => {
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let object = new Parse.Object('AnObject');
object.set('hello', 'world');
return object.save().then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {find: {}, get: {}, readUserFields: ['owner']});
});
}).then(() => {
let q = new Parse.Query('AnObject');
return q.get(object.id);
}).then(() => {
}, (err) => {
expect(err.code).toBe(101);
return Promise.resolve();
}).then(() => {
let q = new Parse.Query('AnObject');
return q.get(object.id, {useMasterKey: true});
}).then((objectAgain) => {
expect(objectAgain).not.toBeUndefined();
expect(objectAgain.id).toBe(object.id);
done();
}, (err) => {
fail('master key should find the object');
done();
})
});
it_exclude_dbs(['postgres'])('should let master key update objects', (done) => {
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let object = new Parse.Object('AnObject');
object.set('hello', 'world');
return object.save().then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {update: {}, writeUserFields: ['owner']});
});
}).then(() => {
return object.save({'hello': 'bar'});
}).then(() => {
}, (err) => {
expect(err.code).toBe(101);
return Promise.resolve();
}).then(() => {
return object.save({'hello': 'baz'}, {useMasterKey: true});
}).then((objectAgain) => {
expect(objectAgain.get('hello')).toBe('baz');
done();
}, (err) => {
fail('master key should save the object');
done();
})
});
it_exclude_dbs(['postgres'])('should let master key delete objects', (done) => {
let config = new Config(Parse.applicationId);
let user = new Parse.User();
let object = new Parse.Object('AnObject');
object.set('hello', 'world');
return object.save().then(() => {
return config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: ['owner']});
});
}).then(() => {
return object.destroy();
}).then(() => {
fail();
}, (err) => {
expect(err.code).toBe(101);
return Promise.resolve();
}).then(() => {
return object.destroy({useMasterKey: true});
}).then((objectAgain) => {
done();
}, (err) => {
fail('master key should destroy the object');
done();
})
});
it('should fail with invalid pointer perms', () => {
let config = new Config(Parse.applicationId);
config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.addClassIfNotExists('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: 'owner'});
}).catch((err) => {
expect(err.code).toBe(Parse.Error.INVALID_JSON);
done();
});
});
it('should fail with invalid pointer perms', () => {
let config = new Config(Parse.applicationId);
config.database.loadSchema().then((schema) => {
// Lock the update, and let only owner write
return schema.addClassIfNotExists('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: ['owner', 'invalid']});
}).catch((err) => {
expect(err.code).toBe(Parse.Error.INVALID_JSON);
done();
});
})
});
+2 -3
View File
@@ -1,7 +1,6 @@
var PromiseRouter = require("../src/PromiseRouter").default;
describe("PromiseRouter", () => {
it("should properly handle rejects", (done) => {
var router = new PromiseRouter();
router.route("GET", "/dummy", (req)=> {
@@ -12,7 +11,7 @@ describe("PromiseRouter", () => {
}, (req) => {
fail("this should not be called");
});
router.routes[0].handler({}).then((result) => {
console.error(result);
fail("this should not be called");
@@ -23,4 +22,4 @@ describe("PromiseRouter", () => {
done();
});
});
})
})
+18 -39
View File
@@ -2,43 +2,33 @@
var request = require('request');
describe("public API", () => {
beforeEach(done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
publicServerURL: 'http://localhost:8378/1'
});
done();
})
it("should get invalid_link.html", (done) => {
request('http://localhost:8378/1/apps/invalid_link.html', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(200);
done();
});
});
it("should get choose_password", (done) => {
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(200);
done();
});
reconfigureServer({
appName: 'unused',
publicServerURL: 'http://localhost:8378/1',
})
.then(() => {
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(200);
done();
});
})
});
it("should get verify_email_success.html", (done) => {
request('http://localhost:8378/1/apps/verify_email_success.html', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(200);
done();
});
});
it("should get password_reset_success.html", (done) => {
request('http://localhost:8378/1/apps/password_reset_success.html', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(200);
@@ -48,20 +38,9 @@ describe("public API", () => {
});
describe("public API without publicServerURL", () => {
beforeEach(done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
});
done();
beforeEach(done => {
reconfigureServer({ appName: 'unused' })
.then(done, fail);
})
it("should get 404 on verify_email", (done) => {
request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse, body) => {
@@ -69,14 +48,14 @@ describe("public API without publicServerURL", () => {
done();
});
});
it("should get 404 choose_password", (done) => {
request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(404);
done();
});
});
it("should get 404 on request_password_reset", (done) => {
request('http://localhost:8378/1/apps/test/request_password_reset', (err, httpResponse, body) => {
expect(httpResponse.statusCode).toBe(404);
+21 -27
View File
@@ -1,7 +1,5 @@
var request = require("request");
function createProduct() {
const file = new Parse.File("name", {
base64: new Buffer("download_file", "utf-8").toString("base64")
@@ -9,7 +7,7 @@ function createProduct() {
return file.save().then(function(){
var product = new Parse.Object("_Product");
product.set({
download: file,
download: file,
icon: file,
title: "a product",
subtitle: "a product",
@@ -18,21 +16,19 @@ function createProduct() {
})
return product.save();
})
}
describe("test validate_receipt endpoint", () => {
beforeEach( done => {
createProduct().then(done).fail(function(err){
console.error(err);
done();
})
})
it("should bypass appstore validation", (done) => {
it_exclude_dbs(['postgres'])("should bypass appstore validation", (done) => {
request.post({
headers: {
'X-Parse-Application-Id': 'test',
@@ -40,7 +36,7 @@ describe("test validate_receipt endpoint", () => {
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: "a-product",
productIdentifier: "a-product",
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
@@ -63,7 +59,7 @@ describe("test validate_receipt endpoint", () => {
}
});
});
it("should fail for missing receipt", (done) => {
request.post({
headers: {
@@ -72,7 +68,7 @@ describe("test validate_receipt endpoint", () => {
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: "a-product",
productIdentifier: "a-product",
bypassAppStoreValidation: true
}
}, function(err, res, body){
@@ -85,7 +81,7 @@ describe("test validate_receipt endpoint", () => {
}
});
});
it("should fail for missing product identifier", (done) => {
request.post({
headers: {
@@ -110,9 +106,9 @@ describe("test validate_receipt endpoint", () => {
}
});
});
it("should bypass appstore validation and not find product", (done) => {
request.post({
headers: {
'X-Parse-Application-Id': 'test',
@@ -120,7 +116,7 @@ describe("test validate_receipt endpoint", () => {
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: "another-product",
productIdentifier: "another-product",
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
@@ -138,9 +134,8 @@ describe("test validate_receipt endpoint", () => {
}
});
});
it("should fail at appstore validation", (done) => {
it("should fail at appstore validation", done => {
request.post({
headers: {
'X-Parse-Application-Id': 'test',
@@ -148,7 +143,7 @@ describe("test validate_receipt endpoint", () => {
url: 'http://localhost:8378/1/validate_purchase',
json: true,
body: {
productIdentifier: "a-product",
productIdentifier: "a-product",
receipt: {
__type: "Bytes",
base64: new Buffer("receipt", "utf-8").toString("base64")
@@ -164,8 +159,8 @@ describe("test validate_receipt endpoint", () => {
done();
});
});
it("should not create a _Product", (done) => {
it_exclude_dbs(['postgres'])("should not create a _Product", (done) => {
var product = new Parse.Object("_Product");
product.save().then(function(){
fail("Should not be able to save");
@@ -175,8 +170,8 @@ describe("test validate_receipt endpoint", () => {
done();
})
});
it("should be able to update a _Product", (done) => {
it_exclude_dbs(['postgres'])("should be able to update a _Product", (done) => {
var query = new Parse.Query("_Product");
query.first().then(function(product){
product.set("title", "a new title");
@@ -190,8 +185,8 @@ describe("test validate_receipt endpoint", () => {
done();
});
});
it("should not be able to remove a require key in a _Product", (done) => {
it_exclude_dbs(['postgres'])("should not be able to remove a require key in a _Product", (done) => {
var query = new Parse.Query("_Product");
query.first().then(function(product){
product.unset("title");
@@ -205,5 +200,4 @@ describe("test validate_receipt endpoint", () => {
done();
});
});
});
+51 -11
View File
@@ -130,7 +130,7 @@ describe('PushController', () => {
done();
});
it('properly increment badges', (done) => {
it_exclude_dbs(['postgres'])('properly increment badges', (done) => {
var payload = {data:{
alert: "Hello World!",
@@ -178,20 +178,19 @@ describe('PushController', () => {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId);
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
Parse.Object.saveAll(installations).then((installations) => {
return pushController.sendPush(payload, {}, config, auth);
}).then((result) => {
done();
}, (err) => {
console.error(err);
fail("should not fail");
done();
});
});
it('properly set badges to 1', (done) => {
it_exclude_dbs(['postgres'])('properly set badges to 1', (done) => {
var payload = {data: {
alert: "Hello World!",
@@ -227,20 +226,19 @@ describe('PushController', () => {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId);
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
Parse.Object.saveAll(installations).then((installations) => {
return pushController.sendPush(payload, {}, config, auth);
}).then((result) => {
done();
}, (err) => {
console.error(err);
fail("should not fail");
done();
});
});
it('properly creates _PushStatus', (done) => {
it_exclude_dbs(['postgres'])('properly creates _PushStatus', (done) => {
var installations = [];
while(installations.length != 10) {
@@ -279,7 +277,7 @@ describe('PushController', () => {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId);
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
Parse.Object.saveAll(installations).then(() => {
return pushController.sendPush(payload, {}, config, auth);
}).then((result) => {
@@ -295,9 +293,12 @@ describe('PushController', () => {
expect(results.length).toBe(1);
let 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(result.get('payload')).toEqual(payload.data);
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({
@@ -317,7 +318,46 @@ describe('PushController', () => {
});
it('should support full RESTQuery for increment', (done) => {
it_exclude_dbs(['postgres'])('should properly report failures in _PushStatus', (done) => {
var pushAdapter = {
send: function(body, installations) {
return installations.map((installation) => {
return Promise.resolve({
deviceType: installation.deviceType
})
})
},
getValidPushTypes: function() {
return ["ios"];
}
}
let where = { 'channels': {
'$ins': ['Giants', 'Mets']
}};
var payload = {data: {
alert: "Hello World!",
badge: 1,
}}
var config = new Config(Parse.applicationId);
var auth = {
isMaster: true
}
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
pushController.sendPush(payload, where, config, auth).then(() => {
fail('should not succeed');
done();
}).catch(() => {
let query = new Parse.Query('_PushStatus');
query.find({useMasterKey: true}).then((results) => {
expect(results.length).toBe(1);
let pushStatus = results[0];
expect(pushStatus.get('status')).toBe('failed');
done();
});
})
});
it_exclude_dbs(['postgres'])('should support full RESTQuery for increment', (done) => {
var payload = {data: {
alert: "Hello World!",
badge: 'Increment',
@@ -348,7 +388,7 @@ describe('PushController', () => {
}
}
var pushController = new PushController(pushAdapter, Parse.applicationId);
var pushController = new PushController(pushAdapter, Parse.applicationId, defaultConfiguration.push);
pushController.sendPush(payload, where, config, auth).then((result) => {
done();
}).catch((err) => {
+6 -3
View File
@@ -67,8 +67,8 @@ describe('PushRouter', () => {
}).toThrow();
done();
});
it('sends a push through REST', (done) => {
it_exclude_dbs(['postgres'])('sends a push through REST', (done) => {
request.post({
url: Parse.serverURL+"/push",
json: true,
@@ -82,8 +82,11 @@ describe('PushRouter', () => {
'X-Parse-Master-Key': Parse.masterKey
}
}, function(err, res, body){
expect(res.headers['x-parse-push-status-id']).not.toBe(undefined);
expect(res.headers['x-parse-push-status-id'].length).toBe(10);
expect(res.headers[''])
expect(body.result).toBe(true);
done();
});
});
});
});
+14
View File
@@ -112,6 +112,20 @@ describe('matchesQuery', function() {
expect(matchesQuery(obj, q)).toBe(false);
});
it('matches queries with doesNotExist constraint', function() {
var obj = {
id: new Id('Item', 'O1'),
count: 15
};
var q = new Parse.Query('Item');
q.doesNotExist('name');
expect(matchesQuery(obj, q)).toBe(true);
q = new Parse.Query('Item');
q.doesNotExist('count');
expect(matchesQuery(obj, q)).toBe(false);
});
it('matches on equality queries', function() {
var day = new Date();
var location = new Parse.GeoPoint({
+175 -55
View File
@@ -1,79 +1,83 @@
"use strict";
// These tests check the "create" / "update" functionality of the REST API.
var auth = require('../src/Auth');
var cache = require('../src/cache');
var Config = require('../src/Config');
var DatabaseAdapter = require('../src/DatabaseAdapter');
var Parse = require('parse/node').Parse;
var rest = require('../src/rest');
var request = require('request');
var config = new Config('test');
var database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
let database = config.database;
describe('rest create', () => {
it('handles _id', (done) => {
rest.create(config, auth.nobody(config), 'Foo', {}).then(() => {
return database.mongoFind('Foo', {});
}).then((results) => {
it_exclude_dbs(['postgres'])('handles _id', done => {
rest.create(config, auth.nobody(config), 'Foo', {})
.then(() => database.adapter.find('Foo', { fields: {} }, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var obj = results[0];
expect(typeof obj._id).toEqual('string');
expect(obj.objectId).toBeUndefined();
expect(typeof obj.objectId).toEqual('string');
expect(obj._id).toBeUndefined();
done();
});
});
it('handles array, object, date', (done) => {
it_exclude_dbs(['postgres'])('handles array, object, date', (done) => {
let now = new Date();
var obj = {
array: [1, 2, 3],
object: {foo: 'bar'},
date: Parse._encode(new Date()),
date: Parse._encode(now),
};
rest.create(config, auth.nobody(config), 'MyClass', obj).then(() => {
return database.mongoFind('MyClass', {}, {});
}).then((results) => {
rest.create(config, auth.nobody(config), 'MyClass', obj)
.then(() => database.adapter.find('MyClass', { fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
} }, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var mob = results[0];
expect(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date instanceof Date).toBe(true);
expect(mob.date.__type).toBe('Date');
expect(new Date(mob.date.iso).getTime()).toBe(now.getTime());
done();
});
});
it('handles object and subdocument', (done) => {
var obj = {
subdoc: {foo: 'bar', wu: 'tan'},
};
rest.create(config, auth.nobody(config), 'MyClass', obj).then(() => {
return database.mongoFind('MyClass', {}, {});
}).then((results) => {
it_exclude_dbs(['postgres'])('handles object and subdocument', done => {
let obj = { subdoc: {foo: 'bar', wu: 'tan'} };
rest.create(config, auth.nobody(config), 'MyClass', obj)
.then(() => database.adapter.find('MyClass', { fields: {} }, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
var mob = results[0];
let mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('tan');
expect(typeof mob._id).toEqual('string');
var obj = {
'subdoc.wu': 'clan',
};
rest.update(config, auth.nobody(config), 'MyClass', mob._id, obj).then(() => {
return database.mongoFind('MyClass', {}, {});
}).then((results) => {
expect(results.length).toEqual(1);
var mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('clan');
done();
});
expect(typeof mob.objectId).toEqual('string');
let obj = { 'subdoc.wu': 'clan' };
return rest.update(config, auth.nobody(config), 'MyClass', mob.objectId, obj)
})
.then(() => database.adapter.find('MyClass', { fields: {} }, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
let mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('clan');
done();
})
.catch(error => {
console.log(error);
fail();
done();
});
});
it('handles create on non-existent class when disabled client class creation', (done) => {
it_exclude_dbs(['postgres'])('handles create on non-existent class when disabled client class creation', (done) => {
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {})
.then(() => {
@@ -87,6 +91,21 @@ describe('rest create', () => {
});
});
it_exclude_dbs(['postgres'])('handles create on existent class when disabled client class creation', (done) => {
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('ClientClassCreation', {}))
.then(actualSchema => {
expect(actualSchema.className).toEqual('ClientClassCreation');
return rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {});
})
.then(() => {
done();
}, err => {
fail('Should not throw error')
});
});
it('handles user signup', (done) => {
var user = {
username: 'asdf',
@@ -103,7 +122,7 @@ describe('rest create', () => {
});
});
it('handles anonymous user signup', (done) => {
it_exclude_dbs(['postgres'])('handles anonymous user signup', (done) => {
var data1 = {
authData: {
anonymous: {
@@ -147,7 +166,7 @@ describe('rest create', () => {
});
});
it('handles anonymous user signup and upgrade to new user', (done) => {
it_exclude_dbs(['postgres'])('handles anonymous user signup and upgrade to new user', (done) => {
var data1 = {
authData: {
anonymous: {
@@ -207,7 +226,7 @@ describe('rest create', () => {
})
});
it('test facebook signup and login', (done) => {
it_exclude_dbs(['postgres'])('test facebook signup and login', (done) => {
var data = {
authData: {
facebook: {
@@ -240,8 +259,8 @@ describe('rest create', () => {
});
});
it('stores pointers with a _p_ prefix', (done) => {
var obj = {
it_exclude_dbs(['postgres'])('stores pointers', done => {
let obj = {
foo: 'bar',
aPointer: {
__type: 'Pointer',
@@ -250,17 +269,23 @@ describe('rest create', () => {
}
};
rest.create(config, auth.nobody(config), 'APointerDarkly', obj)
.then((r) => {
return database.mongoFind('APointerDarkly', {});
}).then((results) => {
expect(results.length).toEqual(1);
var output = results[0];
expect(typeof output._id).toEqual('string');
expect(typeof output._p_aPointer).toEqual('string');
expect(output._p_aPointer).toEqual('JustThePointer$qwerty');
expect(output.aPointer).toBeUndefined();
done();
.then(() => database.adapter.find('APointerDarkly', { fields: {
foo: { type: 'String' },
aPointer: { type: 'Pointer', targetClass: 'JustThePointer' },
}}, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
let output = results[0];
expect(typeof output.foo).toEqual('string');
expect(typeof output._p_aPointer).toEqual('undefined');
expect(output._p_aPointer).toBeUndefined();
expect(output.aPointer).toEqual({
__type: 'Pointer',
className: 'JustThePointer',
objectId: 'qwerty'
});
done();
});
});
it("cannot set objectId", (done) => {
@@ -284,4 +309,99 @@ describe('rest create', () => {
});
});
it_exclude_dbs(['postgres'])("test default session length", (done) => {
var user = {
username: 'asdf',
password: 'zxcv',
foo: 'bar',
};
var now = new Date();
rest.create(config, auth.nobody(config), '_User', user)
.then((r) => {
expect(Object.keys(r.response).length).toEqual(3);
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
return rest.find(config, auth.master(config),
'_Session', {sessionToken: r.response.sessionToken});
})
.then((r) => {
expect(r.results.length).toEqual(1);
var session = r.results[0];
var actual = new Date(session.expiresAt.iso);
var expected = new Date(now.getTime() + (1000 * 3600 * 24 * 365));
expect(actual.getFullYear()).toEqual(expected.getFullYear());
expect(actual.getMonth()).toEqual(expected.getMonth());
expect(actual.getDate()).toEqual(expected.getDate());
expect(actual.getMinutes()).toEqual(expected.getMinutes());
done();
});
});
it_exclude_dbs(['postgres'])("test specified session length", (done) => {
var user = {
username: 'asdf',
password: 'zxcv',
foo: 'bar',
};
var sessionLength = 3600, // 1 Hour ahead
now = new Date(); // For reference later
config.sessionLength = sessionLength;
rest.create(config, auth.nobody(config), '_User', user)
.then((r) => {
expect(Object.keys(r.response).length).toEqual(3);
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
return rest.find(config, auth.master(config),
'_Session', {sessionToken: r.response.sessionToken});
})
.then((r) => {
expect(r.results.length).toEqual(1);
var session = r.results[0];
var actual = new Date(session.expiresAt.iso);
var expected = new Date(now.getTime() + (sessionLength*1000));
expect(actual.getFullYear()).toEqual(expected.getFullYear());
expect(actual.getMonth()).toEqual(expected.getMonth());
expect(actual.getDate()).toEqual(expected.getDate());
expect(actual.getHours()).toEqual(expected.getHours());
expect(actual.getMinutes()).toEqual(expected.getMinutes());
done();
});
});
it_exclude_dbs(['postgres'])("can create a session with no expiration", (done) => {
var user = {
username: 'asdf',
password: 'zxcv',
foo: 'bar'
};
config.expireInactiveSessions = false;
rest.create(config, auth.nobody(config), '_User', user)
.then((r) => {
expect(Object.keys(r.response).length).toEqual(3);
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
return rest.find(config, auth.master(config),
'_Session', {sessionToken: r.response.sessionToken});
})
.then((r) => {
expect(r.results.length).toEqual(1);
var session = r.results[0];
expect(session.expiresAt).toBeUndefined();
done();
});
});
});
+78 -4
View File
@@ -1,3 +1,4 @@
'use strict'
// These tests check the "find" functionality of the REST API.
var auth = require('../src/Auth');
var cache = require('../src/cache');
@@ -8,6 +9,7 @@ var querystring = require('querystring');
var request = require('request');
var config = new Config('test');
let database = config.database;
var nobody = auth.nobody(config);
describe('rest query', () => {
@@ -35,8 +37,38 @@ describe('rest query', () => {
});
});
var data = {
username: 'blah',
password: 'pass',
sessionToken: 'abc123',
}
it_exclude_dbs(['postgres'])('query for user w/ legacy credentials without masterKey has them stripped from results', done => {
database.create('_User', data).then(() => {
return rest.find(config, nobody, '_User')
}).then((result) => {
var user = result.results[0];
expect(user.username).toEqual('blah');
expect(user.sessionToken).toBeUndefined();
expect(user.password).toBeUndefined();
done();
});
});
it_exclude_dbs(['postgres'])('query for user w/ legacy credentials with masterKey has them stripped from results', done => {
database.create('_User', data).then(() => {
return rest.find(config, {isMaster: true}, '_User')
}).then((result) => {
var user = result.results[0];
expect(user.username).toEqual('blah');
expect(user.sessionToken).toBeUndefined();
expect(user.password).toBeUndefined();
done();
});
});
// Created to test a scenario in AnyPic
it('query with include', (done) => {
it_exclude_dbs(['postgres'])('query with include', (done) => {
var photo = {
foo: 'bar'
};
@@ -95,7 +127,7 @@ describe('rest query', () => {
}).catch((error) => { console.log(error); });
});
it('query non-existent class when disabled client class creation', (done) => {
it_exclude_dbs(['postgres'])('query non-existent class when disabled client class creation', (done) => {
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {})
.then(() => {
@@ -109,6 +141,22 @@ describe('rest query', () => {
});
});
it_exclude_dbs(['postgres'])('query existent class when disabled client class creation', (done) => {
var customConfig = Object.assign({}, config, {allowClientClassCreation: false});
config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('ClientClassCreation', {}))
.then(actualSchema => {
expect(actualSchema.className).toEqual('ClientClassCreation');
return rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {});
})
.then((result) => {
expect(result.results.length).toEqual(0);
done();
}, err => {
fail('Should not throw error')
});
});
it('query with wrongly encoded parameter', (done) => {
rest.create(config, nobody, 'TestParameterEncode', {foo: 'bar'}
).then(() => {
@@ -130,7 +178,6 @@ describe('rest query', () => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
expect(b.error).toEqual('Improper encode of parameter');
done();
});
}).then(() => {
@@ -148,10 +195,37 @@ describe('rest query', () => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
expect(b.error).toEqual('Improper encode of parameter');
done();
});
});
});
it('query with limit = 0', (done) => {
rest.create(config, nobody, 'TestObject', {foo: 'baz'}
).then(() => {
return rest.create(config, nobody,
'TestObject', {foo: 'qux'});
}).then(() => {
return rest.find(config, nobody,
'TestObject', {}, {limit: 0});
}).then((response) => {
expect(response.results.length).toEqual(0);
done();
});
});
it_exclude_dbs(['postgres'])('query with limit = 0 and count = 1', (done) => {
rest.create(config, nobody, 'TestObject', {foo: 'baz'}
).then(() => {
return rest.create(config, nobody,
'TestObject', {foo: 'qux'});
}).then(() => {
return rest.find(config, nobody,
'TestObject', {}, {limit: 0, count: 1});
}).then((response) => {
expect(response.results.length).toEqual(0);
expect(response.count).toEqual(2);
done();
});
});
});
+242 -176
View File
@@ -1,7 +1,7 @@
'use strict';
var Config = require('../src/Config');
var Schema = require('../src/Schema');
var SchemaController = require('../src/Controllers/SchemaController');
var dd = require('deep-diff');
var config = new Config('test');
@@ -19,7 +19,7 @@ var hasAllPODobject = () => {
return obj;
};
describe('Schema', () => {
describe('SchemaController', () => {
it('can validate one object', (done) => {
config.database.loadSchema().then((schema) => {
return schema.validateObject('TestObject', {a: 1, b: 'yo', c: false});
@@ -95,7 +95,7 @@ describe('Schema', () => {
});
});
it('class-level permissions test user', (done) => {
it_exclude_dbs(['postgres'])('class-level permissions test user', (done) => {
var user;
createTestUser().then((u) => {
user = u;
@@ -120,57 +120,70 @@ describe('Schema', () => {
});
});
it('class-level permissions test get', (done) => {
var user;
it_exclude_dbs(['postgres'])('class-level permissions test get', (done) => {
var obj;
createTestUser().then((u) => {
user = u;
return config.database.loadSchema();
}).then((schema) => {
// Just to create a valid class
return schema.validateObject('Stuff', {foo: 'bar'});
}).then((schema) => {
var find = {};
var get = {};
get[user.id] = true;
return schema.setPermissions('Stuff', {
'find': find,
'get': get
});
}).then((schema) => {
obj = new Parse.Object('Stuff');
obj.set('foo', 'bar');
return obj.save();
}).then((o) => {
obj = o;
var query = new Parse.Query('Stuff');
return query.find();
}).then((results) => {
fail('Class permissions should have rejected this query.');
done();
}, (e) => {
var query = new Parse.Query('Stuff');
return query.get(obj.id).then((o) => {
createTestUser()
.then(user => {
return config.database.loadSchema()
// Create a valid class
.then(schema => schema.validateObject('Stuff', {foo: 'bar'}))
.then(schema => {
var find = {};
var get = {};
get[user.id] = true;
return schema.setPermissions('Stuff', {
'create': {'*': true},
'find': find,
'get': get
});
}).then((schema) => {
obj = new Parse.Object('Stuff');
obj.set('foo', 'bar');
return obj.save();
}).then((o) => {
obj = o;
var query = new Parse.Query('Stuff');
return query.find();
}).then((results) => {
fail('Class permissions should have rejected this query.');
done();
}, (e) => {
fail('Class permissions should have allowed this get query');
var query = new Parse.Query('Stuff');
return query.get(obj.id).then((o) => {
done();
}, (e) => {
fail('Class permissions should have allowed this get query');
done();
});
});
});
});
it('can add classes without needing an object', done => {
it_exclude_dbs(['postgres'])('can add classes without needing an object', done => {
config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('NewClass', {
foo: {type: 'String'}
}))
.then(result => {
expect(result).toEqual({
_id: 'NewClass',
objectId: 'string',
updatedAt: 'string',
createdAt: 'string',
foo: 'string',
})
.then(actualSchema => {
const expectedSchema = {
className: 'NewClass',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
foo: { type: 'String' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
}
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done();
})
.catch(error => {
@@ -194,22 +207,34 @@ describe('Schema', () => {
});
});
it('will resolve class creation races appropriately', done => {
it_exclude_dbs(['postgres'])('will resolve class creation races appropriately', done => {
// If two callers race to create the same schema, the response to the
// race loser should be the same as if they hadn't been racing.
config.database.loadSchema()
.then(schema => {
var p1 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}});
var p2 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}});
Promise.race([p1, p2]) //Use race because we expect the first completed promise to be the successful one
.then(response => {
expect(response).toEqual({
_id: 'NewClass',
objectId: 'string',
updatedAt: 'string',
createdAt: 'string',
foo: 'string',
});
Promise.race([p1, p2])
.then(actualSchema => {
const expectedSchema = {
className: 'NewClass',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
foo: { type: 'String' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
}
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
});
Promise.all([p1,p2])
.catch(error => {
@@ -359,7 +384,7 @@ describe('Schema', () => {
});
});
it('will create classes', done => {
it_exclude_dbs(['postgres'])('will create classes', done => {
config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('NewClass', {
aNumber: {type: 'Number'},
@@ -373,86 +398,139 @@ describe('Schema', () => {
aPointer: {type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet'},
aRelation: {type: 'Relation', targetClass: 'NewClass'},
}))
.then(mongoObj => {
expect(mongoObj).toEqual({
_id: 'NewClass',
objectId: 'string',
createdAt: 'string',
updatedAt: 'string',
aNumber: 'number',
aString: 'string',
aBool: 'boolean',
aDate: 'date',
aObject: 'object',
aArray: 'array',
aGeoPoint: 'geopoint',
aFile: 'file',
aPointer: '*ThisClassDoesNotExistYet',
aRelation: 'relation<NewClass>',
});
.then(actualSchema => {
const expectedSchema = {
className: 'NewClass',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
aString: { type: 'String' },
aNumber: { type: 'Number' },
aString: { type: 'String' },
aBool: { type: 'Boolean' },
aDate: { type: 'Date' },
aObject: { type: 'Object' },
aArray: { type: 'Array' },
aGeoPoint: { type: 'GeoPoint' },
aFile: { type: 'File' },
aPointer: { type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet' },
aRelation: { type: 'Relation', targetClass: 'NewClass' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
}
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done();
});
});
it('creates the default fields for non-custom classes', done => {
it_exclude_dbs(['postgres'])('creates the default fields for non-custom classes', done => {
config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('_Installation', {
foo: {type: 'Number'},
}))
.then(mongoObj => {
expect(mongoObj).toEqual({
_id: '_Installation',
createdAt: 'string',
updatedAt: 'string',
objectId: 'string',
foo: 'number',
installationId: 'string',
deviceToken: 'string',
channels: 'array',
deviceType: 'string',
pushType: 'string',
GCMSenderId: 'string',
timeZone: 'string',
localeIdentifier: 'string',
badge: 'number',
});
.then(actualSchema => {
const expectedSchema = {
className: '_Installation',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
foo: { type: 'Number' },
installationId: { type: 'String' },
deviceToken: { type: 'String' },
channels: { type: 'Array' },
deviceType: { type: 'String' },
pushType: { type: 'String' },
GCMSenderId: { type: 'String' },
timeZone: { type: 'String' },
localeIdentifier: { type: 'String' },
badge: { type: 'Number' },
appVersion: { type: 'String' },
appName: { type: 'String' },
appIdentifier: { type: 'String' },
parseVersion: { type: 'String' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
}
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done();
});
});
it('creates non-custom classes which include relation field', done => {
it_exclude_dbs(['postgres'])('creates non-custom classes which include relation field', done => {
config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('_Role', {}))
.then(mongoObj => {
expect(mongoObj).toEqual({
_id: '_Role',
createdAt: 'string',
updatedAt: 'string',
objectId: 'string',
name: 'string',
users: 'relation<_User>',
roles: 'relation<_Role>',
});
.then(actualSchema => {
const expectedSchema = {
className: '_Role',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
name: { type: 'String' },
users: { type: 'Relation', targetClass: '_User' },
roles: { type: 'Relation', targetClass: '_Role' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
};
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done();
});
});
it('creates non-custom classes which include pointer field', done => {
it_exclude_dbs(['postgres'])('creates non-custom classes which include pointer field', done => {
config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('_Session', {}))
.then(mongoObj => {
expect(mongoObj).toEqual({
_id: '_Session',
createdAt: 'string',
updatedAt: 'string',
objectId: 'string',
restricted: 'boolean',
user: '*_User',
installationId: 'string',
sessionToken: 'string',
expiresAt: 'date',
createdWith: 'object'
});
.then(actualSchema => {
const expectedSchema = {
className: '_Session',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
restricted: { type: 'Boolean' },
user: { type: 'Pointer', targetClass: '_User' },
installationId: { type: 'String' },
sessionToken: { type: 'String' },
expiresAt: { type: 'Date' },
createdWith: { type: 'Object' },
ACL: { type: 'ACL' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
};
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done();
});
});
@@ -470,7 +548,7 @@ describe('Schema', () => {
});
});
it('can check if a class exists', done => {
it_exclude_dbs(['postgres'])('can check if a class exists', done => {
config.database.loadSchema()
.then(schema => {
return schema.addClassIfNotExists('NewClass', {})
@@ -535,7 +613,7 @@ describe('Schema', () => {
});
});
it('refuses to delete fields that dont exist', done => {
it_exclude_dbs(['postgres'])('refuses to delete fields that dont exist', done => {
hasAllPODobject().save()
.then(() => config.database.loadSchema())
.then(schema => schema.deleteField('missingField', 'HasAllPOD'))
@@ -546,7 +624,7 @@ describe('Schema', () => {
});
});
it('drops related collection when deleting relation field', done => {
it_exclude_dbs(['postgres'])('drops related collection when deleting relation field', done => {
var obj1 = hasAllPODobject();
obj1.save()
.then(savedObj1 => {
@@ -577,20 +655,32 @@ describe('Schema', () => {
});
});
it('can delete relation field when related _Join collection not exist', done => {
it_exclude_dbs(['postgres'])('can delete relation field when related _Join collection not exist', done => {
config.database.loadSchema()
.then(schema => {
schema.addClassIfNotExists('NewClass', {
relationField: {type: 'Relation', targetClass: '_User'}
})
.then(mongoObj => {
expect(mongoObj).toEqual({
_id: 'NewClass',
objectId: 'string',
updatedAt: 'string',
createdAt: 'string',
relationField: 'relation<_User>',
});
.then(actualSchema => {
const expectedSchema = {
className: 'NewClass',
fields: {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
relationField: { type: 'Relation', targetClass: '_User' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
};
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
})
.then(() => config.database.collectionExists('_Join:relationField:NewClass'))
.then(exist => {
@@ -599,17 +689,19 @@ describe('Schema', () => {
.then(() => schema.deleteField('relationField', 'NewClass', config.database))
.then(() => schema.reloadData())
.then(() => {
expect(schema['data']['NewClass']).toEqual({
objectId: 'string',
updatedAt: 'string',
createdAt: 'string'
});
const expectedSchema = {
objectId: { type: 'String' },
updatedAt: { type: 'Date' },
createdAt: { type: 'Date' },
ACL: { type: 'ACL' },
};
expect(dd(schema.data.NewClass, expectedSchema)).toEqual(undefined);
done();
});
});
});
it('can delete string fields and resave as number field', done => {
it_exclude_dbs(['postgres'])('can delete string fields and resave as number field', done => {
Parse.Object.disableSingleInstance();
var obj1 = hasAllPODobject();
var obj2 = hasAllPODobject();
@@ -630,10 +722,14 @@ describe('Schema', () => {
done();
Parse.Object.enableSingleInstance();
});
})
.catch(error => {
fail(error);
done();
});
});
it('can delete pointer fields and resave as string', done => {
it_exclude_dbs(['postgres'])('can delete pointer fields and resave as string', done => {
Parse.Object.disableSingleInstance();
var obj1 = new Parse.Object('NewClass');
obj1.save()
@@ -660,9 +756,9 @@ describe('Schema', () => {
});
it('can merge schemas', done => {
expect(Schema.buildMergedSchemaObject({
expect(SchemaController.buildMergedSchemaObject({
_id: 'SomeClass',
someType: 'number'
someType: { type: 'Number' }
}, {
newType: {type: 'Number'}
})).toEqual({
@@ -673,10 +769,10 @@ describe('Schema', () => {
});
it('can merge deletions', done => {
expect(Schema.buildMergedSchemaObject({
expect(SchemaController.buildMergedSchemaObject({
_id: 'SomeClass',
someType: 'number',
outDatedType: 'string',
someType: { type: 'Number' },
outDatedType: { type: 'String' },
},{
newType: {type: 'GeoPoint'},
outDatedType: {__op: 'Delete'},
@@ -688,47 +784,17 @@ describe('Schema', () => {
});
it('ignore default field when merge with system class', done => {
expect(Schema.buildMergedSchemaObject({
expect(SchemaController.buildMergedSchemaObject({
_id: '_User',
username: 'string',
password: 'string',
authData: 'object',
email: 'string',
emailVerified: 'boolean'
username: { type: 'String' },
password: { type: 'String' },
email: { type: 'String' },
emailVerified: { type: 'Boolean' },
},{
authData: {type: 'string'},
customField: {type: 'string'},
emailVerified: { type: 'String' },
customField: { type: 'String' },
})).toEqual({
customField: {type: 'string'}
});
done();
});
it('handles legacy _client_permissions keys without crashing', done => {
Schema.mongoSchemaToSchemaAPIResponse({
"_id":"_Installation",
"_client_permissions":{
"get":true,
"find":true,
"update":true,
"create":true,
"delete":true,
},
"_metadata":{
"class_permissions":{
"get":{"*":true},
"find":{"*":true},
"update":{"*":true},
"create":{"*":true},
"delete":{"*":true},
"addField":{"*":true},
}
},
"installationId":"string",
"deviceToken":"string",
"deviceType":"string",
"channels":"array",
"user":"*_User",
customField: { type: 'String' }
});
done();
});
+103
View File
@@ -0,0 +1,103 @@
'use strict';
var request = require('request');
const Parse = require("parse/node");
let Config = require('../src/Config');
describe('Uniqueness', function() {
it('fail when create duplicate value in unique field', done => {
let obj = new Parse.Object('UniqueField');
obj.set('unique', 'value');
obj.save().then(() => {
expect(obj.id).not.toBeUndefined();
let config = new Config('test');
return config.database.adapter.ensureUniqueness('UniqueField', { fields: { unique: { __type: 'String' } } }, ['unique'])
})
.then(() => {
let obj = new Parse.Object('UniqueField');
obj.set('unique', 'value');
return obj.save()
}).then(() => {
fail('Saving duplicate field should have failed');
done();
}, error => {
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
done();
});
});
it_exclude_dbs(['postgres'])('unique indexing works on pointer fields', done => {
let obj = new Parse.Object('UniquePointer');
obj.save({ string: 'who cares' })
.then(() => obj.save({ ptr: obj }))
.then(() => {
let config = new Config('test');
return config.database.adapter.ensureUniqueness('UniquePointer', { fields: {
string: { __type: 'String' },
ptr: { __type: 'Pointer', targetClass: 'UniquePointer' }
} }, ['ptr']);
})
.then(() => {
let newObj = new Parse.Object('UniquePointer')
newObj.set('ptr', obj)
return newObj.save()
})
.then(() => {
fail('save should have failed due to duplicate value');
done();
})
.catch(error => {
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
done();
});
});
it_exclude_dbs(['postgres'])('fails when attempting to ensure uniqueness of fields that are not currently unique', done => {
let o1 = new Parse.Object('UniqueFail');
o1.set('key', 'val');
let o2 = new Parse.Object('UniqueFail');
o2.set('key', 'val');
Parse.Object.saveAll([o1, o2])
.then(() => {
let config = new Config('test');
return config.database.adapter.ensureUniqueness('UniqueFail', { fields: { key: { __type: 'String' } } }, ['key']);
})
.catch(error => {
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
done();
});
});
it_exclude_dbs(['postgres'])('can do compound uniqueness', done => {
let config = new Config('test');
config.database.adapter.ensureUniqueness('CompoundUnique', { fields: { k1: { __type: 'String' }, k2: { __type: 'String' } } }, ['k1', 'k2'])
.then(() => {
let o1 = new Parse.Object('CompoundUnique');
o1.set('k1', 'v1');
o1.set('k2', 'v2');
return o1.save();
})
.then(() => {
let o2 = new Parse.Object('CompoundUnique');
o2.set('k1', 'v1');
o2.set('k2', 'not a dupe');
return o2.save();
})
.then(() => {
let o3 = new Parse.Object('CompoundUnique');
o3.set('k1', 'not a dupe');
o3.set('k2', 'v2');
return o3.save();
})
.then(() => {
let o4 = new Parse.Object('CompoundUnique');
o4.set('k1', 'v1');
o4.set('k2', 'v2');
return o4.save();
})
.catch(error => {
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
done();
});
});
});
+303 -380
View File
@@ -1,20 +1,13 @@
"use strict";
var request = require('request');
var Config = require("../src/Config");
describe("Custom Pages Configuration", () => {
let MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions');
let request = require('request');
let Config = require("../src/Config");
describe("Custom Pages, Email Verification, Password Reset", () => {
it("should set the custom pages", (done) => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
customPages: {
invalidLink: "myInvalidLink",
verifyEmailSuccess: "myVerifyEmailSuccess",
@@ -22,60 +15,51 @@ describe("Custom Pages Configuration", () => {
passwordResetSuccess: "myPasswordResetSuccess"
},
publicServerURL: "https://my.public.server.com/1"
})
.then(() => {
var config = new Config("test");
expect(config.invalidLinkURL).toEqual("myInvalidLink");
expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess");
expect(config.choosePasswordURL).toEqual("myChoosePassword");
expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess");
expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email");
expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset");
done();
});
var config = new Config("test");
expect(config.invalidLinkURL).toEqual("myInvalidLink");
expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess");
expect(config.choosePasswordURL).toEqual("myChoosePassword");
expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess");
expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email");
expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset");
done();
});
});
describe("Email Verification", () => {
it('sends verification email if email verification is enabled', done => {
it_exclude_dbs(['postgres'])('sends verification email if email verification is enabled', done => {
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
spyOn(emailAdapter, 'sendVerificationEmail');
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.setEmail('cool_guy@parse.com');
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
user.fetch()
.then(() => {
expect(user.get('emailVerified')).toEqual(false);
})
.then(() => {
spyOn(emailAdapter, 'sendVerificationEmail');
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.setEmail('testIfEnabled@parse.com');
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
user.fetch()
.then(() => {
expect(user.get('emailVerified')).toEqual(false);
done();
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
}
});
});
});
@@ -85,150 +69,129 @@ describe("Email Verification", () => {
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
spyOn(emailAdapter, 'sendVerificationEmail');
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
user.fetch()
.then(() => {
expect(user.get('emailVerified')).toEqual(undefined);
})
.then(() => {
spyOn(emailAdapter, 'sendVerificationEmail');
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
user.fetch()
.then(() => {
expect(user.get('emailVerified')).toEqual(undefined);
done();
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
}
});
});
});
it('does send a validation email when updating the email', done => {
it_exclude_dbs(['postgres'])('does send a validation email when updating the email', done => {
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
spyOn(emailAdapter, 'sendVerificationEmail');
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
user.fetch()
.then((user) => {
user.set("email", "cool_guy@parse.com");
return user.save();
}).then((user) => {
return user.fetch();
}).then(() => {
expect(user.get('emailVerified')).toEqual(false);
// Wait as on update email, we need to fetch the username
setTimeout(function(){
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
done();
}, 200);
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
})
.then(() => {
spyOn(emailAdapter, 'sendVerificationEmail');
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
user.fetch()
.then((user) => {
user.set("email", "testWhenUpdating@parse.com");
return user.save();
}).then((user) => {
return user.fetch();
}).then(() => {
expect(user.get('emailVerified')).toEqual(false);
// Wait as on update email, we need to fetch the username
setTimeout(function(){
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
done();
}, 200);
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
});
});
it('does send a validation email with valid verification link when updating the email', done => {
it_exclude_dbs(['postgres'])('does send a validation email with valid verification link when updating the email', done => {
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
spyOn(emailAdapter, 'sendVerificationEmail').and.callFake((options) => {
expect(options.link).not.toBeNull();
expect(options.link).not.toMatch(/token=undefined/);
Promise.resolve();
});
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
user.fetch()
.then((user) => {
user.set("email", "cool_guy@parse.com");
return user.save();
}).then((user) => {
return user.fetch();
}).then(() => {
expect(user.get('emailVerified')).toEqual(false);
// Wait as on update email, we need to fetch the username
setTimeout(function(){
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
done();
}, 200);
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
})
.then(() => {
spyOn(emailAdapter, 'sendVerificationEmail').and.callFake((options) => {
expect(options.link).not.toBeNull();
expect(options.link).not.toMatch(/token=undefined/);
Promise.resolve();
});
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled();
user.fetch()
.then((user) => {
user.set("email", "testValidLinkWhenUpdating@parse.com");
return user.save();
}).then((user) => {
return user.fetch();
}).then(() => {
expect(user.get('emailVerified')).toEqual(false);
// Wait as on update email, we need to fetch the username
setTimeout(function(){
expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled();
done();
}, 200);
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
});
});
it('does send with a simple adapter', done => {
it_exclude_dbs(['postgres'])('does send with a simple adapter', done => {
var calls = 0;
var emailAdapter = {
sendMail: function(options){
expect(options.to).toBe('cool_guy@parse.com');
expect(options.to).toBe('testSendSimpleAdapter@parse.com');
if (calls == 0) {
expect(options.subject).toEqual('Please verify your e-mail for My Cool App');
expect(options.text.match(/verify_email/)).not.toBe(null);
@@ -240,42 +203,70 @@ describe("Email Verification", () => {
return Promise.resolve();
}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'My Cool App',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.set("email", "cool_guy@parse.com");
user.signUp(null, {
success: function(user) {
expect(calls).toBe(1);
user.fetch()
.then((user) => {
return user.save();
}).then((user) => {
return Parse.User.requestPasswordReset("cool_guy@parse.com");
}).then(() => {
expect(calls).toBe(2);
})
.then(() => {
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.set("email", "testSendSimpleAdapter@parse.com");
user.signUp(null, {
success: function(user) {
expect(calls).toBe(1);
user.fetch()
.then((user) => {
return user.save();
}).then((user) => {
return Parse.User.requestPasswordReset("testSendSimpleAdapter@parse.com").catch((err) => {
fail('Should not fail requesting a password');
done();
})
}).then(() => {
expect(calls).toBe(2);
done();
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
});
},
error: function(userAgain, error) {
fail('Failed to save user');
}
});
});
});
it_exclude_dbs(['postgres'])('fails if you include an emailAdapter, set verifyUserEmails to false, dont set a publicServerURL, and try to send a password reset email (regression test for #1649)', done => {
reconfigureServer({
appName: 'unused',
verifyUserEmails: false,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
apiKey: 'k',
domain: 'd',
}),
})
.then(() => {
let user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.set("email", "testInvalidConfig@parse.com");
user.signUp(null)
.then(user => Parse.User.requestPasswordReset("testInvalidConfig@parse.com"))
.then(result => {
console.log(result);
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.')
done();
});
})
.catch(error => {
fail(JSON.stringify(error));
done();
});
});
@@ -285,41 +276,34 @@ describe("Email Verification", () => {
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: false,
emailAdapter: emailAdapter,
});
spyOn(emailAdapter, 'sendVerificationEmail');
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
user.fetch()
.then(() => {
expect(emailAdapter.sendVerificationEmail.calls.count()).toEqual(0);
expect(user.get('emailVerified')).toEqual(undefined);
})
.then(() => {
spyOn(emailAdapter, 'sendVerificationEmail');
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.signUp(null, {
success: function(user) {
user.fetch()
.then(() => {
expect(emailAdapter.sendVerificationEmail.calls.count()).toEqual(0);
expect(user.get('emailVerified')).toEqual(undefined);
done();
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
});
},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
}
});
});
});
it('receives the app name and user in the adapter', done => {
it_exclude_dbs(['postgres'])('receives the app name and user in the adapter', done => {
var emailAdapter = {
sendVerificationEmail: options => {
expect(options.appName).toEqual('emailing app');
@@ -329,35 +313,28 @@ describe("Email Verification", () => {
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.set('email', 'user@parse.com');
user.signUp(null, {
success: () => {},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
})
.then(() => {
var user = new Parse.User();
user.setPassword("asdf");
user.setUsername("zxcv");
user.set('email', 'user@parse.com');
user.signUp(null, {
success: () => {},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
});
})
it('when you click the link in the email it sets emailVerified to true and redirects you', done => {
it_exclude_dbs(['postgres'])('when you click the link in the email it sets emailVerified to true and redirects you', done => {
var user = new Parse.User();
var emailAdapter = {
sendVerificationEmail: options => {
@@ -380,39 +357,23 @@ describe("Email Verification", () => {
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
})
.then(() => {
user.setPassword("asdf");
user.setUsername("user");
user.set('email', 'user@parse.com');
user.signUp();
});
user.setPassword("asdf");
user.setUsername("user");
user.set('email', 'user@parse.com');
user.signUp();
});
it('redirects you to invalid link if you try to verify email incorrecly', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
sendVerificationEmail: () => Promise.resolve(),
@@ -420,28 +381,21 @@ describe("Email Verification", () => {
sendMail: () => {}
},
publicServerURL: "http://localhost:8378/1"
});
request.get('http://localhost:8378/1/apps/test/verify_email', {
followRedirect: false,
}, (error, response, body) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
done()
})
.then(() => {
request.get('http://localhost:8378/1/apps/test/verify_email', {
followRedirect: false,
}, (error, response, body) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
done()
});
});
});
it('redirects you to invalid link if you try to validate a nonexistant users email', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
sendVerificationEmail: () => Promise.resolve(),
@@ -449,17 +403,19 @@ describe("Email Verification", () => {
sendMail: () => {}
},
publicServerURL: "http://localhost:8378/1"
});
request.get('http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', {
followRedirect: false,
}, (error, response, body) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
done();
})
.then(() => {
request.get('http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', {
followRedirect: false,
}, (error, response, body) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
done();
});
});
});
it('does not update email verified if you use an invalid token', done => {
it_exclude_dbs(['postgres'])('does not update email verified if you use an invalid token', done => {
var user = new Parse.User();
var emailAdapter = {
sendVerificationEmail: options => {
@@ -478,37 +434,27 @@ describe("Email Verification", () => {
sendPasswordResetEmail: () => Promise.resolve(),
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
user.setPassword("asdf");
user.setUsername("zxcv");
user.set('email', 'user@parse.com');
user.signUp(null, {
success: () => {},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
})
.then(() => {
user.setPassword("asdf");
user.setUsername("zxcv");
user.set('email', 'user@parse.com');
user.signUp(null, {
success: () => {},
error: function(userAgain, error) {
fail('Failed to save user');
done();
}
});
});
});
});
describe("Password Reset", () => {
it('should send a password reset link', done => {
it_exclude_dbs(['postgres'])('should send a password reset link', done => {
var user = new Parse.User();
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
@@ -529,47 +475,31 @@ describe("Password Reset", () => {
},
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
user.setPassword("asdf");
user.setUsername("zxcv+zxcv");
user.set('email', 'user@parse.com');
user.signUp().then(() => {
Parse.User.requestPasswordReset('user@parse.com', {
error: (err) => {
console.error(err);
fail("Should not fail");
done();
}
})
.then(() => {
user.setPassword("asdf");
user.setUsername("zxcv+zxcv");
user.set('email', 'user@parse.com');
user.signUp().then(() => {
Parse.User.requestPasswordReset('user@parse.com', {
error: (err) => {
console.error(err);
fail("Should not fail requesting a password");
done();
}
});
});
});
});
it('redirects you to invalid link if you try to request password for a nonexistant users email', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
sendVerificationEmail: () => Promise.resolve(),
@@ -577,17 +507,19 @@ describe("Password Reset", () => {
sendMail: () => {}
},
publicServerURL: "http://localhost:8378/1"
});
request.get('http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', {
followRedirect: false,
}, (error, response, body) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
done();
})
.then(() => {
request.get('http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', {
followRedirect: false,
}, (error, response, body) => {
expect(response.statusCode).toEqual(302);
expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html');
done();
});
});
});
it('should programatically reset password', done => {
it_exclude_dbs(['postgres'])('should programatically reset password', done => {
var user = new Parse.User();
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve(),
@@ -628,9 +560,8 @@ describe("Password Reset", () => {
Parse.User.logIn("zxcv", "hello").then(function(user){
let config = new Config('test');
config.database.adaptiveCollection('_User')
.then(coll => coll.find({ 'username': 'zxcv' }, { limit: 1 }))
.then((results) => {
config.database.adapter.find('_User', { fields: {} }, { 'username': 'zxcv' }, { limit: 1 })
.then(results => {
// _perishable_token should be unset after reset password
expect(results.length).toEqual(1);
expect(results[0]['_perishable_token']).toEqual(undefined);
@@ -647,33 +578,25 @@ describe("Password Reset", () => {
},
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
reconfigureServer({
appName: 'emailing app',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: emailAdapter,
publicServerURL: "http://localhost:8378/1"
});
user.setPassword("asdf");
user.setUsername("zxcv");
user.set('email', 'user@parse.com');
user.signUp().then(() => {
Parse.User.requestPasswordReset('user@parse.com', {
error: (err) => {
console.error(err);
fail("Should not fail");
done();
}
})
.then(() => {
user.setPassword("asdf");
user.setUsername("zxcv");
user.set('email', 'user@parse.com');
user.signUp().then(() => {
Parse.User.requestPasswordReset('user@parse.com', {
error: (err) => {
console.error(err);
fail("Should not fail");
done();
}
});
});
});
});
})
+3
View File
@@ -0,0 +1,3 @@
Parse.Cloud.define('cloudCodeInFile', (req, res) => {
res.success('It is possible to define cloud code in a file.');
});
+3
View File
@@ -0,0 +1,3 @@
Parse.Cloud.define('cloudCodeInFile', (req, res) => {
res.success('It is possible to define cloud code in a file.');
});
-117
View File
@@ -1,117 +0,0 @@
Parse.Cloud.define('hello', function(req, res) {
res.success('Hello world!');
});
Parse.Cloud.beforeSave('BeforeSaveFail', function(req, res) {
res.error('You shall not pass!');
});
Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', function (req, res) {
var query = new Parse.Query('Yolo');
query.find().then(() => {
res.error('Nope');
}, () => {
res.success();
});
});
Parse.Cloud.beforeSave('BeforeSaveUnchanged', function(req, res) {
res.success();
});
Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) {
req.object.set('foo', 'baz');
res.success();
});
Parse.Cloud.afterSave('AfterSaveTest', function(req) {
var obj = new Parse.Object('AfterSaveProof');
obj.set('proof', req.object.id);
obj.save();
});
Parse.Cloud.beforeDelete('BeforeDeleteFail', function(req, res) {
res.error('Nope');
});
Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', function (req, res) {
var query = new Parse.Query('Yolo');
query.find().then(() => {
res.error('Nope');
}, () => {
res.success();
});
});
Parse.Cloud.beforeDelete('BeforeDeleteTest', function(req, res) {
res.success();
});
Parse.Cloud.afterDelete('AfterDeleteTest', function(req) {
var obj = new Parse.Object('AfterDeleteProof');
obj.set('proof', req.object.id);
obj.save();
});
Parse.Cloud.beforeSave('SaveTriggerUser', function(req, res) {
if (req.user && req.user.id) {
res.success();
} else {
res.error('No user present on request object for beforeSave.');
}
});
Parse.Cloud.afterSave('SaveTriggerUser', function(req) {
if (!req.user || !req.user.id) {
console.log('No user present on request object for afterSave.');
}
});
Parse.Cloud.define('foo', function(req, res) {
res.success({
object: {
__type: 'Object',
className: 'Foo',
objectId: '123',
x: 2,
relation: {
__type: 'Object',
className: 'Bar',
objectId: '234',
x: 3
}
},
array: [{
__type: 'Object',
className: 'Bar',
objectId: '345',
x: 2
}],
a: 2
});
});
Parse.Cloud.define('bar', function(req, res) {
res.error('baz');
});
Parse.Cloud.define('requiredParameterCheck', function(req, res) {
res.success();
}, function(params) {
return params.name;
});
Parse.Cloud.define('echoKeys', function(req, res){
return res.success({
applicationId: Parse.applicationId,
masterKey: Parse.masterKey,
javascriptKey: Parse.javascriptKey
})
});
Parse.Cloud.define('createBeforeSaveChangedObject', function(req, res){
var obj = new Parse.Object('BeforeSaveChanged');
obj.save().then(() => {
res.success(obj);
})
})
+6
View File
@@ -0,0 +1,6 @@
{
"arg1": "my_app",
"arg2": "8888",
"arg3": "hello",
"arg4": "/1"
}
+9
View File
@@ -0,0 +1,9 @@
{
"apps": [
{
"arg1": "my_app",
"arg2": 8888,
"arg3": "hello",
"arg4": "/1"
}]
}
+6
View File
@@ -0,0 +1,6 @@
{
"arg1": "my_app",
"arg2": "hello",
"arg3": "hello",
"arg4": "/1"
}
@@ -0,0 +1,16 @@
{
"apps": [
{
"arg1": "my_app",
"arg2": "99999",
"arg3": "hello",
"arg4": "/1"
},
{
"arg1": "my_app2",
"arg2": "9999",
"arg3": "hello",
"arg4": "/1"
}
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"arg1": "my_app",
"arg2": "8888",
"arg3": "hello",
"myArg": "/1"
}
+124 -55
View File
@@ -1,6 +1,7 @@
"use strict"
// Sets up a Parse API server for testing.
jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000;
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 5000;
var cache = require('../src/cache').default;
var DatabaseAdapter = require('../src/DatabaseAdapter');
@@ -8,30 +9,49 @@ var express = require('express');
var facebook = require('../src/authDataManager/facebook');
var ParseServer = require('../src/index').ParseServer;
var path = require('path');
var TestUtils = require('../src/index').TestUtils;
var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
const GridStoreAdapter = require('../src/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter');
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
let databaseAdapter;
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
var postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
databaseAdapter = new PostgresStorageAdapter({
uri: postgresURI,
collectionPrefix: 'test_',
});
} else {
databaseAdapter = new MongoStorageAdapter({
uri: mongoURI,
collectionPrefix: 'test_',
})
}
var databaseURI = process.env.DATABASE_URI;
var cloudMain = process.env.CLOUD_CODE_MAIN || '../spec/cloud/main.js';
var port = 8378;
let gridStoreAdapter = new GridStoreAdapter(mongoURI);
// Default server configuration for tests.
var defaultConfiguration = {
databaseURI: databaseURI,
cloud: cloudMain,
filesAdapter: gridStoreAdapter,
serverURL: 'http://localhost:' + port + '/1',
databaseAdapter,
appId: 'test',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
webhookKey: 'hook',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
push: {
'ios': {
cert: 'prodCert.pem',
key: 'prodKey.pem',
production: true,
bundleId: 'bundleId'
bundleId: 'bundleId',
}
},
oauth: { // Override the facebook provider
@@ -39,36 +59,48 @@ var defaultConfiguration = {
myoauth: {
module: path.resolve(__dirname, "myoauth") // relative path as it's run from src
}
}
},
};
let openConnections = {};
// Set up a default API server for testing with default configuration.
var api = new ParseServer(defaultConfiguration);
var app = express();
app.use('/1', api);
var server = app.listen(port);
server.on('connection', connection => {
let key = `${connection.remoteAddress}:${connection.remotePort}`;
openConnections[key] = connection;
connection.on('close', () => { delete openConnections[key] });
});
// Prevent reinitializing the server from clobbering Cloud Code
delete defaultConfiguration.cloud;
var currentConfiguration;
// Allows testing specific configurations of Parse Server
var setServerConfiguration = configuration => {
// the configuration hasn't changed
if (configuration === currentConfiguration) {
return;
}
DatabaseAdapter.clearDatabaseSettings();
currentConfiguration = configuration;
server.close();
cache.clearCache();
app = express();
api = new ParseServer(configuration);
app.use('/1', api);
server = app.listen(port);
};
const reconfigureServer = changedConfiguration => {
return new Promise((resolve, reject) => {
server.close(() => {
try {
let newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, {
__indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(resolve, reject)
});
cache.clear();
app = express();
api = new ParseServer(newConfiguration);
app.use('/1', api);
var restoreServerConfiguration = () => setServerConfiguration(defaultConfiguration);
server = app.listen(port);
server.on('connection', connection => {
let key = `${connection.remoteAddress}:${connection.remotePort}`;
openConnections[key] = connection;
connection.on('close', () => { delete openConnections[key] });
});
} catch(error) {
reject(error);
}
});
});
}
// Set up a Parse client to talk to our test API server
var Parse = require('parse/node');
@@ -78,23 +110,60 @@ Parse.serverURL = 'http://localhost:' + port + '/1';
// TODO: update tests to work in an A+ way
Parse.Promise.disableAPlusCompliant();
beforeEach(function(done) {
restoreServerConfiguration();
Parse.initialize('test', 'test', 'test');
Parse.serverURL = 'http://localhost:' + port + '/1';
Parse.User.enableUnsafeCurrentUser();
done();
beforeEach(done => {
try {
Parse.User.enableUnsafeCurrentUser();
} catch (error) {
if (error !== 'You need to call Parse.initialize before using Parse.') {
throw error;
}
}
TestUtils.destroyAllDataPermanently()
.catch(error => {
// For tests that connect to their own mongo, there won't be any data to delete.
if (error.message === 'ns not found' || error.message.startsWith('connect ECONNREFUSED')) {
return;
} else {
fail(error);
return;
}
})
.then(reconfigureServer)
.then(() => {
Parse.initialize('test', 'test', 'test');
Parse.serverURL = 'http://localhost:' + port + '/1';
done();
}, error => {
fail(JSON.stringify(error));
done();
})
});
afterEach(function(done) {
Parse.User.logOut().then(() => {
return clearData();
}).then(() => {
let afterLogOut = () => {
if (Object.keys(openConnections).length > 0) {
fail('There were open connections to the server left after the test finished');
}
done();
}, (error) => {
console.log('error in clearData', error);
done();
});
};
Parse.Cloud._removeAllHooks();
databaseAdapter.getAllClasses()
.then(allSchemas => {
allSchemas.forEach((schema) => {
var className = schema.className;
expect(className).toEqual({ asymmetricMatch: className => {
if (!className.startsWith('_')) {
return true;
} else {
// Other system classes will break Parse.com, so make sure that we don't save anything to _SCHEMA that will
// break it.
return ['_User', '_Installation', '_Role', '_Session', '_Product'].includes(className);
}
}});
});
})
.then(() => Parse.User.logOut())
.then(afterLogOut, afterLogOut)
});
var TestObject = Parse.Object.extend({
@@ -133,9 +202,6 @@ function createTestUser(success, error) {
}
}
// Mark the tests that are known to not work.
function notWorking() {}
// Shims for compatibility with the old qunit tests.
function ok(bool, message) {
expect(bool).toBeTruthy(message);
@@ -186,7 +252,7 @@ function arrayContains(arr, item) {
// Normalizes a JSON object.
function normalize(obj) {
if (typeof obj !== 'object') {
if (obj === null || typeof obj !== 'object') {
return JSON.stringify(obj);
}
if (obj instanceof Array) {
@@ -220,25 +286,21 @@ function mockFacebook() {
facebook.validateAuthData = function(authData) {
if (authData.id === '8675309' && authData.access_token === 'jenny') {
return Promise.resolve();
} else {
throw undefined;
}
return Promise.reject();
};
facebook.validateAppId = function(appId, authData) {
if (authData.access_token === 'jenny') {
return Promise.resolve();
} else {
throw undefined;
}
return Promise.reject();
};
return facebook;
}
function clearData() {
var promises = [];
for (var conn in DatabaseAdapter.dbConnections) {
promises.push(DatabaseAdapter.dbConnections[conn].deleteEverything());
}
return Promise.all(promises);
}
// This is polluting, but, it makes it way easier to directly port old tests.
global.Parse = Parse;
@@ -247,7 +309,6 @@ global.Item = Item;
global.Container = Container;
global.create = create;
global.createTestUser = createTestUser;
global.notWorking = notWorking;
global.ok = ok;
global.equal = equal;
global.strictEqual = strictEqual;
@@ -257,9 +318,17 @@ global.expectError = expectError;
global.arrayContains = arrayContains;
global.jequal = jequal;
global.range = range;
global.setServerConfiguration = setServerConfiguration;
global.reconfigureServer = reconfigureServer;
global.defaultConfiguration = defaultConfiguration;
global.it_exclude_dbs = excluded => {
if (excluded.includes(process.env.PARSE_SERVER_TEST_DB)) {
return xit;
} else {
return it;
}
}
// LiveQuery test setting
require('../src/LiveQuery/PLog').logLevel = 'NONE';
var libraryCache = {};
+211 -146
View File
@@ -1,59 +1,78 @@
"use strict"
var request = require('request');
var parseServerPackage = require('../package.json');
var MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions');
var ParseServer = require("../src/index");
var Config = require('../src/Config');
var express = require('express');
describe('server', () => {
it('requires a master key and app id', done => {
expect(setServerConfiguration.bind(undefined, { })).toThrow('You must provide an appId!');
expect(setServerConfiguration.bind(undefined, { appId: 'myId' })).toThrow('You must provide a masterKey!');
expect(setServerConfiguration.bind(undefined, { appId: 'myId', masterKey: 'mk' })).toThrow('You must provide a serverURL!');
done();
});
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
it('fails if database is unreachable', done => {
setServerConfiguration({
databaseURI: 'mongodb://fake:fake@ds043605.mongolab.com:43605/drew3',
serverURL: 'http://localhost:8378/1',
appId: 'test',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
});
//Need to use rest api because saving via JS SDK results in fail() not getting called
request.post({
url: 'http://localhost:8378/1/classes/NewClass',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
body: {},
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(500);
expect(body.code).toEqual(1);
expect(body.message).toEqual('Internal server error.');
describe('server', () => {
it_exclude_dbs(['postgres'])('requires a master key and app id', done => {
reconfigureServer({ appId: undefined })
.catch(error => {
expect(error).toEqual('You must provide an appId!');
return reconfigureServer({ masterKey: undefined });
})
.catch(error => {
expect(error).toEqual('You must provide a masterKey!');
return reconfigureServer({ serverURL: undefined });
})
.catch(error => {
expect(error).toEqual('You must provide a serverURL!');
done();
});
});
it('can load email adapter via object', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
it_exclude_dbs(['postgres'])('support http basic authentication with masterkey', done => {
request.get({
url: 'http://localhost:8378/1/classes/TestObject',
headers: {
'Authorization': 'Basic ' + new Buffer('test:' + 'test').toString('base64')
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
done();
});
});
it_exclude_dbs(['postgres'])('support http basic authentication with javascriptKey', done => {
request.get({
url: 'http://localhost:8378/1/classes/TestObject',
headers: {
'Authorization': 'Basic ' + new Buffer('test:javascript-key=' + 'test').toString('base64')
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
done();
});
});
it_exclude_dbs(['postgres'])('fails if database is unreachable', done => {
reconfigureServer({ databaseAdapter: new MongoStorageAdapter({ uri: 'mongodb://fake:fake@localhost:43605/drew3' }) })
.catch(() => {
//Need to use rest api because saving via JS SDK results in fail() not getting called
request.post({
url: 'http://localhost:8378/1/classes/NewClass',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
body: {},
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(500);
expect(body.code).toEqual(1);
expect(body.message).toEqual('Internal server error.');
done();
});
});
});
it_exclude_dbs(['postgres'])('can load email adapter via object', done => {
reconfigureServer({
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: MockEmailAdapterWithOptions({
fromAddress: 'parse@example.com',
@@ -61,22 +80,12 @@ describe('server', () => {
domain: 'd',
}),
publicServerURL: 'http://localhost:8378/1'
});
done();
}).then(done, fail);
});
it('can load email adapter via class', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
it_exclude_dbs(['postgres'])('can load email adapter via class', done => {
reconfigureServer({
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
class: MockEmailAdapterWithOptions,
@@ -87,22 +96,12 @@ describe('server', () => {
}
},
publicServerURL: 'http://localhost:8378/1'
});
done();
}).then(done, fail);
});
it('can load email adapter via module name', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
it_exclude_dbs(['postgres'])('can load email adapter via module name', done => {
reconfigureServer({
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
module: 'parse-server-simple-mailgun-adapter',
@@ -113,41 +112,25 @@ describe('server', () => {
}
},
publicServerURL: 'http://localhost:8378/1'
});
done();
}).then(done, fail);
});
it('can load email adapter via only module name', done => {
expect(() => setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
it_exclude_dbs(['postgres'])('can load email adapter via only module name', done => {
reconfigureServer({
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: 'parse-server-simple-mailgun-adapter',
publicServerURL: 'http://localhost:8378/1'
})).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
done();
})
.catch(error => {
expect(error).toEqual('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
done();
});
});
it('throws if you initialize email adapter incorrecly', done => {
expect(() => setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
it_exclude_dbs(['postgres'])('throws if you initialize email adapter incorrecly', done => {
reconfigureServer({
appName: 'unused',
javascriptKey: 'test',
dotNetKey: 'windows',
clientKey: 'client',
restAPIKey: 'rest',
masterKey: 'test',
collectionPrefix: 'test_',
fileKey: 'test',
verifyUserEmails: true,
emailAdapter: {
module: 'parse-server-simple-mailgun-adapter',
@@ -156,11 +139,14 @@ describe('server', () => {
}
},
publicServerURL: 'http://localhost:8378/1'
})).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
done();
})
.catch(error => {
expect(error).toEqual('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');
done();
});
});
it('can report the server version', done => {
it_exclude_dbs(['postgres'])('can report the server version', done => {
request.get({
url: 'http://localhost:8378/1/serverInfo',
headers: {
@@ -174,69 +160,148 @@ describe('server', () => {
})
});
it('can create a parse-server', done => {
var parseServer = new ParseServer.default({
it_exclude_dbs(['postgres'])('can create a parse-server v1', done => {
var parseServer = new ParseServer.default(Object.assign({},
defaultConfiguration, {
appId: "aTestApp",
masterKey: "aTestMasterKey",
serverURL: "http://localhost:12666/parse",
databaseURI: 'mongodb://localhost:27017/aTestApp'
});
__indexBuildCompletionCallbackForTests: promise => {
promise
.then(() => {
expect(Parse.applicationId).toEqual("aTestApp");
var app = express();
app.use('/parse', parseServer.app);
expect(Parse.applicationId).toEqual("aTestApp");
var app = express();
app.use('/parse', parseServer.app);
var server = app.listen(12666);
var obj = new Parse.Object("AnObject");
var objId;
obj.save().then((obj) => {
objId = obj.id;
var q = new Parse.Query("AnObject");
return q.first();
}).then((obj) => {
expect(obj.id).toEqual(objId);
server.close();
done();
}).fail((err) => {
server.close();
done();
})
var server = app.listen(12666);
var obj = new Parse.Object("AnObject");
var objId;
obj.save().then((obj) => {
objId = obj.id;
var q = new Parse.Query("AnObject");
return q.first();
}).then((obj) => {
expect(obj.id).toEqual(objId);
server.close(done);
}).fail((err) => {
server.close(done);
})
});
}})
);
});
it('can create a parse-server', done => {
var parseServer = ParseServer.ParseServer({
it_exclude_dbs(['postgres'])('can create a parse-server v2', done => {
let objId;
let server
let parseServer = ParseServer.ParseServer(Object.assign({},
defaultConfiguration, {
appId: "anOtherTestApp",
masterKey: "anOtherTestMasterKey",
serverURL: "http://localhost:12667/parse",
databaseURI: 'mongodb://localhost:27017/anotherTstApp'
});
__indexBuildCompletionCallbackForTests: promise => {
promise
.then(() => {
expect(Parse.applicationId).toEqual("anOtherTestApp");
let app = express();
app.use('/parse', parseServer);
expect(Parse.applicationId).toEqual("anOtherTestApp");
var app = express();
app.use('/parse', parseServer);
var server = app.listen(12667);
var obj = new Parse.Object("AnObject");
var objId;
obj.save().then((obj) => {
objId = obj.id;
var q = new Parse.Query("AnObject");
return q.first();
}).then((obj) => {
expect(obj.id).toEqual(objId);
server.close();
done();
}).fail((err) => {
server.close();
done();
})
server = app.listen(12667);
let obj = new Parse.Object("AnObject");
return obj.save()
})
.then(obj => {
objId = obj.id;
let q = new Parse.Query("AnObject");
return q.first();
})
.then(obj => {
expect(obj.id).toEqual(objId);
server.close(done);
})
.catch(error => {
fail(JSON.stringify(error))
done();
});
}}
));
});
it('has createLiveQueryServer', done => {
it_exclude_dbs(['postgres'])('has createLiveQueryServer', done => {
// original implementation through the factory
expect(typeof ParseServer.ParseServer.createLiveQueryServer).toEqual('function');
// For import calls
expect(typeof ParseServer.default.createLiveQueryServer).toEqual('function');
done();
});
it_exclude_dbs(['postgres'])('core adapters are not exposed anymore', done => {
expect(ParseServer.S3Adapter).toThrow();
expect(ParseServer.GCSAdapter).toThrow('GCSAdapter is not provided by parse-server anymore; please install parse-server-gcs-adapter');
expect(ParseServer.FileSystemAdapter).toThrow();
done();
});
it_exclude_dbs(['postgres'])('properly gives publicServerURL when set', done => {
reconfigureServer({ publicServerURL: 'https://myserver.com/1' })
.then(() => {
var config = new Config('test', 'http://localhost:8378/1');
expect(config.mount).toEqual('https://myserver.com/1');
done();
});
});
it_exclude_dbs(['postgres'])('properly removes trailing slash in mount', done => {
reconfigureServer({})
.then(() => {
var config = new Config('test', 'http://localhost:8378/1/');
expect(config.mount).toEqual('http://localhost:8378/1');
done();
});
});
it_exclude_dbs(['postgres'])('should throw when getting invalid mount', done => {
reconfigureServer({ publicServerURL: 'blabla:/some' })
.catch(error => {
expect(error).toEqual('publicServerURL should be a valid HTTPS URL starting with https://')
done();
})
});
it_exclude_dbs(['postgres'])('fails if the session length is not a number', done => {
reconfigureServer({ sessionLength: 'test' })
.catch(error => {
expect(error).toEqual('Session length must be a valid number.');
done();
});
});
it_exclude_dbs(['postgres'])('fails if the session length is less than or equal to 0', done => {
reconfigureServer({ sessionLength: '-33' })
.catch(error => {
expect(error).toEqual('Session length must be a value greater than 0.');
return reconfigureServer({ sessionLength: '0' })
})
.catch(error => {
expect(error).toEqual('Session length must be a value greater than 0.');
done();
});
});
it_exclude_dbs(['postgres'])('ignores the session length when expireInactiveSessions set to false', (done) => {
reconfigureServer({
sessionLength: '-33',
expireInactiveSessions: false
})
.then(() => reconfigureServer({
sessionLength: '0',
expireInactiveSessions: false
}))
.then(done);
})
it_exclude_dbs(['postgres'])('fails if you try to set revokeSessionOnPasswordReset to non-boolean', done => {
reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' })
.catch(done);
});
});
+219 -86
View File
@@ -86,6 +86,21 @@ var pointersAndRelationsSchema = {
classLevelPermissions: defaultClassLevelPermissions
}
const userSchema = {
"className": "_User",
"fields": {
"objectId": {"type": "String"},
"createdAt": {"type": "Date"},
"updatedAt": {"type": "Date"},
"ACL": {"type": "ACL"},
"username": {"type": "String"},
"password": {"type": "String"},
"email": {"type": "String"},
"emailVerified": {"type": "Boolean"}
},
"classLevelPermissions": defaultClassLevelPermissions,
}
var noAuthHeaders = {
'X-Parse-Application-Id': 'test',
};
@@ -139,18 +154,18 @@ describe('schemas', () => {
});
});
it('responds with empty list when there are no schemas', done => {
it_exclude_dbs(['postgres'])('creates _User schema when server starts', done => {
request.get({
url: 'http://localhost:8378/1/schemas',
json: true,
headers: masterKeyHeaders,
}, (error, response, body) => {
expect(body.results).toEqual([]);
expect(dd(body.results, [userSchema])).toEqual();
done();
});
});
it('responds with a list of schemas after creating objects', done => {
it_exclude_dbs(['postgres'])('responds with a list of schemas after creating objects', done => {
var obj1 = hasAllPODobject();
obj1.save().then(savedObj1 => {
var obj2 = new Parse.Object('HasPointersAndRelations');
@@ -165,15 +180,15 @@ describe('schemas', () => {
headers: masterKeyHeaders,
}, (error, response, body) => {
var expected = {
results: [plainOldDataSchema,pointersAndRelationsSchema]
results: [userSchema,plainOldDataSchema,pointersAndRelationsSchema]
};
expect(body).toEqual(expected);
expect(dd(body, expected)).toEqual(undefined);
done();
})
});
});
it('responds with a single schema', done => {
it_exclude_dbs(['postgres'])('responds with a single schema', done => {
var obj = hasAllPODobject();
obj.save().then(() => {
request.get({
@@ -187,7 +202,7 @@ describe('schemas', () => {
});
});
it('treats class names case sensitively', done => {
it_exclude_dbs(['postgres'])('treats class names case sensitively', done => {
var obj = hasAllPODobject();
obj.save().then(() => {
request.get({
@@ -297,7 +312,7 @@ describe('schemas', () => {
});
});
it('responds with all fields when you create a class', done => {
it_exclude_dbs(['postgres'])('responds with all fields when you create a class', done => {
request.post({
url: 'http://localhost:8378/1/schemas',
headers: masterKeyHeaders,
@@ -326,7 +341,48 @@ describe('schemas', () => {
});
});
it('lets you specify class name in both places', done => {
it_exclude_dbs(['postgres'])('responds with all fields when getting incomplete schema', done => {
config.database.loadSchema()
.then(schemaController => schemaController.addClassIfNotExists('_Installation', {}, defaultClassLevelPermissions))
.then(() => {
request.get({
url: 'http://localhost:8378/1/schemas/_Installation',
headers: masterKeyHeaders,
json: true
}, (error, response, body) => {
expect(dd(body,{
className: '_Installation',
fields: {
objectId: {type: 'String'},
updatedAt: {type: 'Date'},
createdAt: {type: 'Date'},
installationId: {type: 'String'},
deviceToken: {type: 'String'},
channels: {type: 'Array'},
deviceType: {type: 'String'},
pushType: {type: 'String'},
GCMSenderId: {type: 'String'},
timeZone: {type: 'String'},
badge: {type: 'Number'},
appIdentifier: {type: 'String'},
localeIdentifier: {type: 'String'},
appVersion: {type: 'String'},
appName: {type: 'String'},
parseVersion: {type: 'String'},
ACL: {type: 'ACL'}
},
classLevelPermissions: defaultClassLevelPermissions
})).toBeUndefined();
done();
});
})
.catch(error => {
fail(JSON.stringify(error))
done();
});
});
it_exclude_dbs(['postgres'])('lets you specify class name in both places', done => {
request.post({
url: 'http://localhost:8378/1/schemas/NewClass',
headers: masterKeyHeaders,
@@ -401,7 +457,7 @@ describe('schemas', () => {
});
});
it('refuses to put to existing fields, even if it would not be a change', done => {
it_exclude_dbs(['postgres'])('refuses to put to existing fields, even if it would not be a change', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
@@ -423,7 +479,7 @@ describe('schemas', () => {
})
});
it('refuses to delete non-existent fields', done => {
it_exclude_dbs(['postgres'])('refuses to delete non-existent fields', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
@@ -445,7 +501,7 @@ describe('schemas', () => {
});
});
it('refuses to add a geopoint to a class that already has one', done => {
it_exclude_dbs(['postgres'])('refuses to add a geopoint to a class that already has one', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
@@ -491,7 +547,7 @@ describe('schemas', () => {
});
});
it('allows you to delete and add a geopoint in the same request', done => {
it_exclude_dbs(['postgres'])('allows you to delete and add a geopoint in the same request', done => {
var obj = new Parse.Object('NewClass');
obj.set('geo1', new Parse.GeoPoint({latitude: 0, longitude: 0}));
obj.save()
@@ -523,7 +579,7 @@ describe('schemas', () => {
})
});
it('put with no modifications returns all fields', done => {
it_exclude_dbs(['postgres'])('put with no modifications returns all fields', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
@@ -539,7 +595,7 @@ describe('schemas', () => {
})
});
it('lets you add fields', done => {
it_exclude_dbs(['postgres'])('lets you add fields', done => {
request.post({
url: 'http://localhost:8378/1/schemas/NewClass',
headers: masterKeyHeaders,
@@ -589,7 +645,7 @@ describe('schemas', () => {
})
});
it('lets you add fields to system schema', done => {
it_exclude_dbs(['postgres'])('lets you add fields to system schema', done => {
request.post({
url: 'http://localhost:8378/1/schemas/_User',
headers: masterKeyHeaders,
@@ -605,7 +661,7 @@ describe('schemas', () => {
}
}
}, (error, response, body) => {
expect(body).toEqual({
expect(dd(body,{
className: '_User',
fields: {
objectId: {type: 'String'},
@@ -613,20 +669,19 @@ describe('schemas', () => {
createdAt: {type: 'Date'},
username: {type: 'String'},
password: {type: 'String'},
authData: {type: 'Object'},
email: {type: 'String'},
emailVerified: {type: 'Boolean'},
newField: {type: 'String'},
ACL: {type: 'ACL'}
},
classLevelPermissions: defaultClassLevelPermissions
});
})).toBeUndefined();
request.get({
url: 'http://localhost:8378/1/schemas/_User',
headers: masterKeyHeaders,
json: true
}, (error, response, body) => {
expect(body).toEqual({
expect(dd(body,{
className: '_User',
fields: {
objectId: {type: 'String'},
@@ -634,21 +689,20 @@ describe('schemas', () => {
createdAt: {type: 'Date'},
username: {type: 'String'},
password: {type: 'String'},
authData: {type: 'Object'},
email: {type: 'String'},
emailVerified: {type: 'Boolean'},
newField: {type: 'String'},
ACL: {type: 'ACL'}
},
classLevelPermissions: defaultClassLevelPermissions
});
})).toBeUndefined();
done();
});
});
})
});
it('lets you delete multiple fields and add fields', done => {
it_exclude_dbs(['postgres'])('lets you delete multiple fields and add fields', done => {
var obj1 = hasAllPODobject();
obj1.save()
.then(() => {
@@ -698,7 +752,7 @@ describe('schemas', () => {
});
});
it('will not delete any fields if the additions are invalid', done => {
it_exclude_dbs(['postgres'])('will not delete any fields if the additions are invalid', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
@@ -739,7 +793,7 @@ describe('schemas', () => {
});
});
it('refuses to delete non-empty collection', done => {
it_exclude_dbs(['postgres'])('refuses to delete non-empty collection', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
@@ -770,7 +824,7 @@ describe('schemas', () => {
})
});
it('does not fail when deleting nonexistant collections', done => {
it_exclude_dbs(['postgres'])('does not fail when deleting nonexistant collections', done => {
request.del({
url: 'http://localhost:8378/1/schemas/Missing',
headers: masterKeyHeaders,
@@ -782,7 +836,7 @@ describe('schemas', () => {
});
});
it('deletes collections including join tables', done => {
it_exclude_dbs(['postgres'])('deletes collections including join tables', done => {
var obj = new Parse.Object('MyClass');
obj.set('data', 'data');
obj.save()
@@ -833,7 +887,7 @@ describe('schemas', () => {
});
});
it('deletes schema when actual collection does not exist', done => {
it_exclude_dbs(['postgres'])('deletes schema when actual collection does not exist', done => {
request.post({
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
headers: masterKeyHeaders,
@@ -861,7 +915,7 @@ describe('schemas', () => {
});
});
it('deletes schema when actual collection exists', done => {
it_exclude_dbs(['postgres'])('deletes schema when actual collection exists', done => {
request.post({
url: 'http://localhost:8378/1/schemas/NewClassForDelete',
headers: masterKeyHeaders,
@@ -904,7 +958,7 @@ describe('schemas', () => {
});
});
it('should set/get schema permissions', done => {
it_exclude_dbs(['postgres'])('should set/get schema permissions', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
@@ -934,18 +988,10 @@ describe('schemas', () => {
create: {
'role:admin': true
},
get: {
'*': true
},
update: {
'*': true
},
addField: {
'*': true
},
delete: {
'*': true
}
get: {},
update: {},
delete: {},
addField: {}
});
done();
});
@@ -981,7 +1027,7 @@ describe('schemas', () => {
});
});
});
it('should not be able to add a field', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -989,6 +1035,9 @@ describe('schemas', () => {
json: true,
body: {
classLevelPermissions: {
create: {
'*': true
},
find: {
'*': true
},
@@ -1010,15 +1059,15 @@ describe('schemas', () => {
})
})
});
it('should not be able to add a field', done => {
it('should be able to add a field', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
headers: masterKeyHeaders,
json: true,
body: {
classLevelPermissions: {
find: {
create: {
'*': true
},
addField: {
@@ -1038,7 +1087,7 @@ describe('schemas', () => {
})
})
});
it('should throw with invalid userId (>10 chars)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1056,7 +1105,7 @@ describe('schemas', () => {
done();
})
});
it('should throw with invalid userId (<10 chars)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1074,7 +1123,7 @@ describe('schemas', () => {
done();
})
});
it('should throw with invalid userId (invalid char)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1092,7 +1141,7 @@ describe('schemas', () => {
done();
})
});
it('should throw with invalid * (spaces)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1110,7 +1159,7 @@ describe('schemas', () => {
done();
})
});
it('should throw with invalid * (spaces)', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1128,7 +1177,7 @@ describe('schemas', () => {
done();
})
});
it('should throw with invalid value', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1146,7 +1195,7 @@ describe('schemas', () => {
done();
})
});
it('should throw with invalid value', done => {
request.post({
url: 'http://localhost:8378/1/schemas/AClass',
@@ -1164,10 +1213,10 @@ describe('schemas', () => {
done();
})
});
function setPermissionsOnClass(className, permissions, doPut) {
let op = request.post;
if (doPut)
if (doPut)
{
op = request.put;
}
@@ -1190,18 +1239,18 @@ describe('schemas', () => {
})
});
}
it('validate CLP 1', done => {
it_exclude_dbs(['postgres'])('validate CLP 1', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
@@ -1214,7 +1263,7 @@ describe('schemas', () => {
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
let obj = new Parse.Object('AClass');
return obj.save();
return obj.save(null, {useMasterKey: true});
})
}).then(() => {
let query = new Parse.Query('AClass');
@@ -1239,18 +1288,18 @@ describe('schemas', () => {
done();
})
});
it('validate CLP 2', done => {
it_exclude_dbs(['postgres'])('validate CLP 2', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
@@ -1263,7 +1312,7 @@ describe('schemas', () => {
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
let obj = new Parse.Object('AClass');
return obj.save();
return obj.save(null, {useMasterKey: true});
})
}).then(() => {
let query = new Parse.Query('AClass');
@@ -1304,18 +1353,18 @@ describe('schemas', () => {
done();
})
});
it('validate CLP 3', done => {
it_exclude_dbs(['postgres'])('validate CLP 3', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
@@ -1328,7 +1377,7 @@ describe('schemas', () => {
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
let obj = new Parse.Object('AClass');
return obj.save();
return obj.save(null, {useMasterKey: true});
})
}).then(() => {
let query = new Parse.Query('AClass');
@@ -1362,18 +1411,18 @@ describe('schemas', () => {
done();
});
});
it('validate CLP 4', done => {
it_exclude_dbs(['postgres'])('validate CLP 4', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
setPermissionsOnClass('AClass', {
'find': {
'role:admin': true
@@ -1386,7 +1435,7 @@ describe('schemas', () => {
}).then(() => {
return Parse.User.logIn('user', 'user').then(() => {
let obj = new Parse.Object('AClass');
return obj.save();
return obj.save(null, {useMasterKey: true});
})
}).then(() => {
let query = new Parse.Query('AClass');
@@ -1400,7 +1449,7 @@ describe('schemas', () => {
// borked CLP should not affec security
return setPermissionsOnClass('AClass', {
'found': {
'role:admin': true
'role:admin': true
}
}, true).then(() => {
fail("Should not be able to save a borked CLP");
@@ -1430,21 +1479,21 @@ describe('schemas', () => {
done();
})
});
it('validate CLP 5', done => {
it_exclude_dbs(['postgres'])('validate CLP 5', done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let user2 = new Parse.User();
user2.setUsername('user2');
user2.setPassword('user2');
let admin = new Parse.User();
admin.setUsername('admin');
admin.setPassword('admin');
let role = new Parse.Role('admin', new Parse.ACL());
Promise.resolve().then(() => {
return Parse.Object.saveAll([user, user2, admin, role], {useMasterKey: true});
}).then(()=> {
@@ -1495,5 +1544,89 @@ describe('schemas', () => {
}).then(() => {
done();
});
});
});
it('can add field as master (issue #1257)', (done) => {
setPermissionsOnClass('AClass', {
'addField': {}
}).then(() => {
var obj = new Parse.Object('AClass');
obj.set('key', 'value');
return obj.save(null, {useMasterKey: true})
}).then((obj) => {
expect(obj.get('key')).toEqual('value');
done();
}, (err) => {
fail('should not fail');
done();
});
});
it_exclude_dbs(['postgres'])('can login when addFields is false (issue #1355)', (done) => {
setPermissionsOnClass('_User', {
'create': {'*': true},
'addField': {}
}, true).then(() => {
return Parse.User.signUp('foo', 'bar');
}).then((user) => {
expect(user.getUsername()).toBe('foo');
done()
}, error => {
fail(JSON.stringify(error));
done();
})
})
it_exclude_dbs(['postgres'])('gives correct response when deleting a schema with CLPs (regression test #1919)', done => {
new Parse.Object('MyClass').save({ data: 'foo'})
.then(obj => obj.destroy())
.then(() => setPermissionsOnClass('MyClass', { find: {}, get: {} }, true))
.then(() => {
request.del({
url: 'http://localhost:8378/1/schemas/MyClass',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(response.body).toEqual({});
done();
});
});
});
it_exclude_dbs(['postgres'])("regression test for #1991", done => {
let user = new Parse.User();
user.setUsername('user');
user.setPassword('user');
let role = new Parse.Role('admin', new Parse.ACL());
let obj = new Parse.Object('AnObject');
Parse.Object.saveAll([user, role]).then(() => {
role.relation('users').add(user);
return role.save(null, {useMasterKey: true});
}).then(() => {
return setPermissionsOnClass('AnObject', {
'get': {"*": true},
'find': {"*": true},
'create': {'*': true},
'update': {'role:admin': true},
'delete': {'role:admin': true}
})
}).then(() => {
return obj.save();
}).then(() => {
return Parse.User.logIn('user', 'user')
}).then(() => {
return obj.destroy();
}).then((result) => {
let query = new Parse.Query('AnObject');
return query.find();
}).then((results) => {
expect(results.length).toBe(0);
done();
}).catch((err) => {
fail('should not fail');
console.error(err);
done();
});
});
});
-215
View File
@@ -1,215 +0,0 @@
// These tests are unit tests designed to only test transform.js.
var transform = require('../src/transform');
var dummySchema = {
data: {},
getExpectedType: function(className, key) {
if (key == 'userPointer') {
return '*_User';
} else if (key == 'picture') {
return 'file';
} else if (key == 'location') {
return 'geopoint';
}
return;
}
};
describe('transformCreate', () => {
it('a basic number', (done) => {
var input = {five: 5};
var output = transform.transformCreate(dummySchema, null, input);
jequal(input, output);
done();
});
it('built-in timestamps', (done) => {
var input = {
createdAt: "2015-10-06T21:24:50.332Z",
updatedAt: "2015-10-06T21:24:50.332Z"
};
var output = transform.transformCreate(dummySchema, null, input);
expect(output._created_at instanceof Date).toBe(true);
expect(output._updated_at instanceof Date).toBe(true);
done();
});
it('array of pointers', (done) => {
var pointer = {
__type: 'Pointer',
objectId: 'myId',
className: 'Blah',
};
var out = transform.transformCreate(dummySchema, null, {pointers: [pointer]});
jequal([pointer], out.pointers);
done();
});
it('a delete op', (done) => {
var input = {deleteMe: {__op: 'Delete'}};
var output = transform.transformCreate(dummySchema, null, input);
jequal(output, {});
done();
});
it('basic ACL', (done) => {
var input = {ACL: {'0123': {'read': true, 'write': true}}};
var output = transform.transformCreate(dummySchema, null, input);
// This just checks that it doesn't crash, but it should check format.
done();
});
describe('GeoPoints', () => {
it('plain', (done) => {
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
var out = transform.transformCreate(dummySchema, null, {location: geoPoint});
expect(out.location).toEqual([180, -180]);
done();
});
it('in array', (done) => {
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
var out = transform.transformCreate(dummySchema, null, {locations: [geoPoint, geoPoint]});
expect(out.locations).toEqual([geoPoint, geoPoint]);
done();
});
it('in sub-object', (done) => {
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
var out = transform.transformCreate(dummySchema, null, { locations: { start: geoPoint }});
expect(out).toEqual({ locations: { start: geoPoint } });
done();
});
});
});
describe('transformWhere', () => {
it('objectId', (done) => {
var out = transform.transformWhere(dummySchema, null, {objectId: 'foo'});
expect(out._id).toEqual('foo');
done();
});
it('objectId in a list', (done) => {
var input = {
objectId: {'$in': ['one', 'two', 'three']},
};
var output = transform.transformWhere(dummySchema, null, input);
jequal(input.objectId, output._id);
done();
});
});
describe('untransformObject', () => {
it('built-in timestamps', (done) => {
var input = {createdAt: new Date(), updatedAt: new Date()};
var output = transform.untransformObject(dummySchema, null, input);
expect(typeof output.createdAt).toEqual('string');
expect(typeof output.updatedAt).toEqual('string');
done();
});
it('pointer', (done) => {
var input = {_p_userPointer: '_User$123'};
var output = transform.untransformObject(dummySchema, null, input);
expect(typeof output.userPointer).toEqual('object');
expect(output.userPointer).toEqual(
{__type: 'Pointer', className: '_User', objectId: '123'}
);
done();
});
it('null pointer', (done) => {
var input = {_p_userPointer: null};
var output = transform.untransformObject(dummySchema, null, input);
expect(output.userPointer).toBeUndefined();
done();
});
it('file', (done) => {
var input = {picture: 'pic.jpg'};
var output = transform.untransformObject(dummySchema, null, input);
expect(typeof output.picture).toEqual('object');
expect(output.picture).toEqual({__type: 'File', name: 'pic.jpg'});
done();
});
it('geopoint', (done) => {
var input = {location: [180, -180]};
var output = transform.untransformObject(dummySchema, null, input);
expect(typeof output.location).toEqual('object');
expect(output.location).toEqual(
{__type: 'GeoPoint', longitude: 180, latitude: -180}
);
done();
});
});
describe('transformKey', () => {
it('throws out _password', (done) => {
try {
transform.transformKey(dummySchema, '_User', '_password');
fail('should have thrown');
} catch (e) {
done();
}
});
});
describe('transform schema key changes', () => {
it('changes new pointer key', (done) => {
var input = {
somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'}
};
var output = transform.transformCreate(dummySchema, null, input);
expect(typeof output._p_somePointer).toEqual('string');
expect(output._p_somePointer).toEqual('Micro$oft');
done();
});
it('changes existing pointer keys', (done) => {
var input = {
userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'}
};
var output = transform.transformCreate(dummySchema, null, input);
expect(typeof output._p_userPointer).toEqual('string');
expect(output._p_userPointer).toEqual('_User$qwerty');
done();
});
it('changes ACL storage to _rperm and _wperm', (done) => {
var input = {
ACL: {
"*": { "read": true },
"Kevin": { "write": true }
}
};
var output = transform.transformCreate(dummySchema, null, input);
expect(typeof output._rperm).toEqual('object');
expect(typeof output._wperm).toEqual('object');
expect(output.ACL).toBeUndefined();
expect(output._rperm[0]).toEqual('*');
expect(output._wperm[0]).toEqual('Kevin');
done();
});
it('untransforms from _rperm and _wperm to ACL', (done) => {
var input = {
_rperm: ["*"],
_wperm: ["Kevin"]
};
var output = transform.untransformObject(dummySchema, null, input);
expect(typeof output.ACL).toEqual('object');
expect(output._rperm).toBeUndefined();
expect(output._wperm).toBeUndefined();
expect(output.ACL['*']['read']).toEqual(true);
expect(output.ACL['Kevin']['write']).toEqual(true);
done();
});
});
+1 -2
View File
@@ -1,6 +1,5 @@
export function loadAdapter(adapter, defaultAdapter, options) {
if (!adapter)
{
if (!adapter) {
if (!defaultAdapter) {
return options;
}
+27
View File
@@ -0,0 +1,27 @@
export class CacheAdapter {
/**
* Get a value in the cache
* @param key Cache key to get
* @return Promise that will eventually resolve to the value in the cache.
*/
get(key) {}
/**
* Set a value in the cache
* @param key Cache key to set
* @param value Value to set the key
* @param ttl Optional TTL
*/
put(key, value, ttl) {}
/**
* Remove a value from the cache.
* @param key Cache key to remove
*/
del(key) {}
/**
* Empty a cache
*/
clear() {}
}
+65
View File
@@ -0,0 +1,65 @@
const DEFAULT_CACHE_TTL = 5 * 1000;
export class InMemoryCache {
constructor({
ttl = DEFAULT_CACHE_TTL
}) {
this.ttl = ttl;
this.cache = Object.create(null);
}
get(key) {
let record = this.cache[key];
if (record == null) {
return null;
}
// Has Record and isnt expired
if (isNaN(record.expire) || record.expire >= Date.now()) {
return record.value;
}
// Record has expired
delete this.cache[key];
return null;
}
put(key, value, ttl = this.ttl) {
if (ttl < 0 || isNaN(ttl)) {
ttl = NaN;
}
var record = {
value: value,
expire: ttl + Date.now()
}
if (!isNaN(record.expire)) {
record.timeout = setTimeout(() => {
this.del(key);
}, ttl);
}
this.cache[key] = record;
}
del(key) {
var record = this.cache[key];
if (record == null) {
return;
}
if (record.timeout) {
clearTimeout(record.timeout);
}
delete this.cache[key];
}
clear() {
this.cache = Object.create(null);
}
}
export default InMemoryCache;
@@ -0,0 +1,36 @@
import {InMemoryCache} from './InMemoryCache';
export class InMemoryCacheAdapter {
constructor(ctx) {
this.cache = new InMemoryCache(ctx)
}
get(key) {
return new Promise((resolve, reject) => {
let record = this.cache.get(key);
if (record == null) {
return resolve(null);
}
return resolve(JSON.parse(record));
})
}
put(key, value, ttl) {
this.cache.put(key, JSON.stringify(value), ttl);
return Promise.resolve();
}
del(key) {
this.cache.del(key);
return Promise.resolve();
}
clear() {
this.cache.clear();
return Promise.resolve();
}
}
export default InMemoryCacheAdapter;
+4 -2
View File
@@ -7,13 +7,15 @@
*/
import { MongoClient, GridStore, Db} from 'mongodb';
import { FilesAdapter } from './FilesAdapter';
import { FilesAdapter } from './FilesAdapter';
const DefaultMongoURI = 'mongodb://localhost:27017/parse';
export class GridStoreAdapter extends FilesAdapter {
_databaseURI: string;
_connectionPromise: Promise<Db>;
constructor(mongoDatabaseURI: string) {
constructor(mongoDatabaseURI = DefaultMongoURI) {
super();
this._databaseURI = mongoDatabaseURI;
this._connect();
+14 -15
View File
@@ -17,7 +17,7 @@ export default class MongoCollection {
return this._rawFind(query, { skip, limit, sort })
.catch(error => {
// Check for "no geoindex" error
if (error.code != 17007 || !error.message.match(/unable to find index for .geoNear/)) {
if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) {
throw error;
}
// Figure out what key needs an index
@@ -28,7 +28,6 @@ export default class MongoCollection {
var index = {};
index[key] = '2d';
//TODO: condiser moving index creation logic into Schema.js
return this._mongoCollection.createIndex(index)
// Retry, but just once.
.then(() => this._rawFind(query, { skip, limit, sort }));
@@ -45,18 +44,6 @@ export default class MongoCollection {
return this._mongoCollection.count(query, { skip, limit, sort });
}
// Atomically finds and updates an object based on query.
// The result is the promise with an object that was in the database !AFTER! changes.
// Postgres Note: Translates directly to `UPDATE * SET * ... RETURNING *`, which will return data after the change is done.
findOneAndUpdate(query, update) {
// arguments: query, sort, update, options(optional)
// Setting `new` option to true makes it return the after document, not the before one.
return this._mongoCollection.findAndModify(query, [], update, { new: true }).then(document => {
// Value is the object where mongo returns multiple fields.
return document.value;
});
}
insertOne(object) {
return this._mongoCollection.insertOne(object);
}
@@ -65,7 +52,7 @@ export default class MongoCollection {
// If there is nothing that matches the query - does insert
// Postgres Note: `INSERT ... ON CONFLICT UPDATE` that is available since 9.5.
upsertOne(query, update) {
return this._mongoCollection.update(query, update, { upsert: true });
return this._mongoCollection.update(query, update, { upsert: true })
}
updateOne(query, update) {
@@ -84,6 +71,18 @@ export default class MongoCollection {
return this._mongoCollection.deleteMany(query);
}
_ensureSparseUniqueIndexInBackground(indexRequest) {
return new Promise((resolve, reject) => {
this._mongoCollection.ensureIndex(indexRequest, { unique: true, background: true, sparse: true }, (error, indexName) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
drop() {
return this._mongoCollection.drop();
}
@@ -1,51 +1,130 @@
import MongoCollection from './MongoCollection';
function _mongoSchemaQueryFromNameQuery(name: string, query) {
return _mongoSchemaObjectFromNameFields(name, query);
function mongoFieldToParseSchemaField(type) {
if (type[0] === '*') {
return {
type: 'Pointer',
targetClass: type.slice(1),
};
}
if (type.startsWith('relation<')) {
return {
type: 'Relation',
targetClass: type.slice('relation<'.length, type.length - 1),
};
}
switch (type) {
case 'number': return {type: 'Number'};
case 'string': return {type: 'String'};
case 'boolean': return {type: 'Boolean'};
case 'date': return {type: 'Date'};
case 'map':
case 'object': return {type: 'Object'};
case 'array': return {type: 'Array'};
case 'geopoint': return {type: 'GeoPoint'};
case 'file': return {type: 'File'};
case 'bytes': return {type: 'Bytes'};
}
}
function _mongoSchemaObjectFromNameFields(name: string, fields) {
const nonFieldSchemaKeys = ['_id', '_metadata', '_client_permissions'];
function mongoSchemaFieldsToParseSchemaFields(schema) {
var fieldNames = Object.keys(schema).filter(key => nonFieldSchemaKeys.indexOf(key) === -1);
var response = fieldNames.reduce((obj, fieldName) => {
obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName])
return obj;
}, {});
response.ACL = {type: 'ACL'};
response.createdAt = {type: 'Date'};
response.updatedAt = {type: 'Date'};
response.objectId = {type: 'String'};
return response;
}
const emptyCLPS = Object.freeze({
find: {},
get: {},
create: {},
update: {},
delete: {},
addField: {},
});
const defaultCLPS = Object.freeze({
find: {'*': true},
get: {'*': true},
create: {'*': true},
update: {'*': true},
delete: {'*': true},
addField: {'*': true},
});
function mongoSchemaToParseSchema(mongoSchema) {
let clps = defaultCLPS;
if (mongoSchema._metadata && mongoSchema._metadata.class_permissions) {
clps = {...emptyCLPS, ...mongoSchema._metadata.class_permissions};
}
return {
className: mongoSchema._id,
fields: mongoSchemaFieldsToParseSchemaFields(mongoSchema),
classLevelPermissions: clps,
};
}
function _mongoSchemaQueryFromNameQuery(name: string, query) {
let object = { _id: name };
if (fields) {
Object.keys(fields).forEach(key => {
object[key] = fields[key];
if (query) {
Object.keys(query).forEach(key => {
object[key] = query[key];
});
}
return object;
}
export default class MongoSchemaCollection {
// Returns a type suitable for inserting into mongo _SCHEMA collection.
// Does no validation. That is expected to be done in Parse Server.
function parseFieldTypeToMongoFieldType({ type, targetClass }) {
switch (type) {
case 'Pointer': return `*${targetClass}`;
case 'Relation': return `relation<${targetClass}>`;
case 'Number': return 'number';
case 'String': return 'string';
case 'Boolean': return 'boolean';
case 'Date': return 'date';
case 'Object': return 'object';
case 'Array': return 'array';
case 'GeoPoint': return 'geopoint';
case 'File': return 'file';
}
}
class MongoSchemaCollection {
_collection: MongoCollection;
constructor(collection: MongoCollection) {
this._collection = collection;
}
getAllSchemas() {
return this._collection._rawFind({});
_fetchAllSchemasFrom_SCHEMA() {
return this._collection._rawFind({})
.then(schemas => schemas.map(mongoSchemaToParseSchema));
}
findSchema(name: string) {
_fechOneSchemaFrom_SCHEMA(name: string) {
return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(results => {
return results[0];
if (results.length === 1) {
return mongoSchemaToParseSchema(results[0]);
} else {
throw undefined;
}
});
}
// Atomically find and delete an object based on query.
// The result is the promise with an object that was in the database before deleting.
// Postgres Note: Translates directly to `DELETE * FROM ... RETURNING *`, which will return data after delete is done.
findAndDeleteSchema(name: string) {
// arguments: query, sort
return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []).then(document => {
// Value is the object where mongo returns multiple fields.
return document.value;
});
}
addSchema(name: string, fields) {
let mongoObject = _mongoSchemaObjectFromNameFields(name, fields);
return this._collection.insertOne(mongoObject);
return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []);
}
updateSchema(name: string, update) {
@@ -55,4 +134,52 @@ export default class MongoSchemaCollection {
upsertSchema(name: string, query: string, update) {
return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update);
}
// Add a field to the schema. If database does not support the field
// type (e.g. mongo doesn't support more than one GeoPoint in a class) reject with an "Incorrect Type"
// Parse error with a desciptive message. If the field already exists, this function must
// not modify the schema, and must reject with DUPLICATE_VALUE error.
// If this is called for a class that doesn't exist, this function must create that class.
// TODO: throw an error if an unsupported field type is passed. Deciding whether a type is supported
// should be the job of the adapter. Some adapters may not support GeoPoint at all. Others may
// Support additional types that Mongo doesn't, like Money, or something.
// TODO: don't spend an extra query on finding the schema if the type we are trying to add isn't a GeoPoint.
addFieldIfNotExists(className: string, fieldName: string, type: string) {
return this._fechOneSchemaFrom_SCHEMA(className)
.then(schema => {
// The schema exists. Check for existing GeoPoints.
if (type.type === 'GeoPoint') {
// Make sure there are not other geopoint fields
if (Object.keys(schema.fields).some(existingField => schema.fields[existingField].type === 'GeoPoint')) {
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, 'MongoDB only supports one GeoPoint field in a class.');
}
}
return;
}, error => {
// If error is undefined, the schema doesn't exist, and we can create the schema with the field.
// If some other error, reject with it.
if (error === undefined) {
return;
}
throw error;
})
.then(() => {
// We use $exists and $set to avoid overwriting the field type if it
// already exists. (it could have added inbetween the last query and the update)
return this.upsertSchema(
className,
{ [fieldName]: { '$exists': false } },
{ '$set' : { [fieldName]: parseFieldTypeToMongoFieldType(type) } }
);
});
}
}
// Exported for testing reasons and because we haven't moved all mongo schema format
// related logic into the database adapter yet.
MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema
MongoSchemaCollection.parseFieldTypeToMongoFieldType = parseFieldTypeToMongoFieldType
export default MongoSchemaCollection
+305 -32
View File
@@ -1,24 +1,98 @@
import MongoCollection from './MongoCollection';
import MongoCollection from './MongoCollection';
import MongoSchemaCollection from './MongoSchemaCollection';
import {parse as parseUrl, format as formatUrl} from '../../../vendor/mongodbUrl';
import {
parse as parseUrl,
format as formatUrl,
} from '../../../vendor/mongodbUrl';
import {
parseObjectToMongoObjectForCreate,
mongoObjectToParseObject,
transformKey,
transformWhere,
transformUpdate,
} from './MongoTransform';
import _ from 'lodash';
let mongodb = require('mongodb');
let MongoClient = mongodb.MongoClient;
const MongoSchemaCollectionName = '_SCHEMA';
const DefaultMongoURI = 'mongodb://localhost:27017/parse';
const storageAdapterAllCollections = mongoAdapter => {
return mongoAdapter.connect()
.then(() => mongoAdapter.database.collections())
.then(collections => {
return collections.filter(collection => {
if (collection.namespace.match(/\.system\./)) {
return false;
}
// TODO: If you have one app with a collection prefix that happens to be a prefix of another
// apps prefix, this will go very very badly. We should fix that somehow.
return (collection.collectionName.indexOf(mongoAdapter._collectionPrefix) == 0);
});
});
}
const convertParseSchemaToMongoSchema = ({...schema}) => {
delete schema.fields._rperm;
delete schema.fields._wperm;
if (schema.className === '_User') {
// Legacy mongo adapter knows about the difference between password and _hashed_password.
// Future database adapters will only know about _hashed_password.
// Note: Parse Server will bring back password with injectDefaultSchema, so we don't need
// to add _hashed_password back ever.
delete schema.fields._hashed_password;
}
return schema;
}
// Returns { code, error } if invalid, or { result }, an object
// suitable for inserting into _SCHEMA collection, otherwise.
const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPermissions) => {
let mongoObject = {
_id: className,
objectId: 'string',
updatedAt: 'string',
createdAt: 'string'
};
for (let fieldName in fields) {
mongoObject[fieldName] = MongoSchemaCollection.parseFieldTypeToMongoFieldType(fields[fieldName]);
}
if (typeof classLevelPermissions !== 'undefined') {
mongoObject._metadata = mongoObject._metadata || {};
if (!classLevelPermissions) {
delete mongoObject._metadata.class_permissions;
} else {
mongoObject._metadata.class_permissions = classLevelPermissions;
}
}
return mongoObject;
}
export class MongoStorageAdapter {
// Private
_uri: string;
_options: Object;
_collectionPrefix: string;
_mongoOptions: Object;
// Public
connectionPromise;
database;
constructor(uri: string, options: Object) {
constructor({
uri = DefaultMongoURI,
collectionPrefix = '',
mongoOptions = {},
}) {
this._uri = uri;
this._options = options;
this._collectionPrefix = collectionPrefix;
this._mongoOptions = mongoOptions;
}
connect() {
@@ -30,53 +104,252 @@ export class MongoStorageAdapter {
// encoded
const encodedUri = formatUrl(parseUrl(this._uri));
this.connectionPromise = MongoClient.connect(encodedUri, this._options).then(database => {
this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions).then(database => {
this.database = database;
});
return this.connectionPromise;
}
collection(name: string) {
return this.connect().then(() => {
return this.database.collection(name);
});
}
adaptiveCollection(name: string) {
_adaptiveCollection(name: string) {
return this.connect()
.then(() => this.database.collection(name))
.then(() => this.database.collection(this._collectionPrefix + name))
.then(rawCollection => new MongoCollection(rawCollection));
}
schemaCollection(collectionPrefix: string) {
_schemaCollection() {
return this.connect()
.then(() => this.adaptiveCollection(collectionPrefix + MongoSchemaCollectionName))
.then(() => this._adaptiveCollection(MongoSchemaCollectionName))
.then(collection => new MongoSchemaCollection(collection));
}
collectionExists(name: string) {
classExists(name) {
return this.connect().then(() => {
return this.database.listCollections({ name: name }).toArray();
return this.database.listCollections({ name: this._collectionPrefix + name }).toArray();
}).then(collections => {
return collections.length > 0;
});
}
dropCollection(name: string) {
return this.collection(name).then(collection => collection.drop());
setClassLevelPermissions(className, CLPs) {
return this._schemaCollection()
.then(schemaCollection => schemaCollection.updateSchema(className, {
$set: { _metadata: { class_permissions: CLPs } }
}));
}
// Used for testing only right now.
collectionsContaining(match: string) {
return this.connect().then(() => {
return this.database.collections();
}).then(collections => {
return collections.filter(collection => {
if (collection.namespace.match(/\.system\./)) {
return false;
}
return (collection.collectionName.indexOf(match) == 0);
});
createClass(className, schema) {
schema = convertParseSchemaToMongoSchema(schema);
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions);
mongoObject._id = className;
return this._schemaCollection()
.then(schemaCollection => schemaCollection._collection.insertOne(mongoObject))
.then(result => MongoSchemaCollection._TESTmongoSchemaToParseSchema(result.ops[0]))
.catch(error => {
if (error.code === 11000) { //Mongo's duplicate key error
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'Class already exists.');
} else {
throw error;
}
})
}
addFieldIfNotExists(className, fieldName, type) {
return this._schemaCollection()
.then(schemaCollection => schemaCollection.addFieldIfNotExists(className, fieldName, type));
}
// Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.)
// and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible.
deleteClass(className) {
return this._adaptiveCollection(className)
.then(collection => collection.drop())
.catch(error => {
// 'ns not found' means collection was already gone. Ignore deletion attempt.
if (error.message == 'ns not found') {
return;
}
throw error;
})
// We've dropped the collection, now remove the _SCHEMA document
.then(() => this._schemaCollection())
.then(schemaCollection => schemaCollection.findAndDeleteSchema(className))
}
// Delete all data known to this adatper. Used for testing.
deleteAllClasses() {
return storageAdapterAllCollections(this)
.then(collections => Promise.all(collections.map(collection => collection.drop())));
}
// Remove the column and all the data. For Relations, the _Join collection is handled
// specially, this function does not delete _Join columns. It should, however, indicate
// that the relation fields does not exist anymore. In mongo, this means removing it from
// the _SCHEMA collection. There should be no actual data in the collection under the same name
// as the relation column, so it's fine to attempt to delete it. If the fields listed to be
// deleted do not exist, this function should return successfully anyways. Checking for
// attempts to delete non-existent fields is the responsibility of Parse Server.
// Pointer field names are passed for legacy reasons: the original mongo
// format stored pointer field names differently in the database, and therefore
// needed to know the type of the field before it could delete it. Future database
// adatpers should ignore the pointerFieldNames argument. All the field names are in
// fieldNames, they show up additionally in the pointerFieldNames database for use
// by the mongo adapter, which deals with the legacy mongo format.
// This function is not obligated to delete fields atomically. It is given the field
// names in a list so that databases that are capable of deleting fields atomically
// may do so.
// Returns a Promise.
deleteFields(className, schema, fieldNames) {
const mongoFormatNames = fieldNames.map(fieldName => {
if (schema.fields[fieldName].type === 'Pointer') {
return `_p_${fieldName}`
} else {
return fieldName;
}
});
const collectionUpdate = { '$unset' : {} };
mongoFormatNames.forEach(name => {
collectionUpdate['$unset'][name] = null;
});
const schemaUpdate = { '$unset' : {} };
fieldNames.forEach(name => {
schemaUpdate['$unset'][name] = null;
});
return this._adaptiveCollection(className)
.then(collection => collection.updateMany({}, collectionUpdate))
.then(() => this._schemaCollection())
.then(schemaCollection => schemaCollection.updateSchema(className, schemaUpdate));
}
// Return a promise for all schemas known to this adapter, in Parse format. In case the
// schemas cannot be retrieved, returns a promise that rejects. Requirements for the
// rejection reason are TBD.
getAllClasses() {
return this._schemaCollection().then(schemasCollection => schemasCollection._fetchAllSchemasFrom_SCHEMA());
}
// Return a promise for the schema with the given name, in Parse format. If
// this adapter doesn't know about the schema, return a promise that rejects with
// undefined as the reason.
getClass(className) {
return this._schemaCollection()
.then(schemasCollection => schemasCollection._fechOneSchemaFrom_SCHEMA(className))
}
// TODO: As yet not particularly well specified. Creates an object. Maybe shouldn't even need the schema,
// and should infer from the type. Or maybe does need the schema for validations. Or maybe needs
// the schem only for the legacy mongo format. We'll figure that out later.
createObject(className, schema, object) {
schema = convertParseSchemaToMongoSchema(schema);
const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema);
return this._adaptiveCollection(className)
.then(collection => collection.insertOne(mongoObject))
.catch(error => {
if (error.code === 11000) { // Duplicate value
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE,
'A duplicate value for a field with unique values was provided');
}
throw error;
});
}
// Remove all objects that match the given Parse Query.
// If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined.
// If there is some other error, reject with INTERNAL_SERVER_ERROR.
deleteObjectsByQuery(className, schema, query) {
schema = convertParseSchemaToMongoSchema(schema);
return this._adaptiveCollection(className)
.then(collection => {
let mongoWhere = transformWhere(className, query, schema);
return collection.deleteMany(mongoWhere)
})
.then(({ result }) => {
if (result.n === 0) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
}
return Promise.resolve();
}, error => {
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database adapter error');
});
}
// Apply the update to all objects that match the given Parse Query.
updateObjectsByQuery(className, schema, query, update) {
schema = convertParseSchemaToMongoSchema(schema);
const mongoUpdate = transformUpdate(className, update, schema);
const mongoWhere = transformWhere(className, query, schema);
return this._adaptiveCollection(className)
.then(collection => collection.updateMany(mongoWhere, mongoUpdate));
}
// Atomically finds and updates an object based on query.
// Return value not currently well specified.
findOneAndUpdate(className, schema, query, update) {
schema = convertParseSchemaToMongoSchema(schema);
const mongoUpdate = transformUpdate(className, update, schema);
const mongoWhere = transformWhere(className, query, schema);
return this._adaptiveCollection(className)
.then(collection => collection._mongoCollection.findAndModify(mongoWhere, [], mongoUpdate, { new: true }))
.then(result => result.value);
}
// Hopefully we can get rid of this. It's only used for config and hooks.
upsertOneObject(className, schema, query, update) {
schema = convertParseSchemaToMongoSchema(schema);
const mongoUpdate = transformUpdate(className, update, schema);
const mongoWhere = transformWhere(className, query, schema);
return this._adaptiveCollection(className)
.then(collection => collection.upsertOne(mongoWhere, mongoUpdate));
}
// Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }.
find(className, schema, query, { skip, limit, sort }) {
schema = convertParseSchemaToMongoSchema(schema);
let mongoWhere = transformWhere(className, query, schema);
let mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema));
return this._adaptiveCollection(className)
.then(collection => collection.find(mongoWhere, { skip, limit, sort: mongoSort }))
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)))
}
// Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't
// currently know which fields are nullable and which aren't, we ignore that criteria.
// As such, we shouldn't expose this function to users of parse until we have an out-of-band
// Way of determining if a field is nullable. Undefined doesn't count against uniqueness,
// which is why we use sparse indexes.
ensureUniqueness(className, schema, fieldNames) {
schema = convertParseSchemaToMongoSchema(schema);
let indexCreationRequest = {};
let mongoFieldNames = fieldNames.map(fieldName => transformKey(className, fieldName, schema));
mongoFieldNames.forEach(fieldName => {
indexCreationRequest[fieldName] = 1;
});
return this._adaptiveCollection(className)
.then(collection => collection._ensureSparseUniqueIndexInBackground(indexCreationRequest))
.catch(error => {
if (error.code === 11000) {
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'Tried to ensure field uniqueness for a class that already has duplicates.');
} else {
throw error;
}
});
}
// Used in tests
_rawFind(className, query) {
return this._adaptiveCollection(className).then(collection => collection.find(query));
}
// Executs a count.
count(className, schema, query) {
schema = convertParseSchemaToMongoSchema(schema);
return this._adaptiveCollection(className)
.then(collection => collection.count(transformWhere(className, query, schema)));
}
}
@@ -0,0 +1,884 @@
import log from '../../../logger';
import _ from 'lodash';
var mongodb = require('mongodb');
var Parse = require('parse/node').Parse;
const transformKey = (className, fieldName, schema) => {
// Check if the schema is known since it's a built-in field.
switch(fieldName) {
case 'objectId': return '_id';
case 'createdAt': return '_created_at';
case 'updatedAt': return '_updated_at';
case 'sessionToken': return '_session_token';
}
if (schema.fields[fieldName] && schema.fields[fieldName].__type == 'Pointer') {
fieldName = '_p_' + fieldName;
}
return fieldName;
}
const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSchema) => {
// Check if the schema is known since it's a built-in field.
var key = restKey;
var timeField = false;
switch(key) {
case 'objectId':
case '_id':
key = '_id';
break;
case 'createdAt':
case '_created_at':
key = '_created_at';
timeField = true;
break;
case 'updatedAt':
case '_updated_at':
key = '_updated_at';
timeField = true;
break;
case 'sessionToken':
case '_session_token':
key = '_session_token';
break;
case 'expiresAt':
case '_expiresAt':
key = 'expiresAt';
timeField = true;
break;
case '_rperm':
case '_wperm':
return {key: key, value: restValue};
break;
}
if ((parseFormatSchema.fields[key] && parseFormatSchema.fields[key].type === 'Pointer') || (!parseFormatSchema.fields[key] && restValue && restValue.__type == 'Pointer')) {
key = '_p_' + key;
}
// Handle atomic values
var value = transformTopLevelAtom(restValue);
if (value !== CannotTransform) {
if (timeField && (typeof value === 'string')) {
value = new Date(value);
}
return {key, value};
}
// Handle arrays
if (restValue instanceof Array) {
value = restValue.map(transformInteriorValue);
return {key, value};
}
// Handle update operators
if (typeof restValue === 'object' && '__op' in restValue) {
return {key, value: transformUpdateOperator(restValue, false)};
}
// Handle normal objects by recursing
value = _.mapValues(restValue, transformInteriorValue);
return {key, value};
}
const transformInteriorValue = restValue => {
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) {
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
}
// Handle atomic values
var value = transformInteriorAtom(restValue);
if (value !== CannotTransform) {
return value;
}
// Handle arrays
if (restValue instanceof Array) {
return restValue.map(transformInteriorValue);
}
// Handle update operators
if (typeof restValue === 'object' && '__op' in restValue) {
return transformUpdateOperator(restValue, true);
}
// Handle normal objects by recursing
return _.mapValues(restValue, transformInteriorValue);
}
const valueAsDate = value => {
if (typeof value === 'string') {
return new Date(value);
} else if (value instanceof Date) {
return value;
}
return false;
}
function transformQueryKeyValue(className, key, value, schema) {
switch(key) {
case 'createdAt':
if (valueAsDate(value)) {
return {key: '_created_at', value: valueAsDate(value)}
}
key = '_created_at';
break;
case 'updatedAt':
if (valueAsDate(value)) {
return {key: '_updated_at', value: valueAsDate(value)}
}
key = '_updated_at';
break;
case 'expiresAt':
if (valueAsDate(value)) {
return {key: 'expiresAt', value: valueAsDate(value)}
}
break;
case 'objectId': return {key: '_id', value}
case 'sessionToken': return {key: '_session_token', value}
case '_rperm':
case '_wperm':
case '_perishable_token':
case '_email_verify_token': return {key, value}
case '$or':
return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, schema))};
case '$and':
return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, schema))};
default:
// Other auth data
const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/);
if (authDataMatch) {
const provider = authDataMatch[1];
// Special-case auth data.
return {key: `_auth_data_${provider}.id`, value};
}
}
const expectedTypeIsArray =
schema &&
schema.fields[key] &&
schema.fields[key].type === 'Array';
const expectedTypeIsPointer =
schema &&
schema.fields[key] &&
schema.fields[key].type === 'Pointer';
if (expectedTypeIsPointer || !schema && value && value.__type === 'Pointer') {
key = '_p_' + key;
}
// Handle query constraints
if (transformConstraint(value, expectedTypeIsArray) !== CannotTransform) {
return {key, value: transformConstraint(value, expectedTypeIsArray)};
}
if (expectedTypeIsArray && !(value instanceof Array)) {
return {key, value: { '$all' : [value] }};
}
// Handle atomic values
if (transformTopLevelAtom(value) !== CannotTransform) {
return {key, value: transformTopLevelAtom(value)};
} else {
throw new Parse.Error(Parse.Error.INVALID_JSON, `You cannot use ${value} as a query parameter.`);
}
}
// Main exposed method to help run queries.
// restWhere is the "where" clause in REST API form.
// Returns the mongo form of the query.
function transformWhere(className, restWhere, schema) {
let mongoWhere = {};
for (let restKey in restWhere) {
let out = transformQueryKeyValue(className, restKey, restWhere[restKey], schema);
mongoWhere[out.key] = out.value;
}
return mongoWhere;
}
const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) => {
// Check if the schema is known since it's a built-in field.
let transformedValue;
let coercedToDate;
switch(restKey) {
case 'objectId': return {key: '_id', value: restValue};
case 'expiresAt':
transformedValue = transformTopLevelAtom(restValue);
coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue
return {key: 'expiresAt', value: coercedToDate};
case '_rperm':
case '_wperm':
case '_email_verify_token':
case '_hashed_password':
case '_perishable_token': return {key: restKey, value: restValue};
case 'sessionToken': return {key: '_session_token', value: restValue};
default:
// Auth data should have been transformed already
if (restKey.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'can only query on ' + restKey);
}
// Trust that the auth data has been transformed and save it directly
if (restKey.match(/^_auth_data_[a-zA-Z0-9_]+$/)) {
return {key: restKey, value: restValue};
}
}
//skip straight to transformTopLevelAtom for Bytes, they don't show up in the schema for some reason
if (restValue && restValue.__type !== 'Bytes') {
//Note: We may not know the type of a field here, as the user could be saving (null) to a field
//That never existed before, meaning we can't infer the type.
if (schema.fields[restKey] && schema.fields[restKey].type == 'Pointer' || restValue.__type == 'Pointer') {
restKey = '_p_' + restKey;
}
}
// Handle atomic values
var value = transformTopLevelAtom(restValue);
if (value !== CannotTransform) {
return {key: restKey, value: value};
}
// ACLs are handled before this method is called
// If an ACL key still exists here, something is wrong.
if (restKey === 'ACL') {
throw 'There was a problem transforming an ACL.';
}
// Handle arrays
if (restValue instanceof Array) {
value = restValue.map(transformInteriorValue);
return {key: restKey, value: value};
}
// Handle normal objects by recursing
if (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");
}
value = _.mapValues(restValue, transformInteriorValue);
return {key: restKey, value};
}
const parseObjectToMongoObjectForCreate = (className, restCreate, schema) => {
restCreate = addLegacyACL(restCreate);
let mongoCreate = {}
for (let restKey in restCreate) {
let { key, value } = parseObjectKeyValueToMongoObjectKeyValue(
restKey,
restCreate[restKey],
schema
);
if (value !== undefined) {
mongoCreate[key] = value;
}
}
// Use the legacy mongo format for createdAt and updatedAt
if (mongoCreate.createdAt) {
mongoCreate._created_at = new Date(mongoCreate.createdAt.iso || mongoCreate.createdAt);
delete mongoCreate.createdAt;
}
if (mongoCreate.updatedAt) {
mongoCreate._updated_at = new Date(mongoCreate.updatedAt.iso || mongoCreate.updatedAt);
delete mongoCreate.updatedAt;
}
return mongoCreate;
}
// Main exposed method to help update old objects.
const transformUpdate = (className, restUpdate, parseFormatSchema) => {
let mongoUpdate = {};
let acl = addLegacyACL(restUpdate)._acl;
if (acl) {
mongoUpdate.$set = {};
if (acl._rperm) {
mongoUpdate.$set._rperm = acl._rperm;
}
if (acl._wperm) {
mongoUpdate.$set._wperm = acl._wperm;
}
if (acl._acl) {
mongoUpdate.$set._acl = acl._acl;
}
}
for (var restKey in restUpdate) {
var out = transformKeyValueForUpdate(className, restKey, restUpdate[restKey], parseFormatSchema);
// If the output value is an object with any $ keys, it's an
// operator that needs to be lifted onto the top level update
// object.
if (typeof out.value === 'object' && out.value !== null && out.value.__op) {
mongoUpdate[out.value.__op] = mongoUpdate[out.value.__op] || {};
mongoUpdate[out.value.__op][out.key] = out.value.arg;
} else {
mongoUpdate['$set'] = mongoUpdate['$set'] || {};
mongoUpdate['$set'][out.key] = out.value;
}
}
return mongoUpdate;
}
// Add the legacy _acl format.
const addLegacyACL = restObject => {
let restObjectCopy = {...restObject};
let _acl = {};
if (restObject._wperm) {
restObject._wperm.forEach(entry => {
_acl[entry] = { w: true };
});
}
if (restObject._rperm) {
restObject._rperm.forEach(entry => {
if (!(entry in _acl)) {
_acl[entry] = { r: true };
} else {
_acl[entry].r = true;
}
});
}
if (Object.keys(_acl).length > 0) {
restObjectCopy._acl = _acl;
}
return restObjectCopy;
}
// A sentinel value that helper transformations return when they
// cannot perform a transformation
function CannotTransform() {}
const transformInteriorAtom = atom => {
// TODO: check validity harder for the __type-defined types
if (typeof atom === 'object' && atom && !(atom instanceof Date) && atom.__type === 'Pointer') {
return {
__type: 'Pointer',
className: atom.className,
objectId: atom.objectId
};
} else if (typeof atom === 'function' || typeof atom === 'symbol') {
throw new Parse.Error(Parse.Error.INVALID_JSON, `cannot transform value: ${atom}`);
} else if (DateCoder.isValidJSON(atom)) {
return DateCoder.JSONToDatabase(atom);
} else if (BytesCoder.isValidJSON(atom)) {
return BytesCoder.JSONToDatabase(atom);
} else {
return atom;
}
}
// Helper function to transform an atom from REST format to Mongo format.
// An atom is anything that can't contain other expressions. So it
// includes things where objects are used to represent other
// datatypes, like pointers and dates, but it does not include objects
// or arrays with generic stuff inside.
// Raises an error if this cannot possibly be valid REST format.
// Returns CannotTransform if it's just not an atom
function transformTopLevelAtom(atom) {
switch(typeof atom) {
case 'string':
case 'number':
case 'boolean':
return atom;
case 'undefined':
return atom;
case 'symbol':
case 'function':
throw new Parse.Error(Parse.Error.INVALID_JSON, `cannot transform value: ${atom}`);
case 'object':
if (atom instanceof Date) {
// Technically dates are not rest format, but, it seems pretty
// clear what they should be transformed to, so let's just do it.
return atom;
}
if (atom === null) {
return atom;
}
// TODO: check validity harder for the __type-defined types
if (atom.__type == 'Pointer') {
return `${atom.className}$${atom.objectId}`;
}
if (DateCoder.isValidJSON(atom)) {
return DateCoder.JSONToDatabase(atom);
}
if (BytesCoder.isValidJSON(atom)) {
return BytesCoder.JSONToDatabase(atom);
}
if (GeoPointCoder.isValidJSON(atom)) {
return GeoPointCoder.JSONToDatabase(atom);
}
if (FileCoder.isValidJSON(atom)) {
return FileCoder.JSONToDatabase(atom);
}
return CannotTransform;
default:
// I don't think typeof can ever let us get here
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, `really did not expect value: ${atom}`);
}
}
// Transforms a query constraint from REST API format to Mongo format.
// A constraint is something with fields like $lt.
// If it is not a valid constraint but it could be a valid something
// else, return CannotTransform.
// inArray is whether this is an array field.
function transformConstraint(constraint, inArray) {
if (typeof constraint !== 'object' || !constraint) {
return CannotTransform;
}
// keys is the constraints in reverse alphabetical order.
// This is a hack so that:
// $regex is handled before $options
// $nearSphere is handled before $maxDistance
var keys = Object.keys(constraint).sort().reverse();
var answer = {};
for (var key of keys) {
switch(key) {
case '$lt':
case '$lte':
case '$gt':
case '$gte':
case '$exists':
case '$ne':
case '$eq':
answer[key] = inArray ? transformInteriorAtom(constraint[key]) : transformTopLevelAtom(constraint[key]);
if (answer[key] === CannotTransform) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `bad atom: ${atom}`);
}
break;
case '$in':
case '$nin':
var arr = constraint[key];
if (!(arr instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value');
}
answer[key] = arr.map(value => {
let result = inArray ? transformInteriorAtom(value) : transformTopLevelAtom(value);
if (result === CannotTransform) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `bad atom: ${atom}`);
}
return result;
});
break;
case '$all':
var arr = constraint[key];
if (!(arr instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_JSON,
'bad ' + key + ' value');
}
answer[key] = arr.map(transformInteriorAtom);
break;
case '$regex':
var s = constraint[key];
if (typeof s !== 'string') {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad regex: ' + s);
}
answer[key] = s;
break;
case '$options':
answer[key] = constraint[key];
break;
case '$nearSphere':
var point = constraint[key];
answer[key] = [point.longitude, point.latitude];
break;
case '$maxDistance':
answer[key] = constraint[key];
break;
// The SDKs don't seem to use these but they are documented in the
// REST API docs.
case '$maxDistanceInRadians':
answer['$maxDistance'] = constraint[key];
break;
case '$maxDistanceInMiles':
answer['$maxDistance'] = constraint[key] / 3959;
break;
case '$maxDistanceInKilometers':
answer['$maxDistance'] = constraint[key] / 6371;
break;
case '$select':
case '$dontSelect':
throw new Parse.Error(
Parse.Error.COMMAND_UNAVAILABLE,
'the ' + key + ' constraint is not supported yet');
case '$within':
var box = constraint[key]['$box'];
if (!box || box.length != 2) {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
'malformatted $within arg');
}
answer[key] = {
'$box': [
[box[0].longitude, box[0].latitude],
[box[1].longitude, box[1].latitude]
]
};
break;
default:
if (key.match(/^\$+/)) {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
'bad constraint: ' + key);
}
return CannotTransform;
}
}
return answer;
}
// Transforms an update operator from REST format to mongo format.
// To be transformed, the input should have an __op field.
// If flatten is true, this will flatten operators to their static
// data format. For example, an increment of 2 would simply become a
// 2.
// The output for a non-flattened operator is a hash with __op being
// the mongo op, and arg being the argument.
// The output for a flattened operator is just a value.
// Returns undefined if this should be a no-op.
function transformUpdateOperator({
__op,
amount,
objects,
}, flatten) {
switch(__op) {
case 'Delete':
if (flatten) {
return undefined;
} else {
return {__op: '$unset', arg: ''};
}
case 'Increment':
if (typeof amount !== 'number') {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'incrementing must provide a number');
}
if (flatten) {
return amount;
} else {
return {__op: '$inc', arg: amount};
}
case 'Add':
case 'AddUnique':
if (!(objects instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array');
}
var toAdd = objects.map(transformInteriorAtom);
if (flatten) {
return toAdd;
} else {
var mongoOp = {
Add: '$push',
AddUnique: '$addToSet'
}[__op];
return {__op: mongoOp, arg: {'$each': toAdd}};
}
case 'Remove':
if (!(objects instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to remove must be an array');
}
var toRemove = objects.map(transformInteriorAtom);
if (flatten) {
return [];
} else {
return {__op: '$pullAll', arg: toRemove};
}
default:
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, `The ${__op} operator is not supported yet.`);
}
}
const nestedMongoObjectToNestedParseObject = mongoObject => {
switch(typeof mongoObject) {
case 'string':
case 'number':
case 'boolean':
return mongoObject;
case 'undefined':
case 'symbol':
case 'function':
throw 'bad value in mongoObjectToParseObject';
case 'object':
if (mongoObject === null) {
return null;
}
if (mongoObject instanceof Array) {
return mongoObject.map(nestedMongoObjectToNestedParseObject);
}
if (mongoObject instanceof Date) {
return Parse._encode(mongoObject);
}
if (mongoObject instanceof mongodb.Long) {
return mongoObject.toNumber();
}
if (mongoObject instanceof mongodb.Double) {
return mongoObject.value;
}
if (BytesCoder.isValidDatabaseObject(mongoObject)) {
return BytesCoder.databaseToJSON(mongoObject);
}
return _.mapValues(mongoObject, nestedMongoObjectToNestedParseObject);
default:
throw 'unknown js type';
}
}
// Converts from a mongo-format object to a REST-format object.
// Does not strip out anything based on a lack of authentication.
const mongoObjectToParseObject = (className, mongoObject, schema) => {
switch(typeof mongoObject) {
case 'string':
case 'number':
case 'boolean':
return mongoObject;
case 'undefined':
case 'symbol':
case 'function':
throw 'bad value in mongoObjectToParseObject';
case 'object':
if (mongoObject === null) {
return null;
}
if (mongoObject instanceof Array) {
return mongoObject.map(nestedMongoObjectToNestedParseObject);
}
if (mongoObject instanceof Date) {
return Parse._encode(mongoObject);
}
if (mongoObject instanceof mongodb.Long) {
return mongoObject.toNumber();
}
if (mongoObject instanceof mongodb.Double) {
return mongoObject.value;
}
if (BytesCoder.isValidDatabaseObject(mongoObject)) {
return BytesCoder.databaseToJSON(mongoObject);
}
let restObject = {};
if (mongoObject._rperm || mongoObject._wperm) {
restObject._rperm = mongoObject._rperm || [];
restObject._wperm = mongoObject._wperm || [];
delete mongoObject._rperm;
delete mongoObject._wperm;
}
for (var key in mongoObject) {
switch(key) {
case '_id':
restObject['objectId'] = '' + mongoObject[key];
break;
case '_hashed_password':
restObject._hashed_password = mongoObject[key];
break;
case '_acl':
case '_email_verify_token':
case '_perishable_token':
case '_tombstone':
break;
case '_session_token':
restObject['sessionToken'] = mongoObject[key];
break;
case 'updatedAt':
case '_updated_at':
restObject['updatedAt'] = Parse._encode(new Date(mongoObject[key])).iso;
break;
case 'createdAt':
case '_created_at':
restObject['createdAt'] = Parse._encode(new Date(mongoObject[key])).iso;
break;
case 'expiresAt':
case '_expiresAt':
restObject['expiresAt'] = Parse._encode(new Date(mongoObject[key]));
break;
default:
// Check other auth data keys
var authDataMatch = key.match(/^_auth_data_([a-zA-Z0-9_]+)$/);
if (authDataMatch) {
var provider = authDataMatch[1];
restObject['authData'] = restObject['authData'] || {};
restObject['authData'][provider] = mongoObject[key];
break;
}
if (key.indexOf('_p_') == 0) {
var newKey = key.substring(3);
if (!schema.fields[newKey]) {
log.info('transform.js', 'Found a pointer column not in the schema, dropping it.', className, newKey);
break;
}
if (schema.fields[newKey].type !== 'Pointer') {
log.info('transform.js', 'Found a pointer in a non-pointer column, dropping it.', className, key);
break;
}
if (mongoObject[key] === null) {
break;
}
var objData = mongoObject[key].split('$');
if (objData[0] !== schema.fields[newKey].targetClass) {
throw 'pointer to incorrect className';
}
restObject[newKey] = {
__type: 'Pointer',
className: objData[0],
objectId: objData[1]
};
break;
} else if (key[0] == '_' && key != '__type') {
throw ('bad key in untransform: ' + key);
} else {
var value = mongoObject[key];
if (schema.fields[key] && schema.fields[key].type === 'File' && FileCoder.isValidDatabaseObject(value)) {
restObject[key] = FileCoder.databaseToJSON(value);
break;
}
if (schema.fields[key] && schema.fields[key].type === 'GeoPoint' && GeoPointCoder.isValidDatabaseObject(value)) {
restObject[key] = GeoPointCoder.databaseToJSON(value);
break;
}
}
restObject[key] = nestedMongoObjectToNestedParseObject(mongoObject[key]);
}
}
const relationFieldNames = Object.keys(schema.fields).filter(fieldName => schema.fields[fieldName].type === 'Relation');
let relationFields = {};
relationFieldNames.forEach(relationFieldName => {
relationFields[relationFieldName] = {
__type: 'Relation',
className: schema.fields[relationFieldName].targetClass,
}
});
return { ...restObject, ...relationFields };
default:
throw 'unknown js type';
}
}
var DateCoder = {
JSONToDatabase(json) {
return new Date(json.iso);
},
isValidJSON(value) {
return (typeof value === 'object' &&
value !== null &&
value.__type === 'Date'
);
}
};
var BytesCoder = {
databaseToJSON(object) {
return {
__type: 'Bytes',
base64: object.buffer.toString('base64')
};
},
isValidDatabaseObject(object) {
return (object instanceof mongodb.Binary);
},
JSONToDatabase(json) {
return new mongodb.Binary(new Buffer(json.base64, 'base64'));
},
isValidJSON(value) {
return (typeof value === 'object' &&
value !== null &&
value.__type === 'Bytes'
);
}
};
var GeoPointCoder = {
databaseToJSON(object) {
return {
__type: 'GeoPoint',
latitude: object[1],
longitude: object[0]
}
},
isValidDatabaseObject(object) {
return (object instanceof Array &&
object.length == 2
);
},
JSONToDatabase(json) {
return [ json.longitude, json.latitude ];
},
isValidJSON(value) {
return (typeof value === 'object' &&
value !== null &&
value.__type === 'GeoPoint'
);
}
};
var FileCoder = {
databaseToJSON(object) {
return {
__type: 'File',
name: object
}
},
isValidDatabaseObject(object) {
return (typeof object === 'string');
},
JSONToDatabase(json) {
return json.name;
},
isValidJSON(value) {
return (typeof value === 'object' &&
value !== null &&
value.__type === 'File'
);
}
};
module.exports = {
transformKey,
parseObjectToMongoObjectForCreate,
transformUpdate,
transformWhere,
mongoObjectToParseObject,
};
@@ -0,0 +1,431 @@
const pgp = require('pg-promise')();
const PostgresRelationDoesNotExistError = '42P01';
const PostgresDuplicateRelationError = '42P07';
const PostgresDuplicateColumnError = '42701';
const PostgresUniqueIndexViolationError = '23505';
const parseTypeToPostgresType = type => {
switch (type.type) {
case 'String': return 'text';
case 'Date': return 'timestamp';
case 'Object': return 'jsonb';
case 'Boolean': return 'boolean';
case 'Pointer': return 'char(10)';
case 'Number': return 'double precision';
case 'Array':
if (type.contents && type.contents.type === 'String') {
return 'text[]';
} else {
return 'jsonb';
}
default: throw `no type for ${JSON.stringify(type)} yet`;
}
};
const buildWhereClause = ({ schema, query, index }) => {
let patterns = [];
let values = [];
for (let fieldName in query) {
let fieldValue = query[fieldName];
if (typeof fieldValue === 'string') {
patterns.push(`$${index}:name = $${index + 1}`);
values.push(fieldName, fieldValue);
index += 2;
} else if (fieldValue.$ne) {
patterns.push(`$${index}:name <> $${index + 1}`);
values.push(fieldName, fieldValue.$ne);
index += 2;
} else if (fieldName === '$or') {
fieldValue.map(subQuery => buildWhereClause({ schema, query: subQuery, index })).forEach(result => {
patterns.push(result.pattern);
values.push(...result.values);
});
} else if (Array.isArray(fieldValue.$in) && schema.fields[fieldName].type === 'Array') {
let inPatterns = [];
let allowNull = false;
values.push(fieldName);
fieldValue.$in.forEach((listElem, listIndex) => {
if (listElem === null ) {
allowNull = true;
} else {
values.push(listElem);
inPatterns.push(`$${index + 1 + listIndex - (allowNull ? 1 : 0)}`);
}
});
if (allowNull) {
patterns.push(`($${index}:name IS NULL OR $${index}:name && ARRAY[${inPatterns.join(',')}])`);
} else {
patterns.push(`$${index}:name && ARRAY[${inPatterns.join(',')}]`);
}
index = index + 1 + inPatterns.length;
} else if (Array.isArray(fieldValue.$in) && schema.fields[fieldName].type === 'String') {
let inPatterns = [];
values.push(fieldName);
fieldValue.$in.forEach((listElem, listIndex) => {
values.push(listElem);
inPatterns.push(`$${index + 1 + listIndex}`);
});
patterns.push(`$${index}:name IN (${inPatterns.join(',')})`);
index = index + 1 + inPatterns.length;
} else if (fieldValue.__type === 'Pointer') {
patterns.push(`$${index}:name = $${index + 1}`);
values.push(fieldName, fieldValue.objectId);
index += 2;
} else {
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Postgres doesn't support this query type yet`);
}
}
return { pattern: patterns.join(' AND '), values };
}
export class PostgresStorageAdapter {
// Private
_collectionPrefix: string;
_client;
constructor({
uri,
collectionPrefix = '',
}) {
this._collectionPrefix = collectionPrefix;
this._client = pgp(uri);
}
_ensureSchemaCollectionExists() {
return this._client.query('CREATE TABLE "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )')
.catch(error => {
if (error.code === PostgresDuplicateRelationError) {
// Table already exists, must have been created by a different request. Ignore error.
} else {
throw error;
}
});
};
classExists(name) {
return Promise.reject('Not implemented yet.')
}
setClassLevelPermissions(className, CLPs) {
return Promise.reject('Not implemented yet.')
}
createClass(className, schema) {
let valuesArray = [];
let patternsArray = [];
Object.keys(schema.fields).forEach((fieldName, index) => {
valuesArray.push(fieldName);
let parseType = schema.fields[fieldName];
if (['_rperm', '_wperm'].includes(fieldName)) {
parseType.contents = { type: 'String' };
}
valuesArray.push(parseTypeToPostgresType(parseType));
patternsArray.push(`$${index * 2 + 2}:name $${index * 2 + 3}:raw`);
});
return this._ensureSchemaCollectionExists()
.then(() => this._client.query(`CREATE TABLE $1:name (${patternsArray.join(',')})`, [className, ...valuesArray]))
.catch(error => {
if (error.code === PostgresDuplicateRelationError) {
// Table already exists, must have been created by a different request. Ignore error.
} else {
throw error;
}
})
.then(() => this._client.query('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($<className>, $<schema>, true)', { className, schema }))
.then(() => schema);
}
addFieldIfNotExists(className, fieldName, type) {
// TODO: Must be revised for invalid logic...
return this._client.tx("addFieldIfNotExists", t=> {
return t.query('ALTER TABLE $<className:name> ADD COLUMN $<fieldName:name> $<postgresType:raw>', {
className,
fieldName,
postgresType: parseTypeToPostgresType(type)
})
.catch(error => {
if (error.code === PostgresRelationDoesNotExistError) {
return this.createClass(className, {fields: {[fieldName]: type}})
} else if (error.code === PostgresDuplicateColumnError) {
// Column already exists, created by other request. Carry on to
// See if it's the right type.
} else {
throw error;
}
})
.then(() => t.query('SELECT "schema" FROM "_SCHEMA" WHERE "className" = $<className>', {className}))
.then(result => {
if (fieldName in result[0].schema) {
throw "Attempted to add a field that already exists";
} else {
result[0].schema.fields[fieldName] = type;
return t.query(
'UPDATE "_SCHEMA" SET "schema"=$<schema> WHERE "className"=$<className>',
{schema: result[0].schema, className}
);
}
})
});
}
// Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.)
// and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible.
deleteClass(className) {
return Promise.reject('Not implemented yet.')
}
// Delete all data known to this adapter. Used for testing.
deleteAllClasses() {
return this._client.query('SELECT "className" FROM "_SCHEMA"')
.then(results => {
const classes = ['_SCHEMA', ...results.map(result => result.className)];
return this._client.tx(t=>t.batch(classes.map(className=>t.none('DROP TABLE $<className:name>', { className }))));
}, error => {
if (error.code === PostgresRelationDoesNotExistError) {
// No _SCHEMA collection. Don't delete anything.
return;
} else {
throw error;
}
})
}
// Remove the column and all the data. For Relations, the _Join collection is handled
// specially, this function does not delete _Join columns. It should, however, indicate
// that the relation fields does not exist anymore. In mongo, this means removing it from
// the _SCHEMA collection. There should be no actual data in the collection under the same name
// as the relation column, so it's fine to attempt to delete it. If the fields listed to be
// deleted do not exist, this function should return successfully anyways. Checking for
// attempts to delete non-existent fields is the responsibility of Parse Server.
// This function is not obligated to delete fields atomically. It is given the field
// names in a list so that databases that are capable of deleting fields atomically
// may do so.
// Returns a Promise.
deleteFields(className, schema, fieldNames) {
return Promise.reject('Not implemented yet.')
}
// Return a promise for all schemas known to this adapter, in Parse format. In case the
// schemas cannot be retrieved, returns a promise that rejects. Requirements for the
// rejection reason are TBD.
getAllClasses() {
return this._ensureSchemaCollectionExists()
.then(() => this._client.map('SELECT * FROM "_SCHEMA"', null, row => ({ className: row.className, ...row.schema })));
}
// Return a promise for the schema with the given name, in Parse format. If
// this adapter doesn't know about the schema, return a promise that rejects with
// undefined as the reason.
getClass(className) {
return this._client.query('SELECT * FROM "_SCHEMA" WHERE "className"=$<className>', { className })
.then(result => {
if (result.length === 1) {
return result[0].schema;
} else {
throw undefined;
}
});
}
// TODO: remove the mongo format dependency in the return value
createObject(className, schema, object) {
let columnsArray = [];
let valuesArray = [];
Object.keys(object).forEach(fieldName => {
columnsArray.push(fieldName);
switch (schema.fields[fieldName].type) {
case 'Date':
valuesArray.push(object[fieldName].iso);
break;
case 'Pointer':
valuesArray.push(object[fieldName].objectId);
break;
case 'Array':
if (['_rperm', '_wperm'].includes(fieldName)) {
valuesArray.push(object[fieldName]);
} else {
valuesArray.push(JSON.stringify(object[fieldName]));
}
break;
case 'Object':
valuesArray.push(object[fieldName]);
break;
case 'String':
valuesArray.push(object[fieldName]);
break;
case 'Number':
valuesArray.push(object[fieldName]);
break;
case 'Boolean':
valuesArray.push(object[fieldName]);
break;
default:
throw `Type ${schema.fields[fieldName].type} not supported yet`;
break;
}
});
let columnsPattern = columnsArray.map((col, index) => `$${index + 2}:name`).join(',');
let valuesPattern = valuesArray.map((val, index) => `$${index + 2 + columnsArray.length}${(['_rperm','_wperm'].includes(columnsArray[index])) ? '::text[]' : ''}`).join(',');
let qs = `INSERT INTO $1:name (${columnsPattern}) VALUES (${valuesPattern})`
let values = [className, ...columnsArray, ...valuesArray]
return this._client.query(qs, values)
.then(() => ({ ops: [object] }))
.catch(error => {
if (error.code === PostgresUniqueIndexViolationError) {
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
} else {
throw error;
}
})
}
// Remove all objects that match the given Parse Query.
// If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined.
// If there is some other error, reject with INTERNAL_SERVER_ERROR.
deleteObjectsByQuery(className, schema, query) {
return this._client.query(`WITH deleted AS (DELETE FROM $<className:name> RETURNING *) SELECT count(*) FROM deleted`, { className })
.then(result => {
if (result[0].count === 0) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
} else {
return result[0].count;
}
});
}
// Apply the update to all objects that match the given Parse Query.
updateObjectsByQuery(className, schema, query, update) {
return Promise.reject('Not implemented yet.')
}
// Return value not currently well specified.
findOneAndUpdate(className, schema, query, update) {
let conditionPatterns = [];
let updatePatterns = [];
let values = [className]
let index = 2;
for (let fieldName in update) {
let fieldValue = update[fieldName];
if (fieldValue.__op === 'Increment') {
updatePatterns.push(`$${index}:name = COALESCE($${index}:name, 0) + $${index + 1}`);
values.push(fieldName, fieldValue.amount);
index += 2;
} else if (fieldValue.__op === 'Add') {
updatePatterns.push(`$${index}:name = COALESCE($${index}:name, '[]'::jsonb) || $${index + 1}`);
values.push(fieldName, fieldValue.objects);
index += 2;
} else if (fieldValue.__op === 'Remove') {
return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Postgres does not support Remove operator.'));
} else if (fieldValue.__op === 'AddUnique') {
return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Postgres does not support AddUnique operator'));
} else if (fieldName === 'updatedAt') { //TODO: stop special casing this. It should check for __type === 'Date' and use .iso
updatePatterns.push(`$${index}:name = $${index + 1}`)
values.push(fieldName, new Date(fieldValue));
index += 2;
} else if (typeof fieldValue === 'string') {
updatePatterns.push(`$${index}:name = $${index + 1}`);
values.push(fieldName, fieldValue);
index += 2;
} else if (fieldValue.__type === 'Pointer') {
updatePatterns.push(`$${index}:name = $${index + 1}`);
values.push(fieldName, fieldValue.objectId);
index += 2;
} else {
return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Postgres doesn't support update ${JSON.stringify(fieldValue)} yet`));
}
}
let where = buildWhereClause({ schema, index, query })
values.push(...where.values);
let qs = `UPDATE $1:name SET ${updatePatterns.join(',')} WHERE ${where.pattern} RETURNING *`;
return this._client.query(qs, values)
.then(val => val[0]); // TODO: This is unsafe, verification is needed, or a different query method;
}
// Hopefully, we can get rid of this. It's only used for config and hooks.
upsertOneObject(className, schema, query, update) {
return Promise.reject('Not implemented yet.')
}
find(className, schema, query, { skip, limit, sort }) {
let values = [className];
let where = buildWhereClause({ schema, query, index: 2 })
values.push(...where.values);
const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : '';
const limitPattern = limit !== undefined ? `LIMIT $${values.length + 1}` : '';
const qs = `SELECT * FROM $1:name ${wherePattern} ${limitPattern}`;
if (limit !== undefined) {
values.push(limit);
}
return this._client.query(qs, values)
.then(results => results.map(object => {
Object.keys(schema.fields).filter(field => schema.fields[field].type === 'Pointer').forEach(fieldName => {
object[fieldName] = { objectId: object[fieldName], __type: 'Pointer', className: schema.fields[fieldName].targetClass };
});
//TODO: remove this reliance on the mongo format. DB adapter shouldn't know there is a difference between created at and any other date field.
if (object.createdAt) {
object.createdAt = object.createdAt.toISOString();
}
if (object.updatedAt) {
object.updatedAt = object.updatedAt.toISOString();
}
if (object.expiresAt) {
object.expiresAt = { __type: 'Date', iso: object.expiresAt.toISOString() };
}
for (let fieldName in object) {
if (object[fieldName] === null) {
delete object[fieldName];
}
if (object[fieldName] instanceof Date) {
object[fieldName] = { __type: 'Date', iso: object[fieldName].toISOString() };
}
}
return object;
}))
}
// Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't
// currently know which fields are nullable and which aren't, we ignore that criteria.
// As such, we shouldn't expose this function to users of parse until we have an out-of-band
// Way of determining if a field is nullable. Undefined doesn't count against uniqueness,
// which is why we use sparse indexes.
ensureUniqueness(className, schema, fieldNames) {
// Use the same name for every ensureUniqueness attempt, because postgres
// Will happily create the same index with multiple names.
const constraintName = `unique_${fieldNames.sort().join('_')}`;
const constraintPatterns = fieldNames.map((fieldName, index) => `$${index + 3}:name`);
const qs = `ALTER TABLE $1:name ADD CONSTRAINT $2:name UNIQUE (${constraintPatterns.join(',')})`;
return this._client.query(qs,[className, constraintName, ...fieldNames])
.catch(error => {
if (error.code === PostgresDuplicateRelationError && error.message.includes(constraintName)) {
// Index already exists. Ignore error.
} else {
throw error;
}
});
}
// Executes a count.
count(className, schema, query) {
let values = [className];
let where = buildWhereClause({ schema, query, index: 2 });
values.push(...where.values);
const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : '';
const qs = `SELECT COUNT(*) FROM $1:name ${wherePattern}`;
return this._client.query(qs, values)
.then(result => parseInt(result[0].count))
}
}
export default PostgresStorageAdapter;
module.exports = PostgresStorageAdapter; // Required for tests
+114 -102
View File
@@ -2,8 +2,6 @@ var deepcopy = require('deepcopy');
var Parse = require('parse/node').Parse;
var RestQuery = require('./RestQuery');
import cache from './cache';
// An Auth object tells you who is requesting something and whether
// the master key was used.
// userObject is a Parse.User and can be null if there's no user.
@@ -42,33 +40,41 @@ function nobody(config) {
return new Auth({ config, isMaster: false });
}
// Returns a promise that resolves to an Auth object
var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) {
var cachedUser = cache.users.get(sessionToken);
if (cachedUser) {
return Promise.resolve(new Auth({ config, isMaster: false, installationId, user: cachedUser }));
}
var restOptions = {
limit: 1,
include: 'user'
};
var restWhere = {
_session_token: sessionToken
};
var query = new RestQuery(config, master(config), '_Session',
restWhere, restOptions);
return query.execute().then((response) => {
var results = response.results;
if (results.length !== 1 || !results[0]['user']) {
return nobody(config);
return config.cacheController.user.get(sessionToken).then((userJSON) => {
if (userJSON) {
let cachedUser = Parse.Object.fromJSON(userJSON);
return Promise.resolve(new Auth({config, isMaster: false, installationId, user: cachedUser}));
}
var obj = results[0]['user'];
delete obj.password;
obj['className'] = '_User';
obj['sessionToken'] = sessionToken;
let userObject = Parse.Object.fromJSON(obj);
cache.users.set(sessionToken, userObject);
return new Auth({ config, isMaster: false, installationId, user: userObject });
var restOptions = {
limit: 1,
include: 'user'
};
var query = new RestQuery(config, master(config), '_Session', {sessionToken}, restOptions);
return query.execute().then((response) => {
var results = response.results;
if (results.length !== 1 || !results[0]['user']) {
return nobody(config);
}
var now = new Date(),
expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined;
if (expiresAt < now) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
'Session token is expired.');
}
var obj = results[0]['user'];
delete obj.password;
obj['className'] = '_User';
obj['sessionToken'] = sessionToken;
config.cacheController.user.put(sessionToken, obj);
let userObject = Parse.Object.fromJSON(obj);
return new Auth({config, isMaster: false, installationId, user: userObject});
});
});
};
@@ -89,95 +95,101 @@ Auth.prototype.getUserRoles = function() {
// Iterates through the role tree and compiles a users roles
Auth.prototype._loadRoles = function() {
var restWhere = {
'users': {
__type: 'Pointer',
className: '_User',
objectId: this.user.id
}
};
// First get the role ids this user is directly a member of
var query = new RestQuery(this.config, master(this.config), '_Role',
restWhere, {});
return query.execute().then((response) => {
var results = response.results;
if (!results.length) {
this.userRoles = [];
var cacheAdapter = this.config.cacheController;
return cacheAdapter.role.get(this.user.id).then((cachedRoles) => {
if (cachedRoles != null) {
this.fetchedRoles = true;
this.rolePromise = null;
return Promise.resolve(this.userRoles);
this.userRoles = cachedRoles;
return Promise.resolve(cachedRoles);
}
var roleIDs = results.map(r => r.objectId);
var promises = [Promise.resolve(roleIDs)];
for (var role of roleIDs) {
promises.push(this._getAllRoleNamesForId(role));
}
return Promise.all(promises).then((results) => {
var allIDs = [];
for (var x of results) {
Array.prototype.push.apply(allIDs, x);
var restWhere = {
'users': {
__type: 'Pointer',
className: '_User',
objectId: this.user.id
}
var restWhere = {
objectId: {
'$in': allIDs
}
};
var query = new RestQuery(this.config, master(this.config),
'_Role', restWhere, {});
return query.execute();
}).then((response) => {
};
// First get the role ids this user is directly a member of
var query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {});
return query.execute().then((response) => {
var results = response.results;
this.userRoles = results.map((r) => {
return 'role:' + r.name;
});
this.fetchedRoles = true;
this.rolePromise = null;
return Promise.resolve(this.userRoles);
if (!results.length) {
this.userRoles = [];
this.fetchedRoles = true;
this.rolePromise = null;
cacheAdapter.role.put(this.user.id, this.userRoles);
return Promise.resolve(this.userRoles);
}
var rolesMap = results.reduce((m, r) => {
m.names.push(r.name);
m.ids.push(r.objectId);
return m;
}, {ids: [], names: []});
// run the recursive finding
return this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names)
.then((roleNames) => {
this.userRoles = roleNames.map((r) => {
return 'role:' + r;
});
this.fetchedRoles = true;
this.rolePromise = null;
cacheAdapter.role.put(this.user.id, this.userRoles);
return Promise.resolve(this.userRoles);
});
});
});
};
// Given a role object id, get any other roles it is part of
Auth.prototype._getAllRoleNamesForId = function(roleID) {
// As per documentation, a Role inherits AnotherRole
// if this Role is in the roles pointer of this AnotherRole
// Let's find all the roles where this role is in a roles relation
var rolePointer = {
__type: 'Pointer',
className: '_Role',
objectId: roleID
};
var restWhere = {
'roles': rolePointer
};
var query = new RestQuery(this.config, master(this.config), '_Role',
restWhere, {});
// Given a list of roleIds, find all the parent roles, returns a promise with all names
Auth.prototype._getAllRolesNamesForRoleIds = function(roleIDs, names = [], queriedRoles = {}) {
let ins = roleIDs.filter((roleID) => {
return queriedRoles[roleID] !== true;
}).map((roleID) => {
// mark as queried
queriedRoles[roleID] = true;
return {
__type: 'Pointer',
className: '_Role',
objectId: roleID
}
});
// all roles are accounted for, return the names
if (ins.length == 0) {
return Promise.resolve([...new Set(names)]);
}
// Build an OR query across all parentRoles
let restWhere;
if (ins.length == 1) {
restWhere = { 'roles': ins[0] };
} else {
restWhere = { 'roles': { '$in': ins }}
}
let query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {});
return query.execute().then((response) => {
var results = response.results;
// Nothing found
if (!results.length) {
return Promise.resolve([]);
return Promise.resolve(names);
}
var roleIDs = results.map(r => r.objectId);
// we found a list of roles where the roleID
// is referenced in the roles relation,
// Get the roles where those found roles are also
// referenced the same way
var parentRolesPromises = roleIDs.map( (roleId) => {
return this._getAllRoleNamesForId(roleId);
});
parentRolesPromises.push(Promise.resolve(roleIDs));
return Promise.all(parentRolesPromises);
}).then(function(results){
// Flatten
let roleIDs = results.reduce( (memo, result) => {
return memo.concat(result);
}, []);
return Promise.resolve([...new Set(roleIDs)]);
});
};
// Map the results with all Ids and names
let resultMap = results.reduce((memo, role) => {
memo.names.push(role.name);
memo.ids.push(role.objectId);
return memo;
}, {ids: [], names: []});
// store the new found names
names = names.concat(resultMap.names);
// find the next ones, circular roles will be cut
return this._getAllRolesNamesForRoleIds(resultMap.ids, names, queriedRoles)
}).then((names) => {
return Promise.resolve([...new Set(names)])
})
}
module.exports = {
Auth: Auth,
+78 -11
View File
@@ -2,12 +2,21 @@
// configured.
// mount is the URL for the root of the API; includes http, domain, etc.
import cache from './cache';
import AppCache from './cache';
function removeTrailingSlash(str) {
if (!str) {
return str;
}
if (str.endsWith("/")) {
str = str.substr(0, str.length-1);
}
return str;
}
export class Config {
constructor(applicationId: string, mount: string) {
let DatabaseAdapter = require('./DatabaseAdapter');
let cacheInfo = cache.apps.get(applicationId);
let cacheInfo = AppCache.get(applicationId);
if (!cacheInfo) {
return;
}
@@ -18,16 +27,18 @@ export class Config {
this.javascriptKey = cacheInfo.javascriptKey;
this.dotNetKey = cacheInfo.dotNetKey;
this.restAPIKey = cacheInfo.restAPIKey;
this.webhookKey = cacheInfo.webhookKey;
this.fileKey = cacheInfo.fileKey;
this.facebookAppIds = cacheInfo.facebookAppIds;
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
this.database = cacheInfo.databaseController;
this.serverURL = cacheInfo.serverURL;
this.publicServerURL = cacheInfo.publicServerURL;
this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL);
this.verifyUserEmails = cacheInfo.verifyUserEmails;
this.appName = cacheInfo.appName;
this.cacheController = cacheInfo.cacheController;
this.hooksController = cacheInfo.hooksController;
this.filesController = cacheInfo.filesController;
this.pushController = cacheInfo.pushController;
@@ -35,14 +46,39 @@ export class Config {
this.userController = cacheInfo.userController;
this.authDataManager = cacheInfo.authDataManager;
this.customPages = cacheInfo.customPages || {};
this.mount = mount;
this.mount = removeTrailingSlash(mount);
this.liveQueryController = cacheInfo.liveQueryController;
this.sessionLength = cacheInfo.sessionLength;
this.expireInactiveSessions = cacheInfo.expireInactiveSessions;
this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this);
this.revokeSessionOnPasswordReset = cacheInfo.revokeSessionOnPasswordReset;
}
static validate(options) {
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
appName: options.appName,
publicServerURL: options.publicServerURL})
static validate({
verifyUserEmails,
appName,
publicServerURL,
revokeSessionOnPasswordReset,
expireInactiveSessions,
sessionLength,
}) {
this.validateEmailConfiguration({
verifyUserEmails: verifyUserEmails,
appName: appName,
publicServerURL: publicServerURL
})
if (typeof revokeSessionOnPasswordReset !== 'boolean') {
throw 'revokeSessionOnPasswordReset must be a boolean value';
}
if (publicServerURL) {
if (!publicServerURL.startsWith("http://") && !publicServerURL.startsWith("https://")) {
throw "publicServerURL should be a valid HTTPS URL starting with https://"
}
}
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
}
static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {
@@ -56,6 +92,37 @@ export class Config {
}
}
get mount() {
var mount = this._mount;
if (this.publicServerURL) {
mount = this.publicServerURL;
}
return mount;
}
set mount(newValue) {
this._mount = newValue;
}
static validateSessionConfiguration(sessionLength, expireInactiveSessions) {
if (expireInactiveSessions) {
if (isNaN(sessionLength)) {
throw 'Session length must be a valid number.';
}
else if (sessionLength <= 0) {
throw 'Session length must be a value greater than 0.'
}
}
}
generateSessionExpiresAt() {
if (!this.expireInactiveSessions) {
return undefined;
}
var now = new Date();
return new Date(now.getTime() + (this.sessionLength*1000));
}
get invalidLinkURL() {
return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
}
@@ -79,7 +146,7 @@ export class Config {
get verifyEmailURL() {
return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`;
}
};
}
export default Config;
module.exports = Config;
+75
View File
@@ -0,0 +1,75 @@
import AdaptableController from './AdaptableController';
import CacheAdapter from '../Adapters/Cache/CacheAdapter';
const KEY_SEPARATOR_CHAR = ':';
function joinKeys(...keys) {
return keys.join(KEY_SEPARATOR_CHAR);
}
/**
* Prefix all calls to the cache via a prefix string, useful when grouping Cache by object type.
*
* eg "Role" or "Session"
*/
export class SubCache {
constructor(prefix, cacheController) {
this.prefix = prefix;
this.cache = cacheController;
}
get(key) {
let cacheKey = joinKeys(this.prefix, key);
return this.cache.get(cacheKey);
}
put(key, value, ttl) {
let cacheKey = joinKeys(this.prefix, key);
return this.cache.put(cacheKey, value, ttl);
}
del(key) {
let cacheKey = joinKeys(this.prefix, key);
return this.cache.del(cacheKey);
}
clear() {
return this.cache.clear();
}
}
export class CacheController extends AdaptableController {
constructor(adapter, appId, options = {}) {
super(adapter, appId, options);
this.role = new SubCache('role', this);
this.user = new SubCache('user', this);
}
get(key) {
let cacheKey = joinKeys(this.appId, key);
return this.adapter.get(cacheKey).then(null, () => Promise.resolve(null));
}
put(key, value, ttl) {
let cacheKey = joinKeys(this.appId, key);
return this.adapter.put(cacheKey, value, ttl);
}
del(key) {
let cacheKey = joinKeys(this.appId, key);
return this.adapter.del(cacheKey);
}
clear() {
return this.adapter.clear();
}
expectedAdapterType() {
return CacheAdapter;
}
}
export default CacheController;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -6,6 +6,8 @@ import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
import path from 'path';
import mime from 'mime';
const legacyFilesRegex = new RegExp("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}-.*");
export class FilesController extends AdaptableController {
getFileData(config, filename) {
@@ -59,8 +61,13 @@ export class FilesController extends AdaptableController {
continue;
}
let filename = fileObject['name'];
// all filenames starting with "tfss-" should be from files.parsetfss.com
// all filenames starting with a "-" seperated UUID should be from files.parse.com
// all other filenames have been migrated or created from Parse Server
if (filename.indexOf('tfss-') === 0) {
fileObject['url'] = 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename);
} else if (legacyFilesRegex.test(filename)) {
fileObject['url'] = 'http://files.parse.com/' + config.fileKey + '/' + encodeURIComponent(filename);
} else {
fileObject['url'] = this.adapter.getFileLocation(config, filename);
}
+35 -33
View File
@@ -1,20 +1,20 @@
/** @flow weak */
import * as DatabaseAdapter from "../DatabaseAdapter";
import * as triggers from "../triggers";
import * as Parse from "parse/node";
import * as request from "request";
import * as triggers from "../triggers";
import * as Parse from "parse/node";
import * as request from "request";
import { logger } from '../logger';
const DefaultHooksCollectionName = "_Hooks";
export class HooksController {
_applicationId:string;
_collectionPrefix:string;
_collection;
constructor(applicationId:string, collectionPrefix:string = '') {
constructor(applicationId:string, databaseController, webhookKey) {
this._applicationId = applicationId;
this._collectionPrefix = collectionPrefix;
this._webhookKey = webhookKey;
this.database = databaseController;
}
load() {
@@ -26,18 +26,6 @@ export class HooksController {
});
}
getCollection() {
if (this._collection) {
return Promise.resolve(this._collection)
}
let database = DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix);
return database.adaptiveCollection(DefaultHooksCollectionName).then(collection => {
this._collection = collection;
return collection;
});
}
getFunction(functionName) {
return this._getHooks({ functionName: functionName }, 1).then(results => results[0]);
}
@@ -64,16 +52,19 @@ export class HooksController {
return this._removeHooks({ className: className, triggerName: triggerName });
}
_getHooks(query, limit) {
_getHooks(query = {}, limit) {
let options = limit ? { limit: limit } : undefined;
return this.getCollection().then(collection => collection.find(query, options));
return this.database.find(DefaultHooksCollectionName, query).then((results) => {
return results.map((result) => {
delete result.objectId;
return result;
});
});
}
_removeHooks(query) {
return this.getCollection().then(collection => {
return collection.deleteMany(query);
}).then(() => {
return {};
return this.database.destroy(DefaultHooksCollectionName, query).then(() => {
return Promise.resolve({});
});
}
@@ -86,15 +77,13 @@ export class HooksController {
} else {
throw new Parse.Error(143, "invalid hook declaration");
}
return this.getCollection()
.then(collection => collection.upsertOne(query, hook))
.then(() => {
return hook;
});
return this.database.update(DefaultHooksCollectionName, query, hook, {upsert: true}).then(() => {
return Promise.resolve(hook);
})
}
addHookToTriggers(hook) {
var wrappedFunction = wrapToHTTPRequest(hook);
var wrappedFunction = wrapToHTTPRequest(hook, this._webhookKey);
wrappedFunction.url = hook.url;
if (hook.className) {
triggers.addTrigger(hook.triggerName, hook.className, wrappedFunction, this._applicationId)
@@ -168,7 +157,7 @@ export class HooksController {
};
}
function wrapToHTTPRequest(hook) {
function wrapToHTTPRequest(hook, key) {
return (req, res) => {
let jsonBody = {};
for (var i in req) {
@@ -189,10 +178,16 @@ function wrapToHTTPRequest(hook) {
body: JSON.stringify(jsonBody)
};
if (key) {
jsonRequest.headers['X-Parse-Webhook-Key'] = key;
} else {
logger.warn('Making outgoing webhook request without webhookKey being set!');
}
request.post(hook.url, jsonRequest, function (err, httpResponse, body) {
var result;
if (body) {
if (typeof body == "string") {
if (typeof body === "string") {
try {
body = JSON.parse(body);
} catch (e) {
@@ -204,8 +199,15 @@ function wrapToHTTPRequest(hook) {
err = body.error;
}
}
if (err) {
return res.error(err);
} else if (hook.triggerName === 'beforeSave') {
if (typeof result === 'object') {
delete result.createdAt;
delete result.updatedAt;
}
return res.success({object: result});
} else {
return res.success(result);
}
+26 -19
View File
@@ -5,6 +5,8 @@ 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 '../pushStatusHandler';
const FEATURE_NAME = 'push';
@@ -38,12 +40,16 @@ export class PushController extends AdaptableController {
return !!this.adapter;
}
sendPush(body = {}, where = {}, config, auth, wait) {
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) {
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);
@@ -52,47 +58,48 @@ export class PushController extends AdaptableController {
let badgeUpdate = () => {
return Promise.resolve();
}
if (body.data && body.data.badge) {
let badge = body.data.badge;
let op = {};
let restUpdate = {};
if (typeof badge == 'string' && badge.toLowerCase() === 'increment') {
op = { $inc: { badge: 1 } }
restUpdate = { badge: { __op: 'Increment', amount: 1 } }
} else if (Number(badge)) {
op = { $set: { badge: badge } }
restUpdate = { badge: badge }
} else {
throw "Invalid value for badge, expected number or 'Increment'";
}
let updateWhere = deepcopy(where);
badgeUpdate = () => {
let badgeQuery = new RestQuery(config, auth, '_Installation', updateWhere);
return badgeQuery.buildRestWhere().then(() => {
let restWhere = deepcopy(badgeQuery.restWhere);
// Force iOS only devices
if (!restWhere['$and']) {
restWhere['$and'] = [badgeQuery.restWhere];
}
restWhere['$and'].push({
'deviceType': 'ios'
});
return config.database.adaptiveCollection("_Installation")
.then(coll => coll.updateMany(restWhere, op));
})
updateWhere.deviceType = 'ios';
// Build a real RestQuery so we can use it in RestWrite
let restQuery = new RestQuery(config, master(config), '_Installation', updateWhere);
return restQuery.buildRestWhere().then(() => {
let write = new RestWrite(config, master(config), '_Installation', restQuery.restWhere, restUpdate);
write.runOptions.many = true;
return write.execute();
});
}
}
let pushStatus = pushStatusHandler(config);
return Promise.resolve().then(() => {
return pushStatus.setInitial(body, where);
}).then(() => {
onPushStatusSaved(pushStatus.objectId);
return badgeUpdate();
}).then(() => {
return rest.find(config, auth, '_Installation', where);
}).then((response) => {
pushStatus.setRunning();
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);
}).catch((err) => {
pushStatus.fail(err);
return Promise.reject(err);
});
}
+858
View File
@@ -0,0 +1,858 @@
// This class handles schema validation, persistence, and modification.
//
// Each individual Schema object should be immutable. The helpers to
// do things with the Schema just return a new schema when the schema
// is changed.
//
// The canonical place to store this Schema is in the database itself,
// in a _SCHEMA collection. This is not the right way to do it for an
// open source framework, but it's backward compatible, so we're
// keeping it this way for now.
//
// In API-handling code, you should only use the Schema class via the
// DatabaseController. This will let us replace the schema logic for
// different databases.
// TODO: hide all schema logic inside the database adapter.
const Parse = require('parse/node').Parse;
import _ from 'lodash';
const defaultColumns = Object.freeze({
// Contain the default columns for every parse object type (except _Join collection)
_Default: {
"objectId": {type:'String'},
"createdAt": {type:'Date'},
"updatedAt": {type:'Date'},
"ACL": {type:'ACL'},
},
// The additional default columns for the _User collection (in addition to DefaultCols)
_User: {
"username": {type:'String'},
"password": {type:'String'},
"email": {type:'String'},
"emailVerified": {type:'Boolean'},
},
// The additional default columns for the _Installation collection (in addition to DefaultCols)
_Installation: {
"installationId": {type:'String'},
"deviceToken": {type:'String'},
"channels": {type:'Array'},
"deviceType": {type:'String'},
"pushType": {type:'String'},
"GCMSenderId": {type:'String'},
"timeZone": {type:'String'},
"localeIdentifier": {type:'String'},
"badge": {type:'Number'},
"appVersion": {type:'String'},
"appName": {type:'String'},
"appIdentifier": {type:'String'},
"parseVersion": {type:'String'},
},
// The additional default columns for the _Role collection (in addition to DefaultCols)
_Role: {
"name": {type:'String'},
"users": {type:'Relation', targetClass:'_User'},
"roles": {type:'Relation', targetClass:'_Role'}
},
// The additional default columns for the _Session collection (in addition to DefaultCols)
_Session: {
"restricted": {type:'Boolean'},
"user": {type:'Pointer', targetClass:'_User'},
"installationId": {type:'String'},
"sessionToken": {type:'String'},
"expiresAt": {type:'Date'},
"createdWith": {type:'Object'}
},
_Product: {
"productIdentifier": {type:'String'},
"download": {type:'File'},
"downloadName": {type:'String'},
"icon": {type:'File'},
"order": {type:'Number'},
"title": {type:'String'},
"subtitle": {type:'String'},
},
_PushStatus: {
"pushTime": {type:'String'},
"source": {type:'String'}, // rest or webui
"query": {type:'String'}, // the stringified JSON query
"payload": {type:'Object'}, // the JSON payload,
"title": {type:'String'},
"expiry": {type:'Number'},
"status": {type:'String'},
"numSent": {type:'Number'},
"numFailed": {type:'Number'},
"pushHash": {type:'String'},
"errorMessage": {type:'Object'},
"sentPerType": {type:'Object'},
"failedPerType":{type:'Object'},
}
});
const requiredColumns = Object.freeze({
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"],
_Role: ["name", "ACL"]
});
const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product', '_PushStatus']);
const volatileClasses = Object.freeze(['_PushStatus', '_Hooks', '_GlobalConfig']);
// 10 alpha numberic chars + uppercase
const userIdRegex = /^[a-zA-Z0-9]{10}$/;
// Anything that start with role
const roleRegex = /^role:.*/;
// * permission
const publicRegex = /^\*$/
const permissionKeyRegex = Object.freeze([userIdRegex, roleRegex, publicRegex]);
function verifyPermissionKey(key) {
let result = permissionKeyRegex.reduce((isGood, regEx) => {
isGood = isGood || key.match(regEx) != null;
return isGood;
}, false);
if (!result) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${key}' is not a valid key for class level permissions`);
}
}
const CLPValidKeys = Object.freeze(['find', 'get', 'create', 'update', 'delete', 'addField', 'readUserFields', 'writeUserFields']);
function validateCLP(perms, fields) {
if (!perms) {
return;
}
Object.keys(perms).forEach((operation) => {
if (CLPValidKeys.indexOf(operation) == -1) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `${operation} is not a valid operation for class level permissions`);
}
if (operation === 'readUserFields' || operation === 'writeUserFields') {
if (!Array.isArray(perms[operation])) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perms[operation]}' is not a valid value for class level permissions ${operation}`);
} else {
perms[operation].forEach((key) => {
if (!fields[key] || fields[key].type != 'Pointer' || fields[key].targetClass != '_User') {
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${key}' is not a valid column for class level pointer permissions ${operation}`);
}
});
}
return;
}
Object.keys(perms[operation]).forEach((key) => {
verifyPermissionKey(key);
let perm = perms[operation][key];
if (perm !== true) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perm}' is not a valid value for class level permissions ${operation}:${key}:${perm}`);
}
});
});
}
const joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
const classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
function classNameIsValid(className) {
// Valid classes must:
return (
// Be one of _User, _Installation, _Role, _Session OR
systemClasses.indexOf(className) > -1 ||
// Be a join table OR
joinClassRegex.test(className) ||
// Include only alpha-numeric and underscores, and not start with an underscore or number
fieldNameIsValid(className)
);
}
// Valid fields must be alpha-numeric, and not start with an underscore or number
function fieldNameIsValid(fieldName) {
return classAndFieldRegex.test(fieldName);
}
// Checks that it's not trying to clobber one of the default fields of the class.
function fieldNameIsValidForClass(fieldName, className) {
if (!fieldNameIsValid(fieldName)) {
return false;
}
if (defaultColumns._Default[fieldName]) {
return false;
}
if (defaultColumns[className] && defaultColumns[className][fieldName]) {
return false;
}
return true;
}
function invalidClassNameMessage(className) {
return 'Invalid classname: ' + className + ', classnames can only have alphanumeric characters and _, and must start with an alpha character ';
}
const invalidJsonError = new Parse.Error(Parse.Error.INVALID_JSON, "invalid JSON");
const validNonRelationOrPointerTypes = [
'Number',
'String',
'Boolean',
'Date',
'Object',
'Array',
'GeoPoint',
'File',
];
// Returns an error suitable for throwing if the type is invalid
const fieldTypeIsInvalid = ({ type, targetClass }) => {
if (['Pointer', 'Relation'].includes(type)) {
if (!targetClass) {
return new Parse.Error(135, `type ${type} needs a class name`);
} else if (typeof targetClass !== 'string') {
return invalidJsonError;
} else if (!classNameIsValid(targetClass)) {
return new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(targetClass));
} else {
return undefined;
}
}
if (typeof type !== 'string') {
return invalidJsonError;
}
if (!validNonRelationOrPointerTypes.includes(type)) {
return new Parse.Error(Parse.Error.INCORRECT_TYPE, `invalid field type: ${type}`);
}
return undefined;
}
const convertSchemaToAdapterSchema = schema => {
schema = injectDefaultSchema(schema);
delete schema.fields.ACL;
schema.fields._rperm = { type: 'Array' };
schema.fields._wperm = { type: 'Array' };
if (schema.className === '_User') {
delete schema.fields.password;
schema.fields._hashed_password = { type: 'String' };
}
return schema;
}
const convertAdapterSchemaToParseSchema = ({...schema}) => {
delete schema.fields._rperm;
delete schema.fields._wperm;
schema.fields.ACL = { type: 'ACL' };
if (schema.className === '_User') {
delete schema.fields.authData; //Auth data is implicit
delete schema.fields._hashed_password;
schema.fields.password = { type: 'String' };
}
return schema;
}
const injectDefaultSchema = ({className, fields, classLevelPermissions}) => ({
className,
fields: {
...defaultColumns._Default,
...(defaultColumns[className] || {}),
...fields,
},
classLevelPermissions,
})
const dbTypeMatchesObjectType = (dbType, objectType) => {
if (dbType.type !== objectType.type) return false;
if (dbType.targetClass !== objectType.targetClass) return false;
if (dbType === objectType.type) return true;
if (dbType.type === objectType.type) return true;
return false;
}
// Stores the entire schema of the app in a weird hybrid format somewhere between
// the mongo format and the Parse format. Soon, this will all be Parse format.
class SchemaController {
_dbAdapter;
data;
perms;
constructor(databaseAdapter) {
this._dbAdapter = databaseAdapter;
// this.data[className][fieldName] tells you the type of that field, in mongo format
this.data = {};
// this.perms[className][operation] tells you the acl-style permissions
this.perms = {};
}
reloadData() {
this.data = {};
this.perms = {};
return this.getAllClasses()
.then(allSchemas => {
allSchemas.forEach(schema => {
this.data[schema.className] = injectDefaultSchema(schema).fields;
this.perms[schema.className] = schema.classLevelPermissions;
});
// Inject the in-memory classes
volatileClasses.forEach(className => {
this.data[className] = injectDefaultSchema({
className,
fields: {},
classLevelPermissions: {}
});
});
});
}
getAllClasses() {
return this._dbAdapter.getAllClasses()
.then(allSchemas => allSchemas.map(injectDefaultSchema));
}
getOneSchema(className, allowVolatileClasses = false) {
if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) {
return Promise.resolve(this.data[className]);
}
return this._dbAdapter.getClass(className)
.then(injectDefaultSchema)
}
// Create a new class that includes the three default fields.
// ACL is an implicit column that does not get an entry in the
// _SCHEMAS database. Returns a promise that resolves with the
// created schema, in mongo format.
// on success, and rejects with an error on fail. Ensure you
// have authorization (master key, or client class creation
// enabled) before calling this function.
addClassIfNotExists(className, fields = {}, classLevelPermissions) {
var validationError = this.validateNewClass(className, fields, classLevelPermissions);
if (validationError) {
return Promise.reject(validationError);
}
return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields, classLevelPermissions, className }))
.then(convertAdapterSchemaToParseSchema)
.catch(error => {
if (error && error.code === Parse.Error.DUPLICATE_VALUE) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
} else {
throw error;
}
});
}
updateClass(className, submittedFields, classLevelPermissions, database) {
return this.getOneSchema(className)
.then(schema => {
let existingFields = schema.fields;
Object.keys(submittedFields).forEach(name => {
let field = submittedFields[name];
if (existingFields[name] && field.__op !== 'Delete') {
throw new Parse.Error(255, `Field ${name} exists, cannot update.`);
}
if (!existingFields[name] && field.__op === 'Delete') {
throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
}
});
delete existingFields._rperm;
delete existingFields._wperm;
let newSchema = buildMergedSchemaObject(existingFields, submittedFields);
let validationError = this.validateSchemaData(className, newSchema, classLevelPermissions, Object.keys(existingFields));
if (validationError) {
throw new Parse.Error(validationError.code, validationError.error);
}
// Finally we have checked to make sure the request is valid and we can start deleting fields.
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
let deletePromises = [];
let insertedFields = [];
Object.keys(submittedFields).forEach(fieldName => {
if (submittedFields[fieldName].__op === 'Delete') {
const promise = this.deleteField(fieldName, className, database);
deletePromises.push(promise);
} else {
insertedFields.push(fieldName);
}
});
return Promise.all(deletePromises) // Delete Everything
.then(() => this.reloadData()) // Reload our Schema, so we have all the new values
.then(() => {
let promises = insertedFields.map(fieldName => {
const type = submittedFields[fieldName];
return this.enforceFieldExists(className, fieldName, type);
});
return Promise.all(promises);
})
.then(() => this.setPermissions(className, classLevelPermissions, newSchema))
//TODO: Move this logic into the database adapter
.then(() => ({
className: className,
fields: this.data[className],
classLevelPermissions: this.perms[className]
}));
})
.catch(error => {
if (error === undefined) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
} else {
throw error;
}
})
}
// Returns a promise that resolves successfully to the new schema
// object or fails with a reason.
enforceClassExists(className) {
if (this.data[className]) {
return Promise.resolve(this);
}
// We don't have this class. Update the schema
return this.addClassIfNotExists(className)
// The schema update succeeded. Reload the schema
.then(() => this.reloadData())
.catch(error => {
// The schema update failed. This can be okay - it might
// have failed because there's a race condition and a different
// client is making the exact same schema update that we want.
// So just reload the schema.
return this.reloadData();
})
.then(() => {
// Ensure that the schema now validates
if (this.data[className]) {
return this;
} else {
throw new Parse.Error(Parse.Error.INVALID_JSON, `Failed to add ${className}`);
}
})
.catch(error => {
// The schema still doesn't validate. Give up
throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate');
});
}
validateNewClass(className, fields = {}, classLevelPermissions) {
if (this.data[className]) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
}
if (!classNameIsValid(className)) {
return {
code: Parse.Error.INVALID_CLASS_NAME,
error: invalidClassNameMessage(className),
};
}
return this.validateSchemaData(className, fields, classLevelPermissions, []);
}
validateSchemaData(className, fields, classLevelPermissions, existingFieldNames) {
for (let fieldName in fields) {
if (!existingFieldNames.includes(fieldName)) {
if (!fieldNameIsValid(fieldName)) {
return {
code: Parse.Error.INVALID_KEY_NAME,
error: 'invalid field name: ' + fieldName,
};
}
if (!fieldNameIsValidForClass(fieldName, className)) {
return {
code: 136,
error: 'field ' + fieldName + ' cannot be added',
};
}
const error = fieldTypeIsInvalid(fields[fieldName]);
if (error) return { code: error.code, error: error.message };
}
}
for (let fieldName in defaultColumns[className]) {
fields[fieldName] = defaultColumns[className][fieldName];
}
let geoPoints = Object.keys(fields).filter(key => fields[key] && fields[key].type === 'GeoPoint');
if (geoPoints.length > 1) {
return {
code: Parse.Error.INCORRECT_TYPE,
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
};
}
validateCLP(classLevelPermissions, fields);
}
// Sets the Class-level permissions for a given className, which must exist.
setPermissions(className, perms, newSchema) {
if (typeof perms === 'undefined') {
return Promise.resolve();
}
validateCLP(perms, newSchema);
return this._dbAdapter.setClassLevelPermissions(className, perms)
.then(() => this.reloadData());
}
// Returns a promise that resolves successfully to the new schema
// object if the provided className-fieldName-type tuple is valid.
// The className must already be validated.
// If 'freeze' is true, refuse to update the schema for this field.
enforceFieldExists(className, fieldName, type, freeze) {
if (fieldName.indexOf(".") > 0) {
// subdocument key (x.y) => ok if x is of type 'object'
fieldName = fieldName.split(".")[ 0 ];
type = 'Object';
}
if (!fieldNameIsValid(fieldName)) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`);
}
// If someone tries to create a new field with null/undefined as the value, return;
if (!type) {
return Promise.resolve(this);
}
return this.reloadData().then(() => {
let expectedType = this.getExpectedType(className, fieldName);
if (typeof type === 'string') {
type = { type };
}
if (expectedType) {
if (!dbTypeMatchesObjectType(expectedType, type)) {
throw new Parse.Error(
Parse.Error.INCORRECT_TYPE,
`schema mismatch for ${className}.${fieldName}; expected ${expectedType.type || expectedType} but got ${type}`
);
}
}
return this._dbAdapter.addFieldIfNotExists(className, fieldName, type).then(() => {
// The update succeeded. Reload the schema
return this.reloadData();
}, error => {
//TODO: introspect the error and only reload if the error is one for which is makes sense to reload
// The update failed. This can be okay - it might have been a race
// condition where another client updated the schema in the same
// way that we wanted to. So, just reload the schema
return this.reloadData();
}).then(error => {
// Ensure that the schema now validates
if (!dbTypeMatchesObjectType(this.getExpectedType(className, fieldName), type)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `Could not add field ${fieldName}`);
}
return this;
});
});
}
// Delete a field, and remove that data from all objects. This is intended
// to remove unused fields, if other writers are writing objects that include
// this field, the field may reappear. Returns a Promise that resolves with
// no object on success, or rejects with { code, error } on failure.
// Passing the database and prefix is necessary in order to drop relation collections
// and remove fields from objects. Ideally the database would belong to
// a database adapter and this function would close over it or access it via member.
deleteField(fieldName, className, database) {
if (!classNameIsValid(className)) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className));
}
if (!fieldNameIsValid(fieldName)) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${fieldName}`);
}
//Don't allow deleting the default fields.
if (!fieldNameIsValidForClass(fieldName, className)) {
throw new Parse.Error(136, `field ${fieldName} cannot be changed`);
}
return this.getOneSchema(className)
.catch(error => {
if (error === undefined) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
} else {
throw error;
}
})
.then(schema => {
if (!schema.fields[fieldName]) {
throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`);
}
if (schema.fields[fieldName].type == 'Relation') {
//For relations, drop the _Join table
return database.adapter.deleteFields(className, schema, [fieldName])
.then(() => database.adapter.deleteClass(`_Join:${fieldName}:${className}`));
}
return database.adapter.deleteFields(className, schema, [fieldName]);
});
}
// Validates an object provided in REST format.
// Returns a promise that resolves to the new schema if this object is
// valid.
validateObject(className, object, query) {
let geocount = 0;
let promise = this.enforceClassExists(className);
for (let fieldName in object) {
if (object[fieldName] === undefined) {
continue;
}
let expected = getType(object[fieldName]);
if (expected === 'GeoPoint') {
geocount++;
}
if (geocount > 1) {
// Make sure all field validation operations run before we return.
// If not - we are continuing to run logic, but already provided response from the server.
return promise.then(() => {
return Promise.reject(new Parse.Error(Parse.Error.INCORRECT_TYPE,
'there can only be one geopoint field in a class'));
});
}
if (!expected) {
continue;
}
if (fieldName === 'ACL') {
// Every object has ACL implicitly.
continue;
}
promise = promise.then(schema => schema.enforceFieldExists(className, fieldName, expected));
}
promise = thenValidateRequiredColumns(promise, className, object, query);
return promise;
}
// Validates that all the properties are set for the object
validateRequiredColumns(className, object, query) {
let columns = requiredColumns[className];
if (!columns || columns.length == 0) {
return Promise.resolve(this);
}
let missingColumns = columns.filter(function(column){
if (query && query.objectId) {
if (object[column] && typeof object[column] === "object") {
// Trying to delete a required column
return object[column].__op == 'Delete';
}
// Not trying to do anything there
return false;
}
return !object[column]
});
if (missingColumns.length > 0) {
throw new Parse.Error(
Parse.Error.INCORRECT_TYPE,
missingColumns[0]+' is required.');
}
return Promise.resolve(this);
}
// Validates the base CLP for an operation
testBaseCLP(className, aclGroup, operation) {
if (!this.perms[className] || !this.perms[className][operation]) {
return true;
}
let classPerms = this.perms[className];
let perms = classPerms[operation];
// Handle the public scenario quickly
if (perms['*']) {
return true;
}
// Check permissions against the aclGroup provided (array of userId/roles)
if (aclGroup.some(acl => { return perms[acl] === true })) {
return true;
}
return false;
}
// Validates an operation passes class-level-permissions set in the schema
validatePermission(className, aclGroup, operation) {
if (this.testBaseCLP(className, aclGroup, operation)) {
return Promise.resolve();
}
if (!this.perms[className] || !this.perms[className][operation]) {
return true;
}
let classPerms = this.perms[className];
let perms = classPerms[operation];
// No matching CLP, let's check the Pointer permissions
// And handle those later
let permissionField = ['get', 'find'].indexOf(operation) > -1 ? 'readUserFields' : 'writeUserFields';
// Reject create when write lockdown
if (permissionField == 'writeUserFields' && operation == 'create') {
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
'Permission denied for this action.');
}
// Process the readUserFields later
if (Array.isArray(classPerms[permissionField]) && classPerms[permissionField].length > 0) {
return Promise.resolve();
}
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
'Permission denied for this action.');
};
// Returns the expected type for a className+key combination
// or undefined if the schema is not set
getExpectedType(className, fieldName) {
if (this.data && this.data[className]) {
const expectedType = this.data[className][fieldName]
return expectedType === 'map' ? 'Object' : expectedType;
}
return undefined;
};
// Checks if a given class is in the schema.
hasClass(className) {
return this.reloadData().then(() => !!(this.data[className]));
}
}
// Returns a promise for a new Schema.
const load = dbAdapter => {
let schema = new SchemaController(dbAdapter);
return schema.reloadData().then(() => schema);
}
// Builds a new schema (in schema API response format) out of an
// existing mongo schema + a schemas API put request. This response
// does not include the default fields, as it is intended to be passed
// to mongoSchemaFromFieldsAndClassName. No validation is done here, it
// is done in mongoSchemaFromFieldsAndClassName.
function buildMergedSchemaObject(existingFields, putRequest) {
let newSchema = {};
let sysSchemaField = Object.keys(defaultColumns).indexOf(existingFields._id) === -1 ? [] : Object.keys(defaultColumns[existingFields._id]);
for (let oldField in existingFields) {
if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') {
if (sysSchemaField.length > 0 && sysSchemaField.indexOf(oldField) !== -1) {
continue;
}
let fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete'
if (!fieldIsDeleted) {
newSchema[oldField] = existingFields[oldField];
}
}
}
for (let newField in putRequest) {
if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') {
if (sysSchemaField.length > 0 && sysSchemaField.indexOf(newField) !== -1) {
continue;
}
newSchema[newField] = putRequest[newField];
}
}
return newSchema;
}
// Given a schema promise, construct another schema promise that
// validates this field once the schema loads.
function thenValidateRequiredColumns(schemaPromise, className, object, query) {
return schemaPromise.then((schema) => {
return schema.validateRequiredColumns(className, object, query);
});
}
// Gets the type from a REST API formatted object, where 'type' is
// extended past javascript types to include the rest of the Parse
// type system.
// The output should be a valid schema value.
// TODO: ensure that this is compatible with the format used in Open DB
function getType(obj) {
let type = typeof obj;
switch(type) {
case 'boolean':
return 'Boolean';
case 'string':
return 'String';
case 'number':
return 'Number';
case 'map':
case 'object':
if (!obj) {
return undefined;
}
return getObjectType(obj);
case 'function':
case 'symbol':
case 'undefined':
default:
throw 'bad obj: ' + obj;
}
}
// This gets the type for non-JSON types like pointers and files, but
// also gets the appropriate type for $ operators.
// Returns null if the type is unknown.
function getObjectType(obj) {
if (obj instanceof Array) {
return 'Array';
}
if (obj.__type){
switch(obj.__type) {
case 'Pointer' :
if(obj.className) {
return {
type: 'Pointer',
targetClass: obj.className
}
}
case 'File' :
if(obj.name) {
return 'File';
}
case 'Date' :
if(obj.iso) {
return 'Date';
}
case 'GeoPoint' :
if(obj.latitude != null && obj.longitude != null) {
return 'GeoPoint';
}
case 'Bytes' :
if(obj.base64) {
return;
}
default:
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid "+obj.__type);
}
}
if (obj['$ne']) {
return getObjectType(obj['$ne']);
}
if (obj.__op) {
switch(obj.__op) {
case 'Increment':
return 'Number';
case 'Delete':
return null;
case 'Add':
case 'AddUnique':
case 'Remove':
return 'Array';
case 'AddRelation':
case 'RemoveRelation':
return {
type: 'Relation',
targetClass: obj.objects[0].className
}
case 'Batch':
return getObjectType(obj.ops[0]);
default:
throw 'unexpected op: ' + obj.__op;
}
}
return 'Object';
}
export {
load,
classNameIsValid,
fieldNameIsValid,
invalidClassNameMessage,
buildMergedSchemaObject,
systemClasses,
defaultColumns,
convertSchemaToAdapterSchema,
};
+27 -46
View File
@@ -43,40 +43,31 @@ export class UserController extends AdaptableController {
if (!this.shouldVerifyEmails) {
// Trying to verify email when not enabled
// TODO: Better error here.
return Promise.reject();
throw undefined;
}
return this.config.database
.adaptiveCollection('_User')
.then(collection => {
// Need direct database access because verification token is not a parse field
return collection.findOneAndUpdate({
username: username,
_email_verify_token: token
}, {$set: {emailVerified: true}});
})
.then(document => {
if (!document) {
return Promise.reject();
}
return document;
});
let database = this.config.database.WithoutValidation();
return database.update('_User', {
username: username,
_email_verify_token: token
}, {emailVerified: true}).then(document => {
if (!document) {
throw undefined;
}
return Promise.resolve(document);
});
}
checkResetTokenValidity(username, token) {
return this.config.database.adaptiveCollection('_User')
.then(collection => {
return collection.find({
username: username,
_perishable_token: token
}, { limit: 1 });
})
.then(results => {
if (results.length != 1) {
return Promise.reject();
}
return results[0];
});
let database = this.config.database.WithoutValidation();
return database.find('_User', {
username: username,
_perishable_token: token
}, {limit: 1}).then(results => {
if (results.length != 1) {
throw undefined;
}
return results[0];
});
}
getUserIfNeeded(user) {
@@ -94,7 +85,7 @@ export class UserController extends AdaptableController {
var query = new RestQuery(this.config, Auth.master(this.config), '_User', where);
return query.execute().then(function(result){
if (result.results.length != 1) {
return Promise.reject();
throw undefined;
}
return result.results[0];
})
@@ -124,15 +115,8 @@ export class UserController extends AdaptableController {
setPasswordResetToken(email) {
let token = randomString(25);
return this.config.database
.adaptiveCollection('_User')
.then(collection => {
// Need direct database access because verification token is not a parse field
return collection.findOneAndUpdate(
{ email: email}, // query
{ $set: { _perishable_token: token } } // update
);
});
let database = this.config.database.WithoutValidation();
return database.update('_User', {email: email}, {_perishable_token: token});
}
sendPasswordResetEmail(email) {
@@ -166,14 +150,11 @@ export class UserController extends AdaptableController {
updatePassword(username, token, password, config) {
return this.checkResetTokenValidity(username, token).then((user) => {
return updateUserPassword(user._id, password, this.config);
return updateUserPassword(user.objectId, password, this.config);
}).then(() => {
// clear reset password token
return this.config.database.adaptiveCollection('_User').then(function (collection) {
// Need direct database access because verification token is not a parse field
return collection.findOneAndUpdate({ username: username },// query
{ $unset: { _perishable_token: null } } // update
);
return this.config.database.WithoutValidation().update('_User', { username }, {
_perishable_token: {__op: 'Delete'}
});
});
}
+16 -70
View File
@@ -1,75 +1,21 @@
/** @flow weak */
// Database Adapter
//
// Allows you to change the underlying database.
//
// Adapter classes must implement the following methods:
// * a constructor with signature (connectionString, optionsObject)
// * connect()
// * loadSchema()
// * create(className, object)
// * find(className, query, options)
// * update(className, query, update, options)
// * destroy(className, query, options)
// * This list is incomplete and the database process is not fully modularized.
//
// Default is MongoStorageAdapter.
import DatabaseController from './Controllers/DatabaseController';
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
const DefaultDatabaseURI = 'mongodb://localhost:27017/parse';
let adapter = MongoStorageAdapter;
let dbConnections = {};
let databaseURI = DefaultDatabaseURI;
let appDatabaseURIs = {};
let appDatabaseOptions = {};
function setAdapter(databaseAdapter) {
adapter = databaseAdapter;
}
function setDatabaseURI(uri) {
databaseURI = uri;
}
function setAppDatabaseURI(appId, uri) {
appDatabaseURIs[appId] = uri;
}
function setAppDatabaseOptions(appId: string, options: Object) {
appDatabaseOptions[appId] = options;
}
import AppCache from './cache';
//Used by tests
function clearDatabaseSettings() {
appDatabaseURIs = {};
dbConnections = {};
appDatabaseOptions = {};
}
function getDatabaseConnection(appId: string, collectionPrefix: string) {
if (dbConnections[appId]) {
return dbConnections[appId];
function destroyAllDataPermanently() {
if (process.env.TESTING) {
// This is super janky, but destroyAllDataPermanently is
// a janky interface, so we need to have some jankyness
// to support it
return Promise.all(Object.keys(AppCache.cache).map(appId => {
const app = AppCache.get(appId);
if (app.databaseController) {
return app.databaseController.deleteEverything();
} else {
return Promise.resolve();
}
}));
}
var dbURI = (appDatabaseURIs[appId] ? appDatabaseURIs[appId] : databaseURI);
let storageAdapter = new adapter(dbURI, appDatabaseOptions[appId]);
dbConnections[appId] = new DatabaseController(storageAdapter, {
collectionPrefix: collectionPrefix
});
return dbConnections[appId];
throw 'Only supported in test environment';
}
module.exports = {
dbConnections: dbConnections,
getDatabaseConnection: getDatabaseConnection,
setAdapter: setAdapter,
setDatabaseURI: setDatabaseURI,
setAppDatabaseOptions: setAppDatabaseOptions,
setAppDatabaseURI: setAppDatabaseURI,
clearDatabaseSettings: clearDatabaseSettings,
defaultDatabaseURI: databaseURI
};
module.exports = { destroyAllDataPermanently };
+9 -1
View File
@@ -141,6 +141,7 @@ function matchesKeyConstraints(object, key, constraints) {
if (constraints.__type) {
if (constraints.__type === 'Pointer') {
return (
typeof object[key] !== 'undefined' &&
constraints.className === object[key].className &&
constraints.objectId === object[key].objectId
);
@@ -206,7 +207,14 @@ function matchesKeyConstraints(object, key, constraints) {
}
break;
case '$exists':
if (typeof object[key] === 'undefined') {
let propertyExists = typeof object[key] !== 'undefined';
let existenceIsRequired = constraints['$exists'];
if (typeof constraints['$exists'] !== 'boolean') {
// The SDK will never submit a non-boolean for $exists, but if someone
// tries to submit a non-boolean for $exits outside the SDKs, just ignore it.
break;
}
if ((!propertyExists && existenceIsRequired) || (propertyExists && !existenceIsRequired)) {
return false;
}
break;
+92 -30
View File
@@ -1,7 +1,5 @@
// ParseServer - open-source compatible API Server for Parse apps
import 'babel-polyfill';
var batch = require('./batch'),
bodyParser = require('body-parser'),
DatabaseAdapter = require('./DatabaseAdapter'),
@@ -9,11 +7,16 @@ var batch = require('./batch'),
middlewares = require('./middlewares'),
multer = require('multer'),
Parse = require('parse/node').Parse,
path = require('path'),
authDataManager = require('./authDataManager');
if (!global._babelPolyfill) {
require('babel-polyfill');
}
import { logger,
configureLogger } from './logger';
import cache from './cache';
configureLogger } from './logger';
import AppCache from './cache';
import Config from './Config';
import parseServerPackage from '../package.json';
import PromiseRouter from './PromiseRouter';
@@ -21,6 +24,8 @@ import requiredParameter from './requiredParameter';
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
import { ClassesRouter } from './Routers/ClassesRouter';
import { FeaturesRouter } from './Routers/FeaturesRouter';
import { InMemoryCacheAdapter } from './Adapters/Cache/InMemoryCacheAdapter';
import { CacheController } from './Controllers/CacheController';
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { FilesController } from './Controllers/FilesController';
import { FilesRouter } from './Routers/FilesRouter';
@@ -45,15 +50,21 @@ import { SchemasRouter } from './Routers/SchemasRouter';
import { SessionsRouter } from './Routers/SessionsRouter';
import { UserController } from './Controllers/UserController';
import { UsersRouter } from './Routers/UsersRouter';
import { PurgeRouter } from './Routers/PurgeRouter';
import DatabaseController from './Controllers/DatabaseController';
const SchemaController = require('./Controllers/SchemaController');
import ParsePushAdapter from 'parse-server-push-adapter';
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
// Mutate the Parse object to add the Cloud Code handlers
addParseCloud();
const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } };
// ParseServer works like a constructor of an express app.
// The args that we understand are:
// "databaseAdapter": a class like DatabaseController providing create, find,
// update, and delete
// "filesAdapter": a class like GridStoreAdapter providing create, get,
// and delete
// "loggerAdapter": a class like FileLoggerAdapter providing info, error,
@@ -73,8 +84,10 @@ addParseCloud();
// "clientKey": optional key from Parse dashboard
// "dotNetKey": optional key from Parse dashboard
// "restAPIKey": optional key from Parse dashboard
// "webhookKey": optional key from Parse dashboard
// "javascriptKey": optional key from Parse dashboard
// "push": optional key from configure push
// "sessionLength": optional length in seconds for how long Sessions should be valid for
class ParseServer {
@@ -82,19 +95,20 @@ class ParseServer {
appId = requiredParameter('You must provide an appId!'),
masterKey = requiredParameter('You must provide a masterKey!'),
appName,
databaseAdapter,
filesAdapter,
push,
loggerAdapter,
logsFolder,
databaseURI = DatabaseAdapter.defaultDatabaseURI,
databaseURI,
databaseOptions,
databaseAdapter,
cloud,
collectionPrefix = '',
clientKey,
javascriptKey,
dotNetKey,
restAPIKey,
webhookKey,
fileKey = 'invalid-file-key',
facebookAppIds = [],
enableAnonymousUsers = true,
@@ -103,6 +117,7 @@ class ParseServer {
serverURL = requiredParameter('You must provide a serverURL!'),
maxUploadSize = '20mb',
verifyUserEmails = false,
cacheAdapter,
emailAdapter,
publicServerURL,
customPages = {
@@ -111,59 +126,94 @@ class ParseServer {
choosePassword: undefined,
passwordResetSuccess: undefined
},
liveQuery = {}
liveQuery = {},
sessionLength = 31536000, // 1 Year in seconds
expireInactiveSessions = true,
verbose = false,
revokeSessionOnPasswordReset = true,
__indexBuildCompletionCallbackForTests = () => {},
}) {
// Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
Parse.serverURL = serverURL;
if ((databaseOptions || databaseURI || collectionPrefix !== '') && databaseAdapter) {
throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/connectionPrefix.';
} else if (!databaseAdapter) {
databaseAdapter = new MongoStorageAdapter({
uri: databaseURI,
collectionPrefix,
mongoOptions: databaseOptions,
});
} else {
databaseAdapter = loadAdapter(databaseAdapter)
}
if (!filesAdapter && !databaseURI) {
throw 'When using an explicit database adapter, you must also use and explicit filesAdapter.';
}
if (logsFolder) {
configureLogger({
logsFolder
})
}
if (databaseAdapter) {
DatabaseAdapter.setAdapter(databaseAdapter);
}
if (databaseOptions) {
DatabaseAdapter.setAppDatabaseOptions(appId, databaseOptions);
}
if (databaseURI) {
DatabaseAdapter.setAppDatabaseURI(appId, databaseURI);
}
if (cloud) {
addParseCloud();
if (typeof cloud === 'function') {
cloud(Parse)
} else if (typeof cloud === 'string') {
require(cloud);
require(path.resolve(process.cwd(), cloud));
} else {
throw "argument 'cloud' must either be a string or a function";
}
}
if (verbose || process.env.VERBOSE || process.env.VERBOSE_PARSE_SERVER) {
configureLogger({level: 'silly'});
}
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
return new GridStoreAdapter(databaseURI);
});
// Pass the push options too as it works with the default
const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push);
const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push || {});
const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter);
const emailControllerAdapter = loadAdapter(emailAdapter);
const cacheControllerAdapter = loadAdapter(cacheAdapter, InMemoryCacheAdapter, {appId: appId});
// We pass the options and the base class for the adatper,
// Note that passing an instance would work too
const filesController = new FilesController(filesControllerAdapter, appId);
const pushController = new PushController(pushControllerAdapter, appId);
const pushController = new PushController(pushControllerAdapter, appId, push);
const loggerController = new LoggerController(loggerControllerAdapter, appId);
const hooksController = new HooksController(appId, collectionPrefix);
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
const liveQueryController = new LiveQueryController(liveQuery);
const cacheController = new CacheController(cacheControllerAdapter, appId);
const databaseController = new DatabaseController(databaseAdapter);
const hooksController = new HooksController(appId, databaseController, webhookKey);
cache.apps.set(appId, {
// TODO: create indexes on first creation of a _User object. Otherwise it's impossible to
// have a Parse app without it having a _User collection.
let userClassPromise = databaseController.loadSchema()
.then(schema => schema.enforceClassExists('_User'))
let usernameUniqueness = userClassPromise
.then(() => databaseController.adapter.ensureUniqueness('_User', requiredUserFields, ['username']))
.catch(error => {
logger.warn('Unable to ensure uniqueness for usernames: ', error);
return Promise.reject(error);
});
let emailUniqueness = userClassPromise
.then(() => databaseController.adapter.ensureUniqueness('_User', requiredUserFields, ['email']))
.catch(error => {
logger.warn('Unabled to ensure uniqueness for user email addresses: ', error);
return Promise.reject(error);
})
AppCache.put(appId, {
masterKey: masterKey,
serverURL: serverURL,
collectionPrefix: collectionPrefix,
@@ -171,8 +221,10 @@ class ParseServer {
javascriptKey: javascriptKey,
dotNetKey: dotNetKey,
restAPIKey: restAPIKey,
webhookKey: webhookKey,
fileKey: fileKey,
facebookAppIds: facebookAppIds,
cacheController: cacheController,
filesController: filesController,
pushController: pushController,
loggerController: loggerController,
@@ -185,17 +237,26 @@ class ParseServer {
publicServerURL: publicServerURL,
customPages: customPages,
maxUploadSize: maxUploadSize,
liveQueryController: liveQueryController
liveQueryController: liveQueryController,
sessionLength: Number(sessionLength),
expireInactiveSessions: expireInactiveSessions,
revokeSessionOnPasswordReset,
databaseController,
});
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
if (process.env.FACEBOOK_APP_ID) {
cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
AppCache.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
}
Config.validate(cache.apps.get(appId));
this.config = cache.apps.get(appId);
Config.validate(AppCache.get(appId));
this.config = AppCache.get(appId);
hooksController.load();
// Note: Tests will start to fail if any validation happens after this is called.
if (process.env.TESTING) {
__indexBuildCompletionCallbackForTests(Promise.all([usernameUniqueness, emailUniqueness]));
}
}
get app() {
@@ -238,6 +299,7 @@ class ParseServer {
new IAPValidationRouter(),
new FeaturesRouter(),
new GlobalConfigRouter(),
new PurgeRouter(),
];
if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) {
+40 -3
View File
@@ -6,7 +6,8 @@
// components that external developers may be modifying.
import express from 'express';
import log from './logger';
import url from 'url';
import log from './logger';
export default class PromiseRouter {
// Each entry should be an object with:
@@ -154,8 +155,8 @@ export default class PromiseRouter {
function makeExpressHandler(promiseHandler) {
return function(req, res, next) {
try {
log.verbose(req.method, req.originalUrl, req.headers,
JSON.stringify(req.body, null, 2));
log.verbose(req.method, maskSensitiveUrl(req), req.headers,
JSON.stringify(maskSensitiveBody(req), null, 2));
promiseHandler(req).then((result) => {
if (!result.response && !result.location && !result.text) {
log.error('the handler did not include a "response" or a "location" field');
@@ -178,6 +179,11 @@ function makeExpressHandler(promiseHandler) {
return res.send('Found. Redirecting to '+result.location);
}
}
if (result.headers) {
Object.keys(result.headers).forEach((header) => {
res.set(header, result.headers[header]);
})
}
res.json(result.response);
}, (e) => {
log.verbose('error:', e);
@@ -189,3 +195,34 @@ function makeExpressHandler(promiseHandler) {
}
}
}
function maskSensitiveBody(req) {
let maskBody = Object.assign({}, req.body);
let shouldMaskBody = (req.method === 'POST' && req.originalUrl.endsWith('/users')
&& !req.originalUrl.includes('classes')) ||
(req.method === 'PUT' && /users\/\w+$/.test(req.originalUrl)
&& !req.originalUrl.includes('classes')) ||
(req.originalUrl.includes('classes/_User'));
if (shouldMaskBody) {
for (let key of Object.keys(maskBody)) {
if (key == 'password') {
maskBody[key] = '********';
break;
}
}
}
return maskBody;
}
function maskSensitiveUrl(req) {
let maskUrl = req.originalUrl.toString();
let shouldMaskUrl = req.method === 'GET' && req.originalUrl.includes('/login')
&& !req.originalUrl.includes('classes');
if (shouldMaskUrl) {
let password = url.parse(req.originalUrl, true).query.password;
if (password) {
maskUrl = maskUrl.replace('password=' + password, 'password=********')
}
}
return maskUrl;
}
+112 -94
View File
@@ -1,7 +1,7 @@
// An object that encapsulates everything we need to run a 'find'
// operation, encoded in the REST API format.
var Schema = require('./Schema');
var SchemaController = require('./Controllers/SchemaController');
var Parse = require('parse/node').Parse;
import { default as FilesController } from './Controllers/FilesController';
@@ -171,23 +171,39 @@ RestQuery.prototype.redirectClassNameForKey = function() {
// Validates this operation against the allowClientClassCreation config.
RestQuery.prototype.validateClientClassCreation = function() {
let sysClass = Schema.systemClasses;
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
&& sysClass.indexOf(this.className) === -1) {
return this.config.database.collectionExists(this.className).then((hasClass) => {
if (hasClass === true) {
return Promise.resolve();
}
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
'This user is not allowed to access ' +
'non-existent class: ' + this.className);
&& SchemaController.systemClasses.indexOf(this.className) === -1) {
return this.config.database.loadSchema()
.then(schemaController => schemaController.hasClass(this.className))
.then(hasClass => {
if (hasClass !== true) {
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
'This user is not allowed to access ' +
'non-existent class: ' + this.className);
}
});
} else {
return Promise.resolve();
}
};
function transformInQuery(inQueryObject, className, results) {
var values = [];
for (var result of results) {
values.push({
__type: 'Pointer',
className: className,
objectId: result.objectId
});
}
delete inQueryObject['$inQuery'];
if (Array.isArray(inQueryObject['$in'])) {
inQueryObject['$in'] = inQueryObject['$in'].concat(values);
} else {
inQueryObject['$in'] = values;
}
}
// Replaces a $inQuery clause by running the subquery, if there is an
// $inQuery clause.
// The $inQuery clause turns into an $in with values that are just
@@ -213,25 +229,29 @@ RestQuery.prototype.replaceInQuery = function() {
this.config, this.auth, inQueryValue.className,
inQueryValue.where, additionalOptions);
return subquery.execute().then((response) => {
var values = [];
for (var result of response.results) {
values.push({
__type: 'Pointer',
className: subquery.className,
objectId: result.objectId
});
}
delete inQueryObject['$inQuery'];
if (Array.isArray(inQueryObject['$in'])) {
inQueryObject['$in'] = inQueryObject['$in'].concat(values);
} else {
inQueryObject['$in'] = values;
}
transformInQuery(inQueryObject, subquery.className, response.results);
// Recurse to repeat
return this.replaceInQuery();
});
};
function transformNotInQuery(notInQueryObject, className, results) {
var values = [];
for (var result of results) {
values.push({
__type: 'Pointer',
className: className,
objectId: result.objectId
});
}
delete notInQueryObject['$notInQuery'];
if (Array.isArray(notInQueryObject['$nin'])) {
notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values);
} else {
notInQueryObject['$nin'] = values;
}
}
// Replaces a $notInQuery clause by running the subquery, if there is an
// $notInQuery clause.
// The $notInQuery clause turns into a $nin with values that are just
@@ -257,26 +277,25 @@ RestQuery.prototype.replaceNotInQuery = function() {
this.config, this.auth, notInQueryValue.className,
notInQueryValue.where, additionalOptions);
return subquery.execute().then((response) => {
var values = [];
for (var result of response.results) {
values.push({
__type: 'Pointer',
className: subquery.className,
objectId: result.objectId
});
}
delete notInQueryObject['$notInQuery'];
if (Array.isArray(notInQueryObject['$nin'])) {
notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values);
} else {
notInQueryObject['$nin'] = values;
}
transformNotInQuery(notInQueryObject, subquery.className, response.results);
// Recurse to repeat
return this.replaceNotInQuery();
});
};
const transformSelect = (selectObject, key ,objects) => {
var values = [];
for (var result of objects) {
values.push(result[key]);
}
delete selectObject['$select'];
if (Array.isArray(selectObject['$in'])) {
selectObject['$in'] = selectObject['$in'].concat(values);
} else {
selectObject['$in'] = values;
}
}
// Replaces a $select clause by running the subquery, if there is a
// $select clause.
// The $select clause turns into an $in with values selected out of
@@ -308,22 +327,25 @@ RestQuery.prototype.replaceSelect = function() {
this.config, this.auth, selectValue.query.className,
selectValue.query.where, additionalOptions);
return subquery.execute().then((response) => {
var values = [];
for (var result of response.results) {
values.push(result[selectValue.key]);
}
delete selectObject['$select'];
if (Array.isArray(selectObject['$in'])) {
selectObject['$in'] = selectObject['$in'].concat(values);
} else {
selectObject['$in'] = values;
}
transformSelect(selectObject, selectValue.key, response.results);
// Keep replacing $select clauses
return this.replaceSelect();
})
};
const transformDontSelect = (dontSelectObject, key, objects) => {
var values = [];
for (var result of objects) {
values.push(result[key]);
}
delete dontSelectObject['$dontSelect'];
if (Array.isArray(dontSelectObject['$nin'])) {
dontSelectObject['$nin'] = dontSelectObject['$nin'].concat(values);
} else {
dontSelectObject['$nin'] = values;
}
}
// Replaces a $dontSelect clause by running the subquery, if there is a
// $dontSelect clause.
// The $dontSelect clause turns into an $nin with values selected out of
@@ -353,17 +375,7 @@ RestQuery.prototype.replaceDontSelect = function() {
this.config, this.auth, dontSelectValue.query.className,
dontSelectValue.query.where, additionalOptions);
return subquery.execute().then((response) => {
var values = [];
for (var result of response.results) {
values.push(result[dontSelectValue.key]);
}
delete dontSelectObject['$dontSelect'];
if (Array.isArray(dontSelectObject['$nin'])) {
dontSelectObject['$nin'] = dontSelectObject['$nin'].concat(values);
} else {
dontSelectObject['$nin'] = values;
}
transformDontSelect(dontSelectObject, dontSelectValue.key, response.results);
// Keep replacing $dontSelect clauses
return this.replaceDontSelect();
})
@@ -372,9 +384,13 @@ RestQuery.prototype.replaceDontSelect = function() {
// Returns a promise for whether it was successful.
// Populates this.response with an object that only has 'results'.
RestQuery.prototype.runFind = function() {
if (this.findOptions.limit === 0) {
this.response = {results: []};
return Promise.resolve();
}
return this.config.database.find(
this.className, this.restWhere, this.findOptions).then((results) => {
if (this.className == '_User') {
if (this.className === '_User') {
for (var result of results) {
delete result.password;
}
@@ -449,39 +465,42 @@ function includePath(config, auth, response, path) {
if (pointers.length == 0) {
return response;
}
var className = null;
let pointersHash = {};
var objectIds = {};
for (var pointer of pointers) {
if (className === null) {
className = pointer.className;
} else {
if (className != pointer.className) {
throw new Parse.Error(Parse.Error.INVALID_JSON,
'inconsistent type data for include');
}
let className = pointer.className;
// only include the good pointers
if (className) {
pointersHash[className] = pointersHash[className] || [];
pointersHash[className].push(pointer.objectId);
}
objectIds[pointer.objectId] = true;
}
if (!className) {
throw new Parse.Error(Parse.Error.INVALID_JSON,
'bad pointers');
}
let queryPromises = Object.keys(pointersHash).map((className) => {
var where = {'objectId': {'$in': pointersHash[className]}};
var query = new RestQuery(config, auth, className, where);
return query.execute().then((results) => {
results.className = className;
return Promise.resolve(results);
})
})
// Get the objects for all these object ids
var where = {'objectId': {'$in': Object.keys(objectIds)}};
var query = new RestQuery(config, auth, className, where);
return query.execute().then((includeResponse) => {
var replace = {};
for (var obj of includeResponse.results) {
obj.__type = 'Object';
obj.className = className;
return Promise.all(queryPromises).then((responses) => {
var replace = responses.reduce((replace, includeResponse) => {
for (var obj of includeResponse.results) {
obj.__type = 'Object';
obj.className = includeResponse.className;
if(className == "_User"){
delete obj.sessionToken;
if (obj.className == "_User") {
delete obj.sessionToken;
delete obj.authData;
}
replace[obj.objectId] = obj;
}
return replace;
}, {})
replace[obj.objectId] = obj;
}
var resp = {
results: replacePointers(response.results, path, replace)
};
@@ -507,16 +526,14 @@ function findPointers(object, path) {
}
if (typeof object !== 'object') {
throw new Parse.Error(Parse.Error.INVALID_QUERY,
'can only include pointer fields');
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'can only include pointer fields');
}
if (path.length == 0) {
if (object.__type == 'Pointer') {
return [object];
}
throw new Parse.Error(Parse.Error.INVALID_QUERY,
'can only include pointer fields');
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'can only include pointer fields');
}
var subobject = object[path[0]];
@@ -534,15 +551,16 @@ function findPointers(object, path) {
// pointers inflated.
function replacePointers(object, path, replace) {
if (object instanceof Array) {
return object.map((obj) => replacePointers(obj, path, replace));
return object.map((obj) => replacePointers(obj, path, replace))
.filter((obj) => obj != null && obj != undefined);
}
if (typeof object !== 'object') {
return object;
}
if (path.length == 0) {
if (object.__type == 'Pointer') {
if (path.length === 0) {
if (object.__type === 'Pointer') {
return replace[object.objectId];
}
return object;
+206 -148
View File
@@ -2,8 +2,7 @@
// that writes to the database.
// This could be either a "create" or an "update".
import cache from './cache';
var Schema = require('./Schema');
var SchemaController = require('./Controllers/SchemaController');
var deepcopy = require('deepcopy');
var Auth = require('./Auth');
@@ -12,6 +11,8 @@ var cryptoUtils = require('./cryptoUtils');
var passwordCrypto = require('./password');
var Parse = require('parse/node');
var triggers = require('./triggers');
import RestQuery from './RestQuery';
import _ from 'lodash';
// query and data are both provided in REST API format. So data
// types are encoded by plain old objects.
@@ -30,8 +31,7 @@ function RestWrite(config, auth, className, query, data, originalData) {
this.runOptions = {};
if (!query && data.objectId) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'objectId ' +
'is an invalid field name.');
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'objectId is an invalid field name.');
}
// When the operation is complete, this.response may have several
@@ -79,13 +79,17 @@ RestWrite.prototype.execute = function() {
return this.expandFilesForExistingObjects();
}).then(() => {
return this.runDatabaseOperation();
}).then(() => {
return this.createSessionTokenIfNeeded();
}).then(() => {
return this.handleFollowup();
}).then(() => {
return this.runAfterTrigger();
}).then(() => {
return this.cleanUserAuthData();
}).then(() => {
return this.response;
});
})
};
// Uses the Auth object to get the list of roles, adds the user id
@@ -100,26 +104,25 @@ RestWrite.prototype.getUserAndRoleACL = function() {
return this.auth.getUserRoles().then((roles) => {
roles.push(this.auth.user.id);
this.runOptions.acl = this.runOptions.acl.concat(roles);
return Promise.resolve();
return;
});
}else{
} else {
return Promise.resolve();
}
};
// Validates this operation against the allowClientClassCreation config.
RestWrite.prototype.validateClientClassCreation = function() {
let sysClass = Schema.systemClasses;
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
&& sysClass.indexOf(this.className) === -1) {
return this.config.database.collectionExists(this.className).then((hasClass) => {
if (hasClass === true) {
return Promise.resolve();
}
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
'This user is not allowed to access ' +
'non-existent class: ' + this.className);
&& SchemaController.systemClasses.indexOf(this.className) === -1) {
return this.config.database.loadSchema()
.then(schemaController => schemaController.hasClass(this.className))
.then(hasClass => {
if (hasClass !== true) {
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
'This user is not allowed to access ' +
'non-existent class: ' + this.className);
}
});
} else {
return Promise.resolve();
@@ -158,15 +161,18 @@ RestWrite.prototype.runBeforeTrigger = function() {
updatedObject.set(this.sanitizedData());
return Promise.resolve().then(() => {
return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config.applicationId);
return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config);
}).then((response) => {
if (response && response.object) {
if (!_.isEqual(this.data, response.object)) {
this.storage.changedByTrigger = true;
}
this.data = response.object;
this.storage['changedByTrigger'] = true;
// We should delete the objectId for an update write
if (this.query && this.query.objectId) {
delete this.data.objectId
}
return this.validateSchema();
}
});
};
@@ -300,11 +306,12 @@ RestWrite.prototype.handleAuthData = function(authData) {
'this auth is already used');
}
}
}
return Promise.resolve();
}
return;
});
}
// The non-third-party parts of User transformation
RestWrite.prototype.transformUser = function() {
if (this.className !== '_User') {
@@ -313,41 +320,19 @@ RestWrite.prototype.transformUser = function() {
var promise = Promise.resolve();
if (!this.query) {
var token = 'r:' + cryptoUtils.newToken();
this.storage['token'] = token;
promise = promise.then(() => {
var expiresAt = new Date();
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
var sessionData = {
sessionToken: token,
user: {
__type: 'Pointer',
className: '_User',
objectId: this.objectId()
},
createdWith: {
'action': 'signup',
'authProvider': this.storage['authProvider'] || 'password'
},
restricted: false,
installationId: this.auth.installationId,
expiresAt: Parse._encode(expiresAt)
};
if (this.response && this.response.response) {
this.response.response.sessionToken = token;
}
var create = new RestWrite(this.config, Auth.master(this.config),
'_Session', null, sessionData);
return create.execute();
if (this.query) {
// If we're updating a _User object, we need to clear out the cache for that user. Find all their
// session tokens, and remove them from the cache.
promise = new RestQuery(this.config, Auth.master(this.config), '_Session', { user: {
__type: "Pointer",
className: "_User",
objectId: this.objectId(),
}}).execute()
.then(results => {
results.results.forEach(session => this.config.cacheController.user.del(session.sessionToken));
});
}
// If we're updating a _User object, clear the user cache for the session
if (this.query && this.auth.user && this.auth.user.getSessionToken()) {
cache.users.remove(this.auth.user.getSessionToken());
}
return promise.then(() => {
// Transform the password
if (!this.data.password) {
@@ -369,51 +354,80 @@ RestWrite.prototype.transformUser = function() {
}
return;
}
// We need to a find to check for duplicate username in case they are missing the unique index on usernames
// TODO: Check if there is a unique index, and if so, skip this query.
return this.config.database.find(
this.className, {
username: this.data.username,
objectId: {'$ne': this.objectId()}
}, {limit: 1}).then((results) => {
if (results.length > 0) {
throw new Parse.Error(Parse.Error.USERNAME_TAKEN,
'Account already exists for this username');
}
return Promise.resolve();
});
}).then(() => {
if (!this.data.email) {
this.className,
{ username: this.data.username, objectId: {'$ne': this.objectId()} },
{ limit: 1 }
)
.then(results => {
if (results.length > 0) {
throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.');
}
return;
});
})
.then(() => {
if (!this.data.email || this.data.email.__op === 'Delete') {
return;
}
// Validate basic email address format
if (!this.data.email.match(/^.+@.+$/)) {
throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS,
'Email address format is invalid.');
throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'Email address format is invalid.');
}
// Check for email uniqueness
// Same problem for email as above for username
return this.config.database.find(
this.className, {
email: this.data.email,
objectId: {'$ne': this.objectId()}
}, {limit: 1}).then((results) => {
if (results.length > 0) {
throw new Parse.Error(Parse.Error.EMAIL_TAKEN,
'Account already exists for this email ' +
'address');
}
return Promise.resolve();
}).then(() => {
// We updated the email, send a new validation
this.storage['sendVerificationEmail'] = true;
this.config.userController.setEmailVerifyToken(this.data);
return Promise.resolve();
})
});
this.className,
{ email: this.data.email, objectId: {'$ne': this.objectId()} },
{ limit: 1 }
)
.then(results => {
if (results.length > 0) {
throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.');
}
// We updated the email, send a new validation
this.storage['sendVerificationEmail'] = true;
this.config.userController.setEmailVerifyToken(this.data);
});
})
};
RestWrite.prototype.createSessionTokenIfNeeded = function() {
if (this.className !== '_User') {
return;
}
if (this.query) {
return;
}
var token = 'r:' + cryptoUtils.newToken();
var expiresAt = this.config.generateSessionExpiresAt();
var sessionData = {
sessionToken: token,
user: {
__type: 'Pointer',
className: '_User',
objectId: this.objectId()
},
createdWith: {
'action': 'signup',
'authProvider': this.storage['authProvider'] || 'password'
},
restricted: false,
installationId: this.auth.installationId,
expiresAt: Parse._encode(expiresAt)
};
if (this.response && this.response.response) {
this.response.response.sessionToken = token;
}
var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData);
return create.execute();
}
// Handles any followup logic
RestWrite.prototype.handleFollowup = function() {
if (this.storage && this.storage['clearSessions']) {
if (this.storage && this.storage['clearSessions'] && this.config.revokeSessionOnPasswordReset) {
var sessionQuery = {
user: {
__type: 'Pointer',
@@ -434,24 +448,6 @@ RestWrite.prototype.handleFollowup = function() {
}
};
// Handles the _Role class specialness.
// Does nothing if this isn't a role object.
RestWrite.prototype.handleRole = function() {
if (this.response || this.className !== '_Role') {
return;
}
if (!this.auth.user && !this.auth.isMaster) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
'Session token required.');
}
if (!this.data.name) {
throw new Parse.Error(Parse.Error.INVALID_ROLE_NAME,
'Invalid role name.');
}
};
// Handles the _Session class specialness.
// Does nothing if this isn't an installation object.
RestWrite.prototype.handleSession = function() {
@@ -472,8 +468,7 @@ RestWrite.prototype.handleSession = function() {
if (!this.query && !this.auth.isMaster) {
var token = 'r:' + cryptoUtils.newToken();
var expiresAt = new Date();
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
var expiresAt = this.config.generateSessionExpiresAt();
var sessionData = {
sessionToken: token,
user: {
@@ -493,8 +488,7 @@ RestWrite.prototype.handleSession = function() {
}
sessionData[key] = this.data[key];
}
var create = new RestWrite(this.config, Auth.master(this.config),
'_Session', null, sessionData);
var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData);
return create.execute().then((results) => {
if (!results.response) {
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR,
@@ -579,7 +573,7 @@ RestWrite.prototype.handleInstallation = function() {
'deviceType may not be changed in this ' +
'operation');
}
return Promise.resolve();
return;
});
});
}
@@ -658,10 +652,23 @@ RestWrite.prototype.handleInstallation = function() {
// device token.
var delQuery = {
'deviceToken': this.data.deviceToken,
'installationId': {
};
// We have a unique install Id, use that to preserve
// the interesting installation
if (this.data.installationId) {
delQuery['installationId'] = {
'$ne': this.data.installationId
}
};
} else if (idMatch.objectId && this.data.objectId
&& idMatch.objectId == this.data.objectId) {
// we passed an objectId, preserve that instalation
delQuery['objectId'] = {
'$ne': idMatch.objectId
}
} else {
// What to do here? can't really clean up everything...
return idMatch.objectId;
}
if (this.data.appIdentifier) {
delQuery['appIdentifier'] = this.data.appIdentifier;
}
@@ -697,11 +704,14 @@ RestWrite.prototype.runDatabaseOperation = function() {
return;
}
if (this.className === '_Role') {
this.config.cacheController.role.clear();
}
if (this.className === '_User' &&
this.query &&
!this.auth.couldUpdateUserId(this.query.objectId)) {
throw new Parse.Error(Parse.Error.SESSION_MISSING,
'cannot modify user ' + this.query.objectId);
throw new Parse.Error(Parse.Error.SESSION_MISSING, `Cannot modify user ${this.query.objectId}.`);
}
if (this.className === '_Product' && this.data.download) {
@@ -715,50 +725,82 @@ RestWrite.prototype.runDatabaseOperation = function() {
}
if (this.query) {
// Force the user to not lockout
// Matched with parse.com
if (this.className === '_User' && this.data.ACL) {
this.data.ACL[this.query.objectId] = { read: true, write: true };
}
// Run an update
return this.config.database.update(
this.className, this.query, this.data, this.runOptions).then((resp) => {
resp.updatedAt = this.updatedAt;
if (this.storage['changedByTrigger']) {
resp = Object.keys(this.data).reduce((memo, key) => {
memo[key] = resp[key] || this.data[key];
return memo;
}, resp);
}
this.response = {
response: resp
};
});
return this.config.database.update(this.className, this.query, this.data, this.runOptions)
.then(response => {
response.updatedAt = this.updatedAt;
if (this.storage.changedByTrigger) {
Object.keys(this.data).forEach(fieldName => {
response[fieldName] = response[fieldName] || this.data[fieldName];
});
}
this.response = { response };
});
} else {
// Set the default ACL for the new _User
if (!this.data.ACL && this.className === '_User') {
var ACL = {};
if (this.className === '_User') {
var ACL = this.data.ACL;
// default public r/w ACL
if (!ACL) {
ACL = {};
ACL['*'] = { read: true, write: false };
}
// make sure the user is not locked down
ACL[this.data.objectId] = { read: true, write: true };
ACL['*'] = { read: true, write: false };
this.data.ACL = ACL;
}
// Run a create
return this.config.database.create(this.className, this.data, this.runOptions)
.then((resp) => {
Object.assign(resp, {
objectId: this.data.objectId,
createdAt: this.data.createdAt
});
if (this.storage['changedByTrigger']) {
resp = Object.keys(this.data).reduce((memo, key) => {
memo[key] = resp[key] || this.data[key];
return memo;
}, resp);
.catch(error => {
if (this.className !== '_User' || error.code !== Parse.Error.DUPLICATE_VALUE) {
throw error;
}
// If this was a failed user creation due to username or email already taken, we need to
// check whether it was username or email and return the appropriate error.
// TODO: See if we can later do this without additional queries by using named indexes.
return this.config.database.find(
this.className,
{ username: this.data.username, objectId: {'$ne': this.objectId()} },
{ limit: 1 }
)
.then(results => {
if (results.length > 0) {
throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.');
}
if (this.storage['token']) {
resp.sessionToken = this.storage['token'];
return this.config.database.find(
this.className,
{ email: this.data.email, objectId: {'$ne': this.objectId()} },
{ limit: 1 }
);
})
.then(results => {
if (results.length > 0) {
throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.');
}
this.response = {
status: 201,
response: resp,
location: this.location()
};
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
});
})
.then(response => {
response.objectId = this.data.objectId;
response.createdAt = this.data.createdAt;
if (this.storage.changedByTrigger) {
Object.keys(this.data).forEach(fieldName => {
response[fieldName] = response[fieldName] || this.data[fieldName];
});
}
this.response = {
status: 201,
response,
location: this.location()
};
});
}
};
@@ -796,7 +838,7 @@ RestWrite.prototype.runAfterTrigger = function() {
this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject);
// Run afterSave trigger
triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config.applicationId);
triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config);
};
// A helper to figure out what location this operation happens at.
@@ -824,5 +866,21 @@ RestWrite.prototype.sanitizedData = function() {
return Parse._decode(undefined, data);
}
RestWrite.prototype.cleanUserAuthData = function() {
if (this.response && this.response.response && this.className === '_User') {
let user = this.response.response;
if (user.authData) {
Object.keys(user.authData).forEach((provider) => {
if (user.authData[provider] === null) {
delete user.authData[provider];
}
});
if (Object.keys(user.authData).length == 0) {
delete user.authData;
}
}
}
};
export default RestWrite;
module.exports = RestWrite;
+30 -12
View File
@@ -1,11 +1,13 @@
import PromiseRouter from '../PromiseRouter';
import rest from '../rest';
import rest from '../rest';
import url from 'url';
import url from 'url';
const ALLOWED_GET_QUERY_KEYS = ['keys', 'include'];
export class ClassesRouter extends PromiseRouter {
handleFind(req) {
let body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
let options = {};
@@ -14,14 +16,14 @@ export class ClassesRouter extends PromiseRouter {
for (let key of Object.keys(body)) {
if (allowConstraints.indexOf(key) === -1) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Improper encode of parameter');
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid paramater for query: ${key}`);
}
}
if (body.skip) {
options.skip = Number(body.skip);
}
if (body.limit) {
if (body.limit || body.limit === 0) {
options.limit = Number(body.limit);
} else {
options.limit = Number(100);
@@ -59,23 +61,39 @@ export class ClassesRouter extends PromiseRouter {
// Returns a promise for a {response} object.
handleGet(req) {
return rest.find(req.config, req.auth, req.params.className, {objectId: req.params.objectId})
let body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
let options = {};
for (let key of Object.keys(body)) {
if (ALLOWED_GET_QUERY_KEYS.indexOf(key) === -1) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Improper encode of parameter');
}
}
if (typeof body.keys == 'string') {
options.keys = body.keys;
}
if (body.include) {
options.include = String(body.include);
}
return rest.get(req.config, req.auth, req.params.className, req.params.objectId, options)
.then((response) => {
if (!response.results || response.results.length == 0) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
}
if (req.params.className === "_User") {
delete response.results[0].sessionToken;
const user = response.results[0];
if (req.auth.user && user.objectId == req.auth.user.id) {
// Force the session token
response.results[0].sessionToken = req.info.sessionToken;
}
}
}
return { response: response.results[0] };
});
}
@@ -106,7 +124,7 @@ export class ClassesRouter extends PromiseRouter {
}
return json
}
mountRoutes() {
this.route('GET', '/classes/:className', (req) => { return this.handleFind(req); });
this.route('GET', '/classes/:className/:objectId', (req) => { return this.handleGet(req); });
+8 -7
View File
@@ -19,16 +19,16 @@ export class FeaturesRouter extends PromiseRouter {
delete: false,
},
logs: {
level: false,
size: false,
order: false,
until: false,
from: false,
level: true,
size: true,
order: true,
until: true,
from: true,
},
push: {
immediatePush: req.config.pushController.pushIsAvailable,
scheduledPush: false,
storedPushData: false,
storedPushData: req.config.pushController.pushIsAvailable,
pushAudiences: false,
},
schemas: {
@@ -36,9 +36,10 @@ export class FeaturesRouter extends PromiseRouter {
removeField: true,
addClass: true,
removeClass: true,
clearAllDataFromClass: false,
clearAllDataFromClass: true,
exportClass: false,
editClassLevelPermissions: true,
editPointerPermissions: true,
},
};
+7 -8
View File
@@ -1,9 +1,9 @@
import express from 'express';
import BodyParser from 'body-parser';
import * as Middlewares from '../middlewares';
import express from 'express';
import BodyParser from 'body-parser';
import * as Middlewares from '../middlewares';
import { randomHexString } from '../cryptoUtils';
import Config from '../Config';
import mime from 'mime';
import Config from '../Config';
import mime from 'mime';
export class FilesRouter {
@@ -77,8 +77,7 @@ export class FilesRouter {
res.set('Location', result.url);
res.json(result);
}).catch((err) => {
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
'Could not store file.'));
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Could not store file.'));
});
}
@@ -93,4 +92,4 @@ export class FilesRouter {
'Could not delete file.'));
});
}
}
}
+36 -9
View File
@@ -5,13 +5,36 @@ var express = require('express'),
triggers = require('../triggers');
import PromiseRouter from '../PromiseRouter';
import _ from 'lodash';
function parseDate(params) {
return _.mapValues(params, (obj) => {
if (Array.isArray(obj)) {
return obj.map((item) => {
if (item && item.__type == 'Date') {
return new Date(item.iso);
} else if (item && typeof item === 'object') {
return parseDate(item);
} else {
return item;
}
});
} else if (obj && obj.__type == 'Date') {
return new Date(obj.iso);
} else if (obj && typeof obj === 'object') {
return parseDate(obj);
} else {
return obj;
}
});
}
export class FunctionsRouter extends PromiseRouter {
mountRoutes() {
this.route('POST', '/functions/:functionName', FunctionsRouter.handleCloudFunction);
}
static createResponseObject(resolve, reject) {
return {
success: function(result) {
@@ -21,24 +44,29 @@ export class FunctionsRouter extends PromiseRouter {
}
});
},
error: function(error) {
reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error));
error: function(code, message) {
if (!message) {
message = code;
code = Parse.Error.SCRIPT_FAILED;
}
reject(new Parse.Error(code, message));
}
}
}
static handleCloudFunction(req) {
var applicationId = req.config.applicationId;
var theFunction = triggers.getFunction(req.params.functionName, applicationId);
var theValidator = triggers.getValidator(req.params.functionName, applicationId);
if (theFunction) {
const params = Object.assign({}, req.body, req.query);
let params = Object.assign({}, req.body, req.query);
params = parseDate(params);
var request = {
params: params,
master: req.auth && req.auth.isMaster,
user: req.auth && req.auth.user,
installationId: req.info.installationId
installationId: req.info.installationId,
log: req.config.loggerController && req.config.loggerController.adapter
};
if (theValidator && typeof theValidator === "function") {
@@ -61,4 +89,3 @@ export class FunctionsRouter extends PromiseRouter {
}
}
}
+15 -22
View File
@@ -1,37 +1,30 @@
// global_config.js
import PromiseRouter from '../PromiseRouter';
import PromiseRouter from '../PromiseRouter';
import * as middleware from "../middlewares";
export class GlobalConfigRouter extends PromiseRouter {
getGlobalConfig(req) {
return req.config.database.adaptiveCollection('_GlobalConfig')
.then(coll => coll.find({ '_id': 1 }, { limit: 1 }))
.then(results => {
if (results.length != 1) {
// If there is no config in the database - return empty config.
return { response: { params: {} } };
}
let globalConfig = results[0];
return { response: { params: globalConfig.params } };
});
let database = req.config.database.WithoutValidation();
return database.find('_GlobalConfig', { objectId: 1 }, { limit: 1 }).then((results) => {
if (results.length != 1) {
// If there is no config in the database - return empty config.
return { response: { params: {} } };
}
let globalConfig = results[0];
return { response: { params: globalConfig.params } };
});
}
updateGlobalConfig(req) {
const params = req.body.params;
let params = req.body.params;
// Transform in dot notation to make sure it works
const update = Object.keys(params).reduce((acc, key) => {
if(params[key] && params[key].__op && params[key].__op === "Delete") {
if (!acc.$unset) acc.$unset = {};
acc.$unset[`params.${key}`] = "";
} else {
if (!acc.$set) acc.$set = {};
acc.$set[`params.${key}`] = params[key];
}
acc[`params.${key}`] = params[key];
return acc;
}, {});
return req.config.database.adaptiveCollection('_GlobalConfig')
.then(coll => coll.upsertOne({ _id: 1 }, update))
.then(() => ({ response: { result: true } }));
let database = req.config.database.WithoutValidation();
return database.update('_GlobalConfig', {objectId: 1}, update, {upsert: true}).then(() => ({ response: { result: true } }));
}
mountRoutes() {
+8 -9
View File
@@ -1,6 +1,5 @@
import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import { HooksController } from '../Controllers/HooksController';
import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import * as middleware from "../middlewares";
export class HooksRouter extends PromiseRouter {
@@ -26,7 +25,7 @@ export class HooksRouter extends PromiseRouter {
return Promise.resolve({response: foundFunction});
});
}
return hooksController.getFunctions().then((functions) => {
return { response: functions || [] };
}, (err) => {
@@ -37,7 +36,7 @@ export class HooksRouter extends PromiseRouter {
handleGetTriggers(req) {
var hooksController = req.config.hooksController;
if (req.params.className && req.params.triggerName) {
return hooksController.getTrigger(req.params.className, req.params.triggerName).then((foundTrigger) => {
if (!foundTrigger) {
throw new Parse.Error(143,`class ${req.params.className} does not exist`);
@@ -45,7 +44,7 @@ export class HooksRouter extends PromiseRouter {
return Promise.resolve({response: foundTrigger});
});
}
return hooksController.getTriggers().then((triggers) => ({ response: triggers || [] }));
}
@@ -73,10 +72,10 @@ export class HooksRouter extends PromiseRouter {
hook.url = req.body.url
} else {
throw new Parse.Error(143, "invalid hook declaration");
}
}
return this.updateHook(hook, req.config);
}
handlePut(req) {
var body = req.body;
if (body.__op == "Delete") {
@@ -85,7 +84,7 @@ export class HooksRouter extends PromiseRouter {
return this.handleUpdate(req);
}
}
mountRoutes() {
this.route('GET', '/hooks/functions', middleware.promiseEnforceMasterKeyAccess, this.handleGetFunctions.bind(this));
this.route('GET', '/hooks/triggers', middleware.promiseEnforceMasterKeyAccess, this.handleGetTriggers.bind(this));
+12 -10
View File
@@ -6,25 +6,27 @@ import rest from '../rest';
export class InstallationsRouter extends ClassesRouter {
handleFind(req) {
let body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
var options = {};
if (req.body.skip) {
options.skip = Number(req.body.skip);
if (body.skip) {
options.skip = Number(body.skip);
}
if (req.body.limit) {
options.limit = Number(req.body.limit);
if (body.limit || body.limit === 0) {
options.limit = Number(body.limit);
}
if (req.body.order) {
options.order = String(req.body.order);
if (body.order) {
options.order = String(body.order);
}
if (req.body.count) {
if (body.count) {
options.count = true;
}
if (req.body.include) {
options.include = String(req.body.include);
if (body.include) {
options.include = String(body.include);
}
return rest.find(req.config, req.auth,
'_Installation', req.body.where, options)
'_Installation', body.where, options)
.then((response) => {
return {response: response};
});
+24
View File
@@ -0,0 +1,24 @@
import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
export class PurgeRouter extends PromiseRouter {
handlePurge(req) {
return req.config.database.purgeCollection(req.params.className)
.then(() => {
var cacheAdapter = req.config.cacheController;
if (req.params.className == '_Session') {
cacheAdapter.user.clear();
} else if (req.params.className == '_Role') {
cacheAdapter.role.clear();
}
return {response: {}};
});
}
mountRoutes() {
this.route('DELETE', '/purge/:className', middleware.promiseEnforceMasterKeyAccess, (req) => { return this.handlePurge(req); });
}
}
export default PurgeRouter;

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